ignotum 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/cli/bin.mjs +1372 -492
- package/dist/cli/bin.mjs.map +1 -1
- package/dist/runtime/{api-DzcR7spt.js → api-CAgKDij7.js} +38 -2
- package/dist/runtime/api-CAgKDij7.js.map +1 -0
- package/dist/runtime/{api-DR_8vfKg.d.ts → api-Cv3hMbzo.d.ts} +4 -3
- package/dist/runtime/client.d.ts +39 -8
- package/dist/runtime/client.js +505 -131
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js +154 -0
- package/dist/runtime/descriptor-Cyo9FP9n-C0SRVXNW.js.map +1 -0
- package/dist/runtime/id-Bt9XWRGL.js +423 -0
- package/dist/runtime/id-Bt9XWRGL.js.map +1 -0
- package/dist/runtime/{id-Cs82tq9Q-hmYFyqTa.d.ts → id-BzFHf3Wo-DGPCjgrf.d.ts} +13 -13
- package/dist/runtime/id-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/{index-2q9FwJud.d.ts → index-D54flWtH.d.ts} +136 -76
- package/dist/runtime/internal/api.d.ts +1 -1
- package/dist/runtime/internal/api.js +1 -1
- package/dist/runtime/internal/host.d.ts +21 -17
- package/dist/runtime/internal/host.js +22 -8
- package/dist/runtime/internal/host.js.map +1 -1
- package/dist/runtime/internal/server.d.ts +1 -1
- package/dist/runtime/internal/server.js +1 -1
- package/dist/runtime/internal/types.d.ts +1 -1
- package/dist/runtime/internal/types.js +1 -1
- package/dist/runtime/{pagination-Bt3l7QaC-BYEGmLBE.d.ts → pagination-DnKg3dkI-r5ZUxBBx.d.ts} +34 -6
- package/dist/runtime/pagination-Dz0apuB3.d.ts +1 -0
- package/dist/runtime/result-DKAA4gpS.d.ts +1 -0
- package/dist/runtime/{schema-ERFjT8-m.js → schema-1Zs03-iS.js} +135 -17
- package/dist/runtime/schema-1Zs03-iS.js.map +1 -0
- package/dist/runtime/server.d.ts +9 -3
- package/dist/runtime/server.js +5 -3
- package/dist/runtime/server.js.map +1 -1
- package/dist/runtime/{sync-a2EdGSdY.d.ts → sync-Bs8J3fIr.d.ts} +2 -2
- package/package.json +4 -4
- package/src/cli/agent-files.ts +8 -0
- package/src/cli/auth-client.ts +1 -1
- package/src/cli/bin.ts +13 -3
- package/src/cli/build/server.ts +102 -13
- package/src/cli/codegen.ts +7 -4
- package/src/cli/control-client.ts +30 -8
- package/src/cli/deploy.ts +7 -2
- package/src/cli/environment.ts +133 -0
- package/src/client/files.ts +43 -27
- package/src/client/hooks.ts +94 -17
- package/src/client/id.ts +259 -0
- package/src/client/index.ts +5 -2
- package/src/client/page-observers.ts +77 -0
- package/src/client/sync.ts +214 -69
- package/src/dev-runtime/functions.ts +232 -86
- package/src/dev-runtime/id.ts +174 -2
- package/src/dev-runtime/query-cache.ts +105 -0
- package/src/dev-runtime/sync.ts +187 -26
- package/src/server/index.ts +13 -1
- package/dist/runtime/api-DzcR7spt.js.map +0 -1
- package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js +0 -311
- package/dist/runtime/descriptor-C5VA9qRl-DuQsowaQ.js.map +0 -1
- package/dist/runtime/file-C1abuMgd.js +0 -173
- package/dist/runtime/file-C1abuMgd.js.map +0 -1
- package/dist/runtime/pagination-CFJ3xlAt.d.ts +0 -1
- package/dist/runtime/schema-ERFjT8-m.js.map +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { makeIdContext, type IdContext, type SessionState } from "@ignotum/contracts/id";
|
|
1
2
|
import {
|
|
2
3
|
FunctionUnavailable,
|
|
3
4
|
InvalidArguments,
|
|
@@ -6,12 +7,21 @@ import {
|
|
|
6
7
|
type FunctionKind,
|
|
7
8
|
type FunctionResolutionError,
|
|
8
9
|
} from "@ignotum/contracts/runtime/functions";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getFunctionEnvironment,
|
|
12
|
+
type RuntimeSchemaDefinition,
|
|
13
|
+
} from "@ignotum/contracts/runtime/schema";
|
|
10
14
|
import { ErrorValueSchema, type ErrorValue } from "@ignotum/contracts/runtime/result";
|
|
11
15
|
import { RequestId } from "@ignotum/contracts/runtime/id";
|
|
12
16
|
import { IdGenerator } from "@ignotum/shared/id";
|
|
13
17
|
import { encodedFileOccurrencesOf } from "@ignotum/contracts/schema/file";
|
|
14
18
|
import type { RuntimeInvocationResult } from "@ignotum/contracts/runtime/hosted";
|
|
19
|
+
import {
|
|
20
|
+
emptyEnv,
|
|
21
|
+
isDefinedEnv,
|
|
22
|
+
type DefinedEnv,
|
|
23
|
+
type EnvironmentType,
|
|
24
|
+
} from "@ignotum/contracts/schema";
|
|
15
25
|
import {
|
|
16
26
|
FunctionAddress,
|
|
17
27
|
apiFunctionParts,
|
|
@@ -21,7 +31,19 @@ import {
|
|
|
21
31
|
type WireResult,
|
|
22
32
|
} from "@ignotum/contracts/runtime/sync";
|
|
23
33
|
import { FunctionRuntime } from "@ignotum/runtime/functions";
|
|
24
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
Cause,
|
|
36
|
+
Context,
|
|
37
|
+
Effect,
|
|
38
|
+
FileSystem,
|
|
39
|
+
Layer,
|
|
40
|
+
Path,
|
|
41
|
+
Predicate,
|
|
42
|
+
Ref,
|
|
43
|
+
Result,
|
|
44
|
+
Schema,
|
|
45
|
+
Semaphore,
|
|
46
|
+
} from "effect";
|
|
25
47
|
import type { ViteDevServer } from "vite";
|
|
26
48
|
import { normalizePath } from "vite";
|
|
27
49
|
|
|
@@ -31,17 +53,92 @@ import {
|
|
|
31
53
|
type FunctionArguments,
|
|
32
54
|
type RuntimeFunctionDefinition as InternalRuntimeFunctionDefinition,
|
|
33
55
|
} from "../internal/function-definition.js";
|
|
56
|
+
import {
|
|
57
|
+
EnvironmentConfigurationInvalid,
|
|
58
|
+
loadEnvironment,
|
|
59
|
+
type LoadedEnvironment,
|
|
60
|
+
} from "../cli/environment.js";
|
|
34
61
|
|
|
35
62
|
export { FunctionUnavailable, InvalidArguments, UnknownFunction, WrongFunctionKind };
|
|
36
63
|
export type { FunctionArguments, FunctionKind };
|
|
37
64
|
|
|
38
|
-
export type RuntimeHandlerContext = LocalMutationContext | LocalQueryContext
|
|
65
|
+
export type RuntimeHandlerContext = (LocalMutationContext | LocalQueryContext) & {
|
|
66
|
+
readonly id: IdContext;
|
|
67
|
+
};
|
|
39
68
|
export type RuntimeFunctionDefinition = InternalRuntimeFunctionDefinition<RuntimeHandlerContext>;
|
|
40
69
|
|
|
70
|
+
const loadDevelopmentEnvironment = Effect.fn("DevelopmentEnvironment.load")(function* (
|
|
71
|
+
server: ViteDevServer,
|
|
72
|
+
appDirectory: string,
|
|
73
|
+
) {
|
|
74
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
75
|
+
const path = yield* Path.Path;
|
|
76
|
+
const definitionPath = path.join(appDirectory, "server", "env.ts");
|
|
77
|
+
const hasDefinition = yield* fileSystem.exists(definitionPath);
|
|
78
|
+
if (!hasDefinition) return yield* loadEnvironment(appDirectory, emptyEnv, false);
|
|
79
|
+
const loaded = yield* Effect.tryPromise({
|
|
80
|
+
try: () => server.ssrLoadModule(normalizePath(definitionPath)),
|
|
81
|
+
catch: () =>
|
|
82
|
+
EnvironmentConfigurationInvalid.make({
|
|
83
|
+
message: "Ignotum could not load server/env.ts.",
|
|
84
|
+
path: definitionPath,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (!Predicate.isObject(loaded.default) || !isDefinedEnv(loaded.default)) {
|
|
88
|
+
return yield* EnvironmentConfigurationInvalid.make({
|
|
89
|
+
message: "The default export from server/env.ts is not an Ignotum environment.",
|
|
90
|
+
path: definitionPath,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return yield* loadEnvironment(appDirectory, loaded.default, true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export class DevelopmentEnvironment extends Context.Service<
|
|
97
|
+
DevelopmentEnvironment,
|
|
98
|
+
{
|
|
99
|
+
readonly reload: Effect.Effect<void, unknown>;
|
|
100
|
+
readonly snapshot: Effect.Effect<LoadedEnvironment, unknown>;
|
|
101
|
+
}
|
|
102
|
+
>()("ignotum/dev-runtime/functions/DevelopmentEnvironment") {
|
|
103
|
+
static layer(server: ViteDevServer, appDirectory: string) {
|
|
104
|
+
return Layer.effect(
|
|
105
|
+
DevelopmentEnvironment,
|
|
106
|
+
Effect.gen(function* () {
|
|
107
|
+
const fileSystem = yield* FileSystem.FileSystem;
|
|
108
|
+
const path = yield* Path.Path;
|
|
109
|
+
const load = loadDevelopmentEnvironment(server, appDirectory).pipe(
|
|
110
|
+
Effect.provideService(FileSystem.FileSystem, fileSystem),
|
|
111
|
+
Effect.provideService(Path.Path, path),
|
|
112
|
+
);
|
|
113
|
+
const initial = yield* load;
|
|
114
|
+
const current = yield* Ref.make<Result.Result<LoadedEnvironment, unknown>>(
|
|
115
|
+
Result.succeed(initial),
|
|
116
|
+
);
|
|
117
|
+
const fromResult = Result.match({
|
|
118
|
+
onFailure: Effect.fail,
|
|
119
|
+
onSuccess: Effect.succeed,
|
|
120
|
+
});
|
|
121
|
+
const reload = load.pipe(
|
|
122
|
+
Effect.result,
|
|
123
|
+
Effect.flatMap((result) =>
|
|
124
|
+
Ref.set(current, result).pipe(Effect.andThen(fromResult(result))),
|
|
125
|
+
),
|
|
126
|
+
Effect.asVoid,
|
|
127
|
+
);
|
|
128
|
+
return DevelopmentEnvironment.of({
|
|
129
|
+
reload,
|
|
130
|
+
snapshot: Ref.get(current).pipe(Effect.flatMap(fromResult)),
|
|
131
|
+
});
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
41
137
|
export interface ResolvedFunction {
|
|
42
138
|
readonly definition: RuntimeFunctionDefinition;
|
|
43
139
|
readonly args: FunctionArguments;
|
|
44
140
|
readonly schema: RuntimeSchemaDefinition;
|
|
141
|
+
readonly environment?: Readonly<EnvironmentType<DefinedEnv>>;
|
|
45
142
|
}
|
|
46
143
|
|
|
47
144
|
export class FunctionRegistry extends Context.Service<
|
|
@@ -59,6 +156,7 @@ export class FunctionRegistry extends Context.Service<
|
|
|
59
156
|
FunctionRegistry,
|
|
60
157
|
Effect.gen(function* () {
|
|
61
158
|
const path = yield* Path.Path;
|
|
159
|
+
const developmentEnvironment = yield* DevelopmentEnvironment;
|
|
62
160
|
return FunctionRegistry.of({
|
|
63
161
|
resolve: Effect.fn("FunctionRegistry.resolve")(function* (functionAddress, kind, args) {
|
|
64
162
|
const { functionName, moduleName } = apiFunctionParts(functionAddress);
|
|
@@ -84,6 +182,14 @@ export class FunctionRegistry extends Context.Service<
|
|
|
84
182
|
});
|
|
85
183
|
}
|
|
86
184
|
const definition = inspected.definition;
|
|
185
|
+
const environmentDefinition = getFunctionEnvironment(candidate);
|
|
186
|
+
if (environmentDefinition === undefined) {
|
|
187
|
+
return yield* FunctionUnavailable.make({
|
|
188
|
+
cause: new Error(`${functionAddress} has no environment binding.`),
|
|
189
|
+
function: functionAddress,
|
|
190
|
+
message: `Could not load ${functionAddress}.`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
87
193
|
|
|
88
194
|
if (definition._tag !== kind) {
|
|
89
195
|
return yield* WrongFunctionKind.make({
|
|
@@ -105,7 +211,22 @@ export class FunctionRegistry extends Context.Service<
|
|
|
105
211
|
),
|
|
106
212
|
);
|
|
107
213
|
|
|
108
|
-
|
|
214
|
+
const loadedEnvironment = yield* developmentEnvironment.snapshot.pipe(
|
|
215
|
+
Effect.mapError((cause) =>
|
|
216
|
+
FunctionUnavailable.make({
|
|
217
|
+
cause,
|
|
218
|
+
function: functionAddress,
|
|
219
|
+
message: `Could not load ${functionAddress}.`,
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
args: decodedArgs,
|
|
226
|
+
definition,
|
|
227
|
+
environment: loadedEnvironment.value,
|
|
228
|
+
schema: inspected.schema,
|
|
229
|
+
};
|
|
109
230
|
}),
|
|
110
231
|
});
|
|
111
232
|
}),
|
|
@@ -125,6 +246,7 @@ export class FunctionExecutor extends Context.Service<
|
|
|
125
246
|
functionAddress: FunctionAddress,
|
|
126
247
|
kind: FunctionKind,
|
|
127
248
|
resolved: ResolvedFunction,
|
|
249
|
+
identity?: SessionState,
|
|
128
250
|
) => Effect.Effect<RuntimeInvocationResult>;
|
|
129
251
|
}
|
|
130
252
|
>()("ignotum/dev-runtime/functions/FunctionExecutor") {
|
|
@@ -164,91 +286,113 @@ export class FunctionExecutor extends Context.Service<
|
|
|
164
286
|
);
|
|
165
287
|
|
|
166
288
|
return FunctionExecutor.of({
|
|
167
|
-
execute: Effect.fn("FunctionExecutor.execute")(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
289
|
+
execute: Effect.fn("FunctionExecutor.execute")(
|
|
290
|
+
function* (functionAddress, kind, resolved, identity) {
|
|
291
|
+
let readId = false;
|
|
292
|
+
const id = makeIdContext(
|
|
293
|
+
Effect.sync(() => {
|
|
294
|
+
readId = true;
|
|
295
|
+
return identity?.user ?? null;
|
|
173
296
|
}),
|
|
174
297
|
);
|
|
298
|
+
const invoke = (context: LocalMutationContext | LocalQueryContext) =>
|
|
299
|
+
Effect.gen(() =>
|
|
300
|
+
resolved.definition.handler(
|
|
301
|
+
Object.freeze({ ...context, id, env: resolved.environment ?? Object.freeze({}) }),
|
|
302
|
+
resolved.args,
|
|
303
|
+
),
|
|
304
|
+
).pipe(
|
|
305
|
+
Effect.matchEffect({
|
|
306
|
+
onFailure: (error) => encodeFailure(resolved.definition, error),
|
|
307
|
+
onSuccess: (value) => encodeSuccess(resolved.definition, value),
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
175
310
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
result: value,
|
|
188
|
-
dependencies,
|
|
189
|
-
observedRevision,
|
|
190
|
-
} as const;
|
|
191
|
-
return files.length === 0 ? result : { ...result, files };
|
|
192
|
-
},
|
|
193
|
-
),
|
|
194
|
-
)
|
|
195
|
-
: mutationSemaphore.withPermits(1)(
|
|
196
|
-
database
|
|
197
|
-
.trackedMutationTransaction(resolved.schema, (context) =>
|
|
198
|
-
invoke(context).pipe(
|
|
199
|
-
Effect.flatMap((result) =>
|
|
200
|
-
result.type === "Failure"
|
|
201
|
-
? Effect.fail(MutationApplicationFailure.make({ error: result.error }))
|
|
202
|
-
: Effect.succeed(result),
|
|
203
|
-
),
|
|
204
|
-
),
|
|
205
|
-
)
|
|
206
|
-
.pipe(
|
|
207
|
-
Effect.map(
|
|
208
|
-
({ committedRevision, invalidations, value }): RuntimeInvocationResult => ({
|
|
209
|
-
type: "Mutation",
|
|
311
|
+
const execution =
|
|
312
|
+
kind === "Query"
|
|
313
|
+
? database.trackedQueryTransaction(resolved.schema, invoke).pipe(
|
|
314
|
+
Effect.map(
|
|
315
|
+
({ dependencies, observedRevision, value }): RuntimeInvocationResult => {
|
|
316
|
+
const files =
|
|
317
|
+
value.type === "Success" && value.value !== undefined
|
|
318
|
+
? encodedFileOccurrencesOf(value.value)
|
|
319
|
+
: [];
|
|
320
|
+
const result = {
|
|
321
|
+
type: "Query",
|
|
210
322
|
result: value,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
323
|
+
dependencies: readId
|
|
324
|
+
? [...dependencies, { type: "Id" as const }]
|
|
325
|
+
: dependencies,
|
|
326
|
+
observedRevision,
|
|
327
|
+
} as const;
|
|
328
|
+
return files.length === 0 ? result : { ...result, files };
|
|
329
|
+
},
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
: mutationSemaphore.withPermits(1)(
|
|
333
|
+
database
|
|
334
|
+
.trackedMutationTransaction(resolved.schema, (context) =>
|
|
335
|
+
invoke(context).pipe(
|
|
336
|
+
Effect.flatMap((result) =>
|
|
337
|
+
result.type === "Failure"
|
|
338
|
+
? Effect.fail(
|
|
339
|
+
MutationApplicationFailure.make({ error: result.error }),
|
|
340
|
+
)
|
|
341
|
+
: Effect.succeed(result),
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
.pipe(
|
|
346
|
+
Effect.map(
|
|
347
|
+
({
|
|
348
|
+
committedRevision,
|
|
349
|
+
invalidations,
|
|
350
|
+
value,
|
|
351
|
+
}): RuntimeInvocationResult => ({
|
|
218
352
|
type: "Mutation",
|
|
219
|
-
result:
|
|
353
|
+
result: value,
|
|
354
|
+
invalidations,
|
|
355
|
+
committedRevision,
|
|
220
356
|
}),
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Effect.gen(function* () {
|
|
231
|
-
const requestId = yield* ids.generate(RequestId);
|
|
232
|
-
yield* Effect.logError(`${functionAddress} failed during execution.`).pipe(
|
|
233
|
-
Effect.annotateLogs({ cause, function: functionAddress, requestId }),
|
|
357
|
+
),
|
|
358
|
+
Effect.catchTags({
|
|
359
|
+
MutationApplicationFailure: ({ error }) =>
|
|
360
|
+
Effect.succeed<RuntimeInvocationResult>({
|
|
361
|
+
type: "Mutation",
|
|
362
|
+
result: { type: "Failure", error },
|
|
363
|
+
}),
|
|
364
|
+
}),
|
|
365
|
+
),
|
|
234
366
|
);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
367
|
+
|
|
368
|
+
return yield* execution.pipe(
|
|
369
|
+
Effect.orDie,
|
|
370
|
+
Effect.catchCauseIf(
|
|
371
|
+
(cause) => !Cause.hasInterruptsOnly(cause),
|
|
372
|
+
(cause) =>
|
|
373
|
+
Effect.gen(function* () {
|
|
374
|
+
const requestId = yield* ids.generate(RequestId);
|
|
375
|
+
yield* Effect.logError(`${functionAddress} failed during execution.`).pipe(
|
|
376
|
+
Effect.annotateLogs({ cause, function: functionAddress, requestId }),
|
|
377
|
+
);
|
|
378
|
+
const result = {
|
|
379
|
+
type: "Failure" as const,
|
|
380
|
+
error: { _tag: "InternalServerError", requestId },
|
|
381
|
+
};
|
|
382
|
+
if (kind === "Mutation") {
|
|
383
|
+
return { type: "Mutation" as const, result };
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
type: "Query" as const,
|
|
387
|
+
result,
|
|
388
|
+
dependencies: readId ? [{ type: "Id" as const }] : [],
|
|
389
|
+
observedRevision: yield* database.revision,
|
|
390
|
+
};
|
|
391
|
+
}),
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
},
|
|
395
|
+
),
|
|
252
396
|
});
|
|
253
397
|
}),
|
|
254
398
|
);
|
|
@@ -261,10 +405,12 @@ export const functionRuntimeLayer = Layer.effect(
|
|
|
261
405
|
const registry = yield* FunctionRegistry;
|
|
262
406
|
const executor = yield* FunctionExecutor;
|
|
263
407
|
return FunctionRuntime.of({
|
|
264
|
-
prepare: Effect.fn("DevFunctionRuntime.prepare")(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
408
|
+
prepare: Effect.fn("DevFunctionRuntime.prepare")(
|
|
409
|
+
function* (functionAddress, kind, args, identity) {
|
|
410
|
+
const resolved = yield* registry.resolve(functionAddress, kind, args);
|
|
411
|
+
return { execute: executor.execute(functionAddress, kind, resolved, identity) };
|
|
412
|
+
},
|
|
413
|
+
),
|
|
268
414
|
});
|
|
269
415
|
}),
|
|
270
416
|
);
|
package/src/dev-runtime/id.ts
CHANGED
|
@@ -1,3 +1,175 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, Effect, FileSystem, Layer, Path, Schema, Semaphore } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
DevelopmentUsername,
|
|
4
|
+
developmentSelectionKey,
|
|
5
|
+
ProfileFields,
|
|
6
|
+
SessionRequest,
|
|
7
|
+
User,
|
|
8
|
+
UserId,
|
|
9
|
+
type SessionState,
|
|
10
|
+
} from "@ignotum/contracts/id";
|
|
11
|
+
import * as Versioned from "@ignotum/contracts/versioned";
|
|
2
12
|
|
|
3
|
-
|
|
13
|
+
const Profile = Schema.Struct({ user: User, share: ProfileFields, revision: Schema.Natural });
|
|
14
|
+
const ProfilesV1 = Schema.Struct({
|
|
15
|
+
formatVersion: Schema.Literal(1),
|
|
16
|
+
profiles: Schema.Record(Schema.String, Profile),
|
|
17
|
+
});
|
|
18
|
+
const Profiles = Versioned.initial(ProfilesV1);
|
|
19
|
+
const ProfilesJson = Schema.fromJsonString(Profiles);
|
|
20
|
+
|
|
21
|
+
export const localIdPath = "/_ignotum/v1/id";
|
|
22
|
+
export const LocalCompletion = Schema.Struct({
|
|
23
|
+
request: SessionRequest,
|
|
24
|
+
username: DevelopmentUsername,
|
|
25
|
+
name: Schema.String,
|
|
26
|
+
email: Schema.String,
|
|
27
|
+
image: Schema.String,
|
|
28
|
+
share: ProfileFields,
|
|
29
|
+
manage: Schema.optional(Schema.Boolean),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const projection = (user: User, share: ReadonlyArray<string>): User => ({
|
|
33
|
+
id: user.id,
|
|
34
|
+
name: share.includes("name") ? user.name : undefined,
|
|
35
|
+
email: share.includes("email") ? user.email : undefined,
|
|
36
|
+
image: share.includes("image") ? user.image : undefined,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export class LocalId extends Context.Service<
|
|
40
|
+
LocalId,
|
|
41
|
+
{
|
|
42
|
+
readonly profile: (username: string) => Effect.Effect<typeof Profile.Type | undefined>;
|
|
43
|
+
readonly session: (username: string | null, epoch: string) => Effect.Effect<SessionState>;
|
|
44
|
+
readonly complete: (input: typeof LocalCompletion.Type) => Effect.Effect<void>;
|
|
45
|
+
}
|
|
46
|
+
>()("ignotum/dev-runtime/id/LocalId") {
|
|
47
|
+
static readonly layer = (directory: string) =>
|
|
48
|
+
Layer.effect(
|
|
49
|
+
LocalId,
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const fs = yield* FileSystem.FileSystem;
|
|
52
|
+
const path = yield* Path.Path;
|
|
53
|
+
const file = path.join(directory, ".ignotum", "id.json");
|
|
54
|
+
const lock = yield* Semaphore.make(1);
|
|
55
|
+
const load = Effect.fn("LocalId.load")(function* () {
|
|
56
|
+
if (!(yield* fs.exists(file))) return Profiles.make({ formatVersion: 1, profiles: {} });
|
|
57
|
+
return yield* fs
|
|
58
|
+
.readFileString(file)
|
|
59
|
+
.pipe(Effect.flatMap(Schema.decodeEffect(ProfilesJson)));
|
|
60
|
+
});
|
|
61
|
+
return LocalId.of({
|
|
62
|
+
profile: Effect.fn("LocalId.profile")(function* (username) {
|
|
63
|
+
return (yield* load().pipe(Effect.orDie)).profiles[username];
|
|
64
|
+
}),
|
|
65
|
+
session: Effect.fn("LocalId.session")(function* (username, epoch) {
|
|
66
|
+
if (username === null)
|
|
67
|
+
return {
|
|
68
|
+
sessionEpoch: epoch,
|
|
69
|
+
viewRevision: 0,
|
|
70
|
+
user: null,
|
|
71
|
+
validUntil: Number.MAX_SAFE_INTEGER,
|
|
72
|
+
};
|
|
73
|
+
const id = yield* Schema.decodeEffect(UserId)(username).pipe(Effect.orDie);
|
|
74
|
+
const profile = (yield* load().pipe(Effect.orDie)).profiles[id];
|
|
75
|
+
const user: User = projection(profile?.user ?? { id }, profile?.share ?? []);
|
|
76
|
+
return {
|
|
77
|
+
sessionEpoch: epoch,
|
|
78
|
+
viewRevision: profile?.revision ?? 0,
|
|
79
|
+
user,
|
|
80
|
+
validUntil: Number.MAX_SAFE_INTEGER,
|
|
81
|
+
};
|
|
82
|
+
}),
|
|
83
|
+
complete: Effect.fn("LocalId.complete")(function* (input) {
|
|
84
|
+
yield* lock
|
|
85
|
+
.withPermits(1)(
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const stored = yield* load();
|
|
88
|
+
const previous = stored.profiles[input.username];
|
|
89
|
+
const user = User.make({
|
|
90
|
+
id: UserId.make(input.username),
|
|
91
|
+
name: input.name === "" ? undefined : input.name,
|
|
92
|
+
email: input.email === "" ? undefined : input.email,
|
|
93
|
+
image: input.image === "" ? undefined : input.image,
|
|
94
|
+
});
|
|
95
|
+
const share =
|
|
96
|
+
input.manage === true
|
|
97
|
+
? input.share
|
|
98
|
+
: [
|
|
99
|
+
...new Set([
|
|
100
|
+
...(previous?.share ?? []),
|
|
101
|
+
...input.share.filter((field) => input.request.profile.includes(field)),
|
|
102
|
+
]),
|
|
103
|
+
];
|
|
104
|
+
const updated = Profiles.make({
|
|
105
|
+
formatVersion: 1,
|
|
106
|
+
profiles: {
|
|
107
|
+
...stored.profiles,
|
|
108
|
+
[input.username]: {
|
|
109
|
+
user,
|
|
110
|
+
share,
|
|
111
|
+
revision:
|
|
112
|
+
(previous?.revision ?? 0) +
|
|
113
|
+
Number(
|
|
114
|
+
JSON.stringify(
|
|
115
|
+
projection(previous?.user ?? { id: user.id }, previous?.share ?? []),
|
|
116
|
+
) !== JSON.stringify(projection(user, share)),
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
yield* fs.makeDirectory(path.dirname(file), { recursive: true });
|
|
122
|
+
yield* Effect.scoped(
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
const temporary = yield* fs.makeTempFileScoped({
|
|
125
|
+
directory: path.dirname(file),
|
|
126
|
+
prefix: ".id-",
|
|
127
|
+
});
|
|
128
|
+
yield* fs.writeFileString(
|
|
129
|
+
temporary,
|
|
130
|
+
yield* Schema.encodeEffect(ProfilesJson)(updated),
|
|
131
|
+
);
|
|
132
|
+
yield* fs.rename(temporary, file);
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
.pipe(Effect.orDie);
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const escapeHtml = (text: string) =>
|
|
145
|
+
text.replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """);
|
|
146
|
+
|
|
147
|
+
export const localIdPage = (request: typeof SessionRequest.Type): string => {
|
|
148
|
+
const encoded = JSON.stringify(request).replaceAll("<", "\\u003c");
|
|
149
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Ignotum ID · Development</title>
|
|
150
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"><style>
|
|
151
|
+
body{font:16px system-ui;background:#f7f7f5;color:#20211f;max-width:420px;margin:8vh auto;padding:24px}
|
|
152
|
+
label{display:block;margin:16px 0}input:not([type=checkbox]){box-sizing:border-box;width:100%;padding:10px;border:1px solid #bbb;border-radius:6px}
|
|
153
|
+
button{padding:12px 18px;border:0;border-radius:6px;background:#222;color:white;cursor:pointer;margin-right:8px}
|
|
154
|
+
p{line-height:1.5}small{color:#666}</style></head><body><h1>Ignotum ID</h1><p>Choose a local username to try your app.</p>
|
|
155
|
+
<form id="form"><label>Username<input name="username" required pattern="[a-z][a-z0-9_-]{0,63}" placeholder="john"></label>
|
|
156
|
+
<label>Name<input name="name"></label><label>Email<input name="email" type="email"></label><label>Image URL<input name="image" type="url"></label>
|
|
157
|
+
${["name", "email", "image"].map((field) => `<label><input type="checkbox" name="share" value="${escapeHtml(field)}"> Share ${escapeHtml(field)}</label>`).join("")}
|
|
158
|
+
<label><input type="checkbox" name="manage"> Replace existing sharing choices</label>
|
|
159
|
+
<p><small>Profile sharing is optional. No password or account is needed in development.</small></p>
|
|
160
|
+
<button>Continue</button><button type="button" id="cancel">Cancel</button><p id="error"></p></form>
|
|
161
|
+
<script type="module">
|
|
162
|
+
const request=${encoded};const form=document.querySelector('#form');
|
|
163
|
+
const previous=JSON.parse(sessionStorage.getItem('${developmentSelectionKey}')||'null');
|
|
164
|
+
if(previous?.username)form.elements.username.value=previous.username;
|
|
165
|
+
if(request.intent==='requestProfile')form.elements.username.readOnly=true;
|
|
166
|
+
let loading=false;const loadProfile=async()=>{loading=true;try{const response=await fetch('${localIdPath}/profile?username='+encodeURIComponent(form.elements.username.value),{headers:{'x-ignotum-request':'1'}});if(!response.ok)throw new Error('Could not load the local profile.');const profile=await response.json();for(const field of ['name','email','image']){form.elements[field].value=profile?.user[field]??'';form.querySelector('input[name=share][value='+field+']').checked=profile?.share.includes(field)??false;}}finally{loading=false;}};form.elements.username.onchange=loadProfile;if(previous?.username)await loadProfile();
|
|
167
|
+
document.querySelector('#cancel').onclick=()=>{sessionStorage.setItem('ignotum.id.outcome','Cancelled');location.replace(request.returnTo)};
|
|
168
|
+
form.onsubmit=async(e)=>{e.preventDefault();if(loading)return;const data=new FormData(form);
|
|
169
|
+
try{const response=await fetch('${localIdPath}',{method:'POST',headers:{'content-type':'application/json','x-ignotum-request':'1'},body:JSON.stringify({request,username:data.get('username'),name:data.get('name'),email:data.get('email'),image:data.get('image'),share:data.getAll('share'),manage:data.get('manage')==='on'})});
|
|
170
|
+
if(!response.ok)throw new Error('Could not update your local ID.');
|
|
171
|
+
sessionStorage.setItem('${developmentSelectionKey}',JSON.stringify({formatVersion:1,username:data.get('username'),epoch:request.intent==='requestProfile'?previous.epoch:'dev:'+crypto.randomUUID()}));
|
|
172
|
+
sessionStorage.setItem('ignotum.id.outcome',request.intent==='requestProfile'?'ProfileUpdated':'SignedIn');location.replace(request.returnTo);
|
|
173
|
+
}catch(error){document.querySelector('#error').textContent=error.message}}
|
|
174
|
+
</script></body></html>`;
|
|
175
|
+
};
|