@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.
@@ -1,4 +1,27 @@
1
1
  // @bun
2
+ // external-person-output-runtime.ts
3
+ function record(value) {
4
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+ function normalizeExternalPersonOutput(value) {
7
+ if (!record(value) || !record(value.identity) || !record(value.presentation))
8
+ return value;
9
+ return {
10
+ identity: { kind: value.identity.kind, key: value.identity.key, issuer: value.identity.issuer, subject: value.identity.subject },
11
+ presentation: { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl },
12
+ evidence: value.evidence
13
+ };
14
+ }
15
+ function normalizeContributionOutput(value) {
16
+ return record(value) && value.personAttribution !== undefined ? { ...value, personAttribution: normalizeExternalPersonOutput(value.personAttribution) } : value;
17
+ }
18
+ function normalizeProvenanceOutput(value) {
19
+ return record(value) && Array.isArray(value.contributions) ? { ...value, contributions: value.contributions.map(normalizeContributionOutput) } : value;
20
+ }
21
+ function normalizeEvidencePageOutput(value, normalize) {
22
+ return record(value) && Array.isArray(value.items) ? { ...value, items: value.items.map(normalize) } : value;
23
+ }
24
+
2
25
  // model-runtime.ts
3
26
  var ID_MAX_LENGTH = 256;
4
27
  var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
@@ -165,6 +188,7 @@ function hasCompleteCausality(groups, contributions, missing) {
165
188
  }
166
189
  var provenanceCodec = {
167
190
  decode(input) {
191
+ input = normalizeProvenanceOutput(input);
168
192
  if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
169
193
  return err("invalid-input", "Invalid execution provenance.");
170
194
  }
@@ -470,11 +494,11 @@ function createEnhancedDirectory(protocol) {
470
494
  }
471
495
  mappings.set(key, mapping);
472
496
  };
473
- const profile = (source, record, revision) => {
474
- if (!isRecord2(record) || !source.issuers.includes(record.issuer) || !idCodec("issuer").decode(record.issuer).ok || !idCodec("subject").decode(record.subject).ok || !isProfilePresentation(record.presentation) || record.status !== "current" && record.status !== "historical" || !idCodec("revision").decode(revision).ok)
497
+ const profile = (source, record2, revision) => {
498
+ if (!isRecord2(record2) || !source.issuers.includes(record2.issuer) || !idCodec("issuer").decode(record2.issuer).ok || !idCodec("subject").decode(record2.subject).ok || !isProfilePresentation(record2.presentation) || record2.status !== "current" && record2.status !== "historical" || !idCodec("revision").decode(revision).ok)
475
499
  return err("incompatible", "Provider directory record is malformed.");
476
- const issuer = record.issuer;
477
- const subject = record.subject;
500
+ const issuer = record2.issuer;
501
+ const subject = record2.subject;
478
502
  if (typeof issuer !== "string" || typeof subject !== "string")
479
503
  return err("incompatible", "Provider directory record is malformed.");
480
504
  let person;
@@ -485,10 +509,10 @@ function createEnhancedDirectory(protocol) {
485
509
  }
486
510
  if (!person.ok)
487
511
  return person;
488
- if (person.value.kind !== "person" || person.value.issuer !== record.issuer || person.value.subject !== record.subject || !identityKeyCodec.decode(person.value.key).ok) {
512
+ if (person.value.kind !== "person" || person.value.issuer !== record2.issuer || person.value.subject !== record2.subject || !identityKeyCodec.decode(person.value.key).ok) {
489
513
  return err("incompatible", "Provider directory returned an invalid person reference.");
490
514
  }
491
- return profileCodec.decode({ identity: person.value, presentation: record.presentation, revision, status: record.status });
515
+ return profileCodec.decode({ identity: person.value, presentation: record2.presentation, revision, status: record2.status });
492
516
  };
493
517
  return {
494
518
  async search(input, options) {
@@ -514,8 +538,8 @@ function createEnhancedDirectory(protocol) {
514
538
  if (!Array.isArray(result.value.records) || result.value.records.length > input.limit || result.value.nextCursor !== null && !idCodec("cursor").decode(result.value.nextCursor).ok || !idCodec("revision").decode(result.value.revision).ok)
515
539
  return err("incompatible", "Provider directory page is malformed.");
516
540
  const profiles = [];
517
- for (const record of result.value.records) {
518
- const decoded = profile(current.value, record, result.value.revision);
541
+ for (const record2 of result.value.records) {
542
+ const decoded = profile(current.value, record2, result.value.revision);
519
543
  if (!decoded.ok)
520
544
  return decoded;
521
545
  if (input.history === "current" && decoded.value.status !== "current")
@@ -824,7 +848,7 @@ function createHostAdapter(options) {
824
848
  } catch {
825
849
  return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
826
850
  }
827
- return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
851
+ return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeContributionOutput), isContribution2) : result;
828
852
  },
829
853
  async attempts(query, readOptions) {
830
854
  if (!validAttemptQuery(query))
@@ -837,7 +861,7 @@ function createHostAdapter(options) {
837
861
  } catch {
838
862
  return err("unavailable", "Execution history is unavailable.", "after-reconnect");
839
863
  }
840
- return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
864
+ return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeProvenanceOutput), (item) => provenanceCodec.decode(item).ok) : result;
841
865
  }
842
866
  };
843
867
  const subscription = protocol.subscribe((event) => {
@@ -934,16 +958,12 @@ function createHostAdapter(options) {
934
958
  };
935
959
  }
936
960
  function decodeExternalContribution(value, strict = true) {
937
- if (!isRecord2(value) || value.version !== 1 || !isRecord2(value.author) || strict && Object.keys(value).some((key) => !["version", "author", "personAttribution"].includes(key)) || strict && Object.keys(value.author).some((key) => !["subject", "presentation"].includes(key)) || strict && isRecord2(value.author.presentation) && Object.keys(value.author.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)) || !idCodec("subject").decode(value.author.subject).ok || !isProfilePresentation(value.author.presentation)) {
961
+ if (!isRecord2(value) || value.version !== 1 || !isRecord2(value.author) || strict && Object.keys(value).some((key) => !["version", "author", "personAttribution"].includes(key)) || strict && Object.keys(value.author).some((key) => !["subject", "presentation"].includes(key)) || strict && isRecord2(value.author.presentation) && Object.keys(value.author.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)) || (typeof value.author.subject !== "string" || value.author.subject.length === 0 || value.author.subject.length > 256) || !isProfilePresentation(value.author.presentation)) {
938
962
  return err("invalid-input", "Invalid V1 external contribution.");
939
963
  }
940
964
  let person = value.personAttribution;
941
- if (!strict && isRecord2(person) && isRecord2(person.identity))
942
- person = {
943
- identity: { kind: person.identity.kind, key: person.identity.key, issuer: person.identity.issuer, subject: person.identity.subject },
944
- presentation: isRecord2(person.presentation) ? { displayName: person.presentation.displayName, handle: person.presentation.handle, avatarUrl: person.presentation.avatarUrl } : person.presentation,
945
- evidence: person.evidence
946
- };
965
+ if (!strict)
966
+ person = normalizeExternalPersonOutput(person);
947
967
  if (strict && isRecord2(person) && isRecord2(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
948
968
  return err("invalid-input", "Unexpected presentation field.");
949
969
  const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
@@ -966,6 +986,16 @@ function canonicalExternal(value) {
966
986
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
967
987
  return JSON.stringify(value);
968
988
  }
989
+ function decodeExternalContributionOutcomeV1(raw) {
990
+ const decoded = decodeAcceptance(raw);
991
+ if (!decoded.ok || decoded.value.status !== "submitted")
992
+ return decoded;
993
+ if (decoded.value.receipt.evidence !== "host-accepted")
994
+ return err("incompatible", "V1 needs durable native acceptance.");
995
+ const receipt = decoded.value.receipt;
996
+ const contribution = decodeExternalContribution(receipt.externalContribution, false);
997
+ return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
998
+ }
969
999
  function externalContributionMethods(upstream, extension, disposed) {
970
1000
  const support = () => {
971
1001
  if (disposed())
@@ -985,16 +1015,6 @@ function externalContributionMethods(upstream, extension, disposed) {
985
1015
  return err("incompatible", "Malformed V1 external contribution capability.");
986
1016
  return ok(value);
987
1017
  };
988
- const outcome = (raw) => {
989
- const decoded = decodeAcceptance(raw);
990
- if (!decoded.ok || decoded.value.status !== "submitted")
991
- return decoded;
992
- if (decoded.value.receipt.evidence !== "host-accepted")
993
- return err("incompatible", "V1 needs durable native acceptance.");
994
- const receipt = decoded.value.receipt;
995
- const contribution = decodeExternalContribution(receipt.externalContribution, false);
996
- return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
997
- };
998
1018
  return {
999
1019
  externalContributionCapabilities() {
1000
1020
  const negotiated = support();
@@ -1015,7 +1035,7 @@ function externalContributionMethods(upstream, extension, disposed) {
1015
1035
  const frozen = JSON.parse(JSON.stringify(decoded.value));
1016
1036
  const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
1017
1037
  try {
1018
- const result = outcome(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
1038
+ const result = decodeExternalContributionOutcomeV1(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
1019
1039
  if (!result.ok)
1020
1040
  return { status: "indeterminate", operationId, message: result.error.message };
1021
1041
  if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
@@ -1037,7 +1057,7 @@ function externalContributionMethods(upstream, extension, disposed) {
1037
1057
  if (!result.ok)
1038
1058
  return result;
1039
1059
  if (isRecord2(result.value) && result.value.status === "final") {
1040
- const decoded = outcome(result.value.outcome);
1060
+ const decoded = decodeExternalContributionOutcomeV1(result.value.outcome);
1041
1061
  if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
1042
1062
  return err("incompatible", "Host lookup returned a receipt for another operation.");
1043
1063
  }
@@ -1991,39 +2011,39 @@ function createBbUpstreamDriver(options) {
1991
2011
  const active = new Set;
1992
2012
  const listeners = new Set;
1993
2013
  const labelledExternal = new WeakSet;
1994
- const terminateScope = (record, scope) => {
2014
+ const terminateScope = (record2, scope) => {
1995
2015
  if (!scope || scope.released)
1996
2016
  return;
1997
2017
  scope.released = true;
1998
- record.controller.signal.removeEventListener("abort", scope.parentAbort);
2018
+ record2.controller.signal.removeEventListener("abort", scope.parentAbort);
1999
2019
  scope.controller.abort();
2000
- record.scopes.delete(scope);
2020
+ record2.scopes.delete(scope);
2001
2021
  };
2002
- const terminate = (record) => {
2003
- for (const scope of [...record.scopes])
2004
- terminateScope(record, scope);
2005
- record.controller.abort();
2006
- active.delete(record);
2022
+ const terminate = (record2) => {
2023
+ for (const scope of [...record2.scopes])
2024
+ terminateScope(record2, scope);
2025
+ record2.controller.abort();
2026
+ active.delete(record2);
2007
2027
  };
2008
2028
  const known = (context) => {
2009
- const record = records.get(context);
2010
- return !record ? err("invalid-input", "Invocation context was not issued by this binding.") : disposed ? err("disposed", "Upstream binding is disposed.") : record.controller.signal.aborted ? err("expired", "Invocation context has expired.") : ok(record);
2029
+ const record2 = records.get(context);
2030
+ return !record2 ? err("invalid-input", "Invocation context was not issued by this binding.") : disposed ? err("disposed", "Upstream binding is disposed.") : record2.controller.signal.aborted ? err("expired", "Invocation context has expired.") : ok(record2);
2011
2031
  };
2012
2032
  const admission = (context) => {
2013
- const record = known(context);
2014
- if (!record.ok)
2015
- return record;
2016
- if (record.value.origin === "external")
2033
+ const record2 = known(context);
2034
+ if (!record2.ok)
2035
+ return record2;
2036
+ if (record2.value.origin === "external")
2017
2037
  return err("unauthenticated", "External upstream work uses an explicit external source.");
2018
- return record;
2038
+ return record2;
2019
2039
  };
2020
2040
  const session = (context) => {
2021
- const record = known(context);
2022
- if (!record.ok)
2023
- return unavailableSession(instanceId, record.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record.error.message);
2024
- if (record.value.origin === "interactive-user")
2041
+ const record2 = known(context);
2042
+ if (!record2.ok)
2043
+ return unavailableSession(instanceId, record2.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record2.error.message);
2044
+ if (record2.value.origin === "interactive-user")
2025
2045
  return readySession();
2026
- return record.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
2046
+ return record2.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
2027
2047
  };
2028
2048
  const driver = {
2029
2049
  instanceId,
@@ -2041,20 +2061,20 @@ function createBbUpstreamDriver(options) {
2041
2061
  const allowed = admission(context);
2042
2062
  if (!allowed.ok)
2043
2063
  return allowed;
2044
- const record = allowed.value;
2064
+ const record2 = allowed.value;
2045
2065
  let scope;
2046
- const parentAbort = () => terminateScope(record, scope);
2066
+ const parentAbort = () => terminateScope(record2, scope);
2047
2067
  scope = { controller: new AbortController, parentAbort, released: false };
2048
- record.scopes.add(scope);
2049
- const expire = () => terminateScope(record, scope);
2050
- record.controller.signal.addEventListener("abort", parentAbort, { once: true });
2051
- const exposedSession = record.origin === "background" ? machineSession() : readySession();
2052
- const expectedKey = record.origin === "background" ? machineKey.value : singletonKey;
2068
+ record2.scopes.add(scope);
2069
+ const expire = () => terminateScope(record2, scope);
2070
+ record2.controller.signal.addEventListener("abort", parentAbort, { once: true });
2071
+ const exposedSession = record2.origin === "background" ? machineSession() : readySession();
2072
+ const expectedKey = record2.origin === "background" ? machineKey.value : singletonKey;
2053
2073
  return ok({
2054
2074
  signal: scope.controller.signal,
2055
2075
  session: exposedSession,
2056
2076
  validate(expected) {
2057
- if (scope.released || scope.controller.signal.aborted || record.controller.signal.aborted || disposed) {
2077
+ if (scope.released || scope.controller.signal.aborted || record2.controller.signal.aborted || disposed) {
2058
2078
  return err("expired", "Upstream request context is no longer live.");
2059
2079
  }
2060
2080
  return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
@@ -2138,22 +2158,22 @@ function createBbUpstreamDriver(options) {
2138
2158
  throw new Error("Upstream binding is disposed.");
2139
2159
  const controller = new AbortController;
2140
2160
  const invocation = Object.freeze({ origin, signal: controller.signal });
2141
- const record = { origin, controller, scopes: new Set };
2142
- records.set(invocation, record);
2143
- active.add(record);
2161
+ const record2 = { origin, controller, scopes: new Set };
2162
+ records.set(invocation, record2);
2163
+ active.add(record2);
2144
2164
  return invocation;
2145
2165
  },
2146
2166
  release(invocation) {
2147
- const record = records.get(invocation);
2148
- if (record)
2149
- terminate(record);
2167
+ const record2 = records.get(invocation);
2168
+ if (record2)
2169
+ terminate(record2);
2150
2170
  },
2151
2171
  dispose() {
2152
2172
  if (disposed)
2153
2173
  return;
2154
2174
  disposed = true;
2155
- for (const record of [...active])
2156
- terminate(record);
2175
+ for (const record2 of [...active])
2176
+ terminate(record2);
2157
2177
  for (const listener of [...listeners]) {
2158
2178
  try {
2159
2179
  listener({ kind: "disposed" });
@@ -2780,8 +2800,8 @@ function createBbIdentityServerBinding(options) {
2780
2800
  const invocations = new WeakMap;
2781
2801
  const expired = () => err("expired", "Invocation has already settled.");
2782
2802
  const recordFor = (invocation) => {
2783
- const record = invocations.get(invocation);
2784
- return disposed || !record || record.settled || record.signal.aborted ? expired() : ok(record);
2803
+ const record2 = invocations.get(invocation);
2804
+ return disposed || !record2 || record2.settled || record2.signal.aborted ? expired() : ok(record2);
2785
2805
  };
2786
2806
  const unavailableSession2 = () => ({
2787
2807
  status: "unavailable",
@@ -2789,25 +2809,25 @@ function createBbIdentityServerBinding(options) {
2789
2809
  error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
2790
2810
  });
2791
2811
  const openPerson = async (invocation) => {
2792
- const record = recordFor(invocation);
2793
- if (!record.ok)
2794
- return record;
2795
- const opened = await server.personRequest(record.value.request);
2796
- if (record.value.settled || record.value.signal.aborted) {
2812
+ const record2 = recordFor(invocation);
2813
+ if (!record2.ok)
2814
+ return record2;
2815
+ const opened = await server.personRequest(record2.value.request);
2816
+ if (record2.value.settled || record2.value.signal.aborted) {
2797
2817
  if (opened.ok)
2798
2818
  opened.value.dispose();
2799
2819
  return expired();
2800
2820
  }
2801
2821
  if (opened.ok)
2802
- record.value.people.add(opened.value);
2822
+ record2.value.people.add(opened.value);
2803
2823
  return opened;
2804
2824
  };
2805
2825
  const guardEndpoint = async (invocation, operation) => {
2806
- const record = recordFor(invocation);
2807
- if (!record.ok)
2808
- return record;
2809
- const result = await operation(record.value.request);
2810
- return record.value.settled || record.value.signal.aborted ? expired() : result;
2826
+ const record2 = recordFor(invocation);
2827
+ if (!record2.ok)
2828
+ return record2;
2829
+ const result = await operation(record2.value.request);
2830
+ return record2.value.settled || record2.value.signal.aborted ? expired() : result;
2811
2831
  };
2812
2832
  let disposeBinding = () => {};
2813
2833
  const guardedHistory = {
@@ -2828,11 +2848,11 @@ function createBbIdentityServerBinding(options) {
2828
2848
  instanceId: server.instanceId,
2829
2849
  commits: server.commits,
2830
2850
  async session(invocation) {
2831
- const record = recordFor(invocation);
2832
- if (!record.ok)
2851
+ const record2 = recordFor(invocation);
2852
+ if (!record2.ok)
2833
2853
  return unavailableSession2();
2834
- const session = await server.session(record.value.request);
2835
- return record.value.settled || record.value.signal.aborted ? unavailableSession2() : session;
2854
+ const session = await server.session(record2.value.request);
2855
+ return record2.value.settled || record2.value.signal.aborted ? unavailableSession2() : session;
2836
2856
  },
2837
2857
  selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
2838
2858
  personRequest: openPerson,
@@ -2909,18 +2929,18 @@ function createBbIdentityServerBinding(options) {
2909
2929
  return endpointRegistered;
2910
2930
  }
2911
2931
  const createInvocation = (context) => {
2912
- const record = { request: context.request, signal: context.signal, people: new Set, settled: false };
2932
+ const record2 = { request: context.request, signal: context.signal, people: new Set, settled: false };
2913
2933
  const invocation = {
2914
2934
  origin: context.origin,
2915
2935
  signal: context.signal,
2916
2936
  person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
2917
2937
  };
2918
- invocations.set(invocation, record);
2938
+ invocations.set(invocation, record2);
2919
2939
  const finish = () => {
2920
- record.settled = true;
2921
- for (const request of record.people)
2940
+ record2.settled = true;
2941
+ for (const request of record2.people)
2922
2942
  request.dispose();
2923
- record.people.clear();
2943
+ record2.people.clear();
2924
2944
  };
2925
2945
  return { invocation, finish };
2926
2946
  };
@@ -3136,7 +3156,7 @@ function createMultiUserProtocol(instanceId, initial) {
3136
3156
  }
3137
3157
  };
3138
3158
  };
3139
- const record = (subject) => {
3159
+ const record2 = (subject) => {
3140
3160
  const person = people.get(subject);
3141
3161
  return person ? { issuer: ISSUER, subject, presentation: { displayName: person.name, handle: null, avatarUrl: null }, status: "current" } : null;
3142
3162
  };
@@ -3146,11 +3166,11 @@ function createMultiUserProtocol(instanceId, initial) {
3146
3166
  generation: brand("provider-generation", "wire-fake-provider"),
3147
3167
  async directory(input) {
3148
3168
  const query = input.query.toLowerCase();
3149
- const records = [...people.keys()].map(record).filter((r) => r !== null && (r.subject.includes(query) || r.presentation.displayName.toLowerCase().includes(query))).slice(0, input.limit);
3169
+ const records = [...people.keys()].map(record2).filter((r) => r !== null && (r.subject.includes(query) || r.presentation.displayName.toLowerCase().includes(query))).slice(0, input.limit);
3150
3170
  return ok({ records, nextCursor: null, revision: revision() });
3151
3171
  },
3152
3172
  async lookup(subjects) {
3153
- return ok({ revision: revision(), records: subjects.map((s) => ({ issuer: s.issuer, subject: s.subject, record: s.issuer === ISSUER ? record(s.subject) : null })) });
3173
+ return ok({ revision: revision(), records: subjects.map((s) => ({ issuer: s.issuer, subject: s.subject, record: s.issuer === ISSUER ? record2(s.subject) : null })) });
3154
3174
  },
3155
3175
  person(issuer, subject) {
3156
3176
  const key = identityKeyCodec.decode(`${issuer}:${subject}`);
@@ -3402,8 +3422,8 @@ function canonical(value) {
3402
3422
  return JSON.stringify(value) ?? "undefined";
3403
3423
  if (Array.isArray(value))
3404
3424
  return "[" + value.map(canonical).join(",") + "]";
3405
- const record = value;
3406
- return "{" + Object.keys(record).filter((k) => record[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical(record[k])).join(",") + "}";
3425
+ const record2 = value;
3426
+ return "{" + Object.keys(record2).filter((k) => record2[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical(record2[k])).join(",") + "}";
3407
3427
  }
3408
3428
  function id(kind, value) {
3409
3429
  const decoded = idCodec(kind).decode(value);
package/editing.d.ts ADDED
@@ -0,0 +1,137 @@
1
+ /** Opt-in editing/v1. Publication target and actual draft owner remain distinct. */
2
+ import type { Codec, ConflictToken, Disposable, IdentityError, OperationId, OperationLookup, OwnerSessionId, ReadOptions, RequestExpectation, Result, Scheduler, Unsubscribe } from './model.js';
3
+ import type { ServerSession } from './host.js';
4
+ import type { ConflictContext, ConflictDecision, PendingDraft, StateAddress, StateDefinition, StateInvalidation, StateMutation, StateOutcome, StateRead } from './state.js';
5
+ export declare const EDITING_PROTOCOL: 'bb-identity.editing/v1';
6
+ export interface EditingScope<Target> {
7
+ /** Actual actor's draft retention address. Never the owner of a shared document. */
8
+ readonly draftAddress: StateAddress;
9
+ readonly schemaVersion: number;
10
+ /** Feature-owned publication target, distinct from draft actor and session. */
11
+ readonly target: Target;
12
+ readonly expected: RequestExpectation;
13
+ }
14
+ export interface EditingRequest<T, Target> {
15
+ readonly protocol: typeof EDITING_PROTOCOL;
16
+ readonly target: Target;
17
+ readonly mutation: StateMutation<T>;
18
+ }
19
+ export interface EditingDiagnostic { readonly code: string; readonly message: string; readonly path?: readonly (string | number)[]; }
20
+ export type EditingReceipt<T, Target> = {
21
+ readonly protocol: typeof EDITING_PROTOCOL;
22
+ readonly request: EditingRequest<T, Target>;
23
+ readonly outcome: StateOutcome<T> | { readonly status: 'rejected'; readonly operationId: OperationId; readonly diagnostics: readonly EditingDiagnostic[] };
24
+ };
25
+ export type EditingSave<T, Target> = EditingReceipt<T, Target> | {
26
+ readonly protocol: typeof EDITING_PROTOCOL; readonly status: 'indeterminate'; readonly operationId: OperationId;
27
+ };
28
+ export interface EditingTransport<T, Target> {
29
+ /** Construct only after negotiating this version with the feature endpoint; no legacy fallback. */
30
+ readonly protocol: typeof EDITING_PROTOCOL;
31
+ load(scope: EditingScope<Target>, options?: ReadOptions): Promise<Result<StateRead<T>>>;
32
+ save(request: EditingRequest<T, Target>, options?: ReadOptions): Promise<Result<EditingSave<T, Target>>>;
33
+ reconcile(request: EditingRequest<T, Target>, options?: ReadOptions): Promise<Result<OperationLookup<EditingReceipt<T, Target>>>>;
34
+ subscribe(scope: EditingScope<Target>, listener: (event: StateInvalidation) => void): Unsubscribe;
35
+ }
36
+ export interface EditingDraft<T, Target> {
37
+ readonly protocol: typeof EDITING_PROTOCOL;
38
+ readonly target: Target;
39
+ /** desired B and exact inFlight A are independent fields. */
40
+ readonly draft: PendingDraft<T>;
41
+ readonly rejection: EditingReceipt<T, Target> | null;
42
+ }
43
+ export interface EditingCheckpoint<T, Target> { readonly revision: number; readonly draft: EditingDraft<T, Target>; }
44
+ export interface EditingDraftStorage<T, Target> {
45
+ find(scope: EditingScope<Target>, limit: number): Promise<Result<readonly EditingCheckpoint<T, Target>[]>>;
46
+ write(draft: EditingDraft<T, Target>, expectedRevision: number | null): Promise<Result<EditingCheckpoint<T, Target>>>;
47
+ remove(key: { readonly target: Target; readonly draftKey: import('./state.js').DraftKey }, expectedRevision: number): Promise<Result<void>>;
48
+ }
49
+ export interface EditingSnapshot<T, Target> {
50
+ readonly protocol: typeof EDITING_PROTOCOL;
51
+ readonly status: 'loading' | 'loaded' | 'fenced' | 'detached';
52
+ readonly scope: EditingScope<Target>;
53
+ readonly ownerSession: OwnerSessionId;
54
+ readonly desired: T | undefined;
55
+ readonly localGeneration: number;
56
+ readonly acknowledged: StateRead<T> | null;
57
+ readonly dirty: boolean;
58
+ readonly editable: boolean;
59
+ readonly submitted: EditingRequest<T, Target> | null;
60
+ readonly publication: {
61
+ readonly eligible: boolean;
62
+ readonly status: 'idle' | 'saving' | 'unknown' | 'conflict' | 'rejected' | 'fenced';
63
+ readonly message: string | null;
64
+ readonly rejection: EditingReceipt<T, Target> | null;
65
+ readonly conflict: ConflictContext<T> | null;
66
+ };
67
+ readonly checkpoint: {
68
+ readonly status: 'disabled' | 'local-only' | 'writing' | 'backed-up' | 'failed';
69
+ readonly generation: number | null;
70
+ readonly revision: number | null;
71
+ readonly error: IdentityError | null;
72
+ };
73
+ /** Exportable even if persistence failed. Only checkpoint success proves backup. */
74
+ readonly draft: EditingDraft<T, Target> | null;
75
+ }
76
+ export interface EditingState<T, Target> extends Disposable {
77
+ /** Borrowed identity for shared retirement coordination; never disposed by the binding. */
78
+ readonly draftStorage: EditingDraftStorage<T, Target> | null;
79
+ getSnapshot(): EditingSnapshot<T, Target>;
80
+ subscribe(listener: () => void): Unsubscribe;
81
+ start(): Promise<Result<void>>;
82
+ edit(value: T): Result<void>;
83
+ /** Explicit action only; reconcile A never implicitly dispatches B. */
84
+ flush(): Promise<Result<void>>;
85
+ reconnect(options?: { readonly reuseInFlight?: boolean }): Promise<Result<void>>;
86
+ resolveConflict(token: ConflictToken, decision: ConflictDecision<T>): Promise<Result<void>>;
87
+ checkpoint(): Promise<Result<EditingCheckpoint<T, Target> | null>>;
88
+ recover(candidate: EditingCheckpoint<T, Target>): Promise<Result<void>>;
89
+ recoveryCandidates(options?: ReadOptions): Promise<Result<readonly EditingCheckpoint<T, Target>[]>>;
90
+ discardRecovery(candidate: EditingCheckpoint<T, Target>): Promise<Result<void>>;
91
+ detach(reason: 'identity-invalidated' | 'view-change' | 'unmount'): EditingDraft<T, Target> | null;
92
+ close(options: { readonly pending: 'flush' | 'preserve' | 'discard' }): Promise<Result<void>>;
93
+ }
94
+ export declare function createIdentityEditingState<T, Target>(options: {
95
+ readonly draftAddress: StateAddress; readonly target: Target; readonly targetCodec: Codec<Target>;
96
+ readonly expected: RequestExpectation; readonly ownerSession: OwnerSessionId;
97
+ readonly definition: StateDefinition<T>; readonly transport: EditingTransport<T, Target>;
98
+ readonly drafts?: EditingDraftStorage<T, Target>; readonly scheduler?: Scheduler;
99
+ readonly onConflict: (context: ConflictContext<T>) => ConflictDecision<T>;
100
+ readonly initializeEmpty: boolean;
101
+ readonly currentSession: () => Result<ServerSession>;
102
+ }): EditingState<T, Target>;
103
+ export declare function editingCodecs<T, Target>(value: Codec<T>, target: Codec<Target>): {
104
+ /** Actual load/subscription scope. Does not create a mutation or infer shared ownership. */
105
+ readonly scope: Codec<EditingScope<Target>>;
106
+ readonly request: Codec<EditingRequest<T, Target>>;
107
+ readonly receipt: Codec<EditingReceipt<T, Target>>;
108
+ readonly save: Codec<EditingSave<T, Target>>;
109
+ readonly lookup: Codec<OperationLookup<EditingReceipt<T, Target>>>;
110
+ readonly draft: Codec<EditingDraft<T, Target>>;
111
+ };
112
+
113
+ /** Owned by the shared binding lifetime; subject is always the actual actor. */
114
+ export interface EditingBindingOptions<T, Target> {
115
+ readonly resourceKey: string;
116
+ readonly target: Target; readonly targetCodec: Codec<Target>;
117
+ readonly client: import('./client.js').IdentityClient;
118
+ readonly view: import('./client.js').IdentityView;
119
+ create(input: { readonly session: Extract<ServerSession, { status: 'ready' }>;
120
+ readonly subject: import('./model.js').ActorReference; readonly ownerSession: OwnerSessionId }): EditingState<T, Target>;
121
+ readonly onUnpersistedDraft?: (draft: EditingDraft<T, Target>, error: IdentityError) => void;
122
+ }
123
+ export interface IdentityEditingBinding<T, Target> extends Disposable {
124
+ getSnapshot(): EditingSnapshot<T, Target> | { readonly status: 'waiting-for-identity' };
125
+ subscribe(listener: () => void): Unsubscribe;
126
+ currentOwnerSession(): OwnerSessionId | null;
127
+ edit(value: T): Result<void>;
128
+ flush(): Promise<Result<void>>;
129
+ reconnect(owner: OwnerSessionId): Promise<Result<void>>;
130
+ checkpoint(owner: OwnerSessionId): Promise<Result<EditingCheckpoint<T, Target> | null>>;
131
+ recoveryCandidates(owner: OwnerSessionId, options?: ReadOptions): Promise<Result<readonly EditingCheckpoint<T, Target>[]>>;
132
+ recover(owner: OwnerSessionId, checkpoint: EditingCheckpoint<T, Target>): Promise<Result<void>>;
133
+ discardRecovery(owner: OwnerSessionId, checkpoint: EditingCheckpoint<T, Target>): Promise<Result<void>>;
134
+ resolveConflict(owner: OwnerSessionId, token: ConflictToken, decision: ConflictDecision<T>): Promise<Result<void>>;
135
+ }
136
+ export declare function bindIdentityEditingState<T, Target>(options: EditingBindingOptions<T, Target>): IdentityEditingBinding<T, Target>;
137
+ export declare const EDITING_LIMITS: { readonly bytes: number; readonly diagnostics: number; readonly message: number; readonly code: number; readonly path: number };
package/host.d.ts CHANGED
@@ -49,6 +49,7 @@ export type ExternalContributionOutcomeV1 = Exclude<AcceptanceOutcome, { status:
49
49
  readonly externalContribution: ExternalContributionV1;
50
50
  };
51
51
  };
52
+ export declare const externalContributionOutcomeV1Codec: Codec<ExternalContributionOutcomeV1>;
52
53
  /** Context-free support; native storage written by V1 requires a capable host on rollback. */
53
54
  export interface ExternalContributionCapabilityV1 {
54
55
  readonly version: 1; readonly personKinds: readonly ['person'];
@@ -0,0 +1,15 @@
1
+ import type { NativeSourceEvidence, NativeSourceSnapshot } from './native.js';
2
+ import type { Result } from './model.js';
3
+ /** Source conformance only: caller must obtain producer/live receipts separately. */
4
+ export interface NativeSourceConformanceCase {
5
+ readonly name: string;
6
+ readonly snapshot: NativeSourceSnapshot;
7
+ /** Independent semantic oracle, not an expected value generated by the adapter. */
8
+ readonly verify: (result: Result<NativeSourceEvidence>) => void | Promise<void>;
9
+ }
10
+ export declare function defineNativeSourceConformance(options: {
11
+ readonly test: (name: string, run: () => void | Promise<void>) => unknown;
12
+ /** Drive the adopter's real source path. The kit does not construct an alternate adapter. */
13
+ readonly resolve: (snapshot: NativeSourceSnapshot) => Result<NativeSourceEvidence> | Promise<Result<NativeSourceEvidence>>;
14
+ readonly cases: readonly NativeSourceConformanceCase[];
15
+ }): void;