@phosphorco/bb-identity 0.3.0 → 0.4.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,1845 @@
1
+ // model-runtime.ts
2
+ var ID_MAX_LENGTH = 256;
3
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
4
+ function failure(code, message, retry = "never") {
5
+ return { code, message, retry };
6
+ }
7
+ function ok(value) {
8
+ return { ok: true, value };
9
+ }
10
+ function err(code, message, retry = "never") {
11
+ return { ok: false, error: failure(code, message, retry) };
12
+ }
13
+ function isBoundedString(value, maximum = ID_MAX_LENGTH) {
14
+ return typeof value === "string" && value.length > 0 && value.length <= maximum;
15
+ }
16
+ function idCodec(kind) {
17
+ return {
18
+ decode(input) {
19
+ const valid = kind === "server-session" ? isBoundedString(input, 4096) && !/[\u0000-\u0020\u007f]/.test(input) : isBoundedString(input) && ID_PATTERN.test(input);
20
+ if (!valid) {
21
+ return err("invalid-input", `Invalid ${kind} identifier.`);
22
+ }
23
+ return ok(input);
24
+ },
25
+ encode(value) {
26
+ return value;
27
+ }
28
+ };
29
+ }
30
+ var identityKeyCodec = {
31
+ decode(input) {
32
+ if (!isBoundedString(input, 512) || /[\u0000-\u001f\u007f]/.test(input)) {
33
+ return err("invalid-input", "Invalid identity key.");
34
+ }
35
+ return ok(input);
36
+ },
37
+ encode(value) {
38
+ return value;
39
+ }
40
+ };
41
+ function newOperationId() {
42
+ const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
43
+ return `op-${random}`;
44
+ }
45
+ function newConflictToken() {
46
+ return `conflict-${globalThis.crypto?.randomUUID?.() ?? Date.now().toString(36)}`;
47
+ }
48
+
49
+ // state-service-runtime.ts
50
+ function isRecord(value) {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+ function nonEmpty(value) {
54
+ return typeof value === "string" && value.length > 0;
55
+ }
56
+ function decodeAddress(value) {
57
+ if (!isRecord(value) || !nonEmpty(value.collection) || !nonEmpty(value.recordId))
58
+ return err("invalid-input", "Invalid state address.");
59
+ const instanceId = idCodec("instance").decode(value.instanceId);
60
+ const pluginId = idCodec("plugin").decode(value.pluginId);
61
+ const owner = identityKeyCodec.decode(value.owner);
62
+ 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.");
63
+ }
64
+ function decodeVersion(value) {
65
+ 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.");
66
+ }
67
+ function decodeActor(value) {
68
+ if (value === null)
69
+ return ok(null);
70
+ 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)) {
71
+ return err("invalid-input", "Invalid state editor snapshot.");
72
+ }
73
+ const key = identityKeyCodec.decode(value.identity.key);
74
+ if (!key.ok)
75
+ return key;
76
+ const identity = value.identity;
77
+ const presentation = { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl };
78
+ if (identity.kind === "machine") {
79
+ const instanceId = idCodec("instance").decode(identity.instanceId);
80
+ if (!instanceId.ok || value.evidence !== "machine" || identity.hostId !== null && (typeof identity.hostId !== "string" || identity.hostId.length === 0 || identity.hostId.length > 512)) {
81
+ return err("invalid-input", "Invalid machine editor identity.");
82
+ }
83
+ return ok({ identity: { kind: "machine", key: key.value, instanceId: instanceId.value, hostId: identity.hostId }, presentation, evidence: "machine" });
84
+ }
85
+ if (value.evidence === "machine")
86
+ return err("invalid-input", "Machine evidence requires a machine identity.");
87
+ if (identity.kind === "person" && nonEmpty(identity.issuer) && nonEmpty(identity.subject)) {
88
+ return ok({ identity: { kind: "person", key: key.value, issuer: identity.issuer, subject: identity.subject }, presentation, evidence: value.evidence });
89
+ }
90
+ if (identity.kind === "default-user") {
91
+ const instanceId = idCodec("instance").decode(identity.instanceId);
92
+ return instanceId.ok ? ok({ identity: { kind: "default-user", key: key.value, instanceId: instanceId.value }, presentation, evidence: value.evidence }) : instanceId;
93
+ }
94
+ if (identity.kind === "external") {
95
+ const pluginId = idCodec("plugin").decode(identity.pluginId);
96
+ 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.");
97
+ }
98
+ return err("invalid-input", "Invalid editor identity.");
99
+ }
100
+ function decodeEnvelope(value, codec) {
101
+ if (!isRecord(value) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0) {
102
+ return err("invalid-input", "Invalid state envelope.");
103
+ }
104
+ const address = decodeAddress(value.address);
105
+ const version = decodeVersion(value.version);
106
+ const decoded = codec.decode(value.value);
107
+ const editor = decodeActor(value.lastEditedBy);
108
+ 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.");
109
+ }
110
+ function decodeRead(value, codec) {
111
+ if (!isRecord(value) || typeof value.status !== "string")
112
+ return err("invalid-input", "Invalid state read.");
113
+ if (value.status === "present") {
114
+ const envelope = decodeEnvelope(value.envelope, codec);
115
+ return envelope.ok ? ok({ status: "present", envelope: envelope.value }) : envelope;
116
+ }
117
+ if (value.status === "empty") {
118
+ const address = decodeAddress(value.address);
119
+ const version = decodeVersion(value.version);
120
+ return address.ok && version.ok ? ok({ status: "empty", address: address.value, version: version.value }) : err("invalid-input", "Invalid empty state read.");
121
+ }
122
+ if (value.status === "migration-required") {
123
+ const address = decodeAddress(value.address);
124
+ 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.");
125
+ }
126
+ return err("invalid-input", "Unknown state read status.");
127
+ }
128
+ function decodeMutation(value, codec) {
129
+ 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) {
130
+ return err("invalid-input", "Invalid state mutation.");
131
+ }
132
+ const address = decodeAddress(value.address);
133
+ const expectedVersion = decodeVersion(value.expectedVersion);
134
+ const actor = identityKeyCodec.decode(value.expected.actor);
135
+ const session = idCodec("server-session").decode(value.expected.session);
136
+ const ownerSession = idCodec("owner-session").decode(value.ownerSession);
137
+ const operationId = idCodec("operation").decode(value.operationId);
138
+ const decoded = codec.decode(value.value);
139
+ return address.ok && expectedVersion.ok && actor.ok && session.ok && ownerSession.ok && operationId.ok && decoded.ok ? ok({
140
+ kind: value.kind,
141
+ address: address.value,
142
+ expectedVersion: expectedVersion.value,
143
+ expected: { actor: actor.value, session: session.value },
144
+ ownerSession: ownerSession.value,
145
+ localGeneration: value.localGeneration,
146
+ operationId: operationId.value,
147
+ schemaVersion: value.schemaVersion,
148
+ value: decoded.value
149
+ }) : err("invalid-input", "Invalid state mutation fields.");
150
+ }
151
+ function decodeOutcome(value, codec) {
152
+ if (!isRecord(value) || typeof value.status !== "string")
153
+ return err("invalid-input", "Invalid state outcome.");
154
+ const operationId = idCodec("operation").decode(value.operationId);
155
+ if (!operationId.ok)
156
+ return operationId;
157
+ if (value.status === "saved" || value.status === "unchanged") {
158
+ const envelope = decodeEnvelope(value.envelope, codec);
159
+ return envelope.ok ? ok({ status: value.status, envelope: envelope.value, operationId: operationId.value }) : envelope;
160
+ }
161
+ if (value.status === "already-initialized" || value.status === "conflict") {
162
+ const current = decodeRead(value.current, codec);
163
+ return current.ok ? ok({ status: value.status, current: current.value, operationId: operationId.value }) : current;
164
+ }
165
+ return err("invalid-input", "Unknown state outcome status.");
166
+ }
167
+ function decodeSave(value, codec) {
168
+ if (isRecord(value) && value.status === "indeterminate") {
169
+ const operationId = idCodec("operation").decode(value.operationId);
170
+ return operationId.ok ? ok({ status: "indeterminate", operationId: operationId.value }) : operationId;
171
+ }
172
+ return decodeOutcome(value, codec);
173
+ }
174
+ function stateAddressKey(address) {
175
+ return JSON.stringify([address.instanceId, address.pluginId, address.collection, address.recordId, address.owner]);
176
+ }
177
+ function decodeLookup(value, codec) {
178
+ if (!isRecord(value) || typeof value.status !== "string")
179
+ return err("invalid-input", "Invalid state lookup.");
180
+ if (value.status === "final") {
181
+ const outcome = decodeOutcome(value.outcome, codec);
182
+ return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
183
+ }
184
+ if (value.status === "pending")
185
+ return ok({ status: "pending" });
186
+ if (value.status === "absent-final" && value.retry === "same-operation-only")
187
+ return ok({ status: "absent-final", retry: "same-operation-only" });
188
+ if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable"))
189
+ return ok({ status: "unknown", reason: value.reason });
190
+ return err("invalid-input", "Invalid state lookup metadata.");
191
+ }
192
+ function encodeAddress(address) {
193
+ return { instanceId: address.instanceId, pluginId: address.pluginId, collection: address.collection, recordId: address.recordId, owner: address.owner };
194
+ }
195
+ function encodeVersion(version) {
196
+ return { epoch: version.epoch, sequence: version.sequence };
197
+ }
198
+ function encodeActor(actor) {
199
+ return actor === null ? null : { identity: actor.identity, presentation: actor.presentation, evidence: actor.evidence };
200
+ }
201
+ function encodeEnvelope(envelope, codec) {
202
+ return { address: encodeAddress(envelope.address), version: encodeVersion(envelope.version), schemaVersion: envelope.schemaVersion, value: codec.encode(envelope.value), lastEditedBy: encodeActor(envelope.lastEditedBy) };
203
+ }
204
+ function encodeRead(read, codec) {
205
+ 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 } : (() => {
206
+ throw new TypeError("Invalid state read status.");
207
+ })();
208
+ }
209
+ function encodeMutation(mutation, codec) {
210
+ return { ...mutation, address: encodeAddress(mutation.address), expectedVersion: encodeVersion(mutation.expectedVersion), value: codec.encode(mutation.value) };
211
+ }
212
+ function encodeOutcome(outcome, codec) {
213
+ if (outcome.status === "saved" || outcome.status === "unchanged") {
214
+ return { status: outcome.status, envelope: encodeEnvelope(outcome.envelope, codec), operationId: outcome.operationId };
215
+ }
216
+ if ("current" in outcome)
217
+ return { status: outcome.status, current: encodeRead(outcome.current, codec), operationId: outcome.operationId };
218
+ throw new TypeError("Invalid state outcome.");
219
+ }
220
+ function encodeSave(save, codec) {
221
+ return save.status === "indeterminate" ? { status: "indeterminate", operationId: save.operationId } : encodeOutcome(save, codec);
222
+ }
223
+ function stateCodecs(value) {
224
+ return {
225
+ read: { decode: (input) => decodeRead(input, value), encode: (input) => encodeRead(input, value) },
226
+ mutation: { decode: (input) => decodeMutation(input, value), encode: (input) => encodeMutation(input, value) },
227
+ save: { decode: (input) => decodeSave(input, value), encode: (input) => encodeSave(input, value) },
228
+ invalidation: {
229
+ decode(input) {
230
+ if (!isRecord(input))
231
+ return err("invalid-input", "Invalid state invalidation.");
232
+ const address = decodeAddress(input.address);
233
+ const version = decodeVersion(input.version);
234
+ const operationId = input.operationId === null ? ok(null) : idCodec("operation").decode(input.operationId);
235
+ return address.ok && version.ok && operationId.ok ? ok({ address: address.value, version: version.value, operationId: operationId.value }) : err("invalid-input", "Invalid state invalidation fields.");
236
+ },
237
+ encode: (input) => ({ address: encodeAddress(input.address), version: encodeVersion(input.version), operationId: input.operationId })
238
+ },
239
+ lookup: {
240
+ decode: (input) => decodeLookup(input, value),
241
+ encode: (input) => input.status === "final" ? { status: "final", outcome: encodeOutcome(input.outcome, value) } : input
242
+ }
243
+ };
244
+ }
245
+
246
+ // state-controller-runtime.ts
247
+ var defaultScheduler = { now: Date.now, schedule(delay, callback) {
248
+ const timer = setTimeout(callback, delay);
249
+ return () => clearTimeout(timer);
250
+ } };
251
+ function frozen(value) {
252
+ if (value !== null && typeof value === "object") {
253
+ for (const child of Object.values(value))
254
+ frozen(child);
255
+ Object.freeze(value);
256
+ }
257
+ return value;
258
+ }
259
+ function valueClone(codec, value) {
260
+ const result = codec.decode(structuredClone(codec.encode(value)));
261
+ if (!result.ok)
262
+ throw new TypeError(result.error.message);
263
+ return frozen(result.value);
264
+ }
265
+ function version(read) {
266
+ return read.status === "present" ? read.envelope.version : read.version;
267
+ }
268
+ function sameVersion(a, b) {
269
+ return a.status === b.status && version(a).epoch === version(b).epoch && version(a).sequence === version(b).sequence;
270
+ }
271
+ function createStateController(options, editing = false) {
272
+ const address = frozen({ ...options.address });
273
+ const expected = frozen({ ...options.expected });
274
+ const definition = options.definition;
275
+ const codec = definition.codec;
276
+ const codecs = stateCodecs(codec);
277
+ const drafts = options.drafts;
278
+ const initial = valueClone(codec, definition.initialValue());
279
+ const scheduler = options.scheduler ?? defaultScheduler;
280
+ const retryLimit = options.retryLimit ?? 3;
281
+ const debounce = options.debounceMs ?? 300;
282
+ if (!Number.isSafeInteger(retryLimit) || retryLimit < 0 || !Number.isFinite(debounce) || debounce < 0)
283
+ throw new TypeError("Invalid state scheduling limits");
284
+ let lifecycle = "new";
285
+ let epoch = 0;
286
+ let loadId = 0;
287
+ let localGeneration = 0;
288
+ let base = null;
289
+ let desired = null;
290
+ let blocked = null;
291
+ let pending = null;
292
+ let networkActive = false;
293
+ let acknowledgedGeneration = -1;
294
+ let activeSave = null;
295
+ let startWork = null;
296
+ let activeLoad = null;
297
+ let timer = null;
298
+ let subscription = null;
299
+ let remotePending = false;
300
+ let remoteSerial = 0;
301
+ let remoteHint = null;
302
+ let closing = false;
303
+ let checkpoint = null;
304
+ let recoverySource = null;
305
+ let checkpointError = null;
306
+ let checkpointWriting = 0;
307
+ let lastRejection = null;
308
+ let rejectedGeneration = -1;
309
+ let ownerFenced = false;
310
+ let loadFailed = false;
311
+ let automaticSuspensions = 0;
312
+ let automaticEpoch = 0;
313
+ let persistQueue = Promise.resolve();
314
+ let persistSerial = 0;
315
+ const abort = new AbortController;
316
+ const listeners = new Set;
317
+ let snapshot = { status: "loading", address };
318
+ const currentValue = (read) => read.status === "present" ? read.envelope.value : initial;
319
+ const dirty = () => base !== null && desired !== null && !definition.equal(currentValue(base), desired.value);
320
+ const needsSave = () => dirty() || base?.status === "empty" && options.initializeEmpty;
321
+ const active = (captured = epoch) => lifecycle === "active" && captured === epoch;
322
+ const clearTimer = () => {
323
+ timer?.();
324
+ timer = null;
325
+ };
326
+ const draft = () => base === null || desired === null ? null : frozen({ formatVersion: 1, key: { address, actor: expected.actor, ownerSession: options.ownerSession }, schemaVersion: definition.schemaVersion, acknowledged: base, desired: desired.value, localGeneration, inFlight: pending?.mutation ?? null, ...editing ? { rejection: lastRejection, rejectedGeneration } : {} });
327
+ function emit() {
328
+ if (lifecycle === "detached" || lifecycle === "disposed")
329
+ snapshot = { status: "detached", address, draft: draft() };
330
+ else if (blocked)
331
+ snapshot = { status: "blocked", address, ...blocked, draft: draft() };
332
+ else if (base && desired)
333
+ snapshot = { status: "ready", address, ownerSession: options.ownerSession, acknowledged: base, desired: desired.value, dirty: dirty(), saving: networkActive, localGeneration, remotePending };
334
+ else
335
+ snapshot = { status: "loading", address };
336
+ for (const listener of listeners) {
337
+ try {
338
+ listener();
339
+ } catch {}
340
+ }
341
+ }
342
+ function block(reason, message, conflict2 = null) {
343
+ clearTimer();
344
+ blocked = { reason, message, conflict: conflict2 };
345
+ emit();
346
+ }
347
+ function sessionCheck() {
348
+ if (!active())
349
+ return err("disposed", "This state controller is no longer active.");
350
+ if (editing && ownerFenced)
351
+ return err("stale-context", "This editing owner was retired.");
352
+ const result = options.currentSession();
353
+ if (!result.ok) {
354
+ block("identity-unavailable", result.error.message);
355
+ return result;
356
+ }
357
+ const session = result.value;
358
+ if (session.status !== "ready") {
359
+ block("identity-unavailable", session.error.message);
360
+ return { ok: false, error: session.error };
361
+ }
362
+ if (session.instanceId !== address.instanceId || session.actor.identity.key !== expected.actor || session.stamp !== expected.session) {
363
+ if (editing)
364
+ ownerFenced = true;
365
+ block("owner-changed", "The identity session changed.");
366
+ return err("stale-context", "The identity session changed.", "after-refresh");
367
+ }
368
+ return ok(undefined);
369
+ }
370
+ async function safe(run) {
371
+ try {
372
+ return await run();
373
+ } catch (error) {
374
+ return err("unavailable", error instanceof Error ? error.message : "State operation failed.", "after-reconnect");
375
+ }
376
+ }
377
+ function persist(captured = draft()) {
378
+ if (!drafts)
379
+ return Promise.resolve(ok(null));
380
+ if (!captured)
381
+ return Promise.resolve(ok(null));
382
+ const capturedEpoch = epoch;
383
+ const serial = ++persistSerial;
384
+ const work = persistQueue.then(async () => {
385
+ checkpointWriting++;
386
+ if (editing)
387
+ emit();
388
+ const result = await safe(() => drafts.write(captured, checkpoint?.revision ?? null));
389
+ checkpointWriting--;
390
+ if (result.ok) {
391
+ checkpoint = result.value;
392
+ if (editing)
393
+ checkpointError = null;
394
+ } else if (active(capturedEpoch) && serial === persistSerial) {
395
+ if (editing)
396
+ checkpointError = result.error;
397
+ else
398
+ block("storage-error", result.error.message);
399
+ }
400
+ if (editing)
401
+ emit();
402
+ return result;
403
+ });
404
+ persistQueue = work.then(() => {
405
+ return;
406
+ });
407
+ return work;
408
+ }
409
+ function schedule() {
410
+ clearTimer();
411
+ if (editing || !active() || automaticSuspensions || closing || blocked || pending || !needsSave())
412
+ return;
413
+ timer = scheduler.schedule(debounce, () => {
414
+ timer = null;
415
+ sendNew(true);
416
+ });
417
+ }
418
+ function readClone(read) {
419
+ const decoded = codecs.read.decode(structuredClone(codecs.read.encode(read)));
420
+ if (!decoded.ok)
421
+ return decoded;
422
+ const value = decoded.value;
423
+ const returnedAddress = value.status === "present" ? value.envelope.address : value.address;
424
+ if (stateAddressKey(returnedAddress) !== stateAddressKey(address) || value.status === "present" && value.envelope.schemaVersion !== definition.schemaVersion)
425
+ return err("incompatible", "State response does not match this resource.");
426
+ return ok(frozen(value));
427
+ }
428
+ async function applyDecision(context, decision) {
429
+ if (!active() || blocked?.conflict?.token !== context.token)
430
+ return err("stale-context", "Conflict decision was superseded.");
431
+ if (decision.kind === "needs-review") {
432
+ block("conflict", decision.reason, context);
433
+ return ok(undefined);
434
+ }
435
+ if (context.remote.status === "migration-required") {
436
+ block("migration-required", "State migration is required.");
437
+ return err("conflict", "State migration is required.");
438
+ }
439
+ base = context.remote;
440
+ desired = { value: valueClone(codec, decision.kind === "rebase" ? decision.value : currentValue(base)) };
441
+ localGeneration++;
442
+ blocked = null;
443
+ emit();
444
+ const capturedEpoch = epoch;
445
+ const saved = await persist();
446
+ if (!saved.ok)
447
+ return saved;
448
+ if (active(capturedEpoch))
449
+ schedule();
450
+ return ok(undefined);
451
+ }
452
+ async function conflict(cause, remote, original) {
453
+ if (!desired)
454
+ return err("unavailable", "State has no local value.");
455
+ const context = frozen({ cause, token: newConflictToken(), ownerSession: options.ownerSession, base: original, local: desired.value, remote });
456
+ block("conflict", "Remote state conflicts with local intent.", context);
457
+ try {
458
+ return await applyDecision(context, options.onConflict(context));
459
+ } catch (error) {
460
+ if (blocked?.conflict?.token !== context.token)
461
+ return err("stale-context", "Conflict decision was superseded.");
462
+ block("conflict", error instanceof Error ? error.message : "Conflict policy failed.", context);
463
+ return err("conflict", "Conflict policy failed.");
464
+ }
465
+ }
466
+ async function applyRemote(read, cause) {
467
+ if (read.status === "migration-required") {
468
+ block("migration-required", "State migration is required.");
469
+ return err("conflict", "State migration is required.");
470
+ }
471
+ if (pending) {
472
+ remotePending = true;
473
+ emit();
474
+ return ok(undefined);
475
+ }
476
+ if (base && version(base).epoch === version(read).epoch && version(read).sequence < version(base).sequence)
477
+ return ok(undefined);
478
+ if (base && dirty()) {
479
+ if (sameVersion(base, read)) {
480
+ if (!blocked?.conflict && (!editing || blocked?.reason !== "rejected"))
481
+ blocked = null;
482
+ emit();
483
+ schedule();
484
+ return ok(undefined);
485
+ }
486
+ return conflict(cause, read, base);
487
+ }
488
+ base = read;
489
+ desired = { value: valueClone(codec, currentValue(read)) };
490
+ if (!editing || blocked?.reason !== "rejected")
491
+ blocked = null;
492
+ emit();
493
+ schedule();
494
+ return ok(undefined);
495
+ }
496
+ function queueRemote() {
497
+ if (!active() || activeLoad || pending)
498
+ return;
499
+ reload("reconnect");
500
+ }
501
+ function reload(cause) {
502
+ const valid = sessionCheck();
503
+ if (!valid.ok)
504
+ return Promise.resolve(valid);
505
+ const capturedEpoch = epoch;
506
+ const id = ++loadId;
507
+ const serial = remoteSerial;
508
+ remotePending = false;
509
+ const work = (async () => {
510
+ const loaded = await safe(() => options.transport.load({ address, expected }, { signal: abort.signal }));
511
+ if (!active(capturedEpoch) || id !== loadId || serial !== remoteSerial)
512
+ return err("cancelled", "State load was superseded.");
513
+ const live = sessionCheck();
514
+ if (!live.ok)
515
+ return live;
516
+ if (!loaded.ok) {
517
+ loadFailed = true;
518
+ block("storage-error", loaded.error.message);
519
+ return loaded;
520
+ }
521
+ const parsed = readClone(loaded.value);
522
+ if (!parsed.ok) {
523
+ loadFailed = true;
524
+ block("storage-error", parsed.error.message);
525
+ return parsed;
526
+ }
527
+ loadFailed = false;
528
+ return applyRemote(parsed.value, cause);
529
+ })();
530
+ activeLoad = work;
531
+ work.then(() => {
532
+ if (activeLoad === work) {
533
+ activeLoad = null;
534
+ if (active(capturedEpoch) && remoteSerial !== serial)
535
+ queueRemote();
536
+ }
537
+ });
538
+ return work;
539
+ }
540
+ function outcomeCheck(outcome, operationId) {
541
+ if (editing && outcome.status === "rejected")
542
+ return outcome.operationId === operationId ? ok(frozen(outcome)) : err("incompatible", "Rejected receipt has another operation.");
543
+ if (outcome.status === "rejected")
544
+ return err("incompatible", "Rejection is not a legacy state outcome.");
545
+ const parsed = codecs.save.decode(structuredClone(codecs.save.encode(outcome)));
546
+ if (!parsed.ok)
547
+ return parsed;
548
+ if (parsed.value.operationId !== operationId || parsed.value.status === "indeterminate")
549
+ return err("incompatible", "State receipt has a different operation.");
550
+ const read = "envelope" in parsed.value ? { status: "present", envelope: parsed.value.envelope } : parsed.value.current;
551
+ const checked = readClone(read);
552
+ return checked.ok ? ok(frozen(parsed.value)) : checked;
553
+ }
554
+ async function applyOutcome(captured, raw) {
555
+ const checked = outcomeCheck(raw, captured.mutation.operationId);
556
+ if (!checked.ok) {
557
+ block("indeterminate", checked.error.message);
558
+ return checked;
559
+ }
560
+ const outcome = checked.value;
561
+ pending = null;
562
+ loadId++;
563
+ blocked = null;
564
+ if (outcome.status === "rejected") {
565
+ lastRejection = outcome.evidence;
566
+ rejectedGeneration = captured.generation;
567
+ block("rejected", "The submitted edit was definitively rejected.");
568
+ const saved2 = await persist();
569
+ return saved2.ok ? err("invalid-input", "The submitted edit was definitively rejected.") : saved2;
570
+ }
571
+ if (editing && (outcome.status === "saved" || outcome.status === "unchanged")) {
572
+ lastRejection = null;
573
+ rejectedGeneration = -1;
574
+ }
575
+ if (outcome.status === "saved" || outcome.status === "unchanged") {
576
+ base = { status: "present", envelope: outcome.envelope };
577
+ acknowledgedGeneration = Math.max(acknowledgedGeneration, captured.generation);
578
+ if (localGeneration === captured.generation)
579
+ desired = { value: valueClone(codec, outcome.envelope.value) };
580
+ emit();
581
+ } else if ("current" in outcome) {
582
+ if (outcome.current.status === "migration-required") {
583
+ block("migration-required", "State migration is required.");
584
+ return err("conflict", "State migration is required.");
585
+ }
586
+ if (outcome.status === "already-initialized" && localGeneration === captured.generation) {
587
+ base = outcome.current;
588
+ desired = { value: valueClone(codec, currentValue(base)) };
589
+ acknowledgedGeneration = captured.generation;
590
+ emit();
591
+ } else {
592
+ const result = await conflict("save-conflict", outcome.current, captured.base);
593
+ if (!result.ok)
594
+ return result;
595
+ }
596
+ }
597
+ const capturedEpoch = epoch;
598
+ const saved = await persist();
599
+ if (!saved.ok)
600
+ return saved;
601
+ if (active(capturedEpoch)) {
602
+ if (remotePending)
603
+ return reload("reconnect");
604
+ schedule();
605
+ }
606
+ if (editing && snapshot.status === "blocked" && snapshot.reason === "conflict")
607
+ return err("conflict", snapshot.message);
608
+ return ok(undefined);
609
+ }
610
+ function transmit(captured, automatic = false) {
611
+ const capturedEpoch = epoch;
612
+ const dispatchEpoch = automaticEpoch;
613
+ let suppressed = false;
614
+ const work = (async () => {
615
+ networkActive = true;
616
+ emit();
617
+ const persisted = await persist();
618
+ if (!active(capturedEpoch))
619
+ return err("cancelled", "State save was detached.");
620
+ if (!persisted.ok) {
621
+ pending = null;
622
+ networkActive = false;
623
+ emit();
624
+ return persisted;
625
+ }
626
+ if (automatic && (automaticSuspensions || automaticEpoch !== dispatchEpoch)) {
627
+ suppressed = true;
628
+ if (pending === captured)
629
+ pending = null;
630
+ networkActive = false;
631
+ emit();
632
+ const saved = await persist();
633
+ return saved.ok ? ok(undefined) : saved;
634
+ }
635
+ const valid = sessionCheck();
636
+ if (!valid.ok) {
637
+ networkActive = false;
638
+ return valid;
639
+ }
640
+ const response = await safe(() => options.transport.save(captured.mutation, { signal: abort.signal }));
641
+ if (!active(capturedEpoch) || pending !== captured)
642
+ return err("cancelled", "State save was detached.");
643
+ networkActive = false;
644
+ const live = sessionCheck();
645
+ if (!live.ok)
646
+ return live;
647
+ if (!response.ok) {
648
+ block("indeterminate", response.error.message);
649
+ return response;
650
+ }
651
+ if (response.value.operationId !== captured.mutation.operationId) {
652
+ block("indeterminate", "State operation identity changed.");
653
+ return err("incompatible", "State operation identity changed.");
654
+ }
655
+ if (response.value.status === "indeterminate") {
656
+ block("indeterminate", "State save outcome is unknown.");
657
+ return err("unavailable", "State save outcome is unknown.", "same-operation");
658
+ }
659
+ return applyOutcome(captured, response.value);
660
+ })();
661
+ activeSave = work;
662
+ work.then(() => {
663
+ if (activeSave === work)
664
+ activeSave = null;
665
+ if (suppressed)
666
+ schedule();
667
+ });
668
+ return work;
669
+ }
670
+ function sendNew(automatic = false) {
671
+ if (automatic && automaticSuspensions)
672
+ return Promise.resolve(ok(undefined));
673
+ const valid = sessionCheck();
674
+ if (!valid.ok)
675
+ return Promise.resolve(valid);
676
+ if (editing && lastRejection && localGeneration <= rejectedGeneration)
677
+ return Promise.resolve(err("invalid-input", "Correct the rejected draft before submitting a new operation."));
678
+ if (blocked && !(editing && blocked.reason === "rejected" && localGeneration > rejectedGeneration))
679
+ return Promise.resolve(err("conflict", blocked.message));
680
+ if (activeSave)
681
+ return activeSave;
682
+ if (pending)
683
+ return Promise.resolve(err("unavailable", "An earlier save needs reconciliation.", "same-operation"));
684
+ if (!base || !desired || !needsSave())
685
+ return Promise.resolve(ok(undefined));
686
+ const mutation = frozen({ kind: base.status === "empty" ? "initialize" : "replace", address, expectedVersion: { ...version(base) }, expected, ownerSession: options.ownerSession, localGeneration, operationId: newOperationId(), schemaVersion: definition.schemaVersion, value: valueClone(codec, desired.value) });
687
+ if (editing && blocked?.reason === "rejected")
688
+ blocked = null;
689
+ pending = { mutation, base, generation: localGeneration };
690
+ return transmit(pending, automatic);
691
+ }
692
+ let activeReconcile = null;
693
+ function reconcile(allowReplay) {
694
+ if (activeReconcile)
695
+ return activeReconcile;
696
+ const work = performReconcile(allowReplay);
697
+ activeReconcile = work;
698
+ const clear = () => {
699
+ if (activeReconcile === work)
700
+ activeReconcile = null;
701
+ };
702
+ work.then(clear, clear);
703
+ return work;
704
+ }
705
+ async function performReconcile(allowReplay) {
706
+ const captured = pending;
707
+ if (!captured)
708
+ return ok(undefined);
709
+ const capturedEpoch = epoch;
710
+ const valid = sessionCheck();
711
+ if (!valid.ok)
712
+ return valid;
713
+ const result = await safe(() => options.transport.reconcile({ address, expected, operationId: captured.mutation.operationId, mutation: captured.mutation }, { signal: abort.signal }));
714
+ if (!active(capturedEpoch) || pending !== captured)
715
+ return err("cancelled", "State reconciliation was superseded.");
716
+ const live = sessionCheck();
717
+ if (!live.ok)
718
+ return live;
719
+ if (!result.ok) {
720
+ block("indeterminate", result.error.message);
721
+ return result;
722
+ }
723
+ if (result.value.status === "final")
724
+ return applyOutcome(captured, result.value.outcome);
725
+ if (result.value.status === "absent-final" && allowReplay && !editing) {
726
+ if (captured.mutation.expected.actor !== expected.actor || captured.mutation.expected.session !== expected.session) {
727
+ block("draft-recovery", "The original operation cannot be re-authored under a new session.");
728
+ return err("stale-context", "The original operation requires its original session.");
729
+ }
730
+ blocked = null;
731
+ return transmit(captured);
732
+ }
733
+ block("indeterminate", "The previous save remains unresolved; no replacement was sent.");
734
+ return err("unavailable", "The previous save remains unresolved.", "same-operation");
735
+ }
736
+ async function performFlush() {
737
+ clearTimer();
738
+ const valid = sessionCheck();
739
+ if (!valid.ok)
740
+ return valid;
741
+ if (editing) {
742
+ if (activeSave)
743
+ return activeSave;
744
+ if (pending)
745
+ return reconcile(false);
746
+ return sendNew();
747
+ }
748
+ const goal = localGeneration;
749
+ let retries = 0;
750
+ if (activeSave) {
751
+ const result = await activeSave;
752
+ if (!result.ok)
753
+ return result;
754
+ }
755
+ while (active()) {
756
+ if (pending) {
757
+ const result2 = await reconcile(retries++ < retryLimit);
758
+ if (!result2.ok)
759
+ return result2;
760
+ }
761
+ if (blocked)
762
+ return err("conflict", blocked.message);
763
+ if (!needsSave() || acknowledgedGeneration >= goal)
764
+ return ok(undefined);
765
+ const result = await sendNew();
766
+ if (!result.ok)
767
+ return result;
768
+ }
769
+ return err("disposed", "The state controller is detached.");
770
+ }
771
+ async function flush() {
772
+ let cancel = () => {};
773
+ const timeout = new Promise((resolve) => {
774
+ cancel = scheduler.schedule(30000, () => {
775
+ if (active())
776
+ block("indeterminate", "Flush timed out; pending intent is retained.");
777
+ resolve(err("unavailable", "Flush timed out; pending intent is retained.", "same-operation"));
778
+ });
779
+ });
780
+ try {
781
+ return await Promise.race([performFlush(), timeout]);
782
+ } finally {
783
+ cancel();
784
+ }
785
+ }
786
+ function detach() {
787
+ if (lifecycle === "detached" || lifecycle === "disposed")
788
+ return draft();
789
+ lifecycle = "detached";
790
+ epoch++;
791
+ loadId++;
792
+ clearTimer();
793
+ abort.abort();
794
+ subscription?.();
795
+ subscription = null;
796
+ emit();
797
+ return draft();
798
+ }
799
+ return {
800
+ getSnapshot: () => snapshot,
801
+ inspect() {
802
+ const retained = draft();
803
+ const checkpointMatches = checkpoint !== null && retained !== null && checkpoint.draft.localGeneration === retained.localGeneration && JSON.stringify(checkpoint.draft.inFlight ? codecs.mutation.encode(checkpoint.draft.inFlight) : null) === JSON.stringify(retained.inFlight ? codecs.mutation.encode(retained.inFlight) : null) && JSON.stringify(checkpoint.draft.rejection ?? null) === JSON.stringify(retained.rejection ?? null) && JSON.stringify(codecs.read.encode(checkpoint.draft.acknowledged)) === JSON.stringify(codecs.read.encode(retained.acknowledged));
804
+ return {
805
+ draft: retained,
806
+ base,
807
+ desired: desired?.value,
808
+ generation: localGeneration,
809
+ pending: pending?.mutation ?? null,
810
+ saving: networkActive,
811
+ dirty: dirty(),
812
+ saveEligible: needsSave() && (!lastRejection || localGeneration > rejectedGeneration),
813
+ editable: active() && !closing && !ownerFenced && !loadFailed && base !== null && desired !== null && (!blocked || ["conflict", "indeterminate", "rejected", "draft-recovery"].includes(blocked.reason)),
814
+ checkpoint: {
815
+ status: !drafts ? "disabled" : checkpointWriting ? "writing" : checkpointError ? "failed" : checkpointMatches ? "backed-up" : "local-only",
816
+ generation: checkpoint?.draft.localGeneration ?? null,
817
+ revision: checkpoint?.revision ?? null,
818
+ error: checkpointError
819
+ }
820
+ };
821
+ },
822
+ subscribe(listener) {
823
+ if (lifecycle === "disposed")
824
+ return () => {};
825
+ listeners.add(listener);
826
+ return () => listeners.delete(listener);
827
+ },
828
+ start() {
829
+ if (lifecycle === "detached" || lifecycle === "disposed")
830
+ return Promise.resolve(err("disposed", "Create a new state controller after detach."));
831
+ if (startWork)
832
+ return startWork;
833
+ lifecycle = "active";
834
+ subscription = options.transport.subscribe(address, (event) => {
835
+ if (!active() || stateAddressKey(event.address) !== stateAddressKey(address))
836
+ return;
837
+ if (base && event.version.epoch === version(base).epoch && event.version.sequence <= version(base).sequence)
838
+ return;
839
+ if (remoteHint && event.version.epoch === remoteHint.epoch && event.version.sequence <= remoteHint.sequence)
840
+ return;
841
+ remoteHint = { ...event.version };
842
+ remotePending = true;
843
+ remoteSerial++;
844
+ if (blocked?.conflict)
845
+ block("conflict", "Refreshing the remote state before reviewing this conflict.");
846
+ else
847
+ emit();
848
+ queueRemote();
849
+ });
850
+ startWork = reload("dirty-load");
851
+ return startWork;
852
+ },
853
+ edit(value) {
854
+ const valid = sessionCheck();
855
+ if (!valid.ok)
856
+ return valid;
857
+ if (closing || (editing ? ownerFenced || loadFailed || !base || !desired || !!blocked && !["conflict", "indeterminate", "rejected", "draft-recovery"].includes(blocked.reason) : blocked || snapshot.status !== "ready"))
858
+ return err("unavailable", "State is not editable.");
859
+ try {
860
+ desired = { value: valueClone(codec, value) };
861
+ } catch (error) {
862
+ return err("invalid-input", error instanceof Error ? error.message : "Invalid state.");
863
+ }
864
+ localGeneration++;
865
+ if (editing && blocked?.conflict)
866
+ blocked = { ...blocked, conflict: frozen({ ...blocked.conflict, token: newConflictToken(), local: desired.value }) };
867
+ emit();
868
+ schedule();
869
+ return ok(undefined);
870
+ },
871
+ suspendAutomaticDispatch() {
872
+ automaticSuspensions++;
873
+ automaticEpoch++;
874
+ clearTimer();
875
+ let released = false;
876
+ return () => {
877
+ if (released)
878
+ return;
879
+ released = true;
880
+ automaticSuspensions--;
881
+ if (!automaticSuspensions)
882
+ schedule();
883
+ };
884
+ },
885
+ flush,
886
+ async reconnect(input) {
887
+ const valid = sessionCheck();
888
+ if (!valid.ok)
889
+ return valid;
890
+ const existing = activeSave ?? activeReconcile ?? activeLoad;
891
+ if (input?.reuseInFlight && existing)
892
+ return existing;
893
+ if (activeSave) {
894
+ const settled = await activeSave;
895
+ if (!settled.ok && !pending)
896
+ return settled;
897
+ }
898
+ if (pending) {
899
+ const result = await reconcile(false);
900
+ if (!result.ok)
901
+ return result;
902
+ }
903
+ return reload("reconnect");
904
+ },
905
+ async resolveConflict(token, decision) {
906
+ const valid = sessionCheck();
907
+ if (!valid.ok)
908
+ return valid;
909
+ const context = blocked?.conflict;
910
+ if (!context || context.token !== token)
911
+ return err("stale-context", "Conflict decision was superseded.");
912
+ return applyDecision(context, decision);
913
+ },
914
+ async recover(candidate) {
915
+ if (!drafts)
916
+ return err("unsupported", "Draft recovery is disabled for this state.");
917
+ const valid = sessionCheck();
918
+ if (!valid.ok)
919
+ return valid;
920
+ if (activeSave || pending || dirty())
921
+ return err("conflict", "Resolve current edits before recovering another draft.");
922
+ const source = candidate.draft;
923
+ if (source.formatVersion !== 1 || source.key.actor !== expected.actor || source.schemaVersion !== definition.schemaVersion || stateAddressKey(source.key.address) !== stateAddressKey(address))
924
+ return err("invalid-input", "Draft does not match this resource and actor.");
925
+ const parsed = readClone(source.acknowledged);
926
+ if (!parsed.ok || parsed.value.status === "migration-required")
927
+ return err("invalid-input", "Draft base is invalid.");
928
+ let mutation = null;
929
+ if (source.inFlight) {
930
+ const result = codecs.mutation.decode(structuredClone(codecs.mutation.encode(source.inFlight)));
931
+ if (!result.ok || stateAddressKey(result.value.address) !== stateAddressKey(address) || result.value.schemaVersion !== definition.schemaVersion || result.value.expected.actor !== expected.actor || result.value.localGeneration > source.localGeneration)
932
+ return err("invalid-input", "Draft operation does not match its owner or generation.");
933
+ mutation = frozen(result.value);
934
+ }
935
+ if (editing) {
936
+ lastRejection = source.rejection ?? null;
937
+ rejectedGeneration = source.rejectedGeneration ?? -1;
938
+ }
939
+ clearTimer();
940
+ base = parsed.value;
941
+ desired = { value: valueClone(codec, source.desired) };
942
+ localGeneration = source.localGeneration;
943
+ recoverySource = frozen(structuredClone(candidate));
944
+ checkpoint = source.key.ownerSession === options.ownerSession ? recoverySource : null;
945
+ if (mutation) {
946
+ pending = { mutation, base, generation: mutation.localGeneration };
947
+ block("draft-recovery", "Reconciling the original operation.");
948
+ const result = await reconcile(false);
949
+ if (!result.ok)
950
+ return result;
951
+ }
952
+ const capturedEpoch = epoch;
953
+ const loaded = await reload("draft-recovery");
954
+ if (!loaded.ok || !active(capturedEpoch))
955
+ return loaded;
956
+ return ok(undefined);
957
+ },
958
+ async discardRecovery(candidate) {
959
+ if (!drafts)
960
+ return err("unsupported", "Draft recovery is disabled for this state.");
961
+ if (!active())
962
+ return err("disposed", "This state controller is no longer active.");
963
+ const source = candidate.draft;
964
+ if (source.formatVersion !== 1 || source.key.actor !== expected.actor || source.schemaVersion !== definition.schemaVersion || stateAddressKey(source.key.address) !== stateAddressKey(address)) {
965
+ return err("invalid-input", "Draft does not match this resource and actor.");
966
+ }
967
+ const removed = await safe(() => drafts.remove(source.key, candidate.revision));
968
+ if (!removed.ok || !active())
969
+ return removed.ok ? err("disposed", "This state controller is no longer active.") : removed;
970
+ const sameCheckpoint = (entry) => entry !== null && entry.revision === candidate.revision && entry.draft.key.actor === source.key.actor && entry.draft.key.ownerSession === source.key.ownerSession && stateAddressKey(entry.draft.key.address) === stateAddressKey(source.key.address);
971
+ if (sameCheckpoint(checkpoint))
972
+ checkpoint = null;
973
+ if (sameCheckpoint(recoverySource))
974
+ recoverySource = null;
975
+ return ok(undefined);
976
+ },
977
+ async recoveryCandidates(readOptions) {
978
+ if (readOptions?.signal?.aborted)
979
+ return err("cancelled", "Recovery lookup was cancelled.");
980
+ if (!drafts)
981
+ return ok([]);
982
+ const result = await safe(() => drafts.find(address, expected.actor, 20));
983
+ if (readOptions?.signal?.aborted)
984
+ return err("cancelled", "Recovery lookup was cancelled.");
985
+ return result;
986
+ },
987
+ checkpoint: () => persist(),
988
+ detach,
989
+ async close(input) {
990
+ if (!drafts) {
991
+ detach();
992
+ return ok(undefined);
993
+ }
994
+ if (input.pending === "flush") {
995
+ closing = true;
996
+ const result = await flush();
997
+ const retained2 = detach();
998
+ if (!result.ok) {
999
+ await persist(retained2);
1000
+ return result;
1001
+ }
1002
+ return ok(undefined);
1003
+ }
1004
+ const retained = detach();
1005
+ if (input.pending === "preserve") {
1006
+ const result = await persist(retained);
1007
+ return result.ok ? ok(undefined) : result;
1008
+ }
1009
+ await persistQueue;
1010
+ const candidates = new Map;
1011
+ for (const entry of [checkpoint, recoverySource])
1012
+ if (entry)
1013
+ candidates.set(JSON.stringify(entry.draft.key), entry);
1014
+ for (const entry of candidates.values()) {
1015
+ const removed = await safe(() => drafts.remove(entry.draft.key, entry.revision));
1016
+ if (!removed.ok)
1017
+ return removed;
1018
+ }
1019
+ return ok(undefined);
1020
+ },
1021
+ dispose() {
1022
+ detach();
1023
+ lifecycle = "disposed";
1024
+ listeners.clear();
1025
+ }
1026
+ };
1027
+ }
1028
+
1029
+ // rpc-routes-runtime.ts
1030
+ var identityRoutes = Object.freeze({
1031
+ bootstrap: "bb-identity/v1/bootstrap",
1032
+ selfProfile: "bb-identity/v1/selfProfile",
1033
+ search: "bb-identity/v1/search",
1034
+ profiles: "bb-identity/v1/profiles",
1035
+ participants: "bb-identity/v1/participants",
1036
+ participantPreviews: "bb-identity/v1/participantPreviews"
1037
+ });
1038
+ var identityStateRoutes = Object.freeze({
1039
+ load: "bb-identity/v1/state/load",
1040
+ save: "bb-identity/v1/state/save",
1041
+ reconcile: "bb-identity/v1/state/reconcile"
1042
+ });
1043
+ var identityStateRpcMethods = Object.freeze({
1044
+ [identityStateRoutes.load]: "bb-identity.v1.state.load",
1045
+ [identityStateRoutes.save]: "bb-identity.v1.state.save",
1046
+ [identityStateRoutes.reconcile]: "bb-identity.v1.state.reconcile"
1047
+ });
1048
+ var identityRpcMethods = Object.freeze({
1049
+ [identityRoutes.bootstrap]: "bb-identity.v1.bootstrap",
1050
+ [identityRoutes.selfProfile]: "bb-identity.v1.selfProfile",
1051
+ [identityRoutes.search]: "bb-identity.v1.search",
1052
+ [identityRoutes.profiles]: "bb-identity.v1.profiles",
1053
+ [identityRoutes.participants]: "bb-identity.v1.participants",
1054
+ [identityRoutes.participantPreviews]: "bb-identity.v1.participantPreviews",
1055
+ ...identityStateRpcMethods
1056
+ });
1057
+
1058
+ // client-state-binding-runtime.ts
1059
+ var waiting = Object.freeze({ status: "waiting-for-identity" });
1060
+ function bindOwnedIdentityController(options, retirementOwner) {
1061
+ let disposed = false;
1062
+ let viewDisposed = false;
1063
+ let current = null;
1064
+ let pause = null;
1065
+ let snapshot = waiting;
1066
+ let stateHealthy = options.client.connection.getHealth().state.status === "healthy";
1067
+ const listeners = new Set;
1068
+ const retirements = new Map;
1069
+ const publish = () => {
1070
+ for (const listener of [...listeners]) {
1071
+ try {
1072
+ listener();
1073
+ } catch {}
1074
+ }
1075
+ };
1076
+ const hasPending = options.hasPending;
1077
+ const live = () => {
1078
+ if (disposed || viewDisposed)
1079
+ return null;
1080
+ const result = options.client.currentSession();
1081
+ if (!result.ok || result.value.status !== "ready")
1082
+ return null;
1083
+ const session = result.value;
1084
+ const view = options.view.getSnapshot();
1085
+ if (options.target === "viewed-subject" && (view.status !== "ready" || view.session.instanceId !== session.instanceId || view.session.stamp !== session.stamp || view.session.actor.identity.key !== session.actor.identity.key))
1086
+ return null;
1087
+ const subject = options.target === "viewed-subject" && view.status === "ready" ? view.subject : session.actor.identity;
1088
+ return {
1089
+ session,
1090
+ subject,
1091
+ key: JSON.stringify([session.instanceId, session.actor.identity.key, session.stamp, subject.key]),
1092
+ writable: options.editPolicy === "collaborators" || subject.key === session.actor.identity.key
1093
+ };
1094
+ };
1095
+ const valid = (entry) => !disposed && current === entry && live()?.key === entry.key;
1096
+ const failedPreservation = (draft, error) => {
1097
+ if (draft) {
1098
+ try {
1099
+ options.onUnpersistedDraft?.(draft, error);
1100
+ } catch {}
1101
+ }
1102
+ };
1103
+ const retire = (reason, pending = "preserve") => {
1104
+ const entry = current;
1105
+ if (!entry)
1106
+ return;
1107
+ current = null;
1108
+ pause = null;
1109
+ entry.stop();
1110
+ const preserve = hasPending(entry.controller.getSnapshot());
1111
+ const draft = entry.controller.detach(reason);
1112
+ snapshot = entry.controller.getSnapshot();
1113
+ if (!preserve && pending !== "discard") {
1114
+ entry.controller.dispose();
1115
+ publish();
1116
+ return;
1117
+ }
1118
+ const group = retirements.get(entry.recoveryKey) ?? new Set;
1119
+ retirements.set(entry.recoveryKey, group);
1120
+ const task = Promise.resolve().then(() => entry.controller.close({ pending })).then((result) => {
1121
+ if (!result.ok)
1122
+ failedPreservation(draft, result.error);
1123
+ return result;
1124
+ }, () => {
1125
+ const failure2 = err("unavailable", "Draft preservation failed.", "after-refresh");
1126
+ if (!failure2.ok)
1127
+ failedPreservation(draft, failure2.error);
1128
+ return failure2;
1129
+ }).finally(() => {
1130
+ entry.controller.dispose();
1131
+ group.delete(task);
1132
+ if (!group.size)
1133
+ retirements.delete(entry.recoveryKey);
1134
+ if (!disposed)
1135
+ publish();
1136
+ });
1137
+ group.add(task);
1138
+ if (entry.preservation)
1139
+ retirementOwner?.begin(entry.preservation.storage, entry.preservation.scope, task);
1140
+ publish();
1141
+ };
1142
+ let observing = false;
1143
+ let observeAgain = false;
1144
+ const observe = () => {
1145
+ if (disposed || viewDisposed)
1146
+ return;
1147
+ if (observing) {
1148
+ observeAgain = true;
1149
+ return;
1150
+ }
1151
+ observing = true;
1152
+ try {
1153
+ do {
1154
+ observeAgain = false;
1155
+ const next = live();
1156
+ if (!next) {
1157
+ retire("identity-invalidated");
1158
+ return;
1159
+ }
1160
+ if (current?.key === next.key)
1161
+ return;
1162
+ retire("identity-invalidated");
1163
+ if (live()?.key !== next.key) {
1164
+ observeAgain = true;
1165
+ continue;
1166
+ }
1167
+ const owner = idCodec("owner-session").decode(`owner-${newOperationId()}`);
1168
+ if (!owner.ok)
1169
+ throw new Error(owner.error.message);
1170
+ const controller = options.create({ session: next.session, subject: next.subject, ownerSession: owner.value });
1171
+ if (live()?.key !== next.key) {
1172
+ controller.dispose();
1173
+ observeAgain = true;
1174
+ continue;
1175
+ }
1176
+ const preservation = options.preservation?.(controller, next.session) ?? null;
1177
+ const entry = { key: next.key, recoveryKey: JSON.stringify([next.session.instanceId, next.session.actor.identity.key, next.subject.key]), owner: owner.value, controller, stop: () => {}, writable: next.writable, reconnect: null, preservation };
1178
+ current = entry;
1179
+ const update = () => {
1180
+ if (valid(entry)) {
1181
+ snapshot = controller.getSnapshot();
1182
+ publish();
1183
+ }
1184
+ };
1185
+ entry.stop = controller.subscribe(update);
1186
+ update();
1187
+ controller.start().then(update, update);
1188
+ } while (observeAgain && !disposed && !viewDisposed);
1189
+ } finally {
1190
+ observing = false;
1191
+ }
1192
+ };
1193
+ const command = async (owner, run, write = false) => {
1194
+ const entry = current;
1195
+ if (!entry || owner !== entry.owner || !valid(entry))
1196
+ return err("stale-context", "State controller ownership changed.");
1197
+ if (write && (!entry.writable || pause))
1198
+ return err("unsupported", "The selected state is not editable during this view or transition.");
1199
+ let result;
1200
+ try {
1201
+ result = await run(entry.controller);
1202
+ } catch {
1203
+ result = err("unavailable", "State operation failed.", "after-refresh");
1204
+ }
1205
+ return valid(entry) ? result : err("stale-context", "State controller ownership changed.");
1206
+ };
1207
+ const reconnect = (owner) => {
1208
+ const entry = current;
1209
+ if (entry === null) {
1210
+ return Promise.resolve(err("stale-context", "State controller ownership changed or is not writable."));
1211
+ }
1212
+ const eligible = () => current === entry && entry.owner === owner && valid(entry) && entry.writable && pause === null;
1213
+ if (!eligible()) {
1214
+ return Promise.resolve(err("stale-context", "State controller ownership changed or is not writable."));
1215
+ }
1216
+ if (entry.reconnect) {
1217
+ return entry.reconnect.then((result) => eligible() ? result : err("stale-context", "State controller ownership changed."));
1218
+ }
1219
+ const work = (async () => {
1220
+ if (!eligible())
1221
+ return err("stale-context", "State controller ownership changed or is not writable.");
1222
+ let verified;
1223
+ try {
1224
+ verified = await options.client.connection.revalidate();
1225
+ } catch {
1226
+ verified = err("unavailable", "State connection revalidation failed.", "after-refresh");
1227
+ }
1228
+ if (!eligible())
1229
+ return err("stale-context", "State controller ownership changed.");
1230
+ if (!verified.ok)
1231
+ return verified;
1232
+ let recovered;
1233
+ try {
1234
+ recovered = await entry.controller.reconnect();
1235
+ } catch {
1236
+ recovered = err("unavailable", "State reconciliation failed.", "after-refresh");
1237
+ }
1238
+ return eligible() ? recovered : err("stale-context", "State controller ownership changed.");
1239
+ })();
1240
+ entry.reconnect = work;
1241
+ work.finally(() => {
1242
+ if (entry.reconnect === work)
1243
+ entry.reconnect = null;
1244
+ });
1245
+ return work;
1246
+ };
1247
+ const guard = {
1248
+ invalidate(reason) {
1249
+ if (reason === "disposed")
1250
+ viewDisposed = true;
1251
+ retire("identity-invalidated");
1252
+ },
1253
+ inspect(transition) {
1254
+ const entry = current;
1255
+ const unaffected = options.target === "actor" || !entry;
1256
+ if (!unaffected && transition.pending === "block" && hasPending(entry.controller.getSnapshot()))
1257
+ return err("conflict", "Resolve or preserve pending edits before changing view.");
1258
+ const token = {};
1259
+ let committed = false;
1260
+ let resume = null;
1261
+ return ok({
1262
+ async prepare() {
1263
+ if (unaffected)
1264
+ return ok(undefined);
1265
+ if (!valid(entry) || transition.signal.aborted)
1266
+ return err("stale-context", "View transition expired.");
1267
+ pause = token;
1268
+ resume = entry.controller.suspendAutomaticDispatch?.() ?? (() => {});
1269
+ if (transition.pending === "block" && hasPending(entry.controller.getSnapshot()))
1270
+ return err("conflict", "New edits arrived before the view transition.");
1271
+ const result = transition.pending === "flush" ? await entry.controller.flush() : transition.pending === "preserve" ? await entry.controller.checkpoint() : ok(undefined);
1272
+ if (!result.ok)
1273
+ return result;
1274
+ return valid(entry) && !transition.signal.aborted ? ok(undefined) : err("stale-context", "View transition expired.");
1275
+ },
1276
+ commit() {
1277
+ committed = true;
1278
+ if (!unaffected && current === entry)
1279
+ retire("view-change", transition.pending === "discard" ? "discard" : "preserve");
1280
+ resume?.();
1281
+ resume = null;
1282
+ if (pause === token)
1283
+ pause = null;
1284
+ },
1285
+ cancel() {
1286
+ if (!committed && pause === token)
1287
+ pause = null;
1288
+ resume?.();
1289
+ resume = null;
1290
+ }
1291
+ });
1292
+ }
1293
+ };
1294
+ const stopGuard = options.view.registerGuard(guard);
1295
+ const stopView = options.view.subscribe(observe);
1296
+ const stopClient = options.client.subscribe(observe);
1297
+ const stopHealth = options.client.connection.subscribeHealth(() => {
1298
+ const healthy = options.client.connection.getHealth().state.status === "healthy";
1299
+ const reconnect2 = healthy && !stateHealthy;
1300
+ stateHealthy = healthy;
1301
+ const entry = current;
1302
+ if (reconnect2 && entry && valid(entry))
1303
+ entry.controller.reconnect({ reuseInFlight: true });
1304
+ });
1305
+ observe();
1306
+ return {
1307
+ getSnapshot: () => snapshot,
1308
+ subscribe(listener) {
1309
+ if (disposed)
1310
+ return () => {};
1311
+ listeners.add(listener);
1312
+ return () => listeners.delete(listener);
1313
+ },
1314
+ currentOwnerSession: () => current?.owner ?? null,
1315
+ edit(value) {
1316
+ const entry = current;
1317
+ if (!entry || !valid(entry))
1318
+ return err("stale-context", "State identity is not ready.");
1319
+ return entry.writable && !pause ? entry.controller.edit(value) : err("unsupported", "The selected state is read-only or changing view.");
1320
+ },
1321
+ flush() {
1322
+ return current ? command(current.owner, (controller) => controller.flush(), true) : Promise.resolve(err("stale-context", "State identity is not ready."));
1323
+ },
1324
+ reconnect(owner) {
1325
+ return reconnect(owner);
1326
+ },
1327
+ recoveryCandidates(owner, readOptions) {
1328
+ const entry = current;
1329
+ return command(owner, async (controller) => {
1330
+ if (!entry)
1331
+ return err("stale-context", "State controller ownership changed.");
1332
+ if (entry.preservation && retirementOwner) {
1333
+ const settled = await retirementOwner.wait(entry.preservation.storage, entry.preservation.scope, readOptions?.signal);
1334
+ if (!settled.ok)
1335
+ return settled;
1336
+ }
1337
+ while (retirements.get(entry.recoveryKey)?.size) {
1338
+ if (!valid(entry))
1339
+ return err("stale-context", "State controller ownership changed.");
1340
+ if (readOptions?.signal?.aborted)
1341
+ return err("cancelled", "Recovery lookup was cancelled.");
1342
+ const pending = Promise.all([...retirements.get(entry.recoveryKey)]);
1343
+ if (readOptions?.signal) {
1344
+ const signal = readOptions.signal;
1345
+ await new Promise((resolve) => {
1346
+ const done = () => {
1347
+ signal.removeEventListener("abort", done);
1348
+ resolve();
1349
+ };
1350
+ signal.addEventListener("abort", done, { once: true });
1351
+ pending.then(done);
1352
+ if (signal.aborted)
1353
+ done();
1354
+ });
1355
+ } else
1356
+ await pending;
1357
+ }
1358
+ if (!valid(entry))
1359
+ return err("stale-context", "State controller ownership changed.");
1360
+ return controller.recoveryCandidates(readOptions);
1361
+ });
1362
+ },
1363
+ checkpoint(owner) {
1364
+ return command(owner, (controller) => controller.checkpoint());
1365
+ },
1366
+ recover(owner, checkpoint) {
1367
+ return command(owner, (controller) => controller.recover(checkpoint), true);
1368
+ },
1369
+ discardRecovery(owner, checkpoint) {
1370
+ return command(owner, (controller) => controller.discardRecovery(checkpoint), true);
1371
+ },
1372
+ resolveConflict(owner, token, decision) {
1373
+ return command(owner, (controller) => controller.resolveConflict(token, decision), true);
1374
+ },
1375
+ dispose() {
1376
+ if (disposed)
1377
+ return;
1378
+ disposed = true;
1379
+ stopGuard();
1380
+ stopView();
1381
+ stopClient();
1382
+ stopHealth();
1383
+ retire("unmount");
1384
+ listeners.clear();
1385
+ }
1386
+ };
1387
+ }
1388
+
1389
+ // editing-codecs-runtime.ts
1390
+ var EDITING_PROTOCOL = "bb-identity.editing/v1";
1391
+ var EDITING_LIMITS = Object.freeze({ bytes: 4 * 1024 * 1024, diagnostics: 64, message: 4096, code: 128, path: 32 });
1392
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1393
+ function bounded(input) {
1394
+ try {
1395
+ return new TextEncoder().encode(JSON.stringify(input)).byteLength <= EDITING_LIMITS.bytes;
1396
+ } catch {
1397
+ return false;
1398
+ }
1399
+ }
1400
+ function keys(input, allowed) {
1401
+ return Object.keys(input).every((key) => allowed.includes(key));
1402
+ }
1403
+ function canonicalEditingJson(value) {
1404
+ if (value === null || typeof value !== "object")
1405
+ return JSON.stringify(value);
1406
+ if (Array.isArray(value))
1407
+ return `[${value.map(canonicalEditingJson).join(",")}]`;
1408
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalEditingJson(value[key])}`).join(",")}}`;
1409
+ }
1410
+ function editingCodecs(value, target) {
1411
+ const state = stateCodecs(value);
1412
+ const invalid = () => err("invalid-input", "Malformed editing/v1 data.");
1413
+ const scope = {
1414
+ decode(input) {
1415
+ if (!bounded(input) || !record(input) || !keys(input, ["draftAddress", "schemaVersion", "target", "expected"]) || !record(input.draftAddress) || !keys(input.draftAddress, ["instanceId", "pluginId", "collection", "recordId", "owner"]) || !record(input.expected) || !keys(input.expected, ["actor", "session"]) || !Number.isSafeInteger(input.schemaVersion) || Number(input.schemaVersion) < 0)
1416
+ return invalid();
1417
+ const address = input.draftAddress;
1418
+ if (typeof address.collection !== "string" || !address.collection.length || typeof address.recordId !== "string" || !address.recordId.length)
1419
+ return invalid();
1420
+ const instanceId = idCodec("instance").decode(address.instanceId);
1421
+ const pluginId = idCodec("plugin").decode(address.pluginId);
1422
+ const owner = identityKeyCodec.decode(address.owner);
1423
+ const actor = identityKeyCodec.decode(input.expected.actor);
1424
+ const session = idCodec("server-session").decode(input.expected.session);
1425
+ const selected = target.decode(input.target);
1426
+ if (!instanceId.ok || !pluginId.ok || !owner.ok || !actor.ok || !session.ok || !selected.ok || owner.value !== actor.value)
1427
+ return invalid();
1428
+ return ok({
1429
+ draftAddress: { instanceId: instanceId.value, pluginId: pluginId.value, collection: address.collection, recordId: address.recordId, owner: owner.value },
1430
+ schemaVersion: Number(input.schemaVersion),
1431
+ target: selected.value,
1432
+ expected: { actor: actor.value, session: session.value }
1433
+ });
1434
+ },
1435
+ encode(input) {
1436
+ return {
1437
+ draftAddress: {
1438
+ instanceId: idCodec("instance").encode(input.draftAddress.instanceId),
1439
+ pluginId: idCodec("plugin").encode(input.draftAddress.pluginId),
1440
+ collection: input.draftAddress.collection,
1441
+ recordId: input.draftAddress.recordId,
1442
+ owner: identityKeyCodec.encode(input.draftAddress.owner)
1443
+ },
1444
+ schemaVersion: input.schemaVersion,
1445
+ target: target.encode(input.target),
1446
+ expected: { actor: identityKeyCodec.encode(input.expected.actor), session: idCodec("server-session").encode(input.expected.session) }
1447
+ };
1448
+ }
1449
+ };
1450
+ const request = {
1451
+ decode(input) {
1452
+ if (!bounded(input) || !record(input) || !keys(input, ["protocol", "target", "mutation"]) || input.protocol !== EDITING_PROTOCOL)
1453
+ return invalid();
1454
+ if (!record(input.mutation) || !keys(input.mutation, ["kind", "address", "expectedVersion", "expected", "ownerSession", "localGeneration", "operationId", "schemaVersion", "value"]))
1455
+ return invalid();
1456
+ const selected = target.decode(input.target);
1457
+ const mutation = state.mutation.decode(input.mutation);
1458
+ if (!selected.ok || !mutation.ok || mutation.value.address.owner !== mutation.value.expected.actor)
1459
+ return invalid();
1460
+ return ok({ protocol: EDITING_PROTOCOL, target: selected.value, mutation: mutation.value });
1461
+ },
1462
+ encode(input) {
1463
+ return { protocol: input.protocol, target: target.encode(input.target), mutation: state.mutation.encode(input.mutation) };
1464
+ }
1465
+ };
1466
+ const receipt = {
1467
+ decode(input) {
1468
+ if (!bounded(input) || !record(input) || input.protocol !== EDITING_PROTOCOL)
1469
+ return invalid();
1470
+ const submitted = request.decode(input.request);
1471
+ if (!submitted.ok || !record(input.outcome))
1472
+ return invalid();
1473
+ const outcome = input.outcome;
1474
+ if (outcome.status === "rejected") {
1475
+ const operation = idCodec("operation").decode(outcome.operationId);
1476
+ if (!operation.ok || operation.value !== submitted.value.mutation.operationId || !Array.isArray(outcome.diagnostics) || !outcome.diagnostics.length || outcome.diagnostics.length > EDITING_LIMITS.diagnostics)
1477
+ return invalid();
1478
+ const diagnostics = [];
1479
+ for (const diagnostic of outcome.diagnostics) {
1480
+ if (!record(diagnostic) || typeof diagnostic.code !== "string" || !diagnostic.code.length || diagnostic.code.length > EDITING_LIMITS.code || typeof diagnostic.message !== "string" || !diagnostic.message.length || diagnostic.message.length > EDITING_LIMITS.message)
1481
+ return invalid();
1482
+ if (diagnostic.path !== undefined && (!Array.isArray(diagnostic.path) || diagnostic.path.length > EDITING_LIMITS.path || diagnostic.path.some((part) => typeof part !== "string" && !(typeof part === "number" && Number.isSafeInteger(part) && part >= 0)) || diagnostic.path.some((part) => typeof part === "string" && part.length > EDITING_LIMITS.message)))
1483
+ return invalid();
1484
+ diagnostics.push({ code: diagnostic.code, message: diagnostic.message, ...diagnostic.path !== undefined ? { path: diagnostic.path } : {} });
1485
+ }
1486
+ return ok({ protocol: EDITING_PROTOCOL, request: submitted.value, outcome: { status: "rejected", operationId: operation.value, diagnostics } });
1487
+ }
1488
+ const parsed = state.save.decode(outcome);
1489
+ if (!parsed.ok || parsed.value.status === "indeterminate" || parsed.value.operationId !== submitted.value.mutation.operationId)
1490
+ return invalid();
1491
+ const read = "envelope" in parsed.value ? { status: "present", envelope: parsed.value.envelope } : parsed.value.current;
1492
+ const address = read.status === "present" ? read.envelope.address : read.address;
1493
+ if (stateAddressKey(address) !== stateAddressKey(submitted.value.mutation.address) || read.status === "present" && read.envelope.schemaVersion !== submitted.value.mutation.schemaVersion)
1494
+ return invalid();
1495
+ return ok({ protocol: EDITING_PROTOCOL, request: submitted.value, outcome: parsed.value });
1496
+ },
1497
+ encode(input) {
1498
+ return { protocol: input.protocol, request: request.encode(input.request), outcome: input.outcome.status === "rejected" ? { status: "rejected", operationId: input.outcome.operationId, diagnostics: input.outcome.diagnostics } : state.save.encode(input.outcome) };
1499
+ }
1500
+ };
1501
+ const save = {
1502
+ decode(input) {
1503
+ if (record(input) && input.protocol === EDITING_PROTOCOL && input.status === "indeterminate") {
1504
+ const operation = idCodec("operation").decode(input.operationId);
1505
+ return operation.ok ? ok({ protocol: EDITING_PROTOCOL, status: "indeterminate", operationId: operation.value }) : invalid();
1506
+ }
1507
+ return receipt.decode(input);
1508
+ },
1509
+ encode(input) {
1510
+ return "status" in input ? { protocol: input.protocol, status: input.status, operationId: input.operationId } : receipt.encode(input);
1511
+ }
1512
+ };
1513
+ const lookup = {
1514
+ decode(input) {
1515
+ if (!bounded(input) || !record(input))
1516
+ return invalid();
1517
+ if (input.status === "final") {
1518
+ const parsed = receipt.decode(input.outcome);
1519
+ return parsed.ok ? ok({ status: "final", outcome: parsed.value }) : parsed;
1520
+ }
1521
+ if (input.status === "pending")
1522
+ return ok({ status: "pending" });
1523
+ if (input.status === "absent-final" && input.retry === "same-operation-only")
1524
+ return ok({ status: "absent-final", retry: "same-operation-only" });
1525
+ if (input.status === "unknown" && ["unsupported", "expired", "unavailable"].includes(String(input.reason)))
1526
+ return ok({ status: "unknown", reason: input.reason });
1527
+ return invalid();
1528
+ },
1529
+ encode(input) {
1530
+ return input.status === "final" ? { status: "final", outcome: receipt.encode(input.outcome) } : { ...input };
1531
+ }
1532
+ };
1533
+ const draft = {
1534
+ decode(input) {
1535
+ if (!bounded(input) || !record(input) || input.protocol !== EDITING_PROTOCOL || !record(input.draft))
1536
+ return invalid();
1537
+ const selected = target.decode(input.target);
1538
+ const raw = input.draft;
1539
+ if (!selected.ok || raw.formatVersion !== 1 || !record(raw.key) || !Number.isSafeInteger(raw.schemaVersion) || Number(raw.schemaVersion) < 0 || !Number.isSafeInteger(raw.localGeneration) || Number(raw.localGeneration) < 0)
1540
+ return invalid();
1541
+ const acknowledged = state.read.decode(raw.acknowledged);
1542
+ const desired = value.decode(raw.desired);
1543
+ const actor = identityKeyCodec.decode(raw.key.actor);
1544
+ const ownerSession = idCodec("owner-session").decode(raw.key.ownerSession);
1545
+ if (!acknowledged.ok || acknowledged.value.status === "migration-required" || !desired.ok || !actor.ok || !ownerSession.ok)
1546
+ return invalid();
1547
+ const readAddress = acknowledged.value.status === "present" ? acknowledged.value.envelope.address : acknowledged.value.address;
1548
+ if (!record(raw.key.address) || canonicalEditingJson(raw.key.address) !== canonicalEditingJson(readAddress) || readAddress.owner !== actor.value || acknowledged.value.status === "present" && acknowledged.value.envelope.schemaVersion !== raw.schemaVersion)
1549
+ return invalid();
1550
+ const inFlight = raw.inFlight === null ? ok(null) : state.mutation.decode(raw.inFlight);
1551
+ if (!inFlight.ok || inFlight.value && (stateAddressKey(inFlight.value.address) !== stateAddressKey(readAddress) || inFlight.value.expected.actor !== actor.value || inFlight.value.schemaVersion !== raw.schemaVersion || inFlight.value.localGeneration > Number(raw.localGeneration)))
1552
+ return invalid();
1553
+ const rejection = input.rejection === null ? ok(null) : receipt.decode(input.rejection);
1554
+ if (!rejection.ok || rejection.value && (rejection.value.outcome.status !== "rejected" || stateAddressKey(rejection.value.request.mutation.address) !== stateAddressKey(readAddress) || rejection.value.request.mutation.schemaVersion !== raw.schemaVersion || rejection.value.request.mutation.localGeneration > Number(raw.localGeneration) || inFlight.value !== null && (rejection.value.request.mutation.operationId === inFlight.value.operationId || rejection.value.request.mutation.localGeneration >= inFlight.value.localGeneration) || canonicalEditingJson(target.encode(rejection.value.request.target)) !== canonicalEditingJson(target.encode(selected.value))))
1555
+ return invalid();
1556
+ return ok({
1557
+ protocol: EDITING_PROTOCOL,
1558
+ target: selected.value,
1559
+ rejection: rejection.value,
1560
+ draft: {
1561
+ formatVersion: 1,
1562
+ key: { address: readAddress, actor: actor.value, ownerSession: ownerSession.value },
1563
+ schemaVersion: Number(raw.schemaVersion),
1564
+ acknowledged: acknowledged.value,
1565
+ desired: desired.value,
1566
+ localGeneration: Number(raw.localGeneration),
1567
+ inFlight: inFlight.value
1568
+ }
1569
+ });
1570
+ },
1571
+ encode(input) {
1572
+ const raw = input.draft;
1573
+ return {
1574
+ protocol: input.protocol,
1575
+ target: target.encode(input.target),
1576
+ rejection: input.rejection ? receipt.encode(input.rejection) : null,
1577
+ draft: {
1578
+ formatVersion: 1,
1579
+ key: raw.key,
1580
+ schemaVersion: raw.schemaVersion,
1581
+ acknowledged: state.read.encode(raw.acknowledged),
1582
+ desired: value.encode(raw.desired),
1583
+ localGeneration: raw.localGeneration,
1584
+ inFlight: raw.inFlight ? state.mutation.encode(raw.inFlight) : null
1585
+ }
1586
+ };
1587
+ }
1588
+ };
1589
+ return { scope, request, receipt, save, lookup, draft };
1590
+ }
1591
+
1592
+ // editing-runtime.ts
1593
+ function frozen2(value) {
1594
+ if (value && typeof value === "object") {
1595
+ for (const child of Object.values(value))
1596
+ frozen2(child);
1597
+ Object.freeze(value);
1598
+ }
1599
+ return value;
1600
+ }
1601
+ function clone(codec, value) {
1602
+ const result = codec.decode(structuredClone(codec.encode(value)));
1603
+ if (!result.ok)
1604
+ throw new TypeError(result.error.message);
1605
+ return frozen2(result.value);
1606
+ }
1607
+ function createIdentityEditingState(options) {
1608
+ if (options.transport.protocol !== EDITING_PROTOCOL)
1609
+ throw new TypeError("Editing transport requires explicit editing/v1 negotiation.");
1610
+ if (options.draftAddress.owner !== options.expected.actor)
1611
+ throw new TypeError("Draft ownership must be the actual actor, distinct from the shared target.");
1612
+ const target = clone(options.targetCodec, options.target);
1613
+ const scope = frozen2({ draftAddress: { ...options.draftAddress }, target, schemaVersion: options.definition.schemaVersion, expected: { ...options.expected } });
1614
+ const codecs = editingCodecs(options.definition.codec, options.targetCodec);
1615
+ const stateWire = stateCodecs(options.definition.codec);
1616
+ const sameTarget = (candidate) => canonicalEditingJson(options.targetCodec.encode(candidate)) === canonicalEditingJson(options.targetCodec.encode(target));
1617
+ const normalize = (codec, value) => {
1618
+ try {
1619
+ const parsed = codec.decode(structuredClone(codec.encode(value)));
1620
+ return parsed.ok ? ok(frozen2(parsed.value)) : parsed;
1621
+ } catch {
1622
+ return err("incompatible", "Editing response failed codec validation.");
1623
+ }
1624
+ };
1625
+ const request = (mutation) => clone(codecs.request, { protocol: EDITING_PROTOCOL, target, mutation });
1626
+ const checkReceipt = (receipt, submitted) => {
1627
+ if (canonicalEditingJson(codecs.request.encode(receipt.request)) !== canonicalEditingJson(codecs.request.encode(submitted)))
1628
+ return err("invalid-operation", "Editing receipt does not bind the exact immutable request.");
1629
+ return receipt.outcome.status === "rejected" ? ok({ status: "rejected", operationId: receipt.outcome.operationId, evidence: codecs.receipt.encode(receipt) }) : ok(receipt.outcome);
1630
+ };
1631
+ const transport = {
1632
+ async load(_input, readOptions) {
1633
+ const response = await options.transport.load(scope, readOptions);
1634
+ return response.ok ? normalize(stateWire.read, response.value) : response;
1635
+ },
1636
+ async save(mutation, readOptions) {
1637
+ const submitted = request(mutation);
1638
+ const response = await options.transport.save(submitted, readOptions);
1639
+ if (!response.ok)
1640
+ return response;
1641
+ const parsed = normalize(codecs.save, response.value);
1642
+ if (!parsed.ok)
1643
+ return parsed;
1644
+ if ("status" in parsed.value)
1645
+ return parsed.value.operationId === mutation.operationId ? ok({ status: "indeterminate", operationId: mutation.operationId }) : err("invalid-operation", "Unknown outcome names another operation.");
1646
+ return checkReceipt(parsed.value, submitted);
1647
+ },
1648
+ async reconcile(input, readOptions) {
1649
+ const submitted = request(input.mutation);
1650
+ const response = await options.transport.reconcile(submitted, readOptions);
1651
+ if (!response.ok)
1652
+ return response;
1653
+ const parsed = normalize(codecs.lookup, response.value);
1654
+ if (!parsed.ok)
1655
+ return parsed;
1656
+ if (parsed.value.status !== "final")
1657
+ return ok(parsed.value);
1658
+ const checked = checkReceipt(parsed.value.outcome, submitted);
1659
+ return checked.ok ? ok({ status: "final", outcome: checked.value }) : checked;
1660
+ },
1661
+ subscribe: (_address, listener) => options.transport.subscribe(scope, (event) => {
1662
+ const parsed = normalize(stateWire.invalidation, event);
1663
+ if (parsed.ok)
1664
+ listener(parsed.value);
1665
+ })
1666
+ };
1667
+ const toDraft = (draft) => {
1668
+ const rejection = draft.rejection ? codecs.receipt.decode(draft.rejection) : ok(null);
1669
+ if (!rejection.ok)
1670
+ throw new TypeError(rejection.error.message);
1671
+ return clone(codecs.draft, { protocol: EDITING_PROTOCOL, target, draft, rejection: rejection.value });
1672
+ };
1673
+ const fromDraft = (draft) => {
1674
+ const parsed = normalize(codecs.draft, draft);
1675
+ if (!parsed.ok)
1676
+ return parsed;
1677
+ if (!sameTarget(parsed.value.target) || stateAddressKey(parsed.value.draft.key.address) !== stateAddressKey(scope.draftAddress))
1678
+ return err("invalid-input", "Checkpoint belongs to another publication target or draft address.");
1679
+ return ok(frozen2({ ...parsed.value.draft, rejection: parsed.value.rejection ? codecs.receipt.encode(parsed.value.rejection) : null, rejectedGeneration: parsed.value.rejection?.request.mutation.localGeneration ?? -1 }));
1680
+ };
1681
+ const fromCheckpoint = (checkpoint) => {
1682
+ if (!Number.isSafeInteger(checkpoint.revision) || checkpoint.revision < 0)
1683
+ return err("invalid-input", "Invalid checkpoint revision.");
1684
+ const parsed = fromDraft(checkpoint.draft);
1685
+ return parsed.ok ? ok({ revision: checkpoint.revision, draft: parsed.value }) : parsed;
1686
+ };
1687
+ const toCheckpoint = (checkpoint) => frozen2({ revision: checkpoint.revision, draft: toDraft(checkpoint.draft) });
1688
+ const storage = options.drafts ? {
1689
+ async find(_address, _actor, limit) {
1690
+ const found = await options.drafts.find(scope, limit);
1691
+ if (!found.ok)
1692
+ return found;
1693
+ if (found.value.length > limit)
1694
+ return err("limit-exceeded", "Too many editing checkpoints.");
1695
+ const results = [];
1696
+ for (const entry of found.value) {
1697
+ const parsed = fromCheckpoint(entry);
1698
+ if (!parsed.ok)
1699
+ return parsed;
1700
+ results.push(parsed.value);
1701
+ }
1702
+ return ok(results);
1703
+ },
1704
+ async write(draft, revision) {
1705
+ const captured = toDraft(draft);
1706
+ const saved = await options.drafts.write(captured, revision);
1707
+ if (!saved.ok)
1708
+ return saved;
1709
+ if (saved.value.revision <= (revision ?? -1))
1710
+ return err("incompatible", "Checkpoint storage did not advance its conditional revision.");
1711
+ const parsed = fromCheckpoint(saved.value);
1712
+ if (!parsed.ok)
1713
+ return parsed;
1714
+ if (canonicalEditingJson(codecs.draft.encode(saved.value.draft)) !== canonicalEditingJson(codecs.draft.encode(captured)))
1715
+ return err("incompatible", "Checkpoint storage returned different draft content.");
1716
+ return parsed;
1717
+ },
1718
+ remove(key, revision) {
1719
+ return options.drafts.remove({ target, draftKey: key }, revision);
1720
+ }
1721
+ } : undefined;
1722
+ const core = createStateController({
1723
+ address: scope.draftAddress,
1724
+ expected: scope.expected,
1725
+ ownerSession: options.ownerSession,
1726
+ definition: options.definition,
1727
+ transport,
1728
+ ...storage ? { drafts: storage } : {},
1729
+ ...options.scheduler ? { scheduler: options.scheduler } : {},
1730
+ retryLimit: 0,
1731
+ onConflict: options.onConflict,
1732
+ initializeEmpty: options.initializeEmpty,
1733
+ currentSession: options.currentSession
1734
+ }, true);
1735
+ let snapshot;
1736
+ const update = () => {
1737
+ const state = core.getSnapshot();
1738
+ const detail = core.inspect();
1739
+ const blocked = state.status === "blocked" ? state : null;
1740
+ const status = state.status === "detached" ? "detached" : state.status === "loading" ? "loading" : detail.editable ? "loaded" : "fenced";
1741
+ const retained = detail.draft ? toDraft(detail.draft) : null;
1742
+ const reason = blocked?.reason;
1743
+ const publication = status === "fenced" || status === "detached" ? "fenced" : detail.saving ? "saving" : detail.pending ? "unknown" : reason === "conflict" ? "conflict" : reason === "rejected" || retained?.rejection ? "rejected" : "idle";
1744
+ snapshot = frozen2({
1745
+ protocol: EDITING_PROTOCOL,
1746
+ status,
1747
+ scope,
1748
+ ownerSession: options.ownerSession,
1749
+ desired: detail.desired,
1750
+ localGeneration: detail.generation,
1751
+ acknowledged: detail.base,
1752
+ dirty: detail.dirty,
1753
+ editable: detail.editable,
1754
+ submitted: detail.pending ? request(detail.pending) : null,
1755
+ publication: {
1756
+ eligible: detail.saveEligible && detail.editable && !detail.pending && !detail.saving && reason !== "conflict" && detail.checkpoint.status !== "failed",
1757
+ status: publication,
1758
+ message: blocked?.message ?? null,
1759
+ rejection: retained?.rejection ?? null,
1760
+ conflict: blocked?.conflict ?? null
1761
+ },
1762
+ checkpoint: detail.checkpoint,
1763
+ draft: retained
1764
+ });
1765
+ };
1766
+ update();
1767
+ const stop = core.subscribe(update);
1768
+ return {
1769
+ draftStorage: options.drafts ?? null,
1770
+ getSnapshot: () => snapshot,
1771
+ subscribe: (listener) => core.subscribe(listener),
1772
+ start: () => core.start(),
1773
+ edit: (value) => core.edit(value),
1774
+ flush: () => core.flush(),
1775
+ reconnect: (input) => core.reconnect(input),
1776
+ resolveConflict: (token, decision) => core.resolveConflict(token, decision),
1777
+ async checkpoint() {
1778
+ const saved = await core.checkpoint();
1779
+ return saved.ok ? ok(saved.value ? toCheckpoint(saved.value) : null) : saved;
1780
+ },
1781
+ async recoveryCandidates(readOptions) {
1782
+ const found = await core.recoveryCandidates(readOptions);
1783
+ return found.ok ? ok(found.value.map(toCheckpoint)) : found;
1784
+ },
1785
+ recover(candidate) {
1786
+ const parsed = fromCheckpoint(candidate);
1787
+ return parsed.ok ? core.recover(parsed.value) : Promise.resolve(parsed);
1788
+ },
1789
+ discardRecovery(candidate) {
1790
+ const parsed = fromCheckpoint(candidate);
1791
+ return parsed.ok ? core.discardRecovery(parsed.value) : Promise.resolve(parsed);
1792
+ },
1793
+ detach(reason) {
1794
+ const retained = core.detach(reason);
1795
+ return retained ? toDraft(retained) : null;
1796
+ },
1797
+ close: (input) => core.close(input),
1798
+ dispose() {
1799
+ core.dispose();
1800
+ stop();
1801
+ }
1802
+ };
1803
+ }
1804
+ function bindIdentityEditingState(options, retirementOwner) {
1805
+ if (!options.resourceKey || options.resourceKey.length > 1024)
1806
+ throw new TypeError("Editing binding requires a bounded semantic resource key.");
1807
+ const target = clone(options.targetCodec, options.target);
1808
+ const targetKey = canonicalEditingJson(options.targetCodec.encode(target));
1809
+ let currentController = null;
1810
+ return bindOwnedIdentityController({
1811
+ client: options.client,
1812
+ view: options.view,
1813
+ target: "actor",
1814
+ editPolicy: "actor-only",
1815
+ ...options.onUnpersistedDraft ? { onUnpersistedDraft: options.onUnpersistedDraft } : {},
1816
+ hasPending: (state) => state.dirty || state.submitted !== null || state.checkpoint.status === "failed" || state.publication.rejection !== null,
1817
+ create(input) {
1818
+ const controller = options.create(input);
1819
+ const state = controller.getSnapshot();
1820
+ if (canonicalEditingJson(options.targetCodec.encode(state.scope.target)) !== targetKey || state.scope.expected.actor !== input.session.actor.identity.key || state.scope.expected.session !== input.session.stamp || state.scope.draftAddress.owner !== input.session.actor.identity.key || state.ownerSession !== input.ownerSession) {
1821
+ controller.dispose();
1822
+ throw new TypeError("Editing binding factory returned another target or owner.");
1823
+ }
1824
+ currentController = controller;
1825
+ return controller;
1826
+ },
1827
+ preservation(controller, session) {
1828
+ const actual = currentController;
1829
+ const state = controller.getSnapshot();
1830
+ return actual?.draftStorage ? { storage: actual.draftStorage, scope: {
1831
+ address: state.scope.draftAddress,
1832
+ actor: session.actor.identity.key,
1833
+ schemaVersion: state.scope.schemaVersion,
1834
+ targetKey
1835
+ } } : null;
1836
+ }
1837
+ }, retirementOwner);
1838
+ }
1839
+ export {
1840
+ editingCodecs,
1841
+ createIdentityEditingState,
1842
+ bindIdentityEditingState,
1843
+ EDITING_PROTOCOL,
1844
+ EDITING_LIMITS
1845
+ };