@phosphorco/bb-identity 0.2.1 → 0.3.1

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._:@/-]*$/;
@@ -107,7 +130,7 @@ function isOrigin(value) {
107
130
  return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
108
131
  }
109
132
  function isContribution(value) {
110
- return isRecord(value) && isContributionReference(value.reference) && isOrigin(value.author) && (value.latestEditor === null || isActorSnapshot(value.latestEditor)) && isBoundedString(value.acceptedAt, 256) && Array.isArray(value.mentionedPeople) && value.mentionedPeople.every(isPersonReference);
133
+ return isRecord(value) && isContributionReference(value.reference) && isOrigin(value.author) && (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && (value.latestEditor === null || isActorSnapshot(value.latestEditor)) && isBoundedString(value.acceptedAt, 256) && Array.isArray(value.mentionedPeople) && value.mentionedPeople.every(isPersonReference);
111
134
  }
112
135
  function isCorrelation(value) {
113
136
  return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
@@ -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
  }
@@ -184,6 +208,17 @@ var provenanceCodec = {
184
208
  return value;
185
209
  }
186
210
  };
211
+ var externalPersonSnapshotCodec = {
212
+ decode(value) {
213
+ if (!isRecord(value) || Object.keys(value).some((key) => !["identity", "presentation", "evidence"].includes(key)) || value.evidence !== "integration-asserted" || !isActorSnapshot(value) || !isRecord(value.identity) || value.identity.kind !== "person" || Object.keys(value.identity).some((key) => !["kind", "key", "issuer", "subject"].includes(key))) {
214
+ return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
215
+ }
216
+ return ok(value);
217
+ },
218
+ encode(value) {
219
+ return value;
220
+ }
221
+ };
187
222
  function defaultIdentityKey(instance) {
188
223
  return `local:${instance}`;
189
224
  }
@@ -378,7 +413,7 @@ function isContribution2(value) {
378
413
  if (!isRecord2(origin))
379
414
  return false;
380
415
  const validOrigin = origin.kind === "person" ? snapshot(origin.actor) && isRecord2(origin.actor) && isPersonIdentity(origin.actor.identity) : origin.kind === "machine" ? snapshot(origin.actor) && isRecord2(origin.actor) && isMachineIdentity(origin.actor.identity) : origin.kind === "external" ? snapshot(origin.actor) && isRecord2(origin.actor) && isExternalIdentity(origin.actor.identity) : origin.kind === "agent" ? origin.agentId === null || idCodec("agent").decode(origin.agentId).ok : origin.kind === "system" ? typeof origin.reason === "string" && origin.reason.length > 0 && origin.reason.length <= 1024 : origin.kind === "unknown" && (origin.reason === "legacy" || origin.reason === "upstream-unattributed" || origin.reason === "missing-source");
381
- return validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
416
+ return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
382
417
  }
383
418
  function decodeEvidencePage(value, decodeItem) {
384
419
  if (!isRecord2(value) || typeof value.status !== "string")
@@ -459,11 +494,11 @@ function createEnhancedDirectory(protocol) {
459
494
  }
460
495
  mappings.set(key, mapping);
461
496
  };
462
- const profile = (source, record, revision) => {
463
- 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)
464
499
  return err("incompatible", "Provider directory record is malformed.");
465
- const issuer = record.issuer;
466
- const subject = record.subject;
500
+ const issuer = record2.issuer;
501
+ const subject = record2.subject;
467
502
  if (typeof issuer !== "string" || typeof subject !== "string")
468
503
  return err("incompatible", "Provider directory record is malformed.");
469
504
  let person;
@@ -474,10 +509,10 @@ function createEnhancedDirectory(protocol) {
474
509
  }
475
510
  if (!person.ok)
476
511
  return person;
477
- 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) {
478
513
  return err("incompatible", "Provider directory returned an invalid person reference.");
479
514
  }
480
- 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 });
481
516
  };
482
517
  return {
483
518
  async search(input, options) {
@@ -503,8 +538,8 @@ function createEnhancedDirectory(protocol) {
503
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)
504
539
  return err("incompatible", "Provider directory page is malformed.");
505
540
  const profiles = [];
506
- for (const record of result.value.records) {
507
- 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);
508
543
  if (!decoded.ok)
509
544
  return decoded;
510
545
  if (input.history === "current" && decoded.value.status !== "current")
@@ -713,6 +748,7 @@ function createHostAdapter(options) {
713
748
  });
714
749
  const contexts = new WeakMap;
715
750
  return {
751
+ ...externalContributionMethods(upstream, extension, () => disposed),
716
752
  pluginId: upstream.pluginId,
717
753
  instanceId: upstream.instanceId,
718
754
  session: (context) => upstream.session(context),
@@ -758,6 +794,7 @@ function createHostAdapter(options) {
758
794
  const error = protocolError(extension);
759
795
  const unavailableResult = async () => ({ ok: false, error });
760
796
  return {
797
+ ...externalContributionMethods(upstream, extension, () => disposed),
761
798
  pluginId: upstream.pluginId,
762
799
  instanceId: upstream.instanceId,
763
800
  session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
@@ -811,7 +848,7 @@ function createHostAdapter(options) {
811
848
  } catch {
812
849
  return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
813
850
  }
814
- return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
851
+ return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeContributionOutput), isContribution2) : result;
815
852
  },
816
853
  async attempts(query, readOptions) {
817
854
  if (!validAttemptQuery(query))
@@ -824,7 +861,7 @@ function createHostAdapter(options) {
824
861
  } catch {
825
862
  return err("unavailable", "Execution history is unavailable.", "after-reconnect");
826
863
  }
827
- 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;
828
865
  }
829
866
  };
830
867
  const subscription = protocol.subscribe((event) => {
@@ -833,6 +870,7 @@ function createHostAdapter(options) {
833
870
  });
834
871
  const scopes = new WeakMap;
835
872
  return {
873
+ ...externalContributionMethods(upstream, extension, () => disposed),
836
874
  pluginId: upstream.pluginId,
837
875
  instanceId: protocol.instanceId,
838
876
  async session(context) {
@@ -919,6 +957,119 @@ function createHostAdapter(options) {
919
957
  }
920
958
  };
921
959
  }
960
+ function decodeExternalContribution(value, strict = true) {
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)) {
962
+ return err("invalid-input", "Invalid V1 external contribution.");
963
+ }
964
+ let person = value.personAttribution;
965
+ if (!strict)
966
+ person = normalizeExternalPersonOutput(person);
967
+ if (strict && isRecord2(person) && isRecord2(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
968
+ return err("invalid-input", "Unexpected presentation field.");
969
+ const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
970
+ if (decoded && !decoded.ok)
971
+ return decoded;
972
+ return ok({
973
+ version: 1,
974
+ author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
975
+ ...decoded?.ok ? { personAttribution: decoded.value } : {}
976
+ });
977
+ }
978
+ var externalContributionV1Codec = {
979
+ decode: (value) => decodeExternalContribution(value),
980
+ encode: (value) => value
981
+ };
982
+ function canonicalExternal(value) {
983
+ if (Array.isArray(value))
984
+ return `[${value.map(canonicalExternal).join(",")}]`;
985
+ if (isRecord2(value))
986
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
987
+ return JSON.stringify(value);
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
+ }
999
+ function externalContributionMethods(upstream, extension, disposed) {
1000
+ const support = () => {
1001
+ if (disposed())
1002
+ return err("disposed", "Identity host is disposed.");
1003
+ if (extension.status === "absent")
1004
+ return ok(null);
1005
+ if (extension.status !== "supported")
1006
+ return { ok: false, error: protocolError(extension) };
1007
+ const value = extension.protocol.externalContributions;
1008
+ if (value === undefined)
1009
+ return ok(null);
1010
+ if (!isRecord2(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
1011
+ return err("incompatible", "Malformed external contribution capability.");
1012
+ if (!value.versions.includes(1))
1013
+ return ok(null);
1014
+ if (!Array.isArray(value.personKinds) || value.personKinds.length !== 1 || value.personKinds[0] !== "person" || value.history !== "accepted-and-queued" || value.rollback !== "requires-capable-host" || typeof value.acceptV1 !== "function" || typeof value.lookupV1 !== "function")
1015
+ return err("incompatible", "Malformed V1 external contribution capability.");
1016
+ return ok(value);
1017
+ };
1018
+ return {
1019
+ externalContributionCapabilities() {
1020
+ const negotiated = support();
1021
+ return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
1022
+ },
1023
+ async sendExternalContribution(contribution, input, options) {
1024
+ const operationId = input.operationId;
1025
+ const decoded = decodeExternalContribution(contribution);
1026
+ if (!decoded.ok)
1027
+ return { status: "rejected", error: decoded.error };
1028
+ const negotiated = support();
1029
+ if (!negotiated.ok)
1030
+ return { status: "rejected", error: negotiated.error };
1031
+ if (!negotiated.value)
1032
+ return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
1033
+ if (options?.signal?.aborted)
1034
+ return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
1035
+ const frozen = JSON.parse(JSON.stringify(decoded.value));
1036
+ const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
1037
+ try {
1038
+ const result = decodeExternalContributionOutcomeV1(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
1039
+ if (!result.ok)
1040
+ return { status: "indeterminate", operationId, message: result.error.message };
1041
+ if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
1042
+ return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
1043
+ }
1044
+ return result.value;
1045
+ } catch {
1046
+ return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
1047
+ }
1048
+ },
1049
+ async lookupExternalContribution(operationId, options) {
1050
+ const negotiated = support();
1051
+ if (!negotiated.ok)
1052
+ return negotiated;
1053
+ if (!negotiated.value)
1054
+ return ok({ status: "unknown", reason: "unsupported" });
1055
+ try {
1056
+ const result = asResult(await negotiated.value.lookupV1(operationId, options));
1057
+ if (!result.ok)
1058
+ return result;
1059
+ if (isRecord2(result.value) && result.value.status === "final") {
1060
+ const decoded = decodeExternalContributionOutcomeV1(result.value.outcome);
1061
+ if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
1062
+ return err("incompatible", "Host lookup returned a receipt for another operation.");
1063
+ }
1064
+ return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
1065
+ }
1066
+ return decodeLookup(result.value);
1067
+ } catch {
1068
+ return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
1069
+ }
1070
+ }
1071
+ };
1072
+ }
922
1073
  function inspectForkExtension(value) {
923
1074
  if (value === undefined)
924
1075
  return { status: "absent" };
@@ -1860,39 +2011,39 @@ function createBbUpstreamDriver(options) {
1860
2011
  const active = new Set;
1861
2012
  const listeners = new Set;
1862
2013
  const labelledExternal = new WeakSet;
1863
- const terminateScope = (record, scope) => {
2014
+ const terminateScope = (record2, scope) => {
1864
2015
  if (!scope || scope.released)
1865
2016
  return;
1866
2017
  scope.released = true;
1867
- record.controller.signal.removeEventListener("abort", scope.parentAbort);
2018
+ record2.controller.signal.removeEventListener("abort", scope.parentAbort);
1868
2019
  scope.controller.abort();
1869
- record.scopes.delete(scope);
2020
+ record2.scopes.delete(scope);
1870
2021
  };
1871
- const terminate = (record) => {
1872
- for (const scope of [...record.scopes])
1873
- terminateScope(record, scope);
1874
- record.controller.abort();
1875
- 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);
1876
2027
  };
1877
2028
  const known = (context) => {
1878
- const record = records.get(context);
1879
- 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);
1880
2031
  };
1881
2032
  const admission = (context) => {
1882
- const record = known(context);
1883
- if (!record.ok)
1884
- return record;
1885
- if (record.value.origin === "external")
2033
+ const record2 = known(context);
2034
+ if (!record2.ok)
2035
+ return record2;
2036
+ if (record2.value.origin === "external")
1886
2037
  return err("unauthenticated", "External upstream work uses an explicit external source.");
1887
- return record;
2038
+ return record2;
1888
2039
  };
1889
2040
  const session = (context) => {
1890
- const record = known(context);
1891
- if (!record.ok)
1892
- return unavailableSession(instanceId, record.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record.error.message);
1893
- 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")
1894
2045
  return readySession();
1895
- 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.");
1896
2047
  };
1897
2048
  const driver = {
1898
2049
  instanceId,
@@ -1910,20 +2061,20 @@ function createBbUpstreamDriver(options) {
1910
2061
  const allowed = admission(context);
1911
2062
  if (!allowed.ok)
1912
2063
  return allowed;
1913
- const record = allowed.value;
2064
+ const record2 = allowed.value;
1914
2065
  let scope;
1915
- const parentAbort = () => terminateScope(record, scope);
2066
+ const parentAbort = () => terminateScope(record2, scope);
1916
2067
  scope = { controller: new AbortController, parentAbort, released: false };
1917
- record.scopes.add(scope);
1918
- const expire = () => terminateScope(record, scope);
1919
- record.controller.signal.addEventListener("abort", parentAbort, { once: true });
1920
- const exposedSession = record.origin === "background" ? machineSession() : readySession();
1921
- 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;
1922
2073
  return ok({
1923
2074
  signal: scope.controller.signal,
1924
2075
  session: exposedSession,
1925
2076
  validate(expected) {
1926
- 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) {
1927
2078
  return err("expired", "Upstream request context is no longer live.");
1928
2079
  }
1929
2080
  return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
@@ -2007,22 +2158,22 @@ function createBbUpstreamDriver(options) {
2007
2158
  throw new Error("Upstream binding is disposed.");
2008
2159
  const controller = new AbortController;
2009
2160
  const invocation = Object.freeze({ origin, signal: controller.signal });
2010
- const record = { origin, controller, scopes: new Set };
2011
- records.set(invocation, record);
2012
- active.add(record);
2161
+ const record2 = { origin, controller, scopes: new Set };
2162
+ records.set(invocation, record2);
2163
+ active.add(record2);
2013
2164
  return invocation;
2014
2165
  },
2015
2166
  release(invocation) {
2016
- const record = records.get(invocation);
2017
- if (record)
2018
- terminate(record);
2167
+ const record2 = records.get(invocation);
2168
+ if (record2)
2169
+ terminate(record2);
2019
2170
  },
2020
2171
  dispose() {
2021
2172
  if (disposed)
2022
2173
  return;
2023
2174
  disposed = true;
2024
- for (const record of [...active])
2025
- terminate(record);
2175
+ for (const record2 of [...active])
2176
+ terminate(record2);
2026
2177
  for (const listener of [...listeners]) {
2027
2178
  try {
2028
2179
  listener({ kind: "disposed" });
@@ -2187,6 +2338,9 @@ function createIdentityServer(options) {
2187
2338
  },
2188
2339
  toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
2189
2340
  sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
2341
+ externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
2342
+ sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
2343
+ lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
2190
2344
  lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
2191
2345
  dispose
2192
2346
  };
@@ -2646,8 +2800,8 @@ function createBbIdentityServerBinding(options) {
2646
2800
  const invocations = new WeakMap;
2647
2801
  const expired = () => err("expired", "Invocation has already settled.");
2648
2802
  const recordFor = (invocation) => {
2649
- const record = invocations.get(invocation);
2650
- 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);
2651
2805
  };
2652
2806
  const unavailableSession2 = () => ({
2653
2807
  status: "unavailable",
@@ -2655,25 +2809,25 @@ function createBbIdentityServerBinding(options) {
2655
2809
  error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
2656
2810
  });
2657
2811
  const openPerson = async (invocation) => {
2658
- const record = recordFor(invocation);
2659
- if (!record.ok)
2660
- return record;
2661
- const opened = await server.personRequest(record.value.request);
2662
- 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) {
2663
2817
  if (opened.ok)
2664
2818
  opened.value.dispose();
2665
2819
  return expired();
2666
2820
  }
2667
2821
  if (opened.ok)
2668
- record.value.people.add(opened.value);
2822
+ record2.value.people.add(opened.value);
2669
2823
  return opened;
2670
2824
  };
2671
2825
  const guardEndpoint = async (invocation, operation) => {
2672
- const record = recordFor(invocation);
2673
- if (!record.ok)
2674
- return record;
2675
- const result = await operation(record.value.request);
2676
- 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;
2677
2831
  };
2678
2832
  let disposeBinding = () => {};
2679
2833
  const guardedHistory = {
@@ -2694,11 +2848,11 @@ function createBbIdentityServerBinding(options) {
2694
2848
  instanceId: server.instanceId,
2695
2849
  commits: server.commits,
2696
2850
  async session(invocation) {
2697
- const record = recordFor(invocation);
2698
- if (!record.ok)
2851
+ const record2 = recordFor(invocation);
2852
+ if (!record2.ok)
2699
2853
  return unavailableSession2();
2700
- const session = await server.session(record.value.request);
2701
- 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;
2702
2856
  },
2703
2857
  selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
2704
2858
  personRequest: openPerson,
@@ -2708,6 +2862,17 @@ function createBbIdentityServerBinding(options) {
2708
2862
  const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
2709
2863
  return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
2710
2864
  },
2865
+ externalContributionCapabilities: () => disposed ? err("disposed", "Identity binding is disposed.") : server.externalContributionCapabilities(),
2866
+ async sendExternalContribution(contribution, input, readOptions) {
2867
+ if (disposed)
2868
+ return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
2869
+ const decoded = externalContributionV1Codec.decode(contribution);
2870
+ if (!decoded.ok)
2871
+ return { status: "rejected", error: decoded.error };
2872
+ const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, decoded.value.author, input.input) : input.input;
2873
+ return server.sendExternalContribution(decoded.value, { ...input, input: renderedInput }, readOptions);
2874
+ },
2875
+ lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupExternalContribution(operationId, readOptions),
2711
2876
  lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
2712
2877
  history: guardedHistory
2713
2878
  };
@@ -2764,18 +2929,18 @@ function createBbIdentityServerBinding(options) {
2764
2929
  return endpointRegistered;
2765
2930
  }
2766
2931
  const createInvocation = (context) => {
2767
- 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 };
2768
2933
  const invocation = {
2769
2934
  origin: context.origin,
2770
2935
  signal: context.signal,
2771
2936
  person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
2772
2937
  };
2773
- invocations.set(invocation, record);
2938
+ invocations.set(invocation, record2);
2774
2939
  const finish = () => {
2775
- record.settled = true;
2776
- for (const request of record.people)
2940
+ record2.settled = true;
2941
+ for (const request of record2.people)
2777
2942
  request.dispose();
2778
- record.people.clear();
2943
+ record2.people.clear();
2779
2944
  };
2780
2945
  return { invocation, finish };
2781
2946
  };
@@ -2991,7 +3156,7 @@ function createMultiUserProtocol(instanceId, initial) {
2991
3156
  }
2992
3157
  };
2993
3158
  };
2994
- const record = (subject) => {
3159
+ const record2 = (subject) => {
2995
3160
  const person = people.get(subject);
2996
3161
  return person ? { issuer: ISSUER, subject, presentation: { displayName: person.name, handle: null, avatarUrl: null }, status: "current" } : null;
2997
3162
  };
@@ -3001,11 +3166,11 @@ function createMultiUserProtocol(instanceId, initial) {
3001
3166
  generation: brand("provider-generation", "wire-fake-provider"),
3002
3167
  async directory(input) {
3003
3168
  const query = input.query.toLowerCase();
3004
- 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);
3005
3170
  return ok({ records, nextCursor: null, revision: revision() });
3006
3171
  },
3007
3172
  async lookup(subjects) {
3008
- 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 })) });
3009
3174
  },
3010
3175
  person(issuer, subject) {
3011
3176
  const key = identityKeyCodec.decode(`${issuer}:${subject}`);
@@ -3257,8 +3422,8 @@ function canonical(value) {
3257
3422
  return JSON.stringify(value) ?? "undefined";
3258
3423
  if (Array.isArray(value))
3259
3424
  return "[" + value.map(canonical).join(",") + "]";
3260
- const record = value;
3261
- 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(",") + "}";
3262
3427
  }
3263
3428
  function id(kind, value) {
3264
3429
  const decoded = idCodec(kind).decode(value);
package/host.d.ts CHANGED
@@ -37,6 +37,30 @@ export type HostInvalidation =
37
37
  | { readonly kind: 'participants'; readonly threadIds: readonly string[] }
38
38
  | { readonly kind: 'disconnected' | 'reconnected' | 'disposed' };
39
39
  export interface ExternalAuthorInput { readonly subject: string; readonly presentation: ProfilePresentation; }
40
+ export interface ExternalContributionV1 {
41
+ readonly version: 1;
42
+ readonly author: ExternalAuthorInput;
43
+ readonly personAttribution?: import('./model.js').ExternalPersonSnapshotV1;
44
+ }
45
+ export declare const externalContributionV1Codec: Codec<ExternalContributionV1>;
46
+ export type ExternalContributionOutcomeV1 = Exclude<AcceptanceOutcome, { status: 'submitted' }> | {
47
+ readonly status: 'submitted';
48
+ readonly receipt: Extract<import('./model.js').AcceptanceReceipt, { evidence: 'host-accepted' }> & {
49
+ readonly externalContribution: ExternalContributionV1;
50
+ };
51
+ };
52
+ export declare const externalContributionOutcomeV1Codec: Codec<ExternalContributionOutcomeV1>;
53
+ /** Context-free support; native storage written by V1 requires a capable host on rollback. */
54
+ export interface ExternalContributionCapabilityV1 {
55
+ readonly version: 1; readonly personKinds: readonly ['person'];
56
+ readonly history: 'accepted-and-queued'; readonly rollback: 'requires-capable-host';
57
+ }
58
+ export interface ExternalContributionProtocolV1<Input> {
59
+ readonly versions: readonly number[]; readonly personKinds: readonly ['person'];
60
+ readonly history: 'accepted-and-queued'; readonly rollback: 'requires-capable-host';
61
+ acceptV1(request: { readonly contribution: Wire<ExternalContributionV1>; readonly input: Wire<SendInput<Input>> }, options?: ReadOptions): Promise<Wire<ExternalContributionOutcomeV1>>;
62
+ lookupV1(operationId: string, options?: ReadOptions): Promise<Wire<Result<OperationLookup<ExternalContributionOutcomeV1>>>>;
63
+ }
40
64
  export interface IdentityHost<RequestContext, ToolContext, Input> extends Disposable {
41
65
  readonly pluginId: PluginId;
42
66
  readonly instanceId: InstanceId;
@@ -51,6 +75,10 @@ export interface IdentityHost<RequestContext, ToolContext, Input> extends Dispos
51
75
  /** Checks synchronously before upstream dispatch; enhanced host checks again inside acceptance. */
52
76
  send(request: LiveRequest, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
53
77
  sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
78
+ externalContributionCapabilities(): Result<ExternalContributionCapabilityV1 | null>;
79
+ sendExternalContribution(contribution: ExternalContributionV1, input: SendInput<Input>, options?: ReadOptions): Promise<ExternalContributionOutcomeV1>;
80
+ lookupExternalContribution(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<ExternalContributionOutcomeV1>>>;
81
+
54
82
  lookupOperation(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
55
83
  forward(request: LiveRequest, destinationPlugin: string, method: string, input: import('./model.js').Json): Promise<unknown>;
56
84
  registerProvider(provider: IdentityProvider): Promise<Result<ProviderRegistrationV1>>;
@@ -149,6 +177,7 @@ export interface ForkIdentityProvider {
149
177
  /** Core implements these host-owned operations, not IdentityHost's fallback/directory composition. */
150
178
  export interface ForkIdentityProtocolV1<RequestContext, ToolContext, Input> {
151
179
  readonly version: 1;
180
+ readonly externalContributions?: ExternalContributionProtocolV1<Input>;
152
181
  /** Persisted core storage namespace; stable across plugin replacement and never request-derived. */
153
182
  readonly instanceId: string;
154
183
  /** Enables this plugin generation's producer-owned external message rendering in the SDK host. */
package/model.d.ts CHANGED
@@ -99,10 +99,17 @@ export interface ContributionReference {
99
99
  readonly threadId: ThreadId;
100
100
  readonly contributionId: ContributionId;
101
101
  }
102
+ /** Frozen integration assertion; V1 supports canonical people, not default-user. */
103
+ export type ExternalPersonSnapshotV1 = ActorSnapshot & {
104
+ readonly identity: Extract<IdentityReference, { kind: 'person' }>;
105
+ readonly evidence: 'integration-asserted';
106
+ };
107
+ export declare const externalPersonSnapshotCodec: Codec<ExternalPersonSnapshotV1>;
102
108
  /** A contribution is independent of delivery grouping and execution attempts. */
103
109
  export interface Contribution {
104
110
  readonly reference: ContributionReference;
105
111
  readonly author: Origin;
112
+ readonly personAttribution?: ExternalPersonSnapshotV1;
106
113
  readonly latestEditor: ActorSnapshot | null;
107
114
  readonly acceptedAt: string;
108
115
  readonly mentionedPeople: readonly PersonReference[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phosphorco/bb-identity",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Portable identity, provenance, and state contracts for BB integrations.",
package/server.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import type { AcceptanceOutcome, ActorReference, ActorSnapshot, Codec, Directory, Disposable, ExecutionProvenance,
3
3
  IdentityKey, InstanceId, OperationId, OperationLookup, ParticipantReader, PersonReference,
4
4
  PersonSendInput, ReadOptions, RequestExpectation, Result, SendInput, TargetSelection, Unsubscribe } from './model.js';
5
- import type { ExternalAuthorInput, IdentityHost, IdentityCapabilities, ServerSession, HostInvalidation } from './host.js';
5
+ import type { ExternalAuthorInput, ExternalContributionV1, ExternalContributionOutcomeV1, ExternalContributionCapabilityV1, IdentityHost, IdentityCapabilities, ServerSession, HostInvalidation } from './host.js';
6
6
 
7
7
  declare const resolvedTarget: unique symbol;
8
8
  export interface TargetSnapshot {
@@ -61,6 +61,10 @@ export interface IdentityServer<RequestContext, ToolContext, Input> extends Disp
61
61
  personRequest(context: RequestContext, options?: ReadOptions): Promise<Result<PersonRequest<Input>>>;
62
62
  toolProvenance(context: ToolContext): Promise<Result<ExecutionProvenance>>;
63
63
  sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
64
+ externalContributionCapabilities(): Result<ExternalContributionCapabilityV1 | null>;
65
+ sendExternalContribution(contribution: ExternalContributionV1, input: SendInput<Input>, options?: ReadOptions): Promise<ExternalContributionOutcomeV1>;
66
+ lookupExternalContribution(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<ExternalContributionOutcomeV1>>>;
67
+
64
68
  /** Lookup reports retained operation outcomes; IDs are scoped to this instance/plugin. */
65
69
  lookupOperation(operationId: OperationId, options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
66
70
  }