Skip to content

Commit 5f2f35d

Browse files
implement most endpoints
1 parent 16bf849 commit 5f2f35d

File tree

15 files changed

+1333
-6
lines changed

15 files changed

+1333
-6
lines changed

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99

1010
"cSpell.enabled": true,
1111
"cSpell.words": [
12+
"Bitbook",
13+
"Bitizen",
1214
"bitprints",
1315
"BUILDKIT",
16+
"burnbot",
17+
"burnbots",
1418
"cancelables",
1519
"codegen",
1620
"Conforti",
@@ -21,6 +25,7 @@
2125
"dind",
2226
"effectful",
2327
"efffrida",
28+
"encodeable",
2429
"frida",
2530
"frontmost",
2631
"interruptible",

packages/bitprints/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
},
1616
"license": "GPL-3.0-only",
1717
"author": "Leo Conforti <leo@leoconforti.us> (https://leoconforti.us)",
18-
"type": "module",
1918
"sideEffects": [],
19+
"type": "module",
2020
"exports": {
2121
"./package.json": "./package.json",
2222
".": "./src/index.ts",

packages/doorman/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
},
1616
"license": "GPL-3.0-only",
1717
"author": "Leo Conforti <leo@leoconforti.us> (https://leoconforti.us)",
18-
"type": "module",
1918
"sideEffects": [],
19+
"type": "module",
2020
"exports": {
2121
"./package.json": "./package.json",
2222
".": "./src/index.ts",

packages/insight/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
},
1616
"license": "GPL-3.0-only",
1717
"author": "Leo Conforti <leo@leoconforti.us> (https://leoconforti.us)",
18-
"type": "module",
1918
"sideEffects": [],
19+
"type": "module",
2020
"exports": {
2121
"./package.json": "./package.json",
2222
".": "./src/index.ts",

packages/tinytower-sdk/package.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
},
1616
"license": "GPL-3.0-only",
1717
"author": "Leo Conforti <leo@leoconforti.us> (https://leoconforti.us)",
18-
"type": "module",
1918
"sideEffects": [],
19+
"type": "module",
2020
"exports": {
2121
"./package.json": "./package.json",
2222
".": "./src/index.ts",
@@ -35,6 +35,23 @@
3535
"check": "tsc -b tsconfig.json",
3636
"codegen": "build-utils prepare-v4"
3737
},
38+
"dependencies": {
39+
"effect-schemas": "0.0.9"
40+
},
41+
"devDependencies": {
42+
"@effect/cluster": "0.55.0",
43+
"@effect/experimental": "0.57.11",
44+
"@effect/platform": "0.93.6",
45+
"@effect/platform-node": "0.103.0",
46+
"@effect/rpc": "0.72.2",
47+
"@effect/sql": "0.48.6",
48+
"@effect/workflow": "0.15.2",
49+
"effect": "3.19.11"
50+
},
51+
"peerDependencies": {
52+
"@effect/platform": "0.93.6",
53+
"effect": "3.19.11"
54+
},
3855
"publishConfig": {
3956
"access": "public",
4057
"exports": {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import type * as Config from "effect/Config";
2+
import type * as ConfigError from "effect/ConfigError";
3+
import type * as Schema from "effect/Schema";
4+
5+
import * as EffectSchemas from "effect-schemas";
6+
import * as Array from "effect/Array";
7+
import * as Context from "effect/Context";
8+
import * as Effect from "effect/Effect";
9+
import * as Layer from "effect/Layer";
10+
import * as Redacted from "effect/Redacted";
11+
12+
import * as NimblebitConfig from "./NimblebitConfig.ts";
13+
14+
/**
15+
* @since 1.0.0
16+
* @category Auth
17+
*/
18+
export class NimblebitAuth extends Context.Tag("NimblebitAuth")<
19+
NimblebitAuth,
20+
(
21+
| {
22+
readonly host: "https://sync.nimblebit.com";
23+
readonly authKey: Schema.Schema.Type<NimblebitConfig.NimblebitAuthKeySchema>;
24+
}
25+
| {
26+
readonly host: "https://authproxy.tinyburg.app";
27+
readonly authKey: Redacted.Redacted<string>;
28+
}
29+
) & {
30+
readonly sign: (data: string) => Effect.Effect<string, never, never>;
31+
readonly salt: Effect.Effect<Schema.Schema.Type<EffectSchemas.Number.U32>, never, never>;
32+
readonly burnbot: Effect.Effect<Schema.Schema.Type<NimblebitConfig.AuthenticatedPlayerSchema>, never, never>;
33+
}
34+
>() {
35+
private static readonly burnbots: Array.NonEmptyReadonlyArray<
36+
Schema.Schema.Type<NimblebitConfig.AuthenticatedPlayerSchema>
37+
> = [
38+
{
39+
playerId: NimblebitConfig.PlayerIdSchema.make("BPQSY"),
40+
playerAuthKey: NimblebitConfig.PlayerAuthKeySchema.make(
41+
Redacted.make("8dad81ae-2626-41b9-8225-325f4809057f")
42+
),
43+
},
44+
{
45+
playerId: NimblebitConfig.PlayerIdSchema.make("9GV59"),
46+
playerAuthKey: NimblebitConfig.PlayerAuthKeySchema.make(
47+
Redacted.make("be61b26e-330b-41e0-ad2f-48eb79dc3bd6")
48+
),
49+
},
50+
{
51+
playerId: NimblebitConfig.PlayerIdSchema.make("9GV2Y"),
52+
playerAuthKey: NimblebitConfig.PlayerAuthKeySchema.make(
53+
Redacted.make("efe5f6a3-8cd5-4956-897c-ec1db6c26485")
54+
),
55+
},
56+
{
57+
playerId: NimblebitConfig.PlayerIdSchema.make("9GTYN"),
58+
playerAuthKey: NimblebitConfig.PlayerAuthKeySchema.make(
59+
Redacted.make("89f9b90b-4e1e-4b48-af56-df39da7b17a7")
60+
),
61+
},
62+
] as const;
63+
64+
private static readonly getBurnbot = Effect.sync(() => {
65+
// const index = Math.floor(Math.random() * NimblebitAuth.burnbots.length);
66+
// return NimblebitAuth.burnbots[index];
67+
return this.burnbots[0];
68+
});
69+
70+
private static readonly NodeSalt: Effect.Effect<Schema.Schema.Type<EffectSchemas.Number.U32>, never, never> =
71+
Effect.map(
72+
Effect.promise(() => import("node:crypto")),
73+
(crypto) => EffectSchemas.Number.U32.make(crypto.randomBytes(4).readUInt32BE(0))
74+
);
75+
76+
private static readonly WebSalt: Effect.Effect<Schema.Schema.Type<EffectSchemas.Number.U32>, never, never> =
77+
Effect.sync(() => {
78+
const array = new Uint8Array(4);
79+
crypto.getRandomValues(array);
80+
const salt = new DataView(array.buffer).getUint32(0, false);
81+
return EffectSchemas.Number.U32.make(salt);
82+
});
83+
84+
private static readonly NodeMD5 = (data: string): Effect.Effect<string, never, never> =>
85+
Effect.map(
86+
Effect.promise(() => import("node:crypto")),
87+
(crypto) => crypto.createHash("md5").update(data).digest("hex")
88+
);
89+
90+
private static readonly WebMD5 = (data: string): Effect.Effect<string, never, never> =>
91+
Effect.promise(async () => {
92+
const encoder = new TextEncoder();
93+
const encoded = encoder.encode(data);
94+
const hashBuffer = await crypto.subtle.digest("MD5", encoded);
95+
const hashArray = Array.fromIterable(new Uint8Array(hashBuffer));
96+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
97+
});
98+
99+
static readonly NodeDirect = ({
100+
authKey,
101+
}: {
102+
authKey: Schema.Schema.Type<NimblebitConfig.NimblebitAuthKeySchema>;
103+
}): Layer.Layer<NimblebitAuth, never, never> =>
104+
Layer.succeed(this, {
105+
authKey,
106+
host: "https://sync.nimblebit.com",
107+
salt: NimblebitAuth.NodeSalt,
108+
burnbot: NimblebitAuth.getBurnbot,
109+
sign: (data: string) => NimblebitAuth.NodeMD5(data + Redacted.value(authKey)),
110+
});
111+
112+
static readonly WebDirect = ({
113+
authKey,
114+
}: {
115+
authKey: Schema.Schema.Type<NimblebitConfig.NimblebitAuthKeySchema>;
116+
}): Layer.Layer<NimblebitAuth, never, never> =>
117+
Layer.succeed(this, {
118+
authKey,
119+
host: "https://sync.nimblebit.com" as const,
120+
salt: NimblebitAuth.WebSalt,
121+
burnbot: NimblebitAuth.getBurnbot,
122+
sign: (data: string) => NimblebitAuth.WebMD5(data + Redacted.value(authKey)),
123+
});
124+
125+
static readonly NodeDirectConfig = (
126+
config: Config.Config<Schema.Schema.Type<NimblebitConfig.NimblebitAuthKeySchema>>
127+
): Layer.Layer<NimblebitAuth, ConfigError.ConfigError, never> =>
128+
Effect.map(config, (authKey) => NimblebitAuth.NodeDirect({ authKey })).pipe(Layer.unwrapEffect);
129+
130+
static readonly WebDirectConfig = (
131+
config: Config.Config<Schema.Schema.Type<NimblebitConfig.NimblebitAuthKeySchema>>
132+
): Layer.Layer<NimblebitAuth, ConfigError.ConfigError, never> =>
133+
Effect.map(config, (authKey) => NimblebitAuth.WebDirect({ authKey })).pipe(Layer.unwrapEffect);
134+
135+
static readonly NodeTinyburgAuthProxy = ({
136+
authKey,
137+
}: {
138+
authKey: Redacted.Redacted<string>;
139+
}): Layer.Layer<NimblebitAuth, never, never> =>
140+
Layer.succeed(this, {
141+
authKey,
142+
salt: NimblebitAuth.NodeSalt,
143+
burnbot: NimblebitAuth.getBurnbot,
144+
sign: (data: string) => Effect.succeed(data),
145+
host: "https://authproxy.tinyburg.app" as const,
146+
});
147+
148+
static readonly WebTinyburgAuthProxy = ({
149+
authKey,
150+
}: {
151+
authKey: Redacted.Redacted<string>;
152+
}): Layer.Layer<NimblebitAuth, never, never> =>
153+
Layer.succeed(this, {
154+
authKey,
155+
salt: NimblebitAuth.WebSalt,
156+
burnbot: NimblebitAuth.getBurnbot,
157+
sign: (data: string) => Effect.succeed(data),
158+
host: "https://authproxy.tinyburg.app" as const,
159+
});
160+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import * as Config from "effect/Config";
2+
import * as ConfigError from "effect/ConfigError";
3+
import * as Either from "effect/Either";
4+
import * as Function from "effect/Function";
5+
import * as Option from "effect/Option";
6+
import * as Schema from "effect/Schema";
7+
8+
/**
9+
* @since 1.0.0
10+
* @category Schema
11+
*/
12+
export class PlayerIdSchema extends Function.pipe(
13+
Schema.String,
14+
Schema.length(5),
15+
Schema.pattern(/^([\dA-Z]*)$/gm),
16+
Schema.brand("PlayerId")
17+
) {}
18+
19+
/**
20+
* @since 1.0.0
21+
* @category Schema
22+
*/
23+
export class PlayerEmailSchema extends Function.pipe(Schema.String, Schema.Redacted, Schema.brand("PlayerEmail")) {}
24+
25+
/**
26+
* @since 1.0.0
27+
* @category Schema
28+
*/
29+
export class PlayerAuthKeySchema extends Function.pipe(Schema.UUID, Schema.Redacted, Schema.brand("PlayerAuthKey")) {}
30+
31+
/**
32+
* @since 1.0.0
33+
* @category Schema
34+
*/
35+
export class UnauthenticatedPlayerSchema extends Schema.Struct({
36+
playerId: PlayerIdSchema,
37+
playerEmail: PlayerEmailSchema,
38+
}) {}
39+
40+
/**
41+
* @since 1.0.0
42+
* @category Schema
43+
*/
44+
export class AuthenticatedPlayerSchema extends Schema.Struct({
45+
playerId: PlayerIdSchema,
46+
playerAuthKey: PlayerAuthKeySchema,
47+
}) {}
48+
49+
/**
50+
* @since 1.0.0
51+
* @category Schema
52+
*/
53+
export class NimblebitAuthKeySchema extends Function.pipe(
54+
Schema.String,
55+
Schema.Redacted,
56+
Schema.brand("NimblebitAuthKey")
57+
) {}
58+
59+
/**
60+
* @since 1.0.0
61+
* @category Config
62+
*/
63+
export const PlayerIdConfig: Config.Config<Schema.Schema.Type<PlayerIdSchema>> = Schema.Config(
64+
"PLAYER_ID",
65+
PlayerIdSchema
66+
).pipe(Config.withDescription("The player id of your cloud sync account."));
67+
68+
/**
69+
* @since 1.0.0
70+
* @category Config
71+
*/
72+
export const PlayerEmailConfig: Config.Config<Schema.Schema.Type<PlayerEmailSchema>> = Schema.Config(
73+
"PLAYER_EMAIL",
74+
PlayerEmailSchema
75+
).pipe(Config.withDescription("The email address of your cloud sync account."));
76+
77+
/**
78+
* @since 1.0.0
79+
* @category Config
80+
*/
81+
export const PlayerAuthKeyConfig: Config.Config<Schema.Schema.Type<PlayerAuthKeySchema>> = Schema.Config(
82+
"PLAYER_AUTH_KEY",
83+
PlayerAuthKeySchema
84+
).pipe(Config.withDescription("The player auth key of your cloud sync account."));
85+
86+
/**
87+
* @since 1.0.0
88+
* @category Config
89+
*/
90+
export const UnauthenticatedPlayerConfig: Config.Config<Schema.Schema.Type<UnauthenticatedPlayerSchema>> = Config.all({
91+
playerId: PlayerIdConfig,
92+
playerEmail: PlayerEmailConfig,
93+
});
94+
95+
/**
96+
* @since 1.0.0
97+
* @category Config
98+
*/
99+
export const AuthenticatedPlayerConfig: Config.Config<Schema.Schema.Type<AuthenticatedPlayerSchema>> = Config.all({
100+
playerId: PlayerIdConfig,
101+
playerAuthKey: PlayerAuthKeyConfig,
102+
});
103+
104+
/**
105+
* @since 1.0.0
106+
* @category Config
107+
*/
108+
export const PlayerConfig: Config.Config<
109+
Schema.Schema.Type<UnauthenticatedPlayerSchema> | Schema.Schema.Type<AuthenticatedPlayerSchema>
110+
> = Config.mapOrFail(
111+
Config.all({
112+
playerId: PlayerIdConfig,
113+
playerEmail: PlayerEmailConfig.pipe(Config.option),
114+
playerAuthKey: PlayerAuthKeyConfig.pipe(Config.option),
115+
}),
116+
({
117+
playerAuthKey,
118+
playerEmail,
119+
playerId,
120+
}): Either.Either<
121+
| {
122+
playerId: Schema.Schema.Type<PlayerIdSchema>;
123+
playerEmail: Schema.Schema.Type<PlayerEmailSchema>;
124+
}
125+
| {
126+
playerId: Schema.Schema.Type<PlayerIdSchema>;
127+
playerAuthKey: Schema.Schema.Type<PlayerAuthKeySchema>;
128+
},
129+
ConfigError.ConfigError
130+
> => {
131+
// Have email
132+
if (Option.isSome(playerEmail) && Option.isNone(playerAuthKey)) {
133+
return Either.right({ playerId, playerEmail: playerEmail.value });
134+
}
135+
136+
// Have player salt
137+
if (Option.isSome(playerAuthKey) && Option.isNone(playerEmail)) {
138+
return Either.right({ playerId, playerAuthKey: playerAuthKey.value });
139+
}
140+
141+
// Cannot have both email and player salt or neither
142+
return Either.left(ConfigError.InvalidData([], "Either email or player salt must be provided, not both."));
143+
}
144+
);
145+
146+
/**
147+
* @since 1.0.0
148+
* @category Config
149+
*/
150+
export const NimblebitAuthKeyConfig: Config.Config<Schema.Schema.Type<NimblebitAuthKeySchema>> = Schema.Config(
151+
"NIMBLEBIT_AUTH_KEY",
152+
NimblebitAuthKeySchema
153+
);

0 commit comments

Comments
 (0)