@phosphorco/bb-identity 0.1.0

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.
@@ -0,0 +1,380 @@
1
+ // @bun
2
+ // model-runtime.ts
3
+ var ID_MAX_LENGTH = 256;
4
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
5
+ function failure(code, message, retry = "never") {
6
+ return { code, message, retry };
7
+ }
8
+ function ok(value) {
9
+ return { ok: true, value };
10
+ }
11
+ function err(code, message, retry = "never") {
12
+ return { ok: false, error: failure(code, message, retry) };
13
+ }
14
+ function isBoundedString(value, maximum = ID_MAX_LENGTH) {
15
+ return typeof value === "string" && value.length > 0 && value.length <= maximum;
16
+ }
17
+ function idCodec(kind) {
18
+ return {
19
+ decode(input) {
20
+ const valid = kind === "server-session" ? isBoundedString(input, 4096) && !/[\u0000-\u0020\u007f]/.test(input) : isBoundedString(input) && ID_PATTERN.test(input);
21
+ if (!valid) {
22
+ return err("invalid-input", `Invalid ${kind} identifier.`);
23
+ }
24
+ return ok(input);
25
+ },
26
+ encode(value) {
27
+ return value;
28
+ }
29
+ };
30
+ }
31
+ var identityKeyCodec = {
32
+ decode(input) {
33
+ if (!isBoundedString(input, 512) || /[\u0000-\u001f\u007f]/.test(input)) {
34
+ return err("invalid-input", "Invalid identity key.");
35
+ }
36
+ return ok(input);
37
+ },
38
+ encode(value) {
39
+ return value;
40
+ }
41
+ };
42
+
43
+ // state-service-runtime.ts
44
+ function isRecord(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }
47
+ function nonEmpty(value) {
48
+ return typeof value === "string" && value.length > 0;
49
+ }
50
+ function decodeAddress(value) {
51
+ if (!isRecord(value) || !nonEmpty(value.collection) || !nonEmpty(value.recordId))
52
+ return err("invalid-input", "Invalid state address.");
53
+ const instanceId = idCodec("instance").decode(value.instanceId);
54
+ const pluginId = idCodec("plugin").decode(value.pluginId);
55
+ const owner = identityKeyCodec.decode(value.owner);
56
+ return instanceId.ok && pluginId.ok && owner.ok ? ok({ instanceId: instanceId.value, pluginId: pluginId.value, collection: value.collection, recordId: value.recordId, owner: owner.value }) : err("invalid-input", "Invalid state address identifiers.");
57
+ }
58
+ function decodeVersion(value) {
59
+ return isRecord(value) && nonEmpty(value.epoch) && typeof value.sequence === "number" && Number.isInteger(value.sequence) && value.sequence >= 0 ? ok({ epoch: value.epoch, sequence: value.sequence }) : err("invalid-input", "Invalid state version.");
60
+ }
61
+ function decodeActor(value) {
62
+ if (value === null)
63
+ return ok(null);
64
+ if (!isRecord(value) || !isRecord(value.identity) || !isRecord(value.presentation) || !nonEmpty(value.presentation.displayName) || value.presentation.handle !== null && typeof value.presentation.handle !== "string" || value.presentation.avatarUrl !== null && typeof value.presentation.avatarUrl !== "string" || !["provider-verified", "local-user", "upstream-default", "integration-asserted", "legacy", "machine"].includes(value.evidence)) {
65
+ return err("invalid-input", "Invalid state editor snapshot.");
66
+ }
67
+ const key = identityKeyCodec.decode(value.identity.key);
68
+ if (!key.ok)
69
+ return key;
70
+ const identity = value.identity;
71
+ const presentation = { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl };
72
+ if (identity.kind === "machine") {
73
+ const instanceId = idCodec("instance").decode(identity.instanceId);
74
+ if (!instanceId.ok || value.evidence !== "machine" || identity.hostId !== null && (typeof identity.hostId !== "string" || identity.hostId.length === 0 || identity.hostId.length > 512)) {
75
+ return err("invalid-input", "Invalid machine editor identity.");
76
+ }
77
+ return ok({ identity: { kind: "machine", key: key.value, instanceId: instanceId.value, hostId: identity.hostId }, presentation, evidence: "machine" });
78
+ }
79
+ if (value.evidence === "machine")
80
+ return err("invalid-input", "Machine evidence requires a machine identity.");
81
+ if (identity.kind === "person" && nonEmpty(identity.issuer) && nonEmpty(identity.subject)) {
82
+ return ok({ identity: { kind: "person", key: key.value, issuer: identity.issuer, subject: identity.subject }, presentation, evidence: value.evidence });
83
+ }
84
+ if (identity.kind === "default-user") {
85
+ const instanceId = idCodec("instance").decode(identity.instanceId);
86
+ return instanceId.ok ? ok({ identity: { kind: "default-user", key: key.value, instanceId: instanceId.value }, presentation, evidence: value.evidence }) : instanceId;
87
+ }
88
+ if (identity.kind === "external") {
89
+ const pluginId = idCodec("plugin").decode(identity.pluginId);
90
+ return pluginId.ok && nonEmpty(identity.subject) ? ok({ identity: { kind: "external", key: key.value, pluginId: pluginId.value, subject: identity.subject }, presentation, evidence: value.evidence }) : err("invalid-input", "Invalid external editor identity.");
91
+ }
92
+ return err("invalid-input", "Invalid editor identity.");
93
+ }
94
+ function sameAddress(left, right) {
95
+ return left.instanceId === right.instanceId && left.pluginId === right.pluginId && left.collection === right.collection && left.recordId === right.recordId && left.owner === right.owner;
96
+ }
97
+ function decodeEnvelope(value, codec) {
98
+ if (!isRecord(value) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0) {
99
+ return err("invalid-input", "Invalid state envelope.");
100
+ }
101
+ const address = decodeAddress(value.address);
102
+ const version = decodeVersion(value.version);
103
+ const decoded = codec.decode(value.value);
104
+ const editor = decodeActor(value.lastEditedBy);
105
+ return address.ok && version.ok && decoded.ok && editor.ok ? ok({ address: address.value, version: version.value, schemaVersion: value.schemaVersion, value: decoded.value, lastEditedBy: editor.value }) : err("invalid-input", "Invalid state envelope fields.");
106
+ }
107
+ function decodeRead(value, codec) {
108
+ if (!isRecord(value) || typeof value.status !== "string")
109
+ return err("invalid-input", "Invalid state read.");
110
+ if (value.status === "present") {
111
+ const envelope = decodeEnvelope(value.envelope, codec);
112
+ return envelope.ok ? ok({ status: "present", envelope: envelope.value }) : envelope;
113
+ }
114
+ if (value.status === "empty") {
115
+ const address = decodeAddress(value.address);
116
+ const version = decodeVersion(value.version);
117
+ return address.ok && version.ok ? ok({ status: "empty", address: address.value, version: version.value }) : err("invalid-input", "Invalid empty state read.");
118
+ }
119
+ if (value.status === "migration-required") {
120
+ const address = decodeAddress(value.address);
121
+ return address.ok && typeof value.storedSchemaVersion === "number" && Number.isInteger(value.storedSchemaVersion) && value.storedSchemaVersion >= 0 ? ok({ status: "migration-required", address: address.value, storedSchemaVersion: value.storedSchemaVersion }) : err("invalid-input", "Invalid migration-required read.");
122
+ }
123
+ return err("invalid-input", "Unknown state read status.");
124
+ }
125
+ function decodeMutation(value, codec) {
126
+ if (!isRecord(value) || value.kind !== "initialize" && value.kind !== "replace" || !isRecord(value.expected) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0 || typeof value.localGeneration !== "number" || !Number.isInteger(value.localGeneration) || value.localGeneration < 0) {
127
+ return err("invalid-input", "Invalid state mutation.");
128
+ }
129
+ const address = decodeAddress(value.address);
130
+ const expectedVersion = decodeVersion(value.expectedVersion);
131
+ const actor = identityKeyCodec.decode(value.expected.actor);
132
+ const session = idCodec("server-session").decode(value.expected.session);
133
+ const ownerSession = idCodec("owner-session").decode(value.ownerSession);
134
+ const operationId = idCodec("operation").decode(value.operationId);
135
+ const decoded = codec.decode(value.value);
136
+ return address.ok && expectedVersion.ok && actor.ok && session.ok && ownerSession.ok && operationId.ok && decoded.ok ? ok({
137
+ kind: value.kind,
138
+ address: address.value,
139
+ expectedVersion: expectedVersion.value,
140
+ expected: { actor: actor.value, session: session.value },
141
+ ownerSession: ownerSession.value,
142
+ localGeneration: value.localGeneration,
143
+ operationId: operationId.value,
144
+ schemaVersion: value.schemaVersion,
145
+ value: decoded.value
146
+ }) : err("invalid-input", "Invalid state mutation fields.");
147
+ }
148
+ function freezeDeep(value) {
149
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
150
+ for (const child of Object.values(value))
151
+ freezeDeep(child);
152
+ Object.freeze(value);
153
+ }
154
+ return value;
155
+ }
156
+ function cloneTyped(input, codec) {
157
+ try {
158
+ return codec.decode(structuredClone(codec.encode(input)));
159
+ } catch {
160
+ return err("invalid-input", "State value could not be encoded.");
161
+ }
162
+ }
163
+ function immutableMutation(input, codec) {
164
+ const decoded = cloneTyped(input, stateCodecs(codec).mutation);
165
+ return decoded.ok ? ok(freezeDeep(decoded.value)) : decoded;
166
+ }
167
+ function decodeOutcome(value, codec) {
168
+ if (!isRecord(value) || typeof value.status !== "string")
169
+ return err("invalid-input", "Invalid state outcome.");
170
+ const operationId = idCodec("operation").decode(value.operationId);
171
+ if (!operationId.ok)
172
+ return operationId;
173
+ if (value.status === "saved" || value.status === "unchanged") {
174
+ const envelope = decodeEnvelope(value.envelope, codec);
175
+ return envelope.ok ? ok({ status: value.status, envelope: envelope.value, operationId: operationId.value }) : envelope;
176
+ }
177
+ if (value.status === "already-initialized" || value.status === "conflict") {
178
+ const current = decodeRead(value.current, codec);
179
+ return current.ok ? ok({ status: value.status, current: current.value, operationId: operationId.value }) : current;
180
+ }
181
+ return err("invalid-input", "Unknown state outcome status.");
182
+ }
183
+ function decodeSave(value, codec) {
184
+ if (isRecord(value) && value.status === "indeterminate") {
185
+ const operationId = idCodec("operation").decode(value.operationId);
186
+ return operationId.ok ? ok({ status: "indeterminate", operationId: operationId.value }) : operationId;
187
+ }
188
+ return decodeOutcome(value, codec);
189
+ }
190
+ function assertRead(read, address, schemaVersion) {
191
+ const actual = read.status === "present" ? read.envelope.address : read.address;
192
+ if (!sameAddress(actual, address))
193
+ return err("incompatible", "Storage returned a state record for a different address.");
194
+ if (read.status === "present" && read.envelope.schemaVersion !== schemaVersion) {
195
+ return err("incompatible", "Storage returned an unexpected state schema.");
196
+ }
197
+ return ok(read);
198
+ }
199
+ function assertOutcome(outcome, address, schemaVersion, operationId) {
200
+ if (outcome.operationId !== operationId)
201
+ return err("incompatible", "Storage returned an outcome for a different operation.");
202
+ const read = "envelope" in outcome ? { status: "present", envelope: outcome.envelope } : outcome.current;
203
+ const checked = assertRead(read, address, schemaVersion);
204
+ return checked.ok ? ok(outcome) : checked;
205
+ }
206
+ function assertSave(save, address, schemaVersion, operationId) {
207
+ if (save.status === "indeterminate")
208
+ return save.operationId === operationId ? ok(save) : err("incompatible", "Storage returned an indeterminate result for a different operation.");
209
+ return assertOutcome(save, address, schemaVersion, operationId);
210
+ }
211
+ function addressFor(target, recordId, resource, instanceId) {
212
+ if (!nonEmpty(recordId))
213
+ return err("invalid-input", "State record id is required.");
214
+ const snapshot = target.snapshot();
215
+ return ok({ instanceId, pluginId: resource.pluginId, collection: resource.definition.collection, recordId, owner: snapshot.subject.key });
216
+ }
217
+ function stateAddressKey(address) {
218
+ return JSON.stringify([address.instanceId, address.pluginId, address.collection, address.recordId, address.owner]);
219
+ }
220
+ function createStateService(options) {
221
+ const resource = { pluginId: options.pluginId, definition: options.definition };
222
+ const codecs = stateCodecs(options.definition.codec);
223
+ const staticMutation = (target, mutation) => {
224
+ const expected = addressFor(target, mutation.address.recordId, resource, options.instanceId);
225
+ if (!expected.ok)
226
+ return expected;
227
+ const snapshot = target.snapshot();
228
+ return sameAddress(mutation.address, expected.value) && mutation.schemaVersion === options.definition.schemaVersion && mutation.expected.actor === snapshot.expected.actor && mutation.expected.session === snapshot.expected.session ? ok(undefined) : err("invalid-input", "Mutation does not match this state resource and target.");
229
+ };
230
+ return {
231
+ async read(target, recordId, readOptions) {
232
+ if (target.signal.aborted)
233
+ return err("expired", "The request context is no longer live.");
234
+ const address = addressFor(target, recordId, resource, options.instanceId);
235
+ if (!address.ok)
236
+ return address;
237
+ const result = await options.storage.read(address.value, readOptions);
238
+ if (!result.ok)
239
+ return result;
240
+ const decoded = cloneTyped(result.value, codecs.read);
241
+ return decoded.ok ? assertRead(decoded.value, address.value, options.definition.schemaVersion) : decoded;
242
+ },
243
+ async save(target, mutation, readOptions) {
244
+ const submitted = immutableMutation(mutation, options.definition.codec);
245
+ if (!submitted.ok)
246
+ return submitted;
247
+ const valid = staticMutation(target, submitted.value);
248
+ if (!valid.ok)
249
+ return valid;
250
+ const targetSnapshot = target.snapshot();
251
+ const expected = Object.freeze({ actor: targetSnapshot.expected.actor, session: targetSnapshot.expected.session });
252
+ const subject = targetSnapshot.subject.key;
253
+ const editor = freezeDeep({
254
+ identity: { ...targetSnapshot.actor.identity },
255
+ presentation: { ...targetSnapshot.actor.presentation },
256
+ evidence: targetSnapshot.actor.evidence
257
+ });
258
+ const committed = await options.storage.commit({
259
+ mutation: submitted.value,
260
+ validateAtCommit: () => {
261
+ if (readOptions?.signal?.aborted)
262
+ return err("cancelled", "State save was cancelled before durable acceptance.");
263
+ const scope = {
264
+ instanceId: options.instanceId,
265
+ pluginId: options.pluginId,
266
+ collection: options.definition.collection,
267
+ recordId: submitted.value.address.recordId,
268
+ subject,
269
+ expected,
270
+ schemaVersion: options.definition.schemaVersion
271
+ };
272
+ const live = options.commits.validate(target, scope);
273
+ return live.ok ? ok(editor) : live;
274
+ }
275
+ });
276
+ if (!committed.ok)
277
+ return committed;
278
+ const decoded = cloneTyped(committed.value, codecs.save);
279
+ if (!decoded.ok)
280
+ return decoded;
281
+ const checked = assertSave(decoded.value, submitted.value.address, options.definition.schemaVersion, submitted.value.operationId);
282
+ if (!checked.ok)
283
+ return checked;
284
+ const outcome = checked.value;
285
+ const publishedEnvelope = outcome.status === "saved" ? outcome.envelope : outcome.status === "already-initialized" && outcome.current.status === "present" ? outcome.current.envelope : null;
286
+ if (publishedEnvelope) {
287
+ try {
288
+ options.publish({ address: publishedEnvelope.address, version: publishedEnvelope.version, operationId: outcome.operationId });
289
+ } catch {}
290
+ }
291
+ return ok(outcome);
292
+ },
293
+ async reconcile(target, recordId, operationId) {
294
+ const address = addressFor(target, recordId, resource, options.instanceId);
295
+ if (!address.ok)
296
+ return address;
297
+ const result = await options.storage.reconcile({ address: address.value, operationId });
298
+ if (!result.ok)
299
+ return result;
300
+ const decoded = cloneTyped(result.value, codecs.lookup);
301
+ if (!decoded.ok || decoded.value.status !== "final")
302
+ return decoded;
303
+ const checked = assertOutcome(decoded.value.outcome, address.value, options.definition.schemaVersion, operationId);
304
+ return checked.ok ? ok({ status: "final", outcome: checked.value }) : checked;
305
+ }
306
+ };
307
+ }
308
+ function decodeLookup(value, codec) {
309
+ if (!isRecord(value) || typeof value.status !== "string")
310
+ return err("invalid-input", "Invalid state lookup.");
311
+ if (value.status === "final") {
312
+ const outcome = decodeOutcome(value.outcome, codec);
313
+ return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
314
+ }
315
+ if (value.status === "pending")
316
+ return ok({ status: "pending" });
317
+ if (value.status === "absent-final" && value.retry === "same-operation-only")
318
+ return ok({ status: "absent-final", retry: "same-operation-only" });
319
+ if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable"))
320
+ return ok({ status: "unknown", reason: value.reason });
321
+ return err("invalid-input", "Invalid state lookup metadata.");
322
+ }
323
+ function encodeAddress(address) {
324
+ return { instanceId: address.instanceId, pluginId: address.pluginId, collection: address.collection, recordId: address.recordId, owner: address.owner };
325
+ }
326
+ function encodeVersion(version) {
327
+ return { epoch: version.epoch, sequence: version.sequence };
328
+ }
329
+ function encodeActor(actor) {
330
+ return actor === null ? null : { identity: actor.identity, presentation: actor.presentation, evidence: actor.evidence };
331
+ }
332
+ function encodeEnvelope(envelope, codec) {
333
+ return { address: encodeAddress(envelope.address), version: encodeVersion(envelope.version), schemaVersion: envelope.schemaVersion, value: codec.encode(envelope.value), lastEditedBy: encodeActor(envelope.lastEditedBy) };
334
+ }
335
+ function encodeRead(read, codec) {
336
+ return read.status === "present" ? { status: "present", envelope: encodeEnvelope(read.envelope, codec) } : read.status === "empty" ? { status: "empty", address: encodeAddress(read.address), version: encodeVersion(read.version) } : read.status === "migration-required" ? { status: "migration-required", address: encodeAddress(read.address), storedSchemaVersion: read.storedSchemaVersion } : (() => {
337
+ throw new TypeError("Invalid state read status.");
338
+ })();
339
+ }
340
+ function encodeMutation(mutation, codec) {
341
+ return { ...mutation, address: encodeAddress(mutation.address), expectedVersion: encodeVersion(mutation.expectedVersion), value: codec.encode(mutation.value) };
342
+ }
343
+ function encodeOutcome(outcome, codec) {
344
+ if (outcome.status === "saved" || outcome.status === "unchanged") {
345
+ return { status: outcome.status, envelope: encodeEnvelope(outcome.envelope, codec), operationId: outcome.operationId };
346
+ }
347
+ if ("current" in outcome)
348
+ return { status: outcome.status, current: encodeRead(outcome.current, codec), operationId: outcome.operationId };
349
+ throw new TypeError("Invalid state outcome.");
350
+ }
351
+ function encodeSave(save, codec) {
352
+ return save.status === "indeterminate" ? { status: "indeterminate", operationId: save.operationId } : encodeOutcome(save, codec);
353
+ }
354
+ function stateCodecs(value) {
355
+ return {
356
+ read: { decode: (input) => decodeRead(input, value), encode: (input) => encodeRead(input, value) },
357
+ mutation: { decode: (input) => decodeMutation(input, value), encode: (input) => encodeMutation(input, value) },
358
+ save: { decode: (input) => decodeSave(input, value), encode: (input) => encodeSave(input, value) },
359
+ invalidation: {
360
+ decode(input) {
361
+ if (!isRecord(input))
362
+ return err("invalid-input", "Invalid state invalidation.");
363
+ const address = decodeAddress(input.address);
364
+ const version = decodeVersion(input.version);
365
+ const operationId = input.operationId === null ? ok(null) : idCodec("operation").decode(input.operationId);
366
+ return address.ok && version.ok && operationId.ok ? ok({ address: address.value, version: version.value, operationId: operationId.value }) : err("invalid-input", "Invalid state invalidation fields.");
367
+ },
368
+ encode: (input) => ({ address: encodeAddress(input.address), version: encodeVersion(input.version), operationId: input.operationId })
369
+ },
370
+ lookup: {
371
+ decode: (input) => decodeLookup(input, value),
372
+ encode: (input) => input.status === "final" ? { status: "final", outcome: encodeOutcome(input.outcome, value) } : input
373
+ }
374
+ };
375
+ }
376
+ export {
377
+ stateCodecs,
378
+ stateAddressKey,
379
+ createStateService
380
+ };