@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,349 @@
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 decodeEnvelope(value, codec) {
95
+ if (!isRecord(value) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0) {
96
+ return err("invalid-input", "Invalid state envelope.");
97
+ }
98
+ const address = decodeAddress(value.address);
99
+ const version = decodeVersion(value.version);
100
+ const decoded = codec.decode(value.value);
101
+ const editor = decodeActor(value.lastEditedBy);
102
+ 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.");
103
+ }
104
+ function decodeRead(value, codec) {
105
+ if (!isRecord(value) || typeof value.status !== "string")
106
+ return err("invalid-input", "Invalid state read.");
107
+ if (value.status === "present") {
108
+ const envelope = decodeEnvelope(value.envelope, codec);
109
+ return envelope.ok ? ok({ status: "present", envelope: envelope.value }) : envelope;
110
+ }
111
+ if (value.status === "empty") {
112
+ const address = decodeAddress(value.address);
113
+ const version = decodeVersion(value.version);
114
+ return address.ok && version.ok ? ok({ status: "empty", address: address.value, version: version.value }) : err("invalid-input", "Invalid empty state read.");
115
+ }
116
+ if (value.status === "migration-required") {
117
+ const address = decodeAddress(value.address);
118
+ 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.");
119
+ }
120
+ return err("invalid-input", "Unknown state read status.");
121
+ }
122
+ function decodeMutation(value, codec) {
123
+ 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) {
124
+ return err("invalid-input", "Invalid state mutation.");
125
+ }
126
+ const address = decodeAddress(value.address);
127
+ const expectedVersion = decodeVersion(value.expectedVersion);
128
+ const actor = identityKeyCodec.decode(value.expected.actor);
129
+ const session = idCodec("server-session").decode(value.expected.session);
130
+ const ownerSession = idCodec("owner-session").decode(value.ownerSession);
131
+ const operationId = idCodec("operation").decode(value.operationId);
132
+ const decoded = codec.decode(value.value);
133
+ return address.ok && expectedVersion.ok && actor.ok && session.ok && ownerSession.ok && operationId.ok && decoded.ok ? ok({
134
+ kind: value.kind,
135
+ address: address.value,
136
+ expectedVersion: expectedVersion.value,
137
+ expected: { actor: actor.value, session: session.value },
138
+ ownerSession: ownerSession.value,
139
+ localGeneration: value.localGeneration,
140
+ operationId: operationId.value,
141
+ schemaVersion: value.schemaVersion,
142
+ value: decoded.value
143
+ }) : err("invalid-input", "Invalid state mutation fields.");
144
+ }
145
+ function decodeOutcome(value, codec) {
146
+ if (!isRecord(value) || typeof value.status !== "string")
147
+ return err("invalid-input", "Invalid state outcome.");
148
+ const operationId = idCodec("operation").decode(value.operationId);
149
+ if (!operationId.ok)
150
+ return operationId;
151
+ if (value.status === "saved" || value.status === "unchanged") {
152
+ const envelope = decodeEnvelope(value.envelope, codec);
153
+ return envelope.ok ? ok({ status: value.status, envelope: envelope.value, operationId: operationId.value }) : envelope;
154
+ }
155
+ if (value.status === "already-initialized" || value.status === "conflict") {
156
+ const current = decodeRead(value.current, codec);
157
+ return current.ok ? ok({ status: value.status, current: current.value, operationId: operationId.value }) : current;
158
+ }
159
+ return err("invalid-input", "Unknown state outcome status.");
160
+ }
161
+ function decodeSave(value, codec) {
162
+ if (isRecord(value) && value.status === "indeterminate") {
163
+ const operationId = idCodec("operation").decode(value.operationId);
164
+ return operationId.ok ? ok({ status: "indeterminate", operationId: operationId.value }) : operationId;
165
+ }
166
+ return decodeOutcome(value, codec);
167
+ }
168
+ function decodeLookup(value, codec) {
169
+ if (!isRecord(value) || typeof value.status !== "string")
170
+ return err("invalid-input", "Invalid state lookup.");
171
+ if (value.status === "final") {
172
+ const outcome = decodeOutcome(value.outcome, codec);
173
+ return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
174
+ }
175
+ if (value.status === "pending")
176
+ return ok({ status: "pending" });
177
+ if (value.status === "absent-final" && value.retry === "same-operation-only")
178
+ return ok({ status: "absent-final", retry: "same-operation-only" });
179
+ if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable"))
180
+ return ok({ status: "unknown", reason: value.reason });
181
+ return err("invalid-input", "Invalid state lookup metadata.");
182
+ }
183
+ function encodeAddress(address) {
184
+ return { instanceId: address.instanceId, pluginId: address.pluginId, collection: address.collection, recordId: address.recordId, owner: address.owner };
185
+ }
186
+ function encodeVersion(version) {
187
+ return { epoch: version.epoch, sequence: version.sequence };
188
+ }
189
+ function encodeActor(actor) {
190
+ return actor === null ? null : { identity: actor.identity, presentation: actor.presentation, evidence: actor.evidence };
191
+ }
192
+ function encodeEnvelope(envelope, codec) {
193
+ return { address: encodeAddress(envelope.address), version: encodeVersion(envelope.version), schemaVersion: envelope.schemaVersion, value: codec.encode(envelope.value), lastEditedBy: encodeActor(envelope.lastEditedBy) };
194
+ }
195
+ function encodeRead(read, codec) {
196
+ 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 } : (() => {
197
+ throw new TypeError("Invalid state read status.");
198
+ })();
199
+ }
200
+ function encodeMutation(mutation, codec) {
201
+ return { ...mutation, address: encodeAddress(mutation.address), expectedVersion: encodeVersion(mutation.expectedVersion), value: codec.encode(mutation.value) };
202
+ }
203
+ function encodeOutcome(outcome, codec) {
204
+ if (outcome.status === "saved" || outcome.status === "unchanged") {
205
+ return { status: outcome.status, envelope: encodeEnvelope(outcome.envelope, codec), operationId: outcome.operationId };
206
+ }
207
+ if ("current" in outcome)
208
+ return { status: outcome.status, current: encodeRead(outcome.current, codec), operationId: outcome.operationId };
209
+ throw new TypeError("Invalid state outcome.");
210
+ }
211
+ function encodeSave(save, codec) {
212
+ return save.status === "indeterminate" ? { status: "indeterminate", operationId: save.operationId } : encodeOutcome(save, codec);
213
+ }
214
+ function stateCodecs(value) {
215
+ return {
216
+ read: { decode: (input) => decodeRead(input, value), encode: (input) => encodeRead(input, value) },
217
+ mutation: { decode: (input) => decodeMutation(input, value), encode: (input) => encodeMutation(input, value) },
218
+ save: { decode: (input) => decodeSave(input, value), encode: (input) => encodeSave(input, value) },
219
+ invalidation: {
220
+ decode(input) {
221
+ if (!isRecord(input))
222
+ return err("invalid-input", "Invalid state invalidation.");
223
+ const address = decodeAddress(input.address);
224
+ const version = decodeVersion(input.version);
225
+ const operationId = input.operationId === null ? ok(null) : idCodec("operation").decode(input.operationId);
226
+ return address.ok && version.ok && operationId.ok ? ok({ address: address.value, version: version.value, operationId: operationId.value }) : err("invalid-input", "Invalid state invalidation fields.");
227
+ },
228
+ encode: (input) => ({ address: encodeAddress(input.address), version: encodeVersion(input.version), operationId: input.operationId })
229
+ },
230
+ lookup: {
231
+ decode: (input) => decodeLookup(input, value),
232
+ encode: (input) => input.status === "final" ? { status: "final", outcome: encodeOutcome(input.outcome, value) } : input
233
+ }
234
+ };
235
+ }
236
+
237
+ // rpc-routes-runtime.ts
238
+ var identityRoutes = Object.freeze({
239
+ bootstrap: "bb-identity/v1/bootstrap",
240
+ selfProfile: "bb-identity/v1/selfProfile",
241
+ search: "bb-identity/v1/search",
242
+ profiles: "bb-identity/v1/profiles",
243
+ participants: "bb-identity/v1/participants",
244
+ participantPreviews: "bb-identity/v1/participantPreviews"
245
+ });
246
+ var identityStateRoutes = Object.freeze({
247
+ load: "bb-identity/v1/state/load",
248
+ save: "bb-identity/v1/state/save",
249
+ reconcile: "bb-identity/v1/state/reconcile"
250
+ });
251
+ var identityStateRpcMethods = Object.freeze({
252
+ [identityStateRoutes.load]: "bb-identity.v1.state.load",
253
+ [identityStateRoutes.save]: "bb-identity.v1.state.save",
254
+ [identityStateRoutes.reconcile]: "bb-identity.v1.state.reconcile"
255
+ });
256
+ var identityRpcMethods = Object.freeze({
257
+ [identityRoutes.bootstrap]: "bb-identity.v1.bootstrap",
258
+ [identityRoutes.selfProfile]: "bb-identity.v1.selfProfile",
259
+ [identityRoutes.search]: "bb-identity.v1.search",
260
+ [identityRoutes.profiles]: "bb-identity.v1.profiles",
261
+ [identityRoutes.participants]: "bb-identity.v1.participants",
262
+ [identityRoutes.participantPreviews]: "bb-identity.v1.participantPreviews",
263
+ ...identityStateRpcMethods
264
+ });
265
+
266
+ // state-transport-runtime.ts
267
+ function writeUnavailable(connection) {
268
+ const health = connection.getHealth();
269
+ if (health.identity.status === "healthy" && health.state.status === "healthy")
270
+ return null;
271
+ const failed = health.identity.status === "unavailable" ? health.identity : health.state.status === "unavailable" ? health.state : null;
272
+ return failed ? err(failed.error.code, failed.error.message, failed.error.retry) : err("unavailable", "Identity and state links must be healthy before writing.", "after-reconnect");
273
+ }
274
+ function decodeError(value) {
275
+ const codes = new Set(["unavailable", "unauthenticated", "unsupported", "incompatible", "invalid-input", "not-found", "ambiguous", "stale-owner", "stale-context", "conflict", "cancelled", "disposed", "limit-exceeded", "invalid-operation", "expired"]);
276
+ const retries = new Set(["never", "after-refresh", "after-reconnect", "same-operation"]);
277
+ return typeof value === "object" && value !== null && typeof value.code === "string" && codes.has(value.code) && typeof value.message === "string" && typeof value.retry === "string" && retries.has(value.retry) ? ok(value) : err("incompatible", "Malformed state route error.");
278
+ }
279
+ function decodeResult(value, decode) {
280
+ if (!value || typeof value !== "object" || typeof value.ok !== "boolean")
281
+ return err("incompatible", "Malformed state route Result.");
282
+ if (!value.ok) {
283
+ const failure2 = decodeError(value.error);
284
+ return failure2.ok ? { ok: false, error: failure2.value } : failure2;
285
+ }
286
+ return decode(value.value);
287
+ }
288
+ function createStateTransport(options) {
289
+ const codecs = stateCodecs(options.resource.definition.codec);
290
+ const request = async (method, payload, decode, readOptions) => {
291
+ if (readOptions?.signal?.aborted)
292
+ return err("cancelled", "State request was cancelled.");
293
+ try {
294
+ return decodeResult(await options.connection.request(method, payload, readOptions), decode);
295
+ } catch {
296
+ return err("unavailable", "State route request failed.", "after-reconnect");
297
+ }
298
+ };
299
+ return {
300
+ load(input, readOptions) {
301
+ return request(identityStateRoutes.load, { address: input.address, expected: input.expected }, codecs.read.decode, readOptions);
302
+ },
303
+ async save(input, readOptions) {
304
+ const normalized = codecs.mutation.decode(structuredClone(codecs.mutation.encode(input)));
305
+ if (!normalized.ok)
306
+ return normalized;
307
+ if (normalized.value.address.pluginId !== options.resource.pluginId || normalized.value.address.collection !== options.resource.definition.collection || normalized.value.schemaVersion !== options.resource.definition.schemaVersion) {
308
+ return err("invalid-input", "State mutation does not match this resource.");
309
+ }
310
+ const unavailable = writeUnavailable(options.connection);
311
+ if (unavailable)
312
+ return unavailable;
313
+ const refreshed = await options.connection.revalidate(readOptions);
314
+ if (!refreshed.ok)
315
+ return refreshed;
316
+ const after = writeUnavailable(options.connection);
317
+ if (after)
318
+ return after;
319
+ const response = await request(identityStateRoutes.save, codecs.mutation.encode(normalized.value), codecs.save.decode, readOptions);
320
+ if (!response.ok)
321
+ return response;
322
+ const outcome = response.value;
323
+ if (outcome.operationId !== normalized.value.operationId)
324
+ return err("incompatible", "State route returned a different operation.");
325
+ if (outcome.status === "indeterminate")
326
+ return response;
327
+ const read = "envelope" in outcome ? { status: "present", envelope: outcome.envelope } : outcome.current;
328
+ const actual = read.status === "present" ? read.envelope.address : read.address;
329
+ if (actual.instanceId !== normalized.value.address.instanceId || actual.pluginId !== normalized.value.address.pluginId || actual.collection !== normalized.value.address.collection || actual.recordId !== normalized.value.address.recordId || actual.owner !== normalized.value.address.owner || read.status === "present" && read.envelope.schemaVersion !== normalized.value.schemaVersion)
330
+ return err("incompatible", "State route returned a mismatched resource record.");
331
+ return response;
332
+ },
333
+ reconcile(input, readOptions) {
334
+ return request(identityStateRoutes.reconcile, { address: input.address, expected: input.expected, operationId: input.operationId }, codecs.lookup.decode, readOptions);
335
+ },
336
+ subscribe(address, listener) {
337
+ return options.connection.subscribe((event) => {
338
+ if (event.kind !== "state")
339
+ return;
340
+ const value = codecs.invalidation.decode(event.event);
341
+ if (value.ok && value.value.address.instanceId === address.instanceId && value.value.address.pluginId === address.pluginId && value.value.address.collection === address.collection && value.value.address.recordId === address.recordId && value.value.address.owner === address.owner)
342
+ listener(value.value);
343
+ });
344
+ }
345
+ };
346
+ }
347
+ export {
348
+ createStateTransport
349
+ };