@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.
- package/README.md +18 -0
- package/bb.d.ts +3 -0
- package/dist/bb-entry-runtime.js +235 -70
- package/dist/host-runtime.js +169 -12
- package/dist/index-runtime.js +33 -1
- package/dist/model-runtime.js +34 -1
- package/dist/server-runtime.js +3 -0
- package/dist/testing-runtime.js +240 -75
- package/host.d.ts +29 -0
- package/model.d.ts +7 -0
- package/package.json +1 -1
- package/server.d.ts +5 -1
package/README.md
CHANGED
|
@@ -463,3 +463,21 @@ lookup decoding. Later conformance must use a packed artifact with two
|
|
|
463
463
|
independently bundled UI consumers and real upstream→fork→upstream→fork
|
|
464
464
|
sessions. Preserve native content, provider references and upstream grouped-edit
|
|
465
465
|
restrictions in that proof.
|
|
466
|
+
|
|
467
|
+
## External person attribution V1 (0.3)
|
|
468
|
+
|
|
469
|
+
`binding.server.externalContributionCapabilities()` synchronously distinguishes an absent capability (`ok(null)`) from a malformed or incompatible host (`ok: false`). A capable host advertises V1, canonical `person` identities, accepted/queued history, and `rollback: 'requires-capable-host'`. This optional subprotocol fits inside outer protocol 1; legacy `sendExternal` and `lookupOperation` retain their existing wire format and hashes.
|
|
470
|
+
|
|
471
|
+
For a new operation, `sendExternalContribution({version: 1, author, personAttribution}, input)` retains the external Slack/integration origin and an optional canonical person snapshot. The person snapshot contains `identity`, `presentation`, and `evidence: 'integration-asserted'`. It attributes a contribution in a trusted workspace; it conveys no authentication or authorization. Local `default-user` identities are not canonical people.
|
|
472
|
+
|
|
473
|
+
Persist the exact contribution, operation ID, prompt bytes, and selected transport before dispatch. Capability absence may select legacy external-only attribution for a **new** operation. Never upgrade, downgrade, or rebuild an existing operation from a current directory or mapping. A malformed capability is an incompatibility, not permission to fall back.
|
|
474
|
+
|
|
475
|
+
V1 receipts confirm the frozen `externalContribution`. Reconcile V1 exclusively through `lookupExternalContribution`, checking its returned attribution against the reservation. Wrong-operation receipts fail validation. Changed or malformed send receipts and exceptions after dispatch are indeterminate. Unknown/unsupported lookup does not authorize resend; only `absent-final` permits the same immutable operation to be retried. Legacy final receipts remain unchanged.
|
|
476
|
+
|
|
477
|
+
The default host rendering policy adds the integration sender envelope once. A producer such as Rosetta must select `externalMessageRendering: 'producer'` at binding construction; its prompt bytes remain unchanged. A capable native host projects the person into the native timeline while contribution/attempt history keeps the external origin and optional `personAttribution`. This does not add native-message history queries.
|
|
478
|
+
|
|
479
|
+
Native history containing integration-asserted person authors requires a capable runtime even after queues drain or plugins are downgraded. Do not roll back to a host that only accepts provider-verified person authors. Package-only rollback retains the legacy external path; it does not make old native readers understand newer stored authorship.
|
|
480
|
+
|
|
481
|
+
### 0.3.1 decoder corrections
|
|
482
|
+
|
|
483
|
+
V1 external subjects are opaque nonempty strings of at most256 characters, including email-shaped or percent-encoded subjects. They are not BB resource IDs. Contribution and attempt-history output normalize additive person snapshot, identity, and presentation fields while preserving strict V1 input decoding. `externalContributionOutcomeV1Codec` from `/host` validates and normalizes persisted V1 outcomes; consumers must additionally compare submitted receipts with the reserved operation ID and frozen contribution. A stored final lacking that evidence remains uncertainty and must not trigger a resend.
|
package/bb.d.ts
CHANGED
|
@@ -76,6 +76,9 @@ export interface BbInvocationServer {
|
|
|
76
76
|
selfProfile(invocation: BbInvocation): Promise<Result<import('./model.js').IdentityProfile>>;
|
|
77
77
|
personRequest(invocation: BbInvocation): Promise<Result<PersonRequest<BbPromptInput>>>;
|
|
78
78
|
readonly sendExternal: IdentityServer<unknown, unknown, BbPromptInput>['sendExternal'];
|
|
79
|
+
readonly externalContributionCapabilities: IdentityServer<unknown, unknown, BbPromptInput>['externalContributionCapabilities'];
|
|
80
|
+
readonly sendExternalContribution: IdentityServer<unknown, unknown, BbPromptInput>['sendExternalContribution'];
|
|
81
|
+
readonly lookupExternalContribution: IdentityServer<unknown, unknown, BbPromptInput>['lookupExternalContribution'];
|
|
79
82
|
readonly lookupOperation: IdentityServer<unknown, unknown, BbPromptInput>['lookupOperation'];
|
|
80
83
|
/** Bounded, validated provenance reads; disposal fences new raw history calls. */
|
|
81
84
|
readonly history: IdentityServer<unknown, unknown, BbPromptInput>['history'];
|
package/dist/bb-entry-runtime.js
CHANGED
|
@@ -64,6 +64,29 @@ function retainIdentityHttpResponse(response, invocationSignal, requestSignal, r
|
|
|
64
64
|
// bb-upstream-runtime.ts
|
|
65
65
|
import { z } from "zod";
|
|
66
66
|
|
|
67
|
+
// external-person-output-runtime.ts
|
|
68
|
+
function record(value) {
|
|
69
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
function normalizeExternalPersonOutput(value) {
|
|
72
|
+
if (!record(value) || !record(value.identity) || !record(value.presentation))
|
|
73
|
+
return value;
|
|
74
|
+
return {
|
|
75
|
+
identity: { kind: value.identity.kind, key: value.identity.key, issuer: value.identity.issuer, subject: value.identity.subject },
|
|
76
|
+
presentation: { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl },
|
|
77
|
+
evidence: value.evidence
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function normalizeContributionOutput(value) {
|
|
81
|
+
return record(value) && value.personAttribution !== undefined ? { ...value, personAttribution: normalizeExternalPersonOutput(value.personAttribution) } : value;
|
|
82
|
+
}
|
|
83
|
+
function normalizeProvenanceOutput(value) {
|
|
84
|
+
return record(value) && Array.isArray(value.contributions) ? { ...value, contributions: value.contributions.map(normalizeContributionOutput) } : value;
|
|
85
|
+
}
|
|
86
|
+
function normalizeEvidencePageOutput(value, normalize) {
|
|
87
|
+
return record(value) && Array.isArray(value.items) ? { ...value, items: value.items.map(normalize) } : value;
|
|
88
|
+
}
|
|
89
|
+
|
|
67
90
|
// model-runtime.ts
|
|
68
91
|
var ID_MAX_LENGTH = 256;
|
|
69
92
|
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
@@ -172,7 +195,7 @@ function isOrigin(value) {
|
|
|
172
195
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
173
196
|
}
|
|
174
197
|
function isContribution(value) {
|
|
175
|
-
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);
|
|
198
|
+
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);
|
|
176
199
|
}
|
|
177
200
|
function isCorrelation(value) {
|
|
178
201
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -230,6 +253,7 @@ function hasCompleteCausality(groups, contributions, missing) {
|
|
|
230
253
|
}
|
|
231
254
|
var provenanceCodec = {
|
|
232
255
|
decode(input) {
|
|
256
|
+
input = normalizeProvenanceOutput(input);
|
|
233
257
|
if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
|
|
234
258
|
return err("invalid-input", "Invalid execution provenance.");
|
|
235
259
|
}
|
|
@@ -249,6 +273,17 @@ var provenanceCodec = {
|
|
|
249
273
|
return value;
|
|
250
274
|
}
|
|
251
275
|
};
|
|
276
|
+
var externalPersonSnapshotCodec = {
|
|
277
|
+
decode(value) {
|
|
278
|
+
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))) {
|
|
279
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
280
|
+
}
|
|
281
|
+
return ok(value);
|
|
282
|
+
},
|
|
283
|
+
encode(value) {
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
252
287
|
function defaultIdentityKey(instance) {
|
|
253
288
|
return `local:${instance}`;
|
|
254
289
|
}
|
|
@@ -396,39 +431,39 @@ function createBbUpstreamDriver(options) {
|
|
|
396
431
|
const active = new Set;
|
|
397
432
|
const listeners = new Set;
|
|
398
433
|
const labelledExternal = new WeakSet;
|
|
399
|
-
const terminateScope = (
|
|
434
|
+
const terminateScope = (record2, scope) => {
|
|
400
435
|
if (!scope || scope.released)
|
|
401
436
|
return;
|
|
402
437
|
scope.released = true;
|
|
403
|
-
|
|
438
|
+
record2.controller.signal.removeEventListener("abort", scope.parentAbort);
|
|
404
439
|
scope.controller.abort();
|
|
405
|
-
|
|
440
|
+
record2.scopes.delete(scope);
|
|
406
441
|
};
|
|
407
|
-
const terminate = (
|
|
408
|
-
for (const scope of [...
|
|
409
|
-
terminateScope(
|
|
410
|
-
|
|
411
|
-
active.delete(
|
|
442
|
+
const terminate = (record2) => {
|
|
443
|
+
for (const scope of [...record2.scopes])
|
|
444
|
+
terminateScope(record2, scope);
|
|
445
|
+
record2.controller.abort();
|
|
446
|
+
active.delete(record2);
|
|
412
447
|
};
|
|
413
448
|
const known = (context) => {
|
|
414
|
-
const
|
|
415
|
-
return !
|
|
449
|
+
const record2 = records.get(context);
|
|
450
|
+
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);
|
|
416
451
|
};
|
|
417
452
|
const admission = (context) => {
|
|
418
|
-
const
|
|
419
|
-
if (!
|
|
420
|
-
return
|
|
421
|
-
if (
|
|
453
|
+
const record2 = known(context);
|
|
454
|
+
if (!record2.ok)
|
|
455
|
+
return record2;
|
|
456
|
+
if (record2.value.origin === "external")
|
|
422
457
|
return err("unauthenticated", "External upstream work uses an explicit external source.");
|
|
423
|
-
return
|
|
458
|
+
return record2;
|
|
424
459
|
};
|
|
425
460
|
const session = (context) => {
|
|
426
|
-
const
|
|
427
|
-
if (!
|
|
428
|
-
return unavailableSession(instanceId,
|
|
429
|
-
if (
|
|
461
|
+
const record2 = known(context);
|
|
462
|
+
if (!record2.ok)
|
|
463
|
+
return unavailableSession(instanceId, record2.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record2.error.message);
|
|
464
|
+
if (record2.value.origin === "interactive-user")
|
|
430
465
|
return readySession();
|
|
431
|
-
return
|
|
466
|
+
return record2.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
|
|
432
467
|
};
|
|
433
468
|
const driver = {
|
|
434
469
|
instanceId,
|
|
@@ -446,20 +481,20 @@ function createBbUpstreamDriver(options) {
|
|
|
446
481
|
const allowed = admission(context);
|
|
447
482
|
if (!allowed.ok)
|
|
448
483
|
return allowed;
|
|
449
|
-
const
|
|
484
|
+
const record2 = allowed.value;
|
|
450
485
|
let scope;
|
|
451
|
-
const parentAbort = () => terminateScope(
|
|
486
|
+
const parentAbort = () => terminateScope(record2, scope);
|
|
452
487
|
scope = { controller: new AbortController, parentAbort, released: false };
|
|
453
|
-
|
|
454
|
-
const expire = () => terminateScope(
|
|
455
|
-
|
|
456
|
-
const exposedSession =
|
|
457
|
-
const expectedKey =
|
|
488
|
+
record2.scopes.add(scope);
|
|
489
|
+
const expire = () => terminateScope(record2, scope);
|
|
490
|
+
record2.controller.signal.addEventListener("abort", parentAbort, { once: true });
|
|
491
|
+
const exposedSession = record2.origin === "background" ? machineSession() : readySession();
|
|
492
|
+
const expectedKey = record2.origin === "background" ? machineKey.value : singletonKey;
|
|
458
493
|
return ok({
|
|
459
494
|
signal: scope.controller.signal,
|
|
460
495
|
session: exposedSession,
|
|
461
496
|
validate(expected) {
|
|
462
|
-
if (scope.released || scope.controller.signal.aborted ||
|
|
497
|
+
if (scope.released || scope.controller.signal.aborted || record2.controller.signal.aborted || disposed) {
|
|
463
498
|
return err("expired", "Upstream request context is no longer live.");
|
|
464
499
|
}
|
|
465
500
|
return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
|
|
@@ -543,22 +578,22 @@ function createBbUpstreamDriver(options) {
|
|
|
543
578
|
throw new Error("Upstream binding is disposed.");
|
|
544
579
|
const controller = new AbortController;
|
|
545
580
|
const invocation = Object.freeze({ origin, signal: controller.signal });
|
|
546
|
-
const
|
|
547
|
-
records.set(invocation,
|
|
548
|
-
active.add(
|
|
581
|
+
const record2 = { origin, controller, scopes: new Set };
|
|
582
|
+
records.set(invocation, record2);
|
|
583
|
+
active.add(record2);
|
|
549
584
|
return invocation;
|
|
550
585
|
},
|
|
551
586
|
release(invocation) {
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
554
|
-
terminate(
|
|
587
|
+
const record2 = records.get(invocation);
|
|
588
|
+
if (record2)
|
|
589
|
+
terminate(record2);
|
|
555
590
|
},
|
|
556
591
|
dispose() {
|
|
557
592
|
if (disposed)
|
|
558
593
|
return;
|
|
559
594
|
disposed = true;
|
|
560
|
-
for (const
|
|
561
|
-
terminate(
|
|
595
|
+
for (const record2 of [...active])
|
|
596
|
+
terminate(record2);
|
|
562
597
|
for (const listener of [...listeners]) {
|
|
563
598
|
try {
|
|
564
599
|
listener({ kind: "disposed" });
|
|
@@ -766,7 +801,7 @@ function isContribution2(value) {
|
|
|
766
801
|
if (!isRecord3(origin))
|
|
767
802
|
return false;
|
|
768
803
|
const validOrigin = origin.kind === "person" ? snapshot(origin.actor) && isRecord3(origin.actor) && isPersonIdentity(origin.actor.identity) : origin.kind === "machine" ? snapshot(origin.actor) && isRecord3(origin.actor) && isMachineIdentity(origin.actor.identity) : origin.kind === "external" ? snapshot(origin.actor) && isRecord3(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");
|
|
769
|
-
return validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
804
|
+
return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
770
805
|
}
|
|
771
806
|
function decodeEvidencePage(value, decodeItem) {
|
|
772
807
|
if (!isRecord3(value) || typeof value.status !== "string")
|
|
@@ -847,11 +882,11 @@ function createEnhancedDirectory(protocol) {
|
|
|
847
882
|
}
|
|
848
883
|
mappings.set(key, mapping);
|
|
849
884
|
};
|
|
850
|
-
const profile = (source,
|
|
851
|
-
if (!isRecord3(
|
|
885
|
+
const profile = (source, record2, revision) => {
|
|
886
|
+
if (!isRecord3(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)
|
|
852
887
|
return err("incompatible", "Provider directory record is malformed.");
|
|
853
|
-
const issuer =
|
|
854
|
-
const subject =
|
|
888
|
+
const issuer = record2.issuer;
|
|
889
|
+
const subject = record2.subject;
|
|
855
890
|
if (typeof issuer !== "string" || typeof subject !== "string")
|
|
856
891
|
return err("incompatible", "Provider directory record is malformed.");
|
|
857
892
|
let person;
|
|
@@ -862,10 +897,10 @@ function createEnhancedDirectory(protocol) {
|
|
|
862
897
|
}
|
|
863
898
|
if (!person.ok)
|
|
864
899
|
return person;
|
|
865
|
-
if (person.value.kind !== "person" || person.value.issuer !==
|
|
900
|
+
if (person.value.kind !== "person" || person.value.issuer !== record2.issuer || person.value.subject !== record2.subject || !identityKeyCodec.decode(person.value.key).ok) {
|
|
866
901
|
return err("incompatible", "Provider directory returned an invalid person reference.");
|
|
867
902
|
}
|
|
868
|
-
return profileCodec.decode({ identity: person.value, presentation:
|
|
903
|
+
return profileCodec.decode({ identity: person.value, presentation: record2.presentation, revision, status: record2.status });
|
|
869
904
|
};
|
|
870
905
|
return {
|
|
871
906
|
async search(input, options) {
|
|
@@ -891,8 +926,8 @@ function createEnhancedDirectory(protocol) {
|
|
|
891
926
|
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)
|
|
892
927
|
return err("incompatible", "Provider directory page is malformed.");
|
|
893
928
|
const profiles = [];
|
|
894
|
-
for (const
|
|
895
|
-
const decoded = profile(current.value,
|
|
929
|
+
for (const record2 of result.value.records) {
|
|
930
|
+
const decoded = profile(current.value, record2, result.value.revision);
|
|
896
931
|
if (!decoded.ok)
|
|
897
932
|
return decoded;
|
|
898
933
|
if (input.history === "current" && decoded.value.status !== "current")
|
|
@@ -1101,6 +1136,7 @@ function createHostAdapter(options) {
|
|
|
1101
1136
|
});
|
|
1102
1137
|
const contexts = new WeakMap;
|
|
1103
1138
|
return {
|
|
1139
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
1104
1140
|
pluginId: upstream.pluginId,
|
|
1105
1141
|
instanceId: upstream.instanceId,
|
|
1106
1142
|
session: (context) => upstream.session(context),
|
|
@@ -1146,6 +1182,7 @@ function createHostAdapter(options) {
|
|
|
1146
1182
|
const error = protocolError(extension);
|
|
1147
1183
|
const unavailableResult = async () => ({ ok: false, error });
|
|
1148
1184
|
return {
|
|
1185
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
1149
1186
|
pluginId: upstream.pluginId,
|
|
1150
1187
|
instanceId: upstream.instanceId,
|
|
1151
1188
|
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
@@ -1199,7 +1236,7 @@ function createHostAdapter(options) {
|
|
|
1199
1236
|
} catch {
|
|
1200
1237
|
return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
|
|
1201
1238
|
}
|
|
1202
|
-
return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
|
|
1239
|
+
return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeContributionOutput), isContribution2) : result;
|
|
1203
1240
|
},
|
|
1204
1241
|
async attempts(query, readOptions) {
|
|
1205
1242
|
if (!validAttemptQuery(query))
|
|
@@ -1212,7 +1249,7 @@ function createHostAdapter(options) {
|
|
|
1212
1249
|
} catch {
|
|
1213
1250
|
return err("unavailable", "Execution history is unavailable.", "after-reconnect");
|
|
1214
1251
|
}
|
|
1215
|
-
return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
|
|
1252
|
+
return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeProvenanceOutput), (item) => provenanceCodec.decode(item).ok) : result;
|
|
1216
1253
|
}
|
|
1217
1254
|
};
|
|
1218
1255
|
const subscription = protocol.subscribe((event) => {
|
|
@@ -1221,6 +1258,7 @@ function createHostAdapter(options) {
|
|
|
1221
1258
|
});
|
|
1222
1259
|
const scopes = new WeakMap;
|
|
1223
1260
|
return {
|
|
1261
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
1224
1262
|
pluginId: upstream.pluginId,
|
|
1225
1263
|
instanceId: protocol.instanceId,
|
|
1226
1264
|
async session(context) {
|
|
@@ -1307,6 +1345,119 @@ function createHostAdapter(options) {
|
|
|
1307
1345
|
}
|
|
1308
1346
|
};
|
|
1309
1347
|
}
|
|
1348
|
+
function decodeExternalContribution(value, strict = true) {
|
|
1349
|
+
if (!isRecord3(value) || value.version !== 1 || !isRecord3(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 && isRecord3(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)) {
|
|
1350
|
+
return err("invalid-input", "Invalid V1 external contribution.");
|
|
1351
|
+
}
|
|
1352
|
+
let person = value.personAttribution;
|
|
1353
|
+
if (!strict)
|
|
1354
|
+
person = normalizeExternalPersonOutput(person);
|
|
1355
|
+
if (strict && isRecord3(person) && isRecord3(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
|
|
1356
|
+
return err("invalid-input", "Unexpected presentation field.");
|
|
1357
|
+
const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
|
|
1358
|
+
if (decoded && !decoded.ok)
|
|
1359
|
+
return decoded;
|
|
1360
|
+
return ok({
|
|
1361
|
+
version: 1,
|
|
1362
|
+
author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
|
|
1363
|
+
...decoded?.ok ? { personAttribution: decoded.value } : {}
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
var externalContributionV1Codec = {
|
|
1367
|
+
decode: (value) => decodeExternalContribution(value),
|
|
1368
|
+
encode: (value) => value
|
|
1369
|
+
};
|
|
1370
|
+
function canonicalExternal(value) {
|
|
1371
|
+
if (Array.isArray(value))
|
|
1372
|
+
return `[${value.map(canonicalExternal).join(",")}]`;
|
|
1373
|
+
if (isRecord3(value))
|
|
1374
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
|
|
1375
|
+
return JSON.stringify(value);
|
|
1376
|
+
}
|
|
1377
|
+
function decodeExternalContributionOutcomeV1(raw) {
|
|
1378
|
+
const decoded = decodeAcceptance(raw);
|
|
1379
|
+
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
1380
|
+
return decoded;
|
|
1381
|
+
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
1382
|
+
return err("incompatible", "V1 needs durable native acceptance.");
|
|
1383
|
+
const receipt = decoded.value.receipt;
|
|
1384
|
+
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
1385
|
+
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
1386
|
+
}
|
|
1387
|
+
function externalContributionMethods(upstream, extension, disposed) {
|
|
1388
|
+
const support = () => {
|
|
1389
|
+
if (disposed())
|
|
1390
|
+
return err("disposed", "Identity host is disposed.");
|
|
1391
|
+
if (extension.status === "absent")
|
|
1392
|
+
return ok(null);
|
|
1393
|
+
if (extension.status !== "supported")
|
|
1394
|
+
return { ok: false, error: protocolError(extension) };
|
|
1395
|
+
const value = extension.protocol.externalContributions;
|
|
1396
|
+
if (value === undefined)
|
|
1397
|
+
return ok(null);
|
|
1398
|
+
if (!isRecord3(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
|
|
1399
|
+
return err("incompatible", "Malformed external contribution capability.");
|
|
1400
|
+
if (!value.versions.includes(1))
|
|
1401
|
+
return ok(null);
|
|
1402
|
+
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")
|
|
1403
|
+
return err("incompatible", "Malformed V1 external contribution capability.");
|
|
1404
|
+
return ok(value);
|
|
1405
|
+
};
|
|
1406
|
+
return {
|
|
1407
|
+
externalContributionCapabilities() {
|
|
1408
|
+
const negotiated = support();
|
|
1409
|
+
return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
|
|
1410
|
+
},
|
|
1411
|
+
async sendExternalContribution(contribution, input, options) {
|
|
1412
|
+
const operationId = input.operationId;
|
|
1413
|
+
const decoded = decodeExternalContribution(contribution);
|
|
1414
|
+
if (!decoded.ok)
|
|
1415
|
+
return { status: "rejected", error: decoded.error };
|
|
1416
|
+
const negotiated = support();
|
|
1417
|
+
if (!negotiated.ok)
|
|
1418
|
+
return { status: "rejected", error: negotiated.error };
|
|
1419
|
+
if (!negotiated.value)
|
|
1420
|
+
return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
|
|
1421
|
+
if (options?.signal?.aborted)
|
|
1422
|
+
return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
|
|
1423
|
+
const frozen = JSON.parse(JSON.stringify(decoded.value));
|
|
1424
|
+
const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
|
|
1425
|
+
try {
|
|
1426
|
+
const result = decodeExternalContributionOutcomeV1(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
|
|
1427
|
+
if (!result.ok)
|
|
1428
|
+
return { status: "indeterminate", operationId, message: result.error.message };
|
|
1429
|
+
if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
|
|
1430
|
+
return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
|
|
1431
|
+
}
|
|
1432
|
+
return result.value;
|
|
1433
|
+
} catch {
|
|
1434
|
+
return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
|
|
1435
|
+
}
|
|
1436
|
+
},
|
|
1437
|
+
async lookupExternalContribution(operationId, options) {
|
|
1438
|
+
const negotiated = support();
|
|
1439
|
+
if (!negotiated.ok)
|
|
1440
|
+
return negotiated;
|
|
1441
|
+
if (!negotiated.value)
|
|
1442
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
1443
|
+
try {
|
|
1444
|
+
const result = asResult(await negotiated.value.lookupV1(operationId, options));
|
|
1445
|
+
if (!result.ok)
|
|
1446
|
+
return result;
|
|
1447
|
+
if (isRecord3(result.value) && result.value.status === "final") {
|
|
1448
|
+
const decoded = decodeExternalContributionOutcomeV1(result.value.outcome);
|
|
1449
|
+
if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
|
|
1450
|
+
return err("incompatible", "Host lookup returned a receipt for another operation.");
|
|
1451
|
+
}
|
|
1452
|
+
return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
|
|
1453
|
+
}
|
|
1454
|
+
return decodeLookup(result.value);
|
|
1455
|
+
} catch {
|
|
1456
|
+
return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1310
1461
|
function inspectForkExtension(value) {
|
|
1311
1462
|
if (value === undefined)
|
|
1312
1463
|
return { status: "absent" };
|
|
@@ -1464,6 +1615,9 @@ function createIdentityServer(options) {
|
|
|
1464
1615
|
},
|
|
1465
1616
|
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
1466
1617
|
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
1618
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
|
|
1619
|
+
sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
|
|
1620
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
|
|
1467
1621
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
1468
1622
|
dispose
|
|
1469
1623
|
};
|
|
@@ -2283,8 +2437,8 @@ function createBbIdentityServerBinding(options) {
|
|
|
2283
2437
|
const invocations = new WeakMap;
|
|
2284
2438
|
const expired = () => err("expired", "Invocation has already settled.");
|
|
2285
2439
|
const recordFor = (invocation) => {
|
|
2286
|
-
const
|
|
2287
|
-
return disposed || !
|
|
2440
|
+
const record2 = invocations.get(invocation);
|
|
2441
|
+
return disposed || !record2 || record2.settled || record2.signal.aborted ? expired() : ok(record2);
|
|
2288
2442
|
};
|
|
2289
2443
|
const unavailableSession2 = () => ({
|
|
2290
2444
|
status: "unavailable",
|
|
@@ -2292,25 +2446,25 @@ function createBbIdentityServerBinding(options) {
|
|
|
2292
2446
|
error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
|
|
2293
2447
|
});
|
|
2294
2448
|
const openPerson = async (invocation) => {
|
|
2295
|
-
const
|
|
2296
|
-
if (!
|
|
2297
|
-
return
|
|
2298
|
-
const opened = await server.personRequest(
|
|
2299
|
-
if (
|
|
2449
|
+
const record2 = recordFor(invocation);
|
|
2450
|
+
if (!record2.ok)
|
|
2451
|
+
return record2;
|
|
2452
|
+
const opened = await server.personRequest(record2.value.request);
|
|
2453
|
+
if (record2.value.settled || record2.value.signal.aborted) {
|
|
2300
2454
|
if (opened.ok)
|
|
2301
2455
|
opened.value.dispose();
|
|
2302
2456
|
return expired();
|
|
2303
2457
|
}
|
|
2304
2458
|
if (opened.ok)
|
|
2305
|
-
|
|
2459
|
+
record2.value.people.add(opened.value);
|
|
2306
2460
|
return opened;
|
|
2307
2461
|
};
|
|
2308
2462
|
const guardEndpoint = async (invocation, operation) => {
|
|
2309
|
-
const
|
|
2310
|
-
if (!
|
|
2311
|
-
return
|
|
2312
|
-
const result = await operation(
|
|
2313
|
-
return
|
|
2463
|
+
const record2 = recordFor(invocation);
|
|
2464
|
+
if (!record2.ok)
|
|
2465
|
+
return record2;
|
|
2466
|
+
const result = await operation(record2.value.request);
|
|
2467
|
+
return record2.value.settled || record2.value.signal.aborted ? expired() : result;
|
|
2314
2468
|
};
|
|
2315
2469
|
let disposeBinding = () => {};
|
|
2316
2470
|
const guardedHistory = {
|
|
@@ -2331,11 +2485,11 @@ function createBbIdentityServerBinding(options) {
|
|
|
2331
2485
|
instanceId: server.instanceId,
|
|
2332
2486
|
commits: server.commits,
|
|
2333
2487
|
async session(invocation) {
|
|
2334
|
-
const
|
|
2335
|
-
if (!
|
|
2488
|
+
const record2 = recordFor(invocation);
|
|
2489
|
+
if (!record2.ok)
|
|
2336
2490
|
return unavailableSession2();
|
|
2337
|
-
const session = await server.session(
|
|
2338
|
-
return
|
|
2491
|
+
const session = await server.session(record2.value.request);
|
|
2492
|
+
return record2.value.settled || record2.value.signal.aborted ? unavailableSession2() : session;
|
|
2339
2493
|
},
|
|
2340
2494
|
selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
|
|
2341
2495
|
personRequest: openPerson,
|
|
@@ -2345,6 +2499,17 @@ function createBbIdentityServerBinding(options) {
|
|
|
2345
2499
|
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
|
|
2346
2500
|
return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
|
|
2347
2501
|
},
|
|
2502
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity binding is disposed.") : server.externalContributionCapabilities(),
|
|
2503
|
+
async sendExternalContribution(contribution, input, readOptions) {
|
|
2504
|
+
if (disposed)
|
|
2505
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
|
|
2506
|
+
const decoded = externalContributionV1Codec.decode(contribution);
|
|
2507
|
+
if (!decoded.ok)
|
|
2508
|
+
return { status: "rejected", error: decoded.error };
|
|
2509
|
+
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, decoded.value.author, input.input) : input.input;
|
|
2510
|
+
return server.sendExternalContribution(decoded.value, { ...input, input: renderedInput }, readOptions);
|
|
2511
|
+
},
|
|
2512
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupExternalContribution(operationId, readOptions),
|
|
2348
2513
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
|
|
2349
2514
|
history: guardedHistory
|
|
2350
2515
|
};
|
|
@@ -2401,18 +2566,18 @@ function createBbIdentityServerBinding(options) {
|
|
|
2401
2566
|
return endpointRegistered;
|
|
2402
2567
|
}
|
|
2403
2568
|
const createInvocation = (context) => {
|
|
2404
|
-
const
|
|
2569
|
+
const record2 = { request: context.request, signal: context.signal, people: new Set, settled: false };
|
|
2405
2570
|
const invocation = {
|
|
2406
2571
|
origin: context.origin,
|
|
2407
2572
|
signal: context.signal,
|
|
2408
2573
|
person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
|
|
2409
2574
|
};
|
|
2410
|
-
invocations.set(invocation,
|
|
2575
|
+
invocations.set(invocation, record2);
|
|
2411
2576
|
const finish = () => {
|
|
2412
|
-
|
|
2413
|
-
for (const request of
|
|
2577
|
+
record2.settled = true;
|
|
2578
|
+
for (const request of record2.people)
|
|
2414
2579
|
request.dispose();
|
|
2415
|
-
|
|
2580
|
+
record2.people.clear();
|
|
2416
2581
|
};
|
|
2417
2582
|
return { invocation, finish };
|
|
2418
2583
|
};
|