@phosphorco/bb-identity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +465 -0
- package/bb.d.ts +138 -0
- package/client.d.ts +175 -0
- package/dist/bb-entry-runtime.js +2574 -0
- package/dist/client-entry-runtime.js +2705 -0
- package/dist/host-runtime.js +932 -0
- package/dist/index-runtime.js +201 -0
- package/dist/model-runtime.js +207 -0
- package/dist/react-entry-runtime.js +3324 -0
- package/dist/server-runtime.js +263 -0
- package/dist/state-controller-runtime.js +950 -0
- package/dist/state-runtime.js +1200 -0
- package/dist/state-service-runtime.js +380 -0
- package/dist/state-transport-runtime.js +349 -0
- package/dist/testing-runtime.js +4084 -0
- package/host.d.ts +277 -0
- package/index.d.ts +3 -0
- package/model.d.ts +231 -0
- package/package.json +110 -0
- package/react.d.ts +129 -0
- package/server.d.ts +80 -0
- package/state.d.ts +170 -0
- package/testing.d.ts +202 -0
|
@@ -0,0 +1,4084 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// model-runtime.ts
|
|
3
|
+
var ID_MAX_LENGTH = 256;
|
|
4
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
5
|
+
function failure(code, message, retry = "never") {
|
|
6
|
+
return { code, message, retry };
|
|
7
|
+
}
|
|
8
|
+
function ok(value) {
|
|
9
|
+
return { ok: true, value };
|
|
10
|
+
}
|
|
11
|
+
function err(code, message, retry = "never") {
|
|
12
|
+
return { ok: false, error: failure(code, message, retry) };
|
|
13
|
+
}
|
|
14
|
+
function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
function isBoundedString(value, maximum = ID_MAX_LENGTH) {
|
|
18
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
19
|
+
}
|
|
20
|
+
function idCodec(kind) {
|
|
21
|
+
return {
|
|
22
|
+
decode(input) {
|
|
23
|
+
const valid = kind === "server-session" ? isBoundedString(input, 4096) && !/[\u0000-\u0020\u007f]/.test(input) : isBoundedString(input) && ID_PATTERN.test(input);
|
|
24
|
+
if (!valid) {
|
|
25
|
+
return err("invalid-input", `Invalid ${kind} identifier.`);
|
|
26
|
+
}
|
|
27
|
+
return ok(input);
|
|
28
|
+
},
|
|
29
|
+
encode(value) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
var identityKeyCodec = {
|
|
35
|
+
decode(input) {
|
|
36
|
+
if (!isBoundedString(input, 512) || /[\u0000-\u001f\u007f]/.test(input)) {
|
|
37
|
+
return err("invalid-input", "Invalid identity key.");
|
|
38
|
+
}
|
|
39
|
+
return ok(input);
|
|
40
|
+
},
|
|
41
|
+
encode(value) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function decodePresentation(value) {
|
|
46
|
+
return isRecord(value) && isBoundedString(value.displayName, 512) && (value.handle === null || isBoundedString(value.handle, 512)) && (value.avatarUrl === null || isBoundedString(value.avatarUrl, 2048));
|
|
47
|
+
}
|
|
48
|
+
function decodeIdentity(value) {
|
|
49
|
+
if (!isRecord(value) || !identityKeyCodec.decode(value.key).ok)
|
|
50
|
+
return false;
|
|
51
|
+
if (value.kind === "person")
|
|
52
|
+
return isBoundedString(value.issuer, 512) && isBoundedString(value.subject, 512);
|
|
53
|
+
if (value.kind === "default-user")
|
|
54
|
+
return isBoundedString(value.instanceId);
|
|
55
|
+
if (value.kind === "machine") {
|
|
56
|
+
return isBoundedString(value.instanceId) && (value.hostId === null || isBoundedString(value.hostId, 512));
|
|
57
|
+
}
|
|
58
|
+
return value.kind === "external" && isBoundedString(value.pluginId) && isBoundedString(value.subject, 512);
|
|
59
|
+
}
|
|
60
|
+
var profileCodec = {
|
|
61
|
+
decode(input) {
|
|
62
|
+
if (!isRecord(input) || !decodeIdentity(input.identity) || !decodePresentation(input.presentation) || !isBoundedString(input.revision) || input.status !== "current" && input.status !== "historical") {
|
|
63
|
+
return err("invalid-input", "Invalid identity profile.");
|
|
64
|
+
}
|
|
65
|
+
return ok(input);
|
|
66
|
+
},
|
|
67
|
+
encode(value) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
function isContributionReference(value) {
|
|
72
|
+
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && idCodec("contribution").decode(value.contributionId).ok;
|
|
73
|
+
}
|
|
74
|
+
function isPersonReference(value) {
|
|
75
|
+
return isRecord(value) && (value.kind === "person" || value.kind === "default-user") && decodeIdentity(value);
|
|
76
|
+
}
|
|
77
|
+
function isMachineReference(value) {
|
|
78
|
+
return isRecord(value) && value.kind === "machine" && decodeIdentity(value);
|
|
79
|
+
}
|
|
80
|
+
function isActorSnapshot(value) {
|
|
81
|
+
if (!isRecord(value) || !decodeIdentity(value.identity) || !decodePresentation(value.presentation))
|
|
82
|
+
return false;
|
|
83
|
+
const evidence = value.evidence;
|
|
84
|
+
if (evidence !== "provider-verified" && evidence !== "local-user" && evidence !== "upstream-default" && evidence !== "integration-asserted" && evidence !== "legacy" && evidence !== "machine")
|
|
85
|
+
return false;
|
|
86
|
+
return isMachineReference(value.identity) ? evidence === "machine" : evidence !== "machine";
|
|
87
|
+
}
|
|
88
|
+
function isOrigin(value) {
|
|
89
|
+
if (!isRecord(value))
|
|
90
|
+
return false;
|
|
91
|
+
if (value.kind === "person") {
|
|
92
|
+
const actor = value.actor;
|
|
93
|
+
return isActorSnapshot(actor) && isRecord(actor) && isPersonReference(actor.identity);
|
|
94
|
+
}
|
|
95
|
+
if (value.kind === "machine") {
|
|
96
|
+
const actor = value.actor;
|
|
97
|
+
return isActorSnapshot(actor) && isRecord(actor) && isMachineReference(actor.identity) && isRecord(actor) && actor.evidence === "machine";
|
|
98
|
+
}
|
|
99
|
+
if (value.kind === "external") {
|
|
100
|
+
const actor = value.actor;
|
|
101
|
+
return isActorSnapshot(actor) && isRecord(actor) && isRecord(actor.identity) && actor.identity.kind === "external" && actor.evidence !== "machine";
|
|
102
|
+
}
|
|
103
|
+
if (value.kind === "agent")
|
|
104
|
+
return value.agentId === null || isBoundedString(value.agentId);
|
|
105
|
+
if (value.kind === "system")
|
|
106
|
+
return isBoundedString(value.reason, 1024);
|
|
107
|
+
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
108
|
+
}
|
|
109
|
+
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);
|
|
111
|
+
}
|
|
112
|
+
function isCorrelation(value) {
|
|
113
|
+
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
114
|
+
}
|
|
115
|
+
function referenceKey(value) {
|
|
116
|
+
return `${value.threadId}\x00${value.contributionId}`;
|
|
117
|
+
}
|
|
118
|
+
function isInputSource(value) {
|
|
119
|
+
if (!isRecord(value))
|
|
120
|
+
return false;
|
|
121
|
+
if (value.kind === "contribution")
|
|
122
|
+
return isContributionReference(value.reference);
|
|
123
|
+
if (value.kind === "interaction")
|
|
124
|
+
return isBoundedString(value.interactionId) && isOrigin(value.resolver);
|
|
125
|
+
return value.kind === "generated" && (value.purpose === "continuation" || value.purpose === "resolved-resource" || value.purpose === "system-context") && Array.isArray(value.basedOn) && value.basedOn.every(isContributionReference);
|
|
126
|
+
}
|
|
127
|
+
function causalReferences(groups) {
|
|
128
|
+
const references = [];
|
|
129
|
+
for (const group of groups) {
|
|
130
|
+
if (!isRecord(group) || !Array.isArray(group.sources) || !group.sources.every(isInputSource))
|
|
131
|
+
return null;
|
|
132
|
+
for (const source of group.sources) {
|
|
133
|
+
if (!isRecord(source))
|
|
134
|
+
return null;
|
|
135
|
+
if (source.kind === "contribution" && isContributionReference(source.reference))
|
|
136
|
+
references.push(source.reference);
|
|
137
|
+
if (source.kind === "generated" && Array.isArray(source.basedOn) && source.basedOn.every(isContributionReference))
|
|
138
|
+
references.push(...source.basedOn);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return references;
|
|
142
|
+
}
|
|
143
|
+
function hasCompleteCausality(groups, contributions, missing) {
|
|
144
|
+
const references = causalReferences(groups);
|
|
145
|
+
if (references === null || !contributions.every(isContribution) || !missing.every(isContributionReference))
|
|
146
|
+
return false;
|
|
147
|
+
const required = new Set(references.map(referenceKey));
|
|
148
|
+
const found = new Set;
|
|
149
|
+
for (const contribution of contributions) {
|
|
150
|
+
if (!isRecord(contribution) || !isContributionReference(contribution.reference))
|
|
151
|
+
return false;
|
|
152
|
+
const key = referenceKey(contribution.reference);
|
|
153
|
+
if (found.has(key))
|
|
154
|
+
return false;
|
|
155
|
+
found.add(key);
|
|
156
|
+
}
|
|
157
|
+
const absent = new Set;
|
|
158
|
+
for (const reference of missing) {
|
|
159
|
+
const key = referenceKey(reference);
|
|
160
|
+
if (absent.has(key) || found.has(key))
|
|
161
|
+
return false;
|
|
162
|
+
absent.add(key);
|
|
163
|
+
}
|
|
164
|
+
return required.size === found.size + absent.size && [...required].every((key) => found.has(key) || absent.has(key));
|
|
165
|
+
}
|
|
166
|
+
var provenanceCodec = {
|
|
167
|
+
decode(input) {
|
|
168
|
+
if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
|
|
169
|
+
return err("invalid-input", "Invalid execution provenance.");
|
|
170
|
+
}
|
|
171
|
+
if (input.status === "unknown")
|
|
172
|
+
return (input.correlation === null || isCorrelation(input.correlation)) && isBoundedString(input.reason, 1024) ? ok(input) : err("invalid-input", "Unknown provenance needs a valid correlation and reason.");
|
|
173
|
+
if (!Array.isArray(input.inputGroups) || !Array.isArray(input.contributions))
|
|
174
|
+
return err("invalid-input", "Provenance needs input groups and contributions.");
|
|
175
|
+
if (input.status === "known") {
|
|
176
|
+
return isCorrelation(input.correlation) && hasCompleteCausality(input.inputGroups, input.contributions, []) ? ok(input) : err("invalid-input", "Known provenance needs complete correlated evidence.");
|
|
177
|
+
}
|
|
178
|
+
if ((input.correlation === null || isCorrelation(input.correlation)) && Array.isArray(input.missing) && isBoundedString(input.reason, 1024) && hasCompleteCausality(input.inputGroups, input.contributions, input.missing)) {
|
|
179
|
+
return ok(input);
|
|
180
|
+
}
|
|
181
|
+
return err("invalid-input", "Partial provenance needs complete causal accounting.");
|
|
182
|
+
},
|
|
183
|
+
encode(value) {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
function defaultIdentityKey(instance) {
|
|
188
|
+
return `local:${instance}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// host-runtime.ts
|
|
192
|
+
function isRecord2(value) {
|
|
193
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
194
|
+
}
|
|
195
|
+
function unavailable(message) {
|
|
196
|
+
return { code: "unavailable", message, retry: "after-reconnect" };
|
|
197
|
+
}
|
|
198
|
+
function isPresentation(value) {
|
|
199
|
+
return isRecord2(value) && typeof value.displayName === "string" && (value.handle === null || typeof value.handle === "string") && (value.avatarUrl === null || typeof value.avatarUrl === "string");
|
|
200
|
+
}
|
|
201
|
+
function isPersonIdentity(value) {
|
|
202
|
+
if (!isRecord2(value) || !identityKeyCodec.decode(value.key).ok)
|
|
203
|
+
return false;
|
|
204
|
+
return value.kind === "person" ? typeof value.issuer === "string" && typeof value.subject === "string" : value.kind === "default-user" && idCodec("instance").decode(value.instanceId).ok;
|
|
205
|
+
}
|
|
206
|
+
function isMachineIdentity(value) {
|
|
207
|
+
return isRecord2(value) && value.kind === "machine" && identityKeyCodec.decode(value.key).ok && idCodec("instance").decode(value.instanceId).ok && (value.hostId === null || typeof value.hostId === "string" && value.hostId.length > 0 && value.hostId.length <= 512);
|
|
208
|
+
}
|
|
209
|
+
function isActorIdentity(value) {
|
|
210
|
+
return isPersonIdentity(value) || isMachineIdentity(value);
|
|
211
|
+
}
|
|
212
|
+
function isCapabilities(value) {
|
|
213
|
+
return isRecord2(value) && (value.requestIdentity === "singleton" || value.requestIdentity === "host-resolved") && (value.acceptance === "pre-dispatch-check" || value.acceptance === "transactional-check") && (value.forwarding === "singleton-convention" || value.forwarding === "host-bound") && isRecord2(value.directory) && typeof value.directory.search === "boolean" && typeof value.directory.lookup === "boolean" && typeof value.participants === "boolean" && (value.externalSend === "source-labelled" || value.externalSend === "structured") && (value.toolProvenance === "unknown" || value.toolProvenance === "partial" || value.toolProvenance === "causal") && typeof value.operationLookup === "boolean";
|
|
214
|
+
}
|
|
215
|
+
function asResult(value) {
|
|
216
|
+
if (!isRecord2(value) || typeof value.ok !== "boolean")
|
|
217
|
+
return err("incompatible", "Host returned a malformed Result.");
|
|
218
|
+
if (value.ok)
|
|
219
|
+
return "value" in value ? ok(value.value) : err("incompatible", "Host returned a successful Result without a value.");
|
|
220
|
+
const failure2 = decodeError(value.error);
|
|
221
|
+
return failure2.ok ? { ok: false, error: failure2.value } : failure2;
|
|
222
|
+
}
|
|
223
|
+
function decodeError(value) {
|
|
224
|
+
const codes = new Set([
|
|
225
|
+
"unavailable",
|
|
226
|
+
"unauthenticated",
|
|
227
|
+
"unsupported",
|
|
228
|
+
"incompatible",
|
|
229
|
+
"invalid-input",
|
|
230
|
+
"not-found",
|
|
231
|
+
"ambiguous",
|
|
232
|
+
"stale-owner",
|
|
233
|
+
"stale-context",
|
|
234
|
+
"conflict",
|
|
235
|
+
"cancelled",
|
|
236
|
+
"disposed",
|
|
237
|
+
"limit-exceeded",
|
|
238
|
+
"invalid-operation",
|
|
239
|
+
"expired"
|
|
240
|
+
]);
|
|
241
|
+
const retries = new Set(["never", "after-refresh", "after-reconnect", "same-operation"]);
|
|
242
|
+
return isRecord2(value) && typeof value.code === "string" && codes.has(value.code) && typeof value.message === "string" && typeof value.retry === "string" && retries.has(value.retry) ? ok(value) : err("incompatible", "Host returned a malformed identity error.");
|
|
243
|
+
}
|
|
244
|
+
function isReference(value) {
|
|
245
|
+
return isRecord2(value) && idCodec("thread").decode(value.threadId).ok && idCodec("contribution").decode(value.contributionId).ok;
|
|
246
|
+
}
|
|
247
|
+
function isNativeIds(value) {
|
|
248
|
+
return isRecord2(value) && ["deliveryId", "queuedMessageId", "turnId"].every((key) => value[key] === null || typeof value[key] === "string");
|
|
249
|
+
}
|
|
250
|
+
function decodeAcceptance(value) {
|
|
251
|
+
if (!isRecord2(value) || typeof value.status !== "string")
|
|
252
|
+
return err("incompatible", "Host returned a malformed acceptance outcome.");
|
|
253
|
+
if (value.status === "rejected") {
|
|
254
|
+
const error = decodeError(value.error);
|
|
255
|
+
return error.ok ? ok({ status: "rejected", error: error.value }) : error;
|
|
256
|
+
}
|
|
257
|
+
if (value.status === "indeterminate") {
|
|
258
|
+
const operationId2 = idCodec("operation").decode(value.operationId);
|
|
259
|
+
return operationId2.ok && typeof value.message === "string" ? ok({ status: "indeterminate", operationId: operationId2.value, message: value.message }) : err("incompatible", "Host returned malformed indeterminate acceptance metadata.");
|
|
260
|
+
}
|
|
261
|
+
if (value.status !== "submitted" || !isRecord2(value.receipt))
|
|
262
|
+
return err("incompatible", "Host returned an unknown acceptance outcome.");
|
|
263
|
+
const receipt = value.receipt;
|
|
264
|
+
const operationId = idCodec("operation").decode(receipt.operationId);
|
|
265
|
+
if (!operationId.ok || !isNativeIds(receipt.native))
|
|
266
|
+
return err("incompatible", "Host returned malformed acceptance receipt identifiers.");
|
|
267
|
+
if (receipt.evidence === "host-accepted") {
|
|
268
|
+
return typeof receipt.acceptedAt === "string" && Array.isArray(receipt.references) && receipt.references.every(isReference) && receipt.provenance === "structured" && receipt.deduplication === "guaranteed" && typeof receipt.retainedUntil === "string" ? ok(value) : err("incompatible", "Host returned malformed durable acceptance metadata.");
|
|
269
|
+
}
|
|
270
|
+
if (receipt.evidence === "upstream-response") {
|
|
271
|
+
return receipt.acceptedAt === null && receipt.references === null && (receipt.provenance === "source-labelled" || receipt.provenance === "upstream-default") && receipt.deduplication === "not-guaranteed" ? ok(value) : err("incompatible", "Host returned malformed upstream acceptance metadata.");
|
|
272
|
+
}
|
|
273
|
+
return err("incompatible", "Host returned an unknown acceptance receipt evidence value.");
|
|
274
|
+
}
|
|
275
|
+
function decodeLookup(value) {
|
|
276
|
+
if (!isRecord2(value) || typeof value.status !== "string")
|
|
277
|
+
return err("incompatible", "Host returned a malformed operation lookup.");
|
|
278
|
+
if (value.status === "final") {
|
|
279
|
+
const outcome = decodeAcceptance(value.outcome);
|
|
280
|
+
return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
|
|
281
|
+
}
|
|
282
|
+
if (value.status === "pending")
|
|
283
|
+
return ok({ status: "pending" });
|
|
284
|
+
if (value.status === "absent-final" && value.retry === "same-operation-only")
|
|
285
|
+
return ok({ status: "absent-final", retry: "same-operation-only" });
|
|
286
|
+
if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable")) {
|
|
287
|
+
return ok({ status: "unknown", reason: value.reason });
|
|
288
|
+
}
|
|
289
|
+
return err("incompatible", "Host returned malformed operation lookup metadata.");
|
|
290
|
+
}
|
|
291
|
+
var serverSessionCodec = {
|
|
292
|
+
decode(input) {
|
|
293
|
+
if (!isRecord2(input) || typeof input.status !== "string" || !identityKeyCodec.decode(input.instanceId).ok) {
|
|
294
|
+
return err("invalid-input", "Invalid server session.");
|
|
295
|
+
}
|
|
296
|
+
if (input.status !== "ready") {
|
|
297
|
+
if ((input.status === "unavailable" || input.status === "unauthenticated" || input.status === "incompatible") && isRecord2(input.error)) {
|
|
298
|
+
return ok(input);
|
|
299
|
+
}
|
|
300
|
+
return err("invalid-input", "Invalid unavailable server session.");
|
|
301
|
+
}
|
|
302
|
+
if (!isRecord2(input.actor) || !isActorIdentity(input.actor.identity) || !isPresentation(input.actor.presentation) || input.actor.evidence !== "provider-verified" && input.actor.evidence !== "local-user" && input.actor.evidence !== "upstream-default" && input.actor.evidence !== "integration-asserted" && input.actor.evidence !== "legacy" && input.actor.evidence !== "machine" || isMachineIdentity(input.actor.identity) !== (input.actor.evidence === "machine") || !isCapabilities(input.capabilities) || typeof input.stamp !== "string" || input.mode !== "single-user" && input.mode !== "multi-user") {
|
|
303
|
+
return err("invalid-input", "Invalid ready server session.");
|
|
304
|
+
}
|
|
305
|
+
return ok(input);
|
|
306
|
+
},
|
|
307
|
+
encode(value) {
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
function unsupportedDirectory() {
|
|
312
|
+
return {
|
|
313
|
+
search: async () => err("unsupported", "Directory search is unavailable."),
|
|
314
|
+
getMany: async () => err("unsupported", "Directory lookup is unavailable."),
|
|
315
|
+
subscribe: () => () => {}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function unsupportedParticipants() {
|
|
319
|
+
return {
|
|
320
|
+
list: async () => err("unsupported", "Participant history is unavailable."),
|
|
321
|
+
previews: async () => err("unsupported", "Participant previews are unavailable.")
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function unsupportedHistory() {
|
|
325
|
+
return {
|
|
326
|
+
contributions: async (_query) => ok({ status: "unavailable", reason: "unsupported" }),
|
|
327
|
+
attempts: async (_query) => ok({ status: "unavailable", reason: "unsupported" })
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function isProfilePresentation(value) {
|
|
331
|
+
if (!isRecord2(value))
|
|
332
|
+
return false;
|
|
333
|
+
const displayName = value.displayName;
|
|
334
|
+
const handle = value.handle;
|
|
335
|
+
const avatarUrl = value.avatarUrl;
|
|
336
|
+
return typeof displayName === "string" && displayName.length > 0 && displayName.length <= 512 && (handle === null || typeof handle === "string" && handle.length > 0 && handle.length <= 512) && (avatarUrl === null || typeof avatarUrl === "string" && avatarUrl.length > 0 && avatarUrl.length <= 2048);
|
|
337
|
+
}
|
|
338
|
+
function isExternalIdentity(value) {
|
|
339
|
+
return isRecord2(value) && value.kind === "external" && identityKeyCodec.decode(value.key).ok && idCodec("plugin").decode(value.pluginId).ok && idCodec("subject").decode(value.subject).ok;
|
|
340
|
+
}
|
|
341
|
+
function isParticipant(value) {
|
|
342
|
+
if (!isRecord2(value))
|
|
343
|
+
return false;
|
|
344
|
+
const roles = value.roles;
|
|
345
|
+
return (isActorIdentity(value.identity) || isExternalIdentity(value.identity)) && isProfilePresentation(value.presentation) && Array.isArray(roles) && roles.length > 0 && new Set(roles).size === roles.length && roles.every((role) => role === "author" || role === "editor" || role === "mentioned" || role === "interaction-resolver");
|
|
346
|
+
}
|
|
347
|
+
function decodeParticipantPage(value) {
|
|
348
|
+
if (!isRecord2(value) || !Array.isArray(value.items) || value.nextCursor !== null && !idCodec("cursor").decode(value.nextCursor).ok || !idCodec("revision").decode(value.revision).ok || value.coverage !== "complete-history" && value.coverage !== "partial-history") {
|
|
349
|
+
return err("incompatible", "Host returned a malformed participant page.");
|
|
350
|
+
}
|
|
351
|
+
const items = [];
|
|
352
|
+
for (const item of value.items) {
|
|
353
|
+
if (!isParticipant(item) || !isRecord2(item) || !Array.isArray(item.roles))
|
|
354
|
+
return err("incompatible", "Host returned a malformed participant item.");
|
|
355
|
+
const identity = profileCodec.decode({ identity: item.identity, presentation: item.presentation, revision: "participant", status: "current" });
|
|
356
|
+
if (!identity.ok)
|
|
357
|
+
return err("incompatible", "Host returned a malformed participant identity.");
|
|
358
|
+
items.push({ identity: identity.value.identity, presentation: identity.value.presentation, roles: [...item.roles] });
|
|
359
|
+
}
|
|
360
|
+
const cursor = value.nextCursor === null ? null : idCodec("cursor").decode(value.nextCursor);
|
|
361
|
+
const revision = idCodec("revision").decode(value.revision);
|
|
362
|
+
if (cursor !== null && !cursor.ok || !revision.ok)
|
|
363
|
+
return err("incompatible", "Host returned malformed participant identifiers.");
|
|
364
|
+
return ok({ items, nextCursor: cursor === null ? null : cursor.value, revision: revision.value, coverage: value.coverage });
|
|
365
|
+
}
|
|
366
|
+
function isContribution2(value) {
|
|
367
|
+
if (!isRecord2(value) || !isReference(value.reference) || typeof value.acceptedAt !== "string" || value.acceptedAt.length === 0 || value.acceptedAt.length > 256 || !Array.isArray(value.mentionedPeople) || !value.mentionedPeople.every(isPersonIdentity))
|
|
368
|
+
return false;
|
|
369
|
+
const snapshot = (candidate) => {
|
|
370
|
+
if (!isRecord2(candidate) || !(isActorIdentity(candidate.identity) || isExternalIdentity(candidate.identity)) || !isProfilePresentation(candidate.presentation))
|
|
371
|
+
return false;
|
|
372
|
+
const evidence = candidate.evidence;
|
|
373
|
+
if (!["provider-verified", "local-user", "upstream-default", "integration-asserted", "legacy", "machine"].includes(evidence))
|
|
374
|
+
return false;
|
|
375
|
+
return isMachineIdentity(candidate.identity) ? evidence === "machine" : evidence !== "machine";
|
|
376
|
+
};
|
|
377
|
+
const origin = value.author;
|
|
378
|
+
if (!isRecord2(origin))
|
|
379
|
+
return false;
|
|
380
|
+
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));
|
|
382
|
+
}
|
|
383
|
+
function decodeEvidencePage(value, decodeItem) {
|
|
384
|
+
if (!isRecord2(value) || typeof value.status !== "string")
|
|
385
|
+
return err("incompatible", "Host returned a malformed evidence page.");
|
|
386
|
+
if (value.status === "pending")
|
|
387
|
+
return ok({ status: "pending" });
|
|
388
|
+
if (value.status === "unavailable")
|
|
389
|
+
return value.reason === "unsupported" || value.reason === "missing-history" || value.reason === "outage" ? ok({ status: "unavailable", reason: value.reason }) : err("incompatible", "Host returned an invalid evidence-unavailable reason.");
|
|
390
|
+
if (value.status !== "known" && value.status !== "partial" || !Array.isArray(value.items) || !value.items.every(decodeItem) || !Array.isArray(value.missing) || !value.missing.every((missing) => typeof missing === "string" && missing.length > 0 && missing.length <= 512) || value.nextCursor !== null && !idCodec("cursor").decode(value.nextCursor).ok || value.traversal !== "complete" && value.traversal !== "continued" || value.status === "known" && value.missing.length !== 0)
|
|
391
|
+
return err("incompatible", "Host returned malformed evidence metadata.");
|
|
392
|
+
return ok(value);
|
|
393
|
+
}
|
|
394
|
+
function validContributionQuery(query) {
|
|
395
|
+
const cursor = query.cursor === undefined || idCodec("cursor").decode(query.cursor).ok;
|
|
396
|
+
const limit = query.limit === undefined || Number.isInteger(query.limit) && query.limit >= 1 && query.limit <= 200;
|
|
397
|
+
if (!cursor || !limit)
|
|
398
|
+
return false;
|
|
399
|
+
return query.kind === "references" ? query.references.length <= 100 && query.references.every(isReference) : idCodec("thread").decode(query.threadId).ok && idCodec(query.kind === "native-message" ? "message" : "event").decode(query.kind === "native-message" ? query.messageId : query.eventId).ok;
|
|
400
|
+
}
|
|
401
|
+
function validAttemptQuery(query) {
|
|
402
|
+
return (query.cursor === undefined || idCodec("cursor").decode(query.cursor).ok) && (query.limit === undefined || Number.isInteger(query.limit) && query.limit >= 1 && query.limit <= 200) && (query.kind === "operation" ? idCodec("operation").decode(query.operationId).ok : isReference(query.reference));
|
|
403
|
+
}
|
|
404
|
+
function decodeP6rPersonKey(key) {
|
|
405
|
+
const prefix = "p6r-person:v1:";
|
|
406
|
+
if (!key.startsWith(prefix))
|
|
407
|
+
return null;
|
|
408
|
+
const encoded = key.slice(prefix.length);
|
|
409
|
+
const divider = encoded.indexOf(":");
|
|
410
|
+
if (divider < 1 || divider === encoded.length - 1)
|
|
411
|
+
return null;
|
|
412
|
+
const encodedIssuer = encoded.slice(0, divider);
|
|
413
|
+
const encodedSubject = encoded.slice(divider + 1);
|
|
414
|
+
try {
|
|
415
|
+
const issuer = decodeURIComponent(encodedIssuer);
|
|
416
|
+
const subject = decodeURIComponent(encodedSubject);
|
|
417
|
+
return encodeURIComponent(issuer) === encodedIssuer && encodeURIComponent(subject) === encodedSubject && idCodec("issuer").decode(issuer).ok && idCodec("subject").decode(subject).ok ? { issuer, subject } : null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function createEnhancedDirectory(protocol) {
|
|
423
|
+
const mappings = new Map;
|
|
424
|
+
let activeGeneration = null;
|
|
425
|
+
const sources = () => {
|
|
426
|
+
let raw;
|
|
427
|
+
try {
|
|
428
|
+
raw = protocol.directorySources();
|
|
429
|
+
} catch {
|
|
430
|
+
return err("unavailable", "Enhanced provider directory is unavailable.", "after-reconnect");
|
|
431
|
+
}
|
|
432
|
+
if (!Array.isArray(raw))
|
|
433
|
+
return err("incompatible", "Enhanced provider directory sources are malformed.");
|
|
434
|
+
if (raw.length > 1)
|
|
435
|
+
return err("incompatible", "Enhanced protocol exposed multiple active provider directories.");
|
|
436
|
+
for (const source of raw) {
|
|
437
|
+
if (!isRecord2(source) || !Array.isArray(source.issuers) || source.issuers.length === 0 || new Set(source.issuers).size !== source.issuers.length || !source.issuers.every((issuer) => idCodec("issuer").decode(issuer).ok) || !idCodec("provider-generation").decode(source.generation).ok || typeof source.person !== "function" || source.directory !== undefined && typeof source.directory !== "function" || source.lookup !== undefined && typeof source.lookup !== "function")
|
|
438
|
+
return err("incompatible", "Enhanced provider directory source is malformed.");
|
|
439
|
+
}
|
|
440
|
+
const generation = raw[0]?.generation ?? null;
|
|
441
|
+
if (activeGeneration !== generation) {
|
|
442
|
+
mappings.clear();
|
|
443
|
+
activeGeneration = generation;
|
|
444
|
+
}
|
|
445
|
+
return ok(raw);
|
|
446
|
+
};
|
|
447
|
+
const sameGeneration = (generation) => {
|
|
448
|
+
const current = sources();
|
|
449
|
+
if (!current.ok)
|
|
450
|
+
return current;
|
|
451
|
+
const source = current.value[0];
|
|
452
|
+
return source && source.generation === generation ? ok(source) : err("unavailable", "Provider directory generation changed during the read.", "after-reconnect");
|
|
453
|
+
};
|
|
454
|
+
const remember = (key, mapping) => {
|
|
455
|
+
if (mappings.size >= 512 && !mappings.has(key)) {
|
|
456
|
+
const oldest = mappings.keys().next().value;
|
|
457
|
+
if (oldest !== undefined)
|
|
458
|
+
mappings.delete(oldest);
|
|
459
|
+
}
|
|
460
|
+
mappings.set(key, mapping);
|
|
461
|
+
};
|
|
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)
|
|
464
|
+
return err("incompatible", "Provider directory record is malformed.");
|
|
465
|
+
const issuer = record.issuer;
|
|
466
|
+
const subject = record.subject;
|
|
467
|
+
if (typeof issuer !== "string" || typeof subject !== "string")
|
|
468
|
+
return err("incompatible", "Provider directory record is malformed.");
|
|
469
|
+
let person;
|
|
470
|
+
try {
|
|
471
|
+
person = asResult(source.person(issuer, subject));
|
|
472
|
+
} catch {
|
|
473
|
+
return err("unavailable", "Provider person normalization failed.", "after-reconnect");
|
|
474
|
+
}
|
|
475
|
+
if (!person.ok)
|
|
476
|
+
return person;
|
|
477
|
+
if (person.value.kind !== "person" || person.value.issuer !== record.issuer || person.value.subject !== record.subject || !identityKeyCodec.decode(person.value.key).ok) {
|
|
478
|
+
return err("incompatible", "Provider directory returned an invalid person reference.");
|
|
479
|
+
}
|
|
480
|
+
return profileCodec.decode({ identity: person.value, presentation: record.presentation, revision, status: record.status });
|
|
481
|
+
};
|
|
482
|
+
return {
|
|
483
|
+
async search(input, options) {
|
|
484
|
+
if (typeof input.query !== "string" || input.query.length > 512 || !Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100 || !Array.isArray(input.kinds) || input.kinds.some((kind) => kind !== "person" && kind !== "default-user" && kind !== "machine" && kind !== "external") || input.history !== "current" && input.history !== "include-historical")
|
|
485
|
+
return err("invalid-input", "Invalid directory search request.");
|
|
486
|
+
const available = sources();
|
|
487
|
+
if (!available.ok)
|
|
488
|
+
return available;
|
|
489
|
+
const source = available.value[0];
|
|
490
|
+
if (!source || !source.directory || !input.kinds.includes("person"))
|
|
491
|
+
return err("unsupported", "Provider directory search is unavailable.");
|
|
492
|
+
let result;
|
|
493
|
+
try {
|
|
494
|
+
result = asResult(await source.directory(input, options));
|
|
495
|
+
} catch {
|
|
496
|
+
return err("unavailable", "Provider directory search failed.", "after-reconnect");
|
|
497
|
+
}
|
|
498
|
+
if (!result.ok)
|
|
499
|
+
return result;
|
|
500
|
+
const current = sameGeneration(source.generation);
|
|
501
|
+
if (!current.ok)
|
|
502
|
+
return current;
|
|
503
|
+
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
|
+
return err("incompatible", "Provider directory page is malformed.");
|
|
505
|
+
const profiles = [];
|
|
506
|
+
for (const record of result.value.records) {
|
|
507
|
+
const decoded = profile(current.value, record, result.value.revision);
|
|
508
|
+
if (!decoded.ok)
|
|
509
|
+
return decoded;
|
|
510
|
+
if (input.history === "current" && decoded.value.status !== "current")
|
|
511
|
+
return err("incompatible", "Provider directory returned historical data for a current query.");
|
|
512
|
+
if (decoded.value.identity.kind !== "person")
|
|
513
|
+
return err("incompatible", "Provider directory returned a non-person profile.");
|
|
514
|
+
remember(decoded.value.identity.key, { generation: current.value.generation, issuer: decoded.value.identity.issuer, subject: decoded.value.identity.subject });
|
|
515
|
+
profiles.push(decoded.value);
|
|
516
|
+
}
|
|
517
|
+
const nextCursor = result.value.nextCursor === null ? null : idCodec("cursor").decode(result.value.nextCursor);
|
|
518
|
+
const revision = idCodec("revision").decode(result.value.revision);
|
|
519
|
+
if (nextCursor !== null && !nextCursor.ok || !revision.ok)
|
|
520
|
+
return err("incompatible", "Provider directory page identifiers are malformed.");
|
|
521
|
+
return ok({ items: profiles, nextCursor: nextCursor === null ? null : nextCursor.value, revision: revision.value });
|
|
522
|
+
},
|
|
523
|
+
async getMany(input, options) {
|
|
524
|
+
if (!Array.isArray(input.keys) || input.keys.length > 64 || !input.keys.every((key) => identityKeyCodec.decode(key).ok))
|
|
525
|
+
return err("invalid-input", "Invalid profile lookup request.");
|
|
526
|
+
const available = sources();
|
|
527
|
+
if (!available.ok)
|
|
528
|
+
return available;
|
|
529
|
+
const source = available.value[0];
|
|
530
|
+
if (!source || !source.lookup)
|
|
531
|
+
return err("unsupported", "Provider directory lookup is unavailable.");
|
|
532
|
+
const selected = [];
|
|
533
|
+
for (const key of input.keys) {
|
|
534
|
+
const mapped = mappings.get(key);
|
|
535
|
+
if (mapped?.generation === source.generation) {
|
|
536
|
+
selected.push(mapped);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const decoded = decodeP6rPersonKey(key);
|
|
540
|
+
if (!decoded || !source.issuers.includes(decoded.issuer)) {
|
|
541
|
+
selected.push(undefined);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
let person;
|
|
545
|
+
try {
|
|
546
|
+
person = asResult(source.person(decoded.issuer, decoded.subject));
|
|
547
|
+
} catch {
|
|
548
|
+
return err("unavailable", "Provider person normalization failed.", "after-reconnect");
|
|
549
|
+
}
|
|
550
|
+
if (!person.ok)
|
|
551
|
+
return person;
|
|
552
|
+
if (person.value.kind !== "person" || person.value.key !== key || person.value.issuer !== decoded.issuer || person.value.subject !== decoded.subject) {
|
|
553
|
+
return err("incompatible", "Provider directory rejected the canonical profile key.");
|
|
554
|
+
}
|
|
555
|
+
const entry = { generation: source.generation, issuer: decoded.issuer, subject: decoded.subject };
|
|
556
|
+
remember(key, entry);
|
|
557
|
+
selected.push(entry);
|
|
558
|
+
}
|
|
559
|
+
const unresolved = [...new Map(selected.filter((entry) => entry !== undefined).map((entry) => [JSON.stringify([entry.issuer, entry.subject]), entry])).values()];
|
|
560
|
+
if (unresolved.length === 0)
|
|
561
|
+
return ok(input.keys.map((key) => ({ key, status: "missing" })));
|
|
562
|
+
let result;
|
|
563
|
+
try {
|
|
564
|
+
result = asResult(await source.lookup(unresolved.map(({ issuer, subject }) => ({ issuer, subject })), options));
|
|
565
|
+
} catch {
|
|
566
|
+
return err("unavailable", "Provider directory lookup failed.", "after-reconnect");
|
|
567
|
+
}
|
|
568
|
+
if (!result.ok)
|
|
569
|
+
return result;
|
|
570
|
+
const current = sameGeneration(source.generation);
|
|
571
|
+
if (!current.ok)
|
|
572
|
+
return current;
|
|
573
|
+
if (!isRecord2(result.value) || !idCodec("revision").decode(result.value.revision).ok || !Array.isArray(result.value.records) || result.value.records.length !== unresolved.length)
|
|
574
|
+
return err("incompatible", "Provider profile lookup page is malformed.");
|
|
575
|
+
const found = new Map;
|
|
576
|
+
const seen = new Set;
|
|
577
|
+
for (const entry of result.value.records) {
|
|
578
|
+
if (!isRecord2(entry) || typeof entry.issuer !== "string" || typeof entry.subject !== "string")
|
|
579
|
+
return err("incompatible", "Provider profile lookup entry is malformed.");
|
|
580
|
+
const mapping = unresolved.find((candidate) => candidate.issuer === entry.issuer && candidate.subject === entry.subject);
|
|
581
|
+
if (!mapping || seen.has(`${entry.issuer}\x00${entry.subject}`))
|
|
582
|
+
return err("incompatible", "Provider profile lookup response is misaligned.");
|
|
583
|
+
seen.add(`${entry.issuer}\x00${entry.subject}`);
|
|
584
|
+
if (entry.record !== null) {
|
|
585
|
+
if (!isRecord2(entry.record) || entry.record.issuer !== entry.issuer || entry.record.subject !== entry.subject)
|
|
586
|
+
return err("incompatible", "Provider profile lookup identity does not match its requested subject.");
|
|
587
|
+
const decoded = profile(current.value, entry.record, result.value.revision);
|
|
588
|
+
if (!decoded.ok)
|
|
589
|
+
return decoded;
|
|
590
|
+
if (decoded.value.identity.kind !== "person")
|
|
591
|
+
return err("incompatible", "Provider lookup returned a non-person profile.");
|
|
592
|
+
remember(decoded.value.identity.key, { generation: current.value.generation, issuer: decoded.value.identity.issuer, subject: decoded.value.identity.subject });
|
|
593
|
+
found.set(`${entry.issuer}\x00${entry.subject}`, decoded.value);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return ok(input.keys.map((key, index) => {
|
|
597
|
+
const mapping = selected[index];
|
|
598
|
+
const profileValue = mapping === undefined ? undefined : found.get(`${mapping.issuer}\x00${mapping.subject}`);
|
|
599
|
+
return profileValue === undefined ? { key, status: "missing" } : { key, status: "found", profile: profileValue };
|
|
600
|
+
}));
|
|
601
|
+
},
|
|
602
|
+
subscribe: () => () => {}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function sameExpectation(session, expected) {
|
|
606
|
+
return session.actor.identity.key === expected.actor && session.stamp === expected.session ? ok(undefined) : err("stale-context", "The request actor or session changed.", "after-refresh");
|
|
607
|
+
}
|
|
608
|
+
function toLiveRequest(handle) {
|
|
609
|
+
const decoded = serverSessionCodec.decode(handle.session);
|
|
610
|
+
if (!decoded.ok)
|
|
611
|
+
return decoded;
|
|
612
|
+
const session = decoded.value;
|
|
613
|
+
if (session.status !== "ready")
|
|
614
|
+
return err("unauthenticated", "No ready identity session is available.", "after-refresh");
|
|
615
|
+
const request = {
|
|
616
|
+
signal: handle.signal,
|
|
617
|
+
session,
|
|
618
|
+
validate(expected) {
|
|
619
|
+
const local = sameExpectation(session, expected);
|
|
620
|
+
if (!local.ok)
|
|
621
|
+
return local;
|
|
622
|
+
return asResult(handle.validate(expected));
|
|
623
|
+
},
|
|
624
|
+
release: () => handle.release()
|
|
625
|
+
};
|
|
626
|
+
return ok(request);
|
|
627
|
+
}
|
|
628
|
+
function toForkRequest(handle) {
|
|
629
|
+
if (!isRecord2(handle.scope) || typeof handle.scope.release !== "function" || typeof handle.scope.validate !== "function" || !(handle.scope.signal instanceof AbortSignal)) {
|
|
630
|
+
return err("incompatible", "Host returned an invalid enhanced request scope.");
|
|
631
|
+
}
|
|
632
|
+
const request = toLiveRequest(handle);
|
|
633
|
+
return request.ok ? ok({ request: request.value, scope: handle.scope }) : request;
|
|
634
|
+
}
|
|
635
|
+
function normalizeRegistration(raw) {
|
|
636
|
+
const generation = idCodec("provider-generation").decode(raw.generation);
|
|
637
|
+
if (!generation.ok)
|
|
638
|
+
return err("incompatible", "Host returned an invalid provider generation.");
|
|
639
|
+
let disposed = false;
|
|
640
|
+
return ok({
|
|
641
|
+
generation: generation.value,
|
|
642
|
+
configuration: raw.configuration,
|
|
643
|
+
getStatus: () => raw.getStatus(),
|
|
644
|
+
signal: raw.signal,
|
|
645
|
+
subscribe: (listener) => raw.subscribe(listener),
|
|
646
|
+
invalidate: (change) => raw.invalidate(change),
|
|
647
|
+
person(issuer, subject) {
|
|
648
|
+
const result = asResult(raw.person(issuer, subject));
|
|
649
|
+
if (!result.ok)
|
|
650
|
+
return result;
|
|
651
|
+
if (result.value.kind === "default-user" || !identityKeyCodec.decode(result.value.key).ok || result.value.issuer !== issuer || result.value.subject !== subject) {
|
|
652
|
+
return err("incompatible", "Host returned an invalid provider person reference.");
|
|
653
|
+
}
|
|
654
|
+
return ok(result.value);
|
|
655
|
+
},
|
|
656
|
+
dispose() {
|
|
657
|
+
if (disposed)
|
|
658
|
+
return;
|
|
659
|
+
disposed = true;
|
|
660
|
+
raw.dispose();
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
function toForkProvider(provider) {
|
|
665
|
+
const validateReadiness = provider.validateReadiness;
|
|
666
|
+
const directory = provider.directory;
|
|
667
|
+
const lookup = provider.lookup;
|
|
668
|
+
return {
|
|
669
|
+
issuers: provider.issuers,
|
|
670
|
+
...validateReadiness ? { validateReadiness: async (input) => {
|
|
671
|
+
const generation = idCodec("provider-generation").decode(input.generation);
|
|
672
|
+
if (!generation.ok)
|
|
673
|
+
return generation;
|
|
674
|
+
return validateReadiness({ ...input, generation: generation.value });
|
|
675
|
+
} } : {},
|
|
676
|
+
resolve: (evidence) => provider.resolve(evidence),
|
|
677
|
+
...directory ? { directory: (input, options) => directory(input, options) } : {},
|
|
678
|
+
...lookup ? { lookup: (subjects, options) => lookup(subjects, options) } : {}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function wireInput(input, codec) {
|
|
682
|
+
return {
|
|
683
|
+
...input,
|
|
684
|
+
input: input.input.map((value) => codec.encode(value))
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
function protocolError(extension) {
|
|
688
|
+
if (extension.status === "unsupported-version")
|
|
689
|
+
return { code: "incompatible", message: `Unsupported identity protocol version ${extension.version}.`, retry: "never" };
|
|
690
|
+
if (extension.status === "malformed")
|
|
691
|
+
return extension.error;
|
|
692
|
+
return unavailable("The configured identity boundary is unavailable.");
|
|
693
|
+
}
|
|
694
|
+
function createHostAdapter(options) {
|
|
695
|
+
const { upstream, extension } = options;
|
|
696
|
+
const directory = options.directory ?? unsupportedDirectory();
|
|
697
|
+
const participants = unsupportedParticipants();
|
|
698
|
+
const history = unsupportedHistory();
|
|
699
|
+
const listeners = new Set;
|
|
700
|
+
let disposed = false;
|
|
701
|
+
const close = () => {
|
|
702
|
+
if (disposed)
|
|
703
|
+
return;
|
|
704
|
+
disposed = true;
|
|
705
|
+
for (const listener of listeners)
|
|
706
|
+
listener({ kind: "disposed" });
|
|
707
|
+
listeners.clear();
|
|
708
|
+
};
|
|
709
|
+
if (extension.status === "absent") {
|
|
710
|
+
const subscriptions = upstream.subscribe((event) => {
|
|
711
|
+
for (const listener of listeners)
|
|
712
|
+
listener(event);
|
|
713
|
+
});
|
|
714
|
+
const contexts = new WeakMap;
|
|
715
|
+
return {
|
|
716
|
+
pluginId: upstream.pluginId,
|
|
717
|
+
instanceId: upstream.instanceId,
|
|
718
|
+
session: (context) => upstream.session(context),
|
|
719
|
+
selfProfile: (context, readOptions) => upstream.selfProfile(context, readOptions),
|
|
720
|
+
async openPersonRequest(context) {
|
|
721
|
+
const raw = await upstream.openScope(context);
|
|
722
|
+
if (!raw.ok)
|
|
723
|
+
return raw;
|
|
724
|
+
const request = toLiveRequest(raw.value);
|
|
725
|
+
if (request.ok)
|
|
726
|
+
contexts.set(request.value, context);
|
|
727
|
+
return request;
|
|
728
|
+
},
|
|
729
|
+
readToolProvenance: async (context) => ok(upstream.toolCorrelation(context)),
|
|
730
|
+
directory,
|
|
731
|
+
participants,
|
|
732
|
+
history,
|
|
733
|
+
async send(request, input, readOptions) {
|
|
734
|
+
if (request.signal.aborted)
|
|
735
|
+
return { status: "rejected", error: { code: "expired", message: "The request context is no longer live.", retry: "never" } };
|
|
736
|
+
return upstream.submit(input, readOptions);
|
|
737
|
+
},
|
|
738
|
+
sendExternal: (author, input, readOptions) => upstream.submit({ ...input, input: upstream.labelExternal(author, input.input) }, readOptions),
|
|
739
|
+
lookupOperation: async () => ok({ status: "unknown", reason: "unsupported" }),
|
|
740
|
+
async forward(request, destinationPlugin, method, input) {
|
|
741
|
+
const context = contexts.get(request);
|
|
742
|
+
if (!context)
|
|
743
|
+
throw { code: "stale-context", message: "The request scope is no longer live.", retry: "after-refresh" };
|
|
744
|
+
return upstream.forward(context, destinationPlugin, method, input);
|
|
745
|
+
},
|
|
746
|
+
registerProvider: async () => err("unsupported", "Identity provider registration requires the enhanced host."),
|
|
747
|
+
subscribe(listener) {
|
|
748
|
+
listeners.add(listener);
|
|
749
|
+
return () => listeners.delete(listener);
|
|
750
|
+
},
|
|
751
|
+
dispose() {
|
|
752
|
+
subscriptions();
|
|
753
|
+
close();
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
if (extension.status !== "supported") {
|
|
758
|
+
const error = protocolError(extension);
|
|
759
|
+
const unavailableResult = async () => ({ ok: false, error });
|
|
760
|
+
return {
|
|
761
|
+
pluginId: upstream.pluginId,
|
|
762
|
+
instanceId: upstream.instanceId,
|
|
763
|
+
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
764
|
+
selfProfile: unavailableResult,
|
|
765
|
+
openPersonRequest: unavailableResult,
|
|
766
|
+
readToolProvenance: unavailableResult,
|
|
767
|
+
directory,
|
|
768
|
+
participants,
|
|
769
|
+
history,
|
|
770
|
+
send: async () => ({ status: "rejected", error }),
|
|
771
|
+
sendExternal: async () => ({ status: "rejected", error }),
|
|
772
|
+
lookupOperation: unavailableResult,
|
|
773
|
+
forward: async () => {
|
|
774
|
+
throw error;
|
|
775
|
+
},
|
|
776
|
+
registerProvider: unavailableResult,
|
|
777
|
+
subscribe(listener) {
|
|
778
|
+
listeners.add(listener);
|
|
779
|
+
return () => listeners.delete(listener);
|
|
780
|
+
},
|
|
781
|
+
dispose: close
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const protocol = extension.protocol;
|
|
785
|
+
const enhancedDirectory = createEnhancedDirectory(protocol);
|
|
786
|
+
const enhancedParticipants = {
|
|
787
|
+
async list(input, readOptions) {
|
|
788
|
+
if (!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100 || !idCodec("thread").decode(input.threadId).ok || input.cursor !== undefined && !idCodec("cursor").decode(input.cursor).ok)
|
|
789
|
+
return err("invalid-input", "Invalid participant request.");
|
|
790
|
+
if (disposed)
|
|
791
|
+
return err("disposed", "Identity host is disposed.");
|
|
792
|
+
let result;
|
|
793
|
+
try {
|
|
794
|
+
result = asResult(await protocol.participants(input, readOptions));
|
|
795
|
+
} catch {
|
|
796
|
+
return err("unavailable", "Participant history is unavailable.", "after-reconnect");
|
|
797
|
+
}
|
|
798
|
+
return result.ok ? decodeParticipantPage(result.value) : result;
|
|
799
|
+
},
|
|
800
|
+
previews: async () => err("unsupported", "Participant previews are unavailable from this enhanced protocol.")
|
|
801
|
+
};
|
|
802
|
+
const enhancedHistory = {
|
|
803
|
+
async contributions(query, readOptions) {
|
|
804
|
+
if (!validContributionQuery(query))
|
|
805
|
+
return err("invalid-input", "Invalid contribution history request.");
|
|
806
|
+
if (disposed)
|
|
807
|
+
return err("disposed", "Identity host is disposed.");
|
|
808
|
+
let result;
|
|
809
|
+
try {
|
|
810
|
+
result = asResult(await protocol.historyContributions(query, readOptions));
|
|
811
|
+
} catch {
|
|
812
|
+
return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
|
|
813
|
+
}
|
|
814
|
+
return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
|
|
815
|
+
},
|
|
816
|
+
async attempts(query, readOptions) {
|
|
817
|
+
if (!validAttemptQuery(query))
|
|
818
|
+
return err("invalid-input", "Invalid execution history request.");
|
|
819
|
+
if (disposed)
|
|
820
|
+
return err("disposed", "Identity host is disposed.");
|
|
821
|
+
let result;
|
|
822
|
+
try {
|
|
823
|
+
result = asResult(await protocol.historyAttempts(query, readOptions));
|
|
824
|
+
} catch {
|
|
825
|
+
return err("unavailable", "Execution history is unavailable.", "after-reconnect");
|
|
826
|
+
}
|
|
827
|
+
return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
const subscription = protocol.subscribe((event) => {
|
|
831
|
+
for (const listener of listeners)
|
|
832
|
+
listener(event);
|
|
833
|
+
});
|
|
834
|
+
const scopes = new WeakMap;
|
|
835
|
+
return {
|
|
836
|
+
pluginId: upstream.pluginId,
|
|
837
|
+
instanceId: protocol.instanceId,
|
|
838
|
+
async session(context) {
|
|
839
|
+
const decoded = serverSessionCodec.decode(await protocol.session(context));
|
|
840
|
+
if (!decoded.ok)
|
|
841
|
+
return { status: "incompatible", instanceId: protocol.instanceId, error: decoded.error };
|
|
842
|
+
return decoded.value.instanceId === protocol.instanceId ? decoded.value : { status: "incompatible", instanceId: protocol.instanceId, error: { code: "incompatible", message: "Enhanced ready session instance does not match persisted host instance.", retry: "never" } };
|
|
843
|
+
},
|
|
844
|
+
async selfProfile(context, readOptions) {
|
|
845
|
+
const result = asResult(await protocol.selfProfile(context, readOptions));
|
|
846
|
+
if (!result.ok)
|
|
847
|
+
return result;
|
|
848
|
+
return profileCodec.decode(result.value);
|
|
849
|
+
},
|
|
850
|
+
async openPersonRequest(context) {
|
|
851
|
+
const raw = await protocol.openRequest(context);
|
|
852
|
+
if (!raw.ok)
|
|
853
|
+
return raw;
|
|
854
|
+
const enhanced = toForkRequest(raw.value);
|
|
855
|
+
if (!enhanced.ok) {
|
|
856
|
+
raw.value.release();
|
|
857
|
+
return enhanced;
|
|
858
|
+
}
|
|
859
|
+
if (enhanced.value.request.session.instanceId !== protocol.instanceId) {
|
|
860
|
+
raw.value.release();
|
|
861
|
+
return err("incompatible", "Enhanced request instance does not match persisted host instance.");
|
|
862
|
+
}
|
|
863
|
+
scopes.set(enhanced.value.request, enhanced.value.scope);
|
|
864
|
+
return ok(enhanced.value.request);
|
|
865
|
+
},
|
|
866
|
+
async readToolProvenance(context) {
|
|
867
|
+
const result = asResult(await protocol.provenance(context));
|
|
868
|
+
return result.ok ? provenanceCodec.decode(result.value) : result;
|
|
869
|
+
},
|
|
870
|
+
directory: {
|
|
871
|
+
search: (input, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity host is disposed.")) : enhancedDirectory.search(input, readOptions),
|
|
872
|
+
getMany: (input, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity host is disposed.")) : enhancedDirectory.getMany(input, readOptions),
|
|
873
|
+
subscribe: enhancedDirectory.subscribe
|
|
874
|
+
},
|
|
875
|
+
participants: enhancedParticipants,
|
|
876
|
+
history: enhancedHistory,
|
|
877
|
+
async send(request, input, readOptions) {
|
|
878
|
+
const scope = scopes.get(request);
|
|
879
|
+
if (!scope)
|
|
880
|
+
return { status: "rejected", error: { code: "stale-context", message: "The request scope is no longer live.", retry: "after-refresh" } };
|
|
881
|
+
const result = decodeAcceptance(await protocol.accept({ source: { kind: "scope", scope }, input: wireInput(input, upstream.inputCodec) }, readOptions));
|
|
882
|
+
return result.ok ? result.value : { status: "rejected", error: result.error };
|
|
883
|
+
},
|
|
884
|
+
async sendExternal(author, input, readOptions) {
|
|
885
|
+
if (disposed)
|
|
886
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity host is disposed.", retry: "never" } };
|
|
887
|
+
const result = decodeAcceptance(await protocol.accept({ source: { kind: "external", author }, input: wireInput(input, upstream.inputCodec) }, readOptions));
|
|
888
|
+
return result.ok ? result.value : { status: "rejected", error: result.error };
|
|
889
|
+
},
|
|
890
|
+
async lookupOperation(operationId, readOptions) {
|
|
891
|
+
const result = asResult(await protocol.lookup(operationId, readOptions));
|
|
892
|
+
return result.ok ? decodeLookup(result.value) : result;
|
|
893
|
+
},
|
|
894
|
+
async forward(request, destinationPlugin, method, input) {
|
|
895
|
+
const scope = scopes.get(request);
|
|
896
|
+
if (!scope)
|
|
897
|
+
throw { code: "stale-context", message: "The request scope is no longer live.", retry: "after-refresh" };
|
|
898
|
+
return protocol.forwardRpc(scope, { pluginId: destinationPlugin, method }, input);
|
|
899
|
+
},
|
|
900
|
+
async registerProvider(provider) {
|
|
901
|
+
if (disposed)
|
|
902
|
+
return err("disposed", "Identity host is disposed.");
|
|
903
|
+
const result = await protocol.registerProvider(toForkProvider(provider));
|
|
904
|
+
if (!result.ok)
|
|
905
|
+
return result;
|
|
906
|
+
if (disposed) {
|
|
907
|
+
result.value.dispose();
|
|
908
|
+
return err("disposed", "Identity host is disposed.");
|
|
909
|
+
}
|
|
910
|
+
return normalizeRegistration(result.value);
|
|
911
|
+
},
|
|
912
|
+
subscribe(listener) {
|
|
913
|
+
listeners.add(listener);
|
|
914
|
+
return () => listeners.delete(listener);
|
|
915
|
+
},
|
|
916
|
+
dispose() {
|
|
917
|
+
subscription();
|
|
918
|
+
close();
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function inspectForkExtension(value) {
|
|
923
|
+
if (value === undefined)
|
|
924
|
+
return { status: "absent" };
|
|
925
|
+
if (!isRecord2(value) || value.version !== 1 || !idCodec("instance").decode(value.instanceId).ok || typeof value.bindInvocation !== "function" || typeof value.session !== "function" || typeof value.openRequest !== "function" || typeof value.selfProfile !== "function" || typeof value.accept !== "function" || typeof value.lookup !== "function" || typeof value.provenance !== "function" || typeof value.historyContributions !== "function" || typeof value.historyAttempts !== "function" || typeof value.directorySources !== "function" || typeof value.participants !== "function" || typeof value.forwardRpc !== "function" || typeof value.registerProvider !== "function" || typeof value.subscribe !== "function") {
|
|
926
|
+
const version = isRecord2(value) && typeof value.version === "number" ? value.version : null;
|
|
927
|
+
return version === null ? { status: "malformed", error: { code: "incompatible", message: "Malformed experimental_p6rIdentity extension.", retry: "never" } } : { status: "unsupported-version", version };
|
|
928
|
+
}
|
|
929
|
+
return { status: "supported", protocol: value };
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// state-service-runtime.ts
|
|
933
|
+
function isRecord3(value) {
|
|
934
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
935
|
+
}
|
|
936
|
+
function nonEmpty(value) {
|
|
937
|
+
return typeof value === "string" && value.length > 0;
|
|
938
|
+
}
|
|
939
|
+
function decodeAddress(value) {
|
|
940
|
+
if (!isRecord3(value) || !nonEmpty(value.collection) || !nonEmpty(value.recordId))
|
|
941
|
+
return err("invalid-input", "Invalid state address.");
|
|
942
|
+
const instanceId = idCodec("instance").decode(value.instanceId);
|
|
943
|
+
const pluginId = idCodec("plugin").decode(value.pluginId);
|
|
944
|
+
const owner = identityKeyCodec.decode(value.owner);
|
|
945
|
+
return instanceId.ok && pluginId.ok && owner.ok ? ok({ instanceId: instanceId.value, pluginId: pluginId.value, collection: value.collection, recordId: value.recordId, owner: owner.value }) : err("invalid-input", "Invalid state address identifiers.");
|
|
946
|
+
}
|
|
947
|
+
function decodeVersion(value) {
|
|
948
|
+
return isRecord3(value) && nonEmpty(value.epoch) && typeof value.sequence === "number" && Number.isInteger(value.sequence) && value.sequence >= 0 ? ok({ epoch: value.epoch, sequence: value.sequence }) : err("invalid-input", "Invalid state version.");
|
|
949
|
+
}
|
|
950
|
+
function decodeActor(value) {
|
|
951
|
+
if (value === null)
|
|
952
|
+
return ok(null);
|
|
953
|
+
if (!isRecord3(value) || !isRecord3(value.identity) || !isRecord3(value.presentation) || !nonEmpty(value.presentation.displayName) || value.presentation.handle !== null && typeof value.presentation.handle !== "string" || value.presentation.avatarUrl !== null && typeof value.presentation.avatarUrl !== "string" || !["provider-verified", "local-user", "upstream-default", "integration-asserted", "legacy", "machine"].includes(value.evidence)) {
|
|
954
|
+
return err("invalid-input", "Invalid state editor snapshot.");
|
|
955
|
+
}
|
|
956
|
+
const key = identityKeyCodec.decode(value.identity.key);
|
|
957
|
+
if (!key.ok)
|
|
958
|
+
return key;
|
|
959
|
+
const identity = value.identity;
|
|
960
|
+
const presentation = { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl };
|
|
961
|
+
if (identity.kind === "machine") {
|
|
962
|
+
const instanceId = idCodec("instance").decode(identity.instanceId);
|
|
963
|
+
if (!instanceId.ok || value.evidence !== "machine" || identity.hostId !== null && (typeof identity.hostId !== "string" || identity.hostId.length === 0 || identity.hostId.length > 512)) {
|
|
964
|
+
return err("invalid-input", "Invalid machine editor identity.");
|
|
965
|
+
}
|
|
966
|
+
return ok({ identity: { kind: "machine", key: key.value, instanceId: instanceId.value, hostId: identity.hostId }, presentation, evidence: "machine" });
|
|
967
|
+
}
|
|
968
|
+
if (value.evidence === "machine")
|
|
969
|
+
return err("invalid-input", "Machine evidence requires a machine identity.");
|
|
970
|
+
if (identity.kind === "person" && nonEmpty(identity.issuer) && nonEmpty(identity.subject)) {
|
|
971
|
+
return ok({ identity: { kind: "person", key: key.value, issuer: identity.issuer, subject: identity.subject }, presentation, evidence: value.evidence });
|
|
972
|
+
}
|
|
973
|
+
if (identity.kind === "default-user") {
|
|
974
|
+
const instanceId = idCodec("instance").decode(identity.instanceId);
|
|
975
|
+
return instanceId.ok ? ok({ identity: { kind: "default-user", key: key.value, instanceId: instanceId.value }, presentation, evidence: value.evidence }) : instanceId;
|
|
976
|
+
}
|
|
977
|
+
if (identity.kind === "external") {
|
|
978
|
+
const pluginId = idCodec("plugin").decode(identity.pluginId);
|
|
979
|
+
return pluginId.ok && nonEmpty(identity.subject) ? ok({ identity: { kind: "external", key: key.value, pluginId: pluginId.value, subject: identity.subject }, presentation, evidence: value.evidence }) : err("invalid-input", "Invalid external editor identity.");
|
|
980
|
+
}
|
|
981
|
+
return err("invalid-input", "Invalid editor identity.");
|
|
982
|
+
}
|
|
983
|
+
function sameAddress(left, right) {
|
|
984
|
+
return left.instanceId === right.instanceId && left.pluginId === right.pluginId && left.collection === right.collection && left.recordId === right.recordId && left.owner === right.owner;
|
|
985
|
+
}
|
|
986
|
+
function decodeEnvelope(value, codec) {
|
|
987
|
+
if (!isRecord3(value) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0) {
|
|
988
|
+
return err("invalid-input", "Invalid state envelope.");
|
|
989
|
+
}
|
|
990
|
+
const address = decodeAddress(value.address);
|
|
991
|
+
const version = decodeVersion(value.version);
|
|
992
|
+
const decoded = codec.decode(value.value);
|
|
993
|
+
const editor = decodeActor(value.lastEditedBy);
|
|
994
|
+
return address.ok && version.ok && decoded.ok && editor.ok ? ok({ address: address.value, version: version.value, schemaVersion: value.schemaVersion, value: decoded.value, lastEditedBy: editor.value }) : err("invalid-input", "Invalid state envelope fields.");
|
|
995
|
+
}
|
|
996
|
+
function decodeRead(value, codec) {
|
|
997
|
+
if (!isRecord3(value) || typeof value.status !== "string")
|
|
998
|
+
return err("invalid-input", "Invalid state read.");
|
|
999
|
+
if (value.status === "present") {
|
|
1000
|
+
const envelope = decodeEnvelope(value.envelope, codec);
|
|
1001
|
+
return envelope.ok ? ok({ status: "present", envelope: envelope.value }) : envelope;
|
|
1002
|
+
}
|
|
1003
|
+
if (value.status === "empty") {
|
|
1004
|
+
const address = decodeAddress(value.address);
|
|
1005
|
+
const version = decodeVersion(value.version);
|
|
1006
|
+
return address.ok && version.ok ? ok({ status: "empty", address: address.value, version: version.value }) : err("invalid-input", "Invalid empty state read.");
|
|
1007
|
+
}
|
|
1008
|
+
if (value.status === "migration-required") {
|
|
1009
|
+
const address = decodeAddress(value.address);
|
|
1010
|
+
return address.ok && typeof value.storedSchemaVersion === "number" && Number.isInteger(value.storedSchemaVersion) && value.storedSchemaVersion >= 0 ? ok({ status: "migration-required", address: address.value, storedSchemaVersion: value.storedSchemaVersion }) : err("invalid-input", "Invalid migration-required read.");
|
|
1011
|
+
}
|
|
1012
|
+
return err("invalid-input", "Unknown state read status.");
|
|
1013
|
+
}
|
|
1014
|
+
function decodeMutation(value, codec) {
|
|
1015
|
+
if (!isRecord3(value) || value.kind !== "initialize" && value.kind !== "replace" || !isRecord3(value.expected) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0 || typeof value.localGeneration !== "number" || !Number.isInteger(value.localGeneration) || value.localGeneration < 0) {
|
|
1016
|
+
return err("invalid-input", "Invalid state mutation.");
|
|
1017
|
+
}
|
|
1018
|
+
const address = decodeAddress(value.address);
|
|
1019
|
+
const expectedVersion = decodeVersion(value.expectedVersion);
|
|
1020
|
+
const actor = identityKeyCodec.decode(value.expected.actor);
|
|
1021
|
+
const session = idCodec("server-session").decode(value.expected.session);
|
|
1022
|
+
const ownerSession = idCodec("owner-session").decode(value.ownerSession);
|
|
1023
|
+
const operationId = idCodec("operation").decode(value.operationId);
|
|
1024
|
+
const decoded = codec.decode(value.value);
|
|
1025
|
+
return address.ok && expectedVersion.ok && actor.ok && session.ok && ownerSession.ok && operationId.ok && decoded.ok ? ok({
|
|
1026
|
+
kind: value.kind,
|
|
1027
|
+
address: address.value,
|
|
1028
|
+
expectedVersion: expectedVersion.value,
|
|
1029
|
+
expected: { actor: actor.value, session: session.value },
|
|
1030
|
+
ownerSession: ownerSession.value,
|
|
1031
|
+
localGeneration: value.localGeneration,
|
|
1032
|
+
operationId: operationId.value,
|
|
1033
|
+
schemaVersion: value.schemaVersion,
|
|
1034
|
+
value: decoded.value
|
|
1035
|
+
}) : err("invalid-input", "Invalid state mutation fields.");
|
|
1036
|
+
}
|
|
1037
|
+
function freezeDeep(value) {
|
|
1038
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1039
|
+
for (const child of Object.values(value))
|
|
1040
|
+
freezeDeep(child);
|
|
1041
|
+
Object.freeze(value);
|
|
1042
|
+
}
|
|
1043
|
+
return value;
|
|
1044
|
+
}
|
|
1045
|
+
function cloneTyped(input, codec) {
|
|
1046
|
+
try {
|
|
1047
|
+
return codec.decode(structuredClone(codec.encode(input)));
|
|
1048
|
+
} catch {
|
|
1049
|
+
return err("invalid-input", "State value could not be encoded.");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function immutableMutation(input, codec) {
|
|
1053
|
+
const decoded = cloneTyped(input, stateCodecs(codec).mutation);
|
|
1054
|
+
return decoded.ok ? ok(freezeDeep(decoded.value)) : decoded;
|
|
1055
|
+
}
|
|
1056
|
+
function decodeOutcome(value, codec) {
|
|
1057
|
+
if (!isRecord3(value) || typeof value.status !== "string")
|
|
1058
|
+
return err("invalid-input", "Invalid state outcome.");
|
|
1059
|
+
const operationId = idCodec("operation").decode(value.operationId);
|
|
1060
|
+
if (!operationId.ok)
|
|
1061
|
+
return operationId;
|
|
1062
|
+
if (value.status === "saved" || value.status === "unchanged") {
|
|
1063
|
+
const envelope = decodeEnvelope(value.envelope, codec);
|
|
1064
|
+
return envelope.ok ? ok({ status: value.status, envelope: envelope.value, operationId: operationId.value }) : envelope;
|
|
1065
|
+
}
|
|
1066
|
+
if (value.status === "already-initialized" || value.status === "conflict") {
|
|
1067
|
+
const current = decodeRead(value.current, codec);
|
|
1068
|
+
return current.ok ? ok({ status: value.status, current: current.value, operationId: operationId.value }) : current;
|
|
1069
|
+
}
|
|
1070
|
+
return err("invalid-input", "Unknown state outcome status.");
|
|
1071
|
+
}
|
|
1072
|
+
function decodeSave(value, codec) {
|
|
1073
|
+
if (isRecord3(value) && value.status === "indeterminate") {
|
|
1074
|
+
const operationId = idCodec("operation").decode(value.operationId);
|
|
1075
|
+
return operationId.ok ? ok({ status: "indeterminate", operationId: operationId.value }) : operationId;
|
|
1076
|
+
}
|
|
1077
|
+
return decodeOutcome(value, codec);
|
|
1078
|
+
}
|
|
1079
|
+
function assertRead(read, address, schemaVersion) {
|
|
1080
|
+
const actual = read.status === "present" ? read.envelope.address : read.address;
|
|
1081
|
+
if (!sameAddress(actual, address))
|
|
1082
|
+
return err("incompatible", "Storage returned a state record for a different address.");
|
|
1083
|
+
if (read.status === "present" && read.envelope.schemaVersion !== schemaVersion) {
|
|
1084
|
+
return err("incompatible", "Storage returned an unexpected state schema.");
|
|
1085
|
+
}
|
|
1086
|
+
return ok(read);
|
|
1087
|
+
}
|
|
1088
|
+
function assertOutcome(outcome, address, schemaVersion, operationId) {
|
|
1089
|
+
if (outcome.operationId !== operationId)
|
|
1090
|
+
return err("incompatible", "Storage returned an outcome for a different operation.");
|
|
1091
|
+
const read = "envelope" in outcome ? { status: "present", envelope: outcome.envelope } : outcome.current;
|
|
1092
|
+
const checked = assertRead(read, address, schemaVersion);
|
|
1093
|
+
return checked.ok ? ok(outcome) : checked;
|
|
1094
|
+
}
|
|
1095
|
+
function assertSave(save, address, schemaVersion, operationId) {
|
|
1096
|
+
if (save.status === "indeterminate")
|
|
1097
|
+
return save.operationId === operationId ? ok(save) : err("incompatible", "Storage returned an indeterminate result for a different operation.");
|
|
1098
|
+
return assertOutcome(save, address, schemaVersion, operationId);
|
|
1099
|
+
}
|
|
1100
|
+
function addressFor(target, recordId, resource, instanceId) {
|
|
1101
|
+
if (!nonEmpty(recordId))
|
|
1102
|
+
return err("invalid-input", "State record id is required.");
|
|
1103
|
+
const snapshot = target.snapshot();
|
|
1104
|
+
return ok({ instanceId, pluginId: resource.pluginId, collection: resource.definition.collection, recordId, owner: snapshot.subject.key });
|
|
1105
|
+
}
|
|
1106
|
+
function createStateService(options) {
|
|
1107
|
+
const resource = { pluginId: options.pluginId, definition: options.definition };
|
|
1108
|
+
const codecs = stateCodecs(options.definition.codec);
|
|
1109
|
+
const staticMutation = (target, mutation) => {
|
|
1110
|
+
const expected = addressFor(target, mutation.address.recordId, resource, options.instanceId);
|
|
1111
|
+
if (!expected.ok)
|
|
1112
|
+
return expected;
|
|
1113
|
+
const snapshot = target.snapshot();
|
|
1114
|
+
return sameAddress(mutation.address, expected.value) && mutation.schemaVersion === options.definition.schemaVersion && mutation.expected.actor === snapshot.expected.actor && mutation.expected.session === snapshot.expected.session ? ok(undefined) : err("invalid-input", "Mutation does not match this state resource and target.");
|
|
1115
|
+
};
|
|
1116
|
+
return {
|
|
1117
|
+
async read(target, recordId, readOptions) {
|
|
1118
|
+
if (target.signal.aborted)
|
|
1119
|
+
return err("expired", "The request context is no longer live.");
|
|
1120
|
+
const address = addressFor(target, recordId, resource, options.instanceId);
|
|
1121
|
+
if (!address.ok)
|
|
1122
|
+
return address;
|
|
1123
|
+
const result = await options.storage.read(address.value, readOptions);
|
|
1124
|
+
if (!result.ok)
|
|
1125
|
+
return result;
|
|
1126
|
+
const decoded = cloneTyped(result.value, codecs.read);
|
|
1127
|
+
return decoded.ok ? assertRead(decoded.value, address.value, options.definition.schemaVersion) : decoded;
|
|
1128
|
+
},
|
|
1129
|
+
async save(target, mutation, readOptions) {
|
|
1130
|
+
const submitted = immutableMutation(mutation, options.definition.codec);
|
|
1131
|
+
if (!submitted.ok)
|
|
1132
|
+
return submitted;
|
|
1133
|
+
const valid = staticMutation(target, submitted.value);
|
|
1134
|
+
if (!valid.ok)
|
|
1135
|
+
return valid;
|
|
1136
|
+
const targetSnapshot = target.snapshot();
|
|
1137
|
+
const expected = Object.freeze({ actor: targetSnapshot.expected.actor, session: targetSnapshot.expected.session });
|
|
1138
|
+
const subject = targetSnapshot.subject.key;
|
|
1139
|
+
const editor = freezeDeep({
|
|
1140
|
+
identity: { ...targetSnapshot.actor.identity },
|
|
1141
|
+
presentation: { ...targetSnapshot.actor.presentation },
|
|
1142
|
+
evidence: targetSnapshot.actor.evidence
|
|
1143
|
+
});
|
|
1144
|
+
const committed = await options.storage.commit({
|
|
1145
|
+
mutation: submitted.value,
|
|
1146
|
+
validateAtCommit: () => {
|
|
1147
|
+
if (readOptions?.signal?.aborted)
|
|
1148
|
+
return err("cancelled", "State save was cancelled before durable acceptance.");
|
|
1149
|
+
const scope = {
|
|
1150
|
+
instanceId: options.instanceId,
|
|
1151
|
+
pluginId: options.pluginId,
|
|
1152
|
+
collection: options.definition.collection,
|
|
1153
|
+
recordId: submitted.value.address.recordId,
|
|
1154
|
+
subject,
|
|
1155
|
+
expected,
|
|
1156
|
+
schemaVersion: options.definition.schemaVersion
|
|
1157
|
+
};
|
|
1158
|
+
const live = options.commits.validate(target, scope);
|
|
1159
|
+
return live.ok ? ok(editor) : live;
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
if (!committed.ok)
|
|
1163
|
+
return committed;
|
|
1164
|
+
const decoded = cloneTyped(committed.value, codecs.save);
|
|
1165
|
+
if (!decoded.ok)
|
|
1166
|
+
return decoded;
|
|
1167
|
+
const checked = assertSave(decoded.value, submitted.value.address, options.definition.schemaVersion, submitted.value.operationId);
|
|
1168
|
+
if (!checked.ok)
|
|
1169
|
+
return checked;
|
|
1170
|
+
const outcome = checked.value;
|
|
1171
|
+
const publishedEnvelope = outcome.status === "saved" ? outcome.envelope : outcome.status === "already-initialized" && outcome.current.status === "present" ? outcome.current.envelope : null;
|
|
1172
|
+
if (publishedEnvelope) {
|
|
1173
|
+
try {
|
|
1174
|
+
options.publish({ address: publishedEnvelope.address, version: publishedEnvelope.version, operationId: outcome.operationId });
|
|
1175
|
+
} catch {}
|
|
1176
|
+
}
|
|
1177
|
+
return ok(outcome);
|
|
1178
|
+
},
|
|
1179
|
+
async reconcile(target, recordId, operationId) {
|
|
1180
|
+
const address = addressFor(target, recordId, resource, options.instanceId);
|
|
1181
|
+
if (!address.ok)
|
|
1182
|
+
return address;
|
|
1183
|
+
const result = await options.storage.reconcile({ address: address.value, operationId });
|
|
1184
|
+
if (!result.ok)
|
|
1185
|
+
return result;
|
|
1186
|
+
const decoded = cloneTyped(result.value, codecs.lookup);
|
|
1187
|
+
if (!decoded.ok || decoded.value.status !== "final")
|
|
1188
|
+
return decoded;
|
|
1189
|
+
const checked = assertOutcome(decoded.value.outcome, address.value, options.definition.schemaVersion, operationId);
|
|
1190
|
+
return checked.ok ? ok({ status: "final", outcome: checked.value }) : checked;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function decodeLookup2(value, codec) {
|
|
1195
|
+
if (!isRecord3(value) || typeof value.status !== "string")
|
|
1196
|
+
return err("invalid-input", "Invalid state lookup.");
|
|
1197
|
+
if (value.status === "final") {
|
|
1198
|
+
const outcome = decodeOutcome(value.outcome, codec);
|
|
1199
|
+
return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
|
|
1200
|
+
}
|
|
1201
|
+
if (value.status === "pending")
|
|
1202
|
+
return ok({ status: "pending" });
|
|
1203
|
+
if (value.status === "absent-final" && value.retry === "same-operation-only")
|
|
1204
|
+
return ok({ status: "absent-final", retry: "same-operation-only" });
|
|
1205
|
+
if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable"))
|
|
1206
|
+
return ok({ status: "unknown", reason: value.reason });
|
|
1207
|
+
return err("invalid-input", "Invalid state lookup metadata.");
|
|
1208
|
+
}
|
|
1209
|
+
function encodeAddress(address) {
|
|
1210
|
+
return { instanceId: address.instanceId, pluginId: address.pluginId, collection: address.collection, recordId: address.recordId, owner: address.owner };
|
|
1211
|
+
}
|
|
1212
|
+
function encodeVersion(version) {
|
|
1213
|
+
return { epoch: version.epoch, sequence: version.sequence };
|
|
1214
|
+
}
|
|
1215
|
+
function encodeActor(actor) {
|
|
1216
|
+
return actor === null ? null : { identity: actor.identity, presentation: actor.presentation, evidence: actor.evidence };
|
|
1217
|
+
}
|
|
1218
|
+
function encodeEnvelope(envelope, codec) {
|
|
1219
|
+
return { address: encodeAddress(envelope.address), version: encodeVersion(envelope.version), schemaVersion: envelope.schemaVersion, value: codec.encode(envelope.value), lastEditedBy: encodeActor(envelope.lastEditedBy) };
|
|
1220
|
+
}
|
|
1221
|
+
function encodeRead(read, codec) {
|
|
1222
|
+
return read.status === "present" ? { status: "present", envelope: encodeEnvelope(read.envelope, codec) } : read.status === "empty" ? { status: "empty", address: encodeAddress(read.address), version: encodeVersion(read.version) } : read.status === "migration-required" ? { status: "migration-required", address: encodeAddress(read.address), storedSchemaVersion: read.storedSchemaVersion } : (() => {
|
|
1223
|
+
throw new TypeError("Invalid state read status.");
|
|
1224
|
+
})();
|
|
1225
|
+
}
|
|
1226
|
+
function encodeMutation(mutation, codec) {
|
|
1227
|
+
return { ...mutation, address: encodeAddress(mutation.address), expectedVersion: encodeVersion(mutation.expectedVersion), value: codec.encode(mutation.value) };
|
|
1228
|
+
}
|
|
1229
|
+
function encodeOutcome(outcome, codec) {
|
|
1230
|
+
if (outcome.status === "saved" || outcome.status === "unchanged") {
|
|
1231
|
+
return { status: outcome.status, envelope: encodeEnvelope(outcome.envelope, codec), operationId: outcome.operationId };
|
|
1232
|
+
}
|
|
1233
|
+
if ("current" in outcome)
|
|
1234
|
+
return { status: outcome.status, current: encodeRead(outcome.current, codec), operationId: outcome.operationId };
|
|
1235
|
+
throw new TypeError("Invalid state outcome.");
|
|
1236
|
+
}
|
|
1237
|
+
function encodeSave(save, codec) {
|
|
1238
|
+
return save.status === "indeterminate" ? { status: "indeterminate", operationId: save.operationId } : encodeOutcome(save, codec);
|
|
1239
|
+
}
|
|
1240
|
+
function stateCodecs(value) {
|
|
1241
|
+
return {
|
|
1242
|
+
read: { decode: (input) => decodeRead(input, value), encode: (input) => encodeRead(input, value) },
|
|
1243
|
+
mutation: { decode: (input) => decodeMutation(input, value), encode: (input) => encodeMutation(input, value) },
|
|
1244
|
+
save: { decode: (input) => decodeSave(input, value), encode: (input) => encodeSave(input, value) },
|
|
1245
|
+
invalidation: {
|
|
1246
|
+
decode(input) {
|
|
1247
|
+
if (!isRecord3(input))
|
|
1248
|
+
return err("invalid-input", "Invalid state invalidation.");
|
|
1249
|
+
const address = decodeAddress(input.address);
|
|
1250
|
+
const version = decodeVersion(input.version);
|
|
1251
|
+
const operationId = input.operationId === null ? ok(null) : idCodec("operation").decode(input.operationId);
|
|
1252
|
+
return address.ok && version.ok && operationId.ok ? ok({ address: address.value, version: version.value, operationId: operationId.value }) : err("invalid-input", "Invalid state invalidation fields.");
|
|
1253
|
+
},
|
|
1254
|
+
encode: (input) => ({ address: encodeAddress(input.address), version: encodeVersion(input.version), operationId: input.operationId })
|
|
1255
|
+
},
|
|
1256
|
+
lookup: {
|
|
1257
|
+
decode: (input) => decodeLookup2(input, value),
|
|
1258
|
+
encode: (input) => input.status === "final" ? { status: "final", outcome: encodeOutcome(input.outcome, value) } : input
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// client-connection-runtime.ts
|
|
1264
|
+
var errorCodes = new Set([
|
|
1265
|
+
"unavailable",
|
|
1266
|
+
"unauthenticated",
|
|
1267
|
+
"unsupported",
|
|
1268
|
+
"incompatible",
|
|
1269
|
+
"invalid-input",
|
|
1270
|
+
"not-found",
|
|
1271
|
+
"ambiguous",
|
|
1272
|
+
"stale-owner",
|
|
1273
|
+
"stale-context",
|
|
1274
|
+
"conflict",
|
|
1275
|
+
"cancelled",
|
|
1276
|
+
"disposed",
|
|
1277
|
+
"limit-exceeded",
|
|
1278
|
+
"invalid-operation",
|
|
1279
|
+
"expired"
|
|
1280
|
+
]);
|
|
1281
|
+
var retryKinds = new Set(["never", "after-refresh", "after-reconnect", "same-operation"]);
|
|
1282
|
+
function isRecord4(value) {
|
|
1283
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1284
|
+
}
|
|
1285
|
+
function decodeError2(value) {
|
|
1286
|
+
if (!isRecord4(value) || typeof value.code !== "string" || !errorCodes.has(value.code) || typeof value.message !== "string" || typeof value.retry !== "string" || !retryKinds.has(value.retry)) {
|
|
1287
|
+
return err("incompatible", "Malformed identity transport error.");
|
|
1288
|
+
}
|
|
1289
|
+
return ok({ code: value.code, message: value.message, retry: value.retry });
|
|
1290
|
+
}
|
|
1291
|
+
function isJson(value) {
|
|
1292
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1293
|
+
return true;
|
|
1294
|
+
if (Array.isArray(value))
|
|
1295
|
+
return value.every(isJson);
|
|
1296
|
+
return isRecord4(value) && Object.values(value).every(isJson);
|
|
1297
|
+
}
|
|
1298
|
+
var jsonCodec = {
|
|
1299
|
+
decode(value) {
|
|
1300
|
+
return isJson(value) ? ok(value) : err("invalid-input", "Expected JSON payload.");
|
|
1301
|
+
},
|
|
1302
|
+
encode(value) {
|
|
1303
|
+
return value;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
var stateInvalidationCodec = stateCodecs(jsonCodec).invalidation;
|
|
1307
|
+
var clientInvalidationCodec = {
|
|
1308
|
+
decode(value) {
|
|
1309
|
+
if (!isRecord4(value) || typeof value.kind !== "string")
|
|
1310
|
+
return err("invalid-input", "Invalid identity invalidation.");
|
|
1311
|
+
if (value.kind === "session") {
|
|
1312
|
+
const reason = value.reason;
|
|
1313
|
+
return reason === undefined ? ok({ kind: "session" }) : reason === "actor" || reason === "provider" || reason === "capabilities" ? ok({ kind: "session", reason }) : err("invalid-input", "Invalid session invalidation reason.");
|
|
1314
|
+
}
|
|
1315
|
+
if (value.kind === "disconnected" || value.kind === "reconnected")
|
|
1316
|
+
return ok({ kind: value.kind });
|
|
1317
|
+
if (value.kind === "directory" && Array.isArray(value.keys)) {
|
|
1318
|
+
const keys = value.keys.map((key) => identityKeyCodec.decode(key));
|
|
1319
|
+
const revision = value.revision === null ? ok(null) : idCodec("revision").decode(value.revision);
|
|
1320
|
+
return keys.every((key) => key.ok) && revision.ok ? ok({ kind: "directory", keys: keys.map((key) => key.value), revision: revision.value }) : err("invalid-input", "Invalid directory invalidation keys.");
|
|
1321
|
+
}
|
|
1322
|
+
if (value.kind === "participants" && Array.isArray(value.threadIds)) {
|
|
1323
|
+
const threadIds = value.threadIds.map((id) => idCodec("thread").decode(id));
|
|
1324
|
+
return threadIds.every((id) => id.ok) ? ok({ kind: "participants", threadIds: threadIds.map((id) => id.value) }) : err("invalid-input", "Invalid participant invalidation thread IDs.");
|
|
1325
|
+
}
|
|
1326
|
+
return err("invalid-input", "Unknown identity invalidation.");
|
|
1327
|
+
},
|
|
1328
|
+
encode(value) {
|
|
1329
|
+
return value;
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
function decodeEvent(value) {
|
|
1333
|
+
const client = clientInvalidationCodec.decode(value);
|
|
1334
|
+
if (client.ok)
|
|
1335
|
+
return client.value;
|
|
1336
|
+
if (isRecord4(value) && value.kind === "state") {
|
|
1337
|
+
const state = stateInvalidationCodec.decode(value.event);
|
|
1338
|
+
if (state.ok)
|
|
1339
|
+
return { kind: "state", event: state.value };
|
|
1340
|
+
}
|
|
1341
|
+
return null;
|
|
1342
|
+
}
|
|
1343
|
+
function cloneHealth(value) {
|
|
1344
|
+
const clone = (link) => link.status === "unavailable" ? { status: "unavailable", error: { ...link.error } } : { status: link.status };
|
|
1345
|
+
return { generation: value.generation, identity: clone(value.identity), state: clone(value.state) };
|
|
1346
|
+
}
|
|
1347
|
+
function cancelled(options) {
|
|
1348
|
+
return options?.signal?.aborted ? err("cancelled", "Identity request was cancelled.") : null;
|
|
1349
|
+
}
|
|
1350
|
+
function createIdentityConnection(inputs) {
|
|
1351
|
+
let disposed = false;
|
|
1352
|
+
const channel = (subscribeSource) => {
|
|
1353
|
+
const listeners = new Set;
|
|
1354
|
+
let source = null;
|
|
1355
|
+
const close = () => {
|
|
1356
|
+
const previous = source;
|
|
1357
|
+
source = null;
|
|
1358
|
+
if (previous) {
|
|
1359
|
+
previous.closed = true;
|
|
1360
|
+
try {
|
|
1361
|
+
previous.stop?.();
|
|
1362
|
+
} catch {}
|
|
1363
|
+
previous.stop = null;
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
return {
|
|
1367
|
+
subscribe(listener) {
|
|
1368
|
+
if (disposed)
|
|
1369
|
+
return () => {};
|
|
1370
|
+
const entry = { observer: listener };
|
|
1371
|
+
listeners.add(entry);
|
|
1372
|
+
if (!source) {
|
|
1373
|
+
const current = { closed: false, stop: null };
|
|
1374
|
+
source = current;
|
|
1375
|
+
try {
|
|
1376
|
+
const stop = subscribeSource((value) => {
|
|
1377
|
+
if (disposed || current.closed)
|
|
1378
|
+
return;
|
|
1379
|
+
for (const entry2 of [...listeners]) {
|
|
1380
|
+
if (disposed || current.closed)
|
|
1381
|
+
break;
|
|
1382
|
+
if (listeners.has(entry2)) {
|
|
1383
|
+
try {
|
|
1384
|
+
entry2.observer(value);
|
|
1385
|
+
} catch {}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
if (current.closed) {
|
|
1390
|
+
try {
|
|
1391
|
+
stop();
|
|
1392
|
+
} catch {}
|
|
1393
|
+
} else
|
|
1394
|
+
current.stop = stop;
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
if (source === current)
|
|
1397
|
+
source = null;
|
|
1398
|
+
current.closed = true;
|
|
1399
|
+
listeners.delete(entry);
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
let removed = false;
|
|
1404
|
+
return () => {
|
|
1405
|
+
if (removed)
|
|
1406
|
+
return;
|
|
1407
|
+
removed = true;
|
|
1408
|
+
listeners.delete(entry);
|
|
1409
|
+
if (!listeners.size)
|
|
1410
|
+
close();
|
|
1411
|
+
};
|
|
1412
|
+
},
|
|
1413
|
+
dispose() {
|
|
1414
|
+
close();
|
|
1415
|
+
listeners.clear();
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
};
|
|
1419
|
+
const events = channel((listener) => inputs.subscribe((payload) => {
|
|
1420
|
+
const event = decodeEvent(payload);
|
|
1421
|
+
if (event)
|
|
1422
|
+
listener(event);
|
|
1423
|
+
}));
|
|
1424
|
+
const healthEvents = channel((listener) => inputs.subscribeHealth(() => listener()));
|
|
1425
|
+
return {
|
|
1426
|
+
getHealth() {
|
|
1427
|
+
return disposed ? { generation: -1, identity: { status: "unavailable", error: { code: "disposed", message: "Identity connection is disposed.", retry: "never" } }, state: { status: "unavailable", error: { code: "disposed", message: "Identity connection is disposed.", retry: "never" } } } : cloneHealth(inputs.getHealth());
|
|
1428
|
+
},
|
|
1429
|
+
subscribeHealth(listener) {
|
|
1430
|
+
return healthEvents.subscribe(listener);
|
|
1431
|
+
},
|
|
1432
|
+
async request(method, input, options) {
|
|
1433
|
+
const aborted = cancelled(options);
|
|
1434
|
+
if (aborted && !aborted.ok)
|
|
1435
|
+
throw new Error(aborted.error.message);
|
|
1436
|
+
if (disposed)
|
|
1437
|
+
throw new Error("Identity connection is disposed.");
|
|
1438
|
+
return inputs.request(method, input, options);
|
|
1439
|
+
},
|
|
1440
|
+
subscribe(listener) {
|
|
1441
|
+
return events.subscribe(listener);
|
|
1442
|
+
},
|
|
1443
|
+
async revalidate(options) {
|
|
1444
|
+
const aborted = cancelled(options);
|
|
1445
|
+
if (aborted)
|
|
1446
|
+
return aborted;
|
|
1447
|
+
if (disposed)
|
|
1448
|
+
return err("disposed", "Identity connection is disposed.");
|
|
1449
|
+
try {
|
|
1450
|
+
const result = await inputs.revalidate(options);
|
|
1451
|
+
if (disposed)
|
|
1452
|
+
return err("disposed", "Identity connection is disposed.");
|
|
1453
|
+
const aborted2 = cancelled(options);
|
|
1454
|
+
if (aborted2)
|
|
1455
|
+
return aborted2;
|
|
1456
|
+
if (!isRecord4(result) || typeof result.ok !== "boolean")
|
|
1457
|
+
return err("incompatible", "Malformed connection revalidation result.");
|
|
1458
|
+
if (result.ok)
|
|
1459
|
+
return ok(undefined);
|
|
1460
|
+
const failure2 = decodeError2(result.error);
|
|
1461
|
+
return failure2.ok ? { ok: false, error: failure2.value } : failure2;
|
|
1462
|
+
} catch {
|
|
1463
|
+
if (disposed)
|
|
1464
|
+
return err("disposed", "Identity connection is disposed.");
|
|
1465
|
+
const aborted2 = cancelled(options);
|
|
1466
|
+
if (aborted2)
|
|
1467
|
+
return aborted2;
|
|
1468
|
+
return err("unavailable", "Identity connection revalidation failed.", "after-reconnect");
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
dispose() {
|
|
1472
|
+
if (disposed)
|
|
1473
|
+
return;
|
|
1474
|
+
disposed = true;
|
|
1475
|
+
events.dispose();
|
|
1476
|
+
healthEvents.dispose();
|
|
1477
|
+
inputs.dispose?.();
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// rpc-routes-runtime.ts
|
|
1483
|
+
var identityRoutes = Object.freeze({
|
|
1484
|
+
bootstrap: "bb-identity/v1/bootstrap",
|
|
1485
|
+
selfProfile: "bb-identity/v1/selfProfile",
|
|
1486
|
+
search: "bb-identity/v1/search",
|
|
1487
|
+
profiles: "bb-identity/v1/profiles",
|
|
1488
|
+
participants: "bb-identity/v1/participants",
|
|
1489
|
+
participantPreviews: "bb-identity/v1/participantPreviews"
|
|
1490
|
+
});
|
|
1491
|
+
var identityStateRoutes = Object.freeze({
|
|
1492
|
+
load: "bb-identity/v1/state/load",
|
|
1493
|
+
save: "bb-identity/v1/state/save",
|
|
1494
|
+
reconcile: "bb-identity/v1/state/reconcile"
|
|
1495
|
+
});
|
|
1496
|
+
var identityStateRpcMethods = Object.freeze({
|
|
1497
|
+
[identityStateRoutes.load]: "bb-identity.v1.state.load",
|
|
1498
|
+
[identityStateRoutes.save]: "bb-identity.v1.state.save",
|
|
1499
|
+
[identityStateRoutes.reconcile]: "bb-identity.v1.state.reconcile"
|
|
1500
|
+
});
|
|
1501
|
+
var identityRpcMethods = Object.freeze({
|
|
1502
|
+
[identityRoutes.bootstrap]: "bb-identity.v1.bootstrap",
|
|
1503
|
+
[identityRoutes.selfProfile]: "bb-identity.v1.selfProfile",
|
|
1504
|
+
[identityRoutes.search]: "bb-identity.v1.search",
|
|
1505
|
+
[identityRoutes.profiles]: "bb-identity.v1.profiles",
|
|
1506
|
+
[identityRoutes.participants]: "bb-identity.v1.participants",
|
|
1507
|
+
[identityRoutes.participantPreviews]: "bb-identity.v1.participantPreviews",
|
|
1508
|
+
...identityStateRpcMethods
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// client-runtime.ts
|
|
1512
|
+
var errorCodes2 = new Set([
|
|
1513
|
+
"unavailable",
|
|
1514
|
+
"unauthenticated",
|
|
1515
|
+
"unsupported",
|
|
1516
|
+
"incompatible",
|
|
1517
|
+
"invalid-input",
|
|
1518
|
+
"not-found",
|
|
1519
|
+
"ambiguous",
|
|
1520
|
+
"stale-owner",
|
|
1521
|
+
"stale-context",
|
|
1522
|
+
"conflict",
|
|
1523
|
+
"cancelled",
|
|
1524
|
+
"disposed",
|
|
1525
|
+
"limit-exceeded",
|
|
1526
|
+
"invalid-operation",
|
|
1527
|
+
"expired"
|
|
1528
|
+
]);
|
|
1529
|
+
var retryKinds2 = new Set(["never", "after-refresh", "after-reconnect", "same-operation"]);
|
|
1530
|
+
function isRecord5(value) {
|
|
1531
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1532
|
+
}
|
|
1533
|
+
function decodeError3(value) {
|
|
1534
|
+
return isRecord5(value) && typeof value.code === "string" && errorCodes2.has(value.code) && typeof value.message === "string" && typeof value.retry === "string" && retryKinds2.has(value.retry) ? ok({ code: value.code, message: value.message, retry: value.retry }) : err("incompatible", "Malformed identity route error.");
|
|
1535
|
+
}
|
|
1536
|
+
function decodeResult(value, decode) {
|
|
1537
|
+
if (!isRecord5(value) || typeof value.ok !== "boolean")
|
|
1538
|
+
return err("incompatible", "Malformed identity route Result.");
|
|
1539
|
+
if (!value.ok) {
|
|
1540
|
+
const failure2 = decodeError3(value.error);
|
|
1541
|
+
return failure2.ok ? { ok: false, error: failure2.value } : failure2;
|
|
1542
|
+
}
|
|
1543
|
+
return decode(value.value);
|
|
1544
|
+
}
|
|
1545
|
+
function unavailable2(connection) {
|
|
1546
|
+
const link = connection.getHealth().identity;
|
|
1547
|
+
if (link.status === "unavailable")
|
|
1548
|
+
return err(link.error.code, link.error.message, link.error.retry);
|
|
1549
|
+
return link.status === "healthy" ? null : err("unavailable", "Identity transport is reconnecting.", "after-reconnect");
|
|
1550
|
+
}
|
|
1551
|
+
function cancelled2(options) {
|
|
1552
|
+
return options?.signal?.aborted ? err("cancelled", "Identity request was cancelled.") : null;
|
|
1553
|
+
}
|
|
1554
|
+
function decodePage(value) {
|
|
1555
|
+
if (!isRecord5(value) || !Array.isArray(value.items) || value.nextCursor !== null && typeof value.nextCursor !== "string" || typeof value.revision !== "string") {
|
|
1556
|
+
return err("incompatible", "Malformed directory page.");
|
|
1557
|
+
}
|
|
1558
|
+
const profiles = value.items.map((item) => profileCodec.decode(item));
|
|
1559
|
+
return profiles.every((profile) => profile.ok) ? ok({ items: profiles.map((profile) => profile.value), nextCursor: value.nextCursor, revision: value.revision }) : err("incompatible", "Malformed directory profile.");
|
|
1560
|
+
}
|
|
1561
|
+
function decodeLookups(value) {
|
|
1562
|
+
if (!Array.isArray(value))
|
|
1563
|
+
return err("incompatible", "Malformed profile lookup list.");
|
|
1564
|
+
const values = [];
|
|
1565
|
+
for (const entry of value) {
|
|
1566
|
+
if (!isRecord5(entry))
|
|
1567
|
+
return err("incompatible", "Malformed profile lookup.");
|
|
1568
|
+
const key = identityKeyCodec.decode(entry.key);
|
|
1569
|
+
if (!key.ok)
|
|
1570
|
+
return err("incompatible", "Malformed profile lookup key.");
|
|
1571
|
+
if (entry.status === "missing")
|
|
1572
|
+
values.push({ key: key.value, status: "missing" });
|
|
1573
|
+
else if (entry.status === "found") {
|
|
1574
|
+
const profile = profileCodec.decode(entry.profile);
|
|
1575
|
+
if (!profile.ok || profile.value.identity.key !== key.value)
|
|
1576
|
+
return err("incompatible", "Malformed profile lookup profile.");
|
|
1577
|
+
values.push({ key: key.value, status: "found", profile: profile.value });
|
|
1578
|
+
} else
|
|
1579
|
+
return err("incompatible", "Malformed profile lookup status.");
|
|
1580
|
+
}
|
|
1581
|
+
return ok(values);
|
|
1582
|
+
}
|
|
1583
|
+
function decodeParticipant(value) {
|
|
1584
|
+
if (!isRecord5(value) || !Array.isArray(value.roles) || !value.roles.every((role) => role === "author" || role === "editor" || role === "mentioned" || role === "interaction-resolver"))
|
|
1585
|
+
return err("incompatible", "Malformed participant.");
|
|
1586
|
+
const profile = profileCodec.decode({ identity: value.identity, presentation: value.presentation, revision: "participant", status: "current" });
|
|
1587
|
+
return profile.ok ? ok({ identity: profile.value.identity, presentation: profile.value.presentation, roles: [...value.roles] }) : err("incompatible", "Malformed participant identity.");
|
|
1588
|
+
}
|
|
1589
|
+
function decodeParticipantPage2(value) {
|
|
1590
|
+
if (!isRecord5(value) || !Array.isArray(value.items) || value.nextCursor !== null && typeof value.nextCursor !== "string" || typeof value.revision !== "string" || value.coverage !== "complete-history" && value.coverage !== "partial-history")
|
|
1591
|
+
return err("incompatible", "Malformed participant page.");
|
|
1592
|
+
const items = value.items.map(decodeParticipant);
|
|
1593
|
+
return items.every((item) => item.ok) ? ok({ items: items.map((item) => item.value), nextCursor: value.nextCursor, revision: value.revision, coverage: value.coverage }) : err("incompatible", "Malformed participant item.");
|
|
1594
|
+
}
|
|
1595
|
+
function decodePreviews(value) {
|
|
1596
|
+
if (!Array.isArray(value))
|
|
1597
|
+
return err("incompatible", "Malformed participant previews.");
|
|
1598
|
+
const result = [];
|
|
1599
|
+
for (const preview of value) {
|
|
1600
|
+
if (!isRecord5(preview) || !Array.isArray(preview.participants) || typeof preview.hasMore !== "boolean" || typeof preview.revision !== "string" || preview.coverage !== "complete-history" && preview.coverage !== "partial-history")
|
|
1601
|
+
return err("incompatible", "Malformed participant preview.");
|
|
1602
|
+
const threadId = idCodec("thread").decode(preview.threadId);
|
|
1603
|
+
const revision = idCodec("revision").decode(preview.revision);
|
|
1604
|
+
if (!threadId.ok || !revision.ok)
|
|
1605
|
+
return err("incompatible", "Malformed participant preview identifiers.");
|
|
1606
|
+
const participants = preview.participants.map(decodeParticipant);
|
|
1607
|
+
if (!participants.every((participant) => participant.ok))
|
|
1608
|
+
return err("incompatible", "Malformed participant preview item.");
|
|
1609
|
+
result.push({ threadId: threadId.value, participants: participants.map((participant) => participant.value), hasMore: preview.hasMore, coverage: preview.coverage, revision: revision.value });
|
|
1610
|
+
}
|
|
1611
|
+
return ok(result);
|
|
1612
|
+
}
|
|
1613
|
+
function createIdentityClientTransport(connection) {
|
|
1614
|
+
const call = async (method, input, decode, options) => {
|
|
1615
|
+
const stopped = cancelled2(options);
|
|
1616
|
+
if (stopped)
|
|
1617
|
+
return stopped;
|
|
1618
|
+
const failed = unavailable2(connection);
|
|
1619
|
+
if (failed)
|
|
1620
|
+
return failed;
|
|
1621
|
+
try {
|
|
1622
|
+
return decodeResult(await connection.request(method, input, options), decode);
|
|
1623
|
+
} catch {
|
|
1624
|
+
return err("unavailable", "Identity route request failed.", "after-reconnect");
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
const directory = {
|
|
1628
|
+
search: (input, options) => call(identityRoutes.search, input, decodePage, options),
|
|
1629
|
+
getMany: (input, options) => call(identityRoutes.profiles, input, decodeLookups, options),
|
|
1630
|
+
subscribe(keys, listener) {
|
|
1631
|
+
return connection.subscribe((event) => {
|
|
1632
|
+
if (event.kind !== "directory")
|
|
1633
|
+
return;
|
|
1634
|
+
if (!keys.length || !event.keys.length || event.keys.some((key) => keys.includes(key)))
|
|
1635
|
+
listener({ keys: event.keys, revision: event.revision });
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
const participants = {
|
|
1640
|
+
list: (input, options) => call(identityRoutes.participants, input, decodeParticipantPage2, options),
|
|
1641
|
+
previews: (input, options) => call(identityRoutes.participantPreviews, input, decodePreviews, options)
|
|
1642
|
+
};
|
|
1643
|
+
return {
|
|
1644
|
+
bootstrap: (options) => call(identityRoutes.bootstrap, {}, serverSessionCodec.decode, options),
|
|
1645
|
+
selfProfile: (options) => call(identityRoutes.selfProfile, {}, profileCodec.decode, options),
|
|
1646
|
+
directory,
|
|
1647
|
+
participants,
|
|
1648
|
+
subscribe(listener) {
|
|
1649
|
+
return connection.subscribe((event) => {
|
|
1650
|
+
if (event.kind !== "state")
|
|
1651
|
+
listener(event);
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// http-lifetime-runtime.ts
|
|
1658
|
+
function retainIdentityHttpResponse(response, invocationSignal, requestSignal, release) {
|
|
1659
|
+
let settled = false;
|
|
1660
|
+
const finish = () => {
|
|
1661
|
+
if (settled)
|
|
1662
|
+
return;
|
|
1663
|
+
settled = true;
|
|
1664
|
+
invocationSignal.removeEventListener("abort", abort);
|
|
1665
|
+
requestSignal.removeEventListener("abort", abort);
|
|
1666
|
+
release();
|
|
1667
|
+
};
|
|
1668
|
+
let reader;
|
|
1669
|
+
let stream;
|
|
1670
|
+
const abort = () => {
|
|
1671
|
+
if (settled)
|
|
1672
|
+
return;
|
|
1673
|
+
finish();
|
|
1674
|
+
stream?.error(new Error("Identity HTTP invocation expired."));
|
|
1675
|
+
reader?.cancel().catch(() => {
|
|
1676
|
+
return;
|
|
1677
|
+
});
|
|
1678
|
+
};
|
|
1679
|
+
if (!response.body) {
|
|
1680
|
+
finish();
|
|
1681
|
+
return response;
|
|
1682
|
+
}
|
|
1683
|
+
reader = response.body.getReader();
|
|
1684
|
+
const body = new ReadableStream({
|
|
1685
|
+
start(controller) {
|
|
1686
|
+
stream = controller;
|
|
1687
|
+
invocationSignal.addEventListener("abort", abort, { once: true });
|
|
1688
|
+
requestSignal.addEventListener("abort", abort, { once: true });
|
|
1689
|
+
if (invocationSignal.aborted || requestSignal.aborted)
|
|
1690
|
+
abort();
|
|
1691
|
+
},
|
|
1692
|
+
async pull(controller) {
|
|
1693
|
+
if (settled)
|
|
1694
|
+
return;
|
|
1695
|
+
try {
|
|
1696
|
+
const item = await reader.read();
|
|
1697
|
+
if (settled)
|
|
1698
|
+
return;
|
|
1699
|
+
if (item.done) {
|
|
1700
|
+
finish();
|
|
1701
|
+
controller.close();
|
|
1702
|
+
} else
|
|
1703
|
+
controller.enqueue(item.value);
|
|
1704
|
+
} catch (cause) {
|
|
1705
|
+
if (!settled) {
|
|
1706
|
+
finish();
|
|
1707
|
+
controller.error(cause);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
},
|
|
1711
|
+
cancel(reason) {
|
|
1712
|
+
finish();
|
|
1713
|
+
return reader.cancel(reason);
|
|
1714
|
+
}
|
|
1715
|
+
});
|
|
1716
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// bb-upstream-runtime.ts
|
|
1720
|
+
import { z } from "zod";
|
|
1721
|
+
function isRecord6(value) {
|
|
1722
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1723
|
+
}
|
|
1724
|
+
function isJson2(value) {
|
|
1725
|
+
return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every(isJson2) || isRecord6(value) && Object.values(value).every(isJson2);
|
|
1726
|
+
}
|
|
1727
|
+
function cloneJson(value) {
|
|
1728
|
+
return structuredClone(value);
|
|
1729
|
+
}
|
|
1730
|
+
function clonePluginRpcInput(value) {
|
|
1731
|
+
return structuredClone(value);
|
|
1732
|
+
}
|
|
1733
|
+
function freeze(value) {
|
|
1734
|
+
if (value && typeof value === "object") {
|
|
1735
|
+
Object.freeze(value);
|
|
1736
|
+
for (const child of Object.values(value))
|
|
1737
|
+
freeze(child);
|
|
1738
|
+
}
|
|
1739
|
+
return value;
|
|
1740
|
+
}
|
|
1741
|
+
function escapeExternalEnvelopeValue(value) {
|
|
1742
|
+
return value.replace(/[\[\]\r\n%<>]/g, (character) => encodeURIComponent(character));
|
|
1743
|
+
}
|
|
1744
|
+
function renderExternalPrompt(pluginId, author, input) {
|
|
1745
|
+
const displayName = author.presentation.handle ?? author.presentation.displayName;
|
|
1746
|
+
const label = escapeExternalEnvelopeValue(`${pluginId}:${displayName.length ? displayName : "External source"}`);
|
|
1747
|
+
return Object.freeze([
|
|
1748
|
+
{ type: "text", text: `[message received from external source]
|
|
1749
|
+
[sender=${label}]
|
|
1750
|
+
`, mentions: [], visibility: "agent-only" },
|
|
1751
|
+
...input,
|
|
1752
|
+
{ type: "text", text: `
|
|
1753
|
+
[/sender=${label}]`, mentions: [], visibility: "agent-only" }
|
|
1754
|
+
]);
|
|
1755
|
+
}
|
|
1756
|
+
function defaultScheduler() {
|
|
1757
|
+
return {
|
|
1758
|
+
now: () => Date.now(),
|
|
1759
|
+
schedule(delayMs, callback) {
|
|
1760
|
+
const timeout = setTimeout(callback, delayMs);
|
|
1761
|
+
return () => clearTimeout(timeout);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
var promptInputCodec = {
|
|
1766
|
+
decode(value) {
|
|
1767
|
+
if (!isRecord6(value) || !isJson2(value) || typeof value.type !== "string")
|
|
1768
|
+
return err("invalid-input", "Prompt input must be a JSON SDK prompt part.");
|
|
1769
|
+
const valid = value.type === "text" ? typeof value.text === "string" && (value.mentions === undefined || Array.isArray(value.mentions)) : value.type === "image" ? typeof value.url === "string" : value.type === "localImage" ? typeof value.path === "string" : value.type === "localFile" ? typeof value.path === "string" : false;
|
|
1770
|
+
return valid ? ok(freeze(structuredClone(value))) : err("invalid-input", "Invalid installed-SDK prompt input shape.");
|
|
1771
|
+
},
|
|
1772
|
+
encode(value) {
|
|
1773
|
+
const encoded = value;
|
|
1774
|
+
if (!isJson2(encoded))
|
|
1775
|
+
throw new TypeError("Prompt input codec received non-JSON data.");
|
|
1776
|
+
return cloneJson(encoded);
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
function unavailableSession(instanceId, code, message) {
|
|
1780
|
+
return { status: "unauthenticated", instanceId, error: { code, message, retry: "never" } };
|
|
1781
|
+
}
|
|
1782
|
+
function nativeIds(value) {
|
|
1783
|
+
if (!isRecord6(value) || value.ok !== true)
|
|
1784
|
+
return err("incompatible", "Malformed upstream dispatch response.");
|
|
1785
|
+
if (!Object.hasOwn(value, "delivery"))
|
|
1786
|
+
return ok({ deliveryId: null, queuedMessageId: null, turnId: null });
|
|
1787
|
+
if (value.delivery === "sent")
|
|
1788
|
+
return ok({ deliveryId: null, queuedMessageId: null, turnId: null });
|
|
1789
|
+
if (value.delivery === "queued" && isRecord6(value.queuedMessage) && typeof value.queuedMessage.id === "string" && value.queuedMessage.id.length) {
|
|
1790
|
+
return ok({ deliveryId: null, queuedMessageId: value.queuedMessage.id, turnId: null });
|
|
1791
|
+
}
|
|
1792
|
+
return err("incompatible", "Malformed upstream dispatch response.");
|
|
1793
|
+
}
|
|
1794
|
+
function createBbUpstreamDriver(options) {
|
|
1795
|
+
const externalMessageRendering = options.externalMessageRendering ?? "host";
|
|
1796
|
+
if (externalMessageRendering !== "host" && externalMessageRendering !== "producer") {
|
|
1797
|
+
return err("invalid-input", "Invalid external message rendering mode.");
|
|
1798
|
+
}
|
|
1799
|
+
const instance = idCodec("instance").decode(options.stateNamespace ?? "local");
|
|
1800
|
+
const plugin = idCodec("plugin").decode(options.bb.pluginId);
|
|
1801
|
+
if (!instance.ok || !plugin.ok)
|
|
1802
|
+
return err("invalid-input", "The upstream binding requires valid plugin and state namespace IDs.");
|
|
1803
|
+
const instanceId = instance.value;
|
|
1804
|
+
const pluginId = plugin.value;
|
|
1805
|
+
const singletonKey = defaultIdentityKey(instanceId);
|
|
1806
|
+
const machineKey = identityKeyCodec.decode("p6r-machine:v1:" + encodeURIComponent(instanceId) + ":server");
|
|
1807
|
+
const stamp = idCodec("server-session").decode("singleton:" + instanceId);
|
|
1808
|
+
const revision = idCodec("revision").decode("singleton:" + instanceId);
|
|
1809
|
+
if (!machineKey.ok || !stamp.ok || !revision.ok)
|
|
1810
|
+
return err("incompatible", "Unable to construct stable upstream identity identifiers.");
|
|
1811
|
+
const machineIdentity = { kind: "machine", key: machineKey.value, instanceId, hostId: null };
|
|
1812
|
+
const capabilities = {
|
|
1813
|
+
requestIdentity: "singleton",
|
|
1814
|
+
acceptance: "pre-dispatch-check",
|
|
1815
|
+
forwarding: "singleton-convention",
|
|
1816
|
+
directory: { search: false, lookup: false },
|
|
1817
|
+
participants: false,
|
|
1818
|
+
externalSend: "source-labelled",
|
|
1819
|
+
toolProvenance: "unknown",
|
|
1820
|
+
operationLookup: false
|
|
1821
|
+
};
|
|
1822
|
+
const readySession = () => freeze({
|
|
1823
|
+
status: "ready",
|
|
1824
|
+
instanceId,
|
|
1825
|
+
mode: "single-user",
|
|
1826
|
+
actor: {
|
|
1827
|
+
identity: { kind: "default-user", key: singletonKey, instanceId },
|
|
1828
|
+
presentation: { displayName: "Local user", handle: null, avatarUrl: null },
|
|
1829
|
+
evidence: "upstream-default"
|
|
1830
|
+
},
|
|
1831
|
+
stamp: stamp.value,
|
|
1832
|
+
capabilities
|
|
1833
|
+
});
|
|
1834
|
+
const machineSession = () => freeze({
|
|
1835
|
+
status: "ready",
|
|
1836
|
+
instanceId,
|
|
1837
|
+
mode: "single-user",
|
|
1838
|
+
actor: {
|
|
1839
|
+
identity: machineIdentity,
|
|
1840
|
+
presentation: { displayName: "BB machine", handle: null, avatarUrl: null },
|
|
1841
|
+
evidence: "machine"
|
|
1842
|
+
},
|
|
1843
|
+
stamp: stamp.value,
|
|
1844
|
+
capabilities
|
|
1845
|
+
});
|
|
1846
|
+
const selfProfile = () => freeze({
|
|
1847
|
+
identity: { kind: "default-user", key: singletonKey, instanceId },
|
|
1848
|
+
presentation: { displayName: "Local user", handle: null, avatarUrl: null },
|
|
1849
|
+
revision: revision.value,
|
|
1850
|
+
status: "current"
|
|
1851
|
+
});
|
|
1852
|
+
const machineProfile = () => freeze({
|
|
1853
|
+
identity: machineIdentity,
|
|
1854
|
+
presentation: { displayName: "BB machine", handle: null, avatarUrl: null },
|
|
1855
|
+
revision: revision.value,
|
|
1856
|
+
status: "current"
|
|
1857
|
+
});
|
|
1858
|
+
let disposed = false;
|
|
1859
|
+
const records = new WeakMap;
|
|
1860
|
+
const active = new Set;
|
|
1861
|
+
const listeners = new Set;
|
|
1862
|
+
const labelledExternal = new WeakSet;
|
|
1863
|
+
const terminateScope = (record, scope) => {
|
|
1864
|
+
if (!scope || scope.released)
|
|
1865
|
+
return;
|
|
1866
|
+
scope.released = true;
|
|
1867
|
+
record.controller.signal.removeEventListener("abort", scope.parentAbort);
|
|
1868
|
+
scope.controller.abort();
|
|
1869
|
+
record.scopes.delete(scope);
|
|
1870
|
+
};
|
|
1871
|
+
const terminate = (record) => {
|
|
1872
|
+
for (const scope of [...record.scopes])
|
|
1873
|
+
terminateScope(record, scope);
|
|
1874
|
+
record.controller.abort();
|
|
1875
|
+
active.delete(record);
|
|
1876
|
+
};
|
|
1877
|
+
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);
|
|
1880
|
+
};
|
|
1881
|
+
const admission = (context) => {
|
|
1882
|
+
const record = known(context);
|
|
1883
|
+
if (!record.ok)
|
|
1884
|
+
return record;
|
|
1885
|
+
if (record.value.origin === "external")
|
|
1886
|
+
return err("unauthenticated", "External upstream work uses an explicit external source.");
|
|
1887
|
+
return record;
|
|
1888
|
+
};
|
|
1889
|
+
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")
|
|
1894
|
+
return readySession();
|
|
1895
|
+
return record.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
|
|
1896
|
+
};
|
|
1897
|
+
const driver = {
|
|
1898
|
+
instanceId,
|
|
1899
|
+
pluginId,
|
|
1900
|
+
inputCodec: promptInputCodec,
|
|
1901
|
+
scheduler: options.scheduler ?? defaultScheduler(),
|
|
1902
|
+
async session(context) {
|
|
1903
|
+
return session(context);
|
|
1904
|
+
},
|
|
1905
|
+
async selfProfile(context) {
|
|
1906
|
+
const allowed = admission(context);
|
|
1907
|
+
return allowed.ok ? ok(allowed.value.origin === "background" ? machineProfile() : selfProfile()) : allowed;
|
|
1908
|
+
},
|
|
1909
|
+
async openScope(context) {
|
|
1910
|
+
const allowed = admission(context);
|
|
1911
|
+
if (!allowed.ok)
|
|
1912
|
+
return allowed;
|
|
1913
|
+
const record = allowed.value;
|
|
1914
|
+
let scope;
|
|
1915
|
+
const parentAbort = () => terminateScope(record, scope);
|
|
1916
|
+
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;
|
|
1922
|
+
return ok({
|
|
1923
|
+
signal: scope.controller.signal,
|
|
1924
|
+
session: exposedSession,
|
|
1925
|
+
validate(expected) {
|
|
1926
|
+
if (scope.released || scope.controller.signal.aborted || record.controller.signal.aborted || disposed) {
|
|
1927
|
+
return err("expired", "Upstream request context is no longer live.");
|
|
1928
|
+
}
|
|
1929
|
+
return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
|
|
1930
|
+
},
|
|
1931
|
+
release: expire
|
|
1932
|
+
});
|
|
1933
|
+
},
|
|
1934
|
+
async submit(input, readOptions) {
|
|
1935
|
+
if (disposed)
|
|
1936
|
+
return { status: "rejected", error: { code: "disposed", message: "Upstream binding is disposed.", retry: "never" } };
|
|
1937
|
+
if (readOptions?.signal?.aborted)
|
|
1938
|
+
return { status: "rejected", error: { code: "cancelled", message: "Submission was cancelled before dispatch.", retry: "never" } };
|
|
1939
|
+
const operation = idCodec("operation").decode(input.operationId);
|
|
1940
|
+
const thread = idCodec("thread").decode(input.threadId);
|
|
1941
|
+
if (!operation.ok || !thread.ok || !["auto", "start", "steer-if-active", "queue-if-active"].includes(input.mode)) {
|
|
1942
|
+
return { status: "rejected", error: { code: "invalid-input", message: "Invalid upstream submission metadata.", retry: "never" } };
|
|
1943
|
+
}
|
|
1944
|
+
const labelled = labelledExternal.has(input.input);
|
|
1945
|
+
const prompts = [];
|
|
1946
|
+
for (const item of input.input) {
|
|
1947
|
+
const decoded = promptInputCodec.decode(item);
|
|
1948
|
+
if (!decoded.ok)
|
|
1949
|
+
return { status: "rejected", error: decoded.error };
|
|
1950
|
+
prompts.push(decoded.value);
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
const response = await options.bb.sdk.threads.send({ threadId: thread.value, mode: input.mode, input: prompts });
|
|
1954
|
+
const native = nativeIds(response);
|
|
1955
|
+
if (!native.ok)
|
|
1956
|
+
return { status: "indeterminate", operationId: operation.value, message: "Upstream dispatch returned an unrecognized response after submission." };
|
|
1957
|
+
return {
|
|
1958
|
+
status: "submitted",
|
|
1959
|
+
receipt: {
|
|
1960
|
+
evidence: "upstream-response",
|
|
1961
|
+
operationId: operation.value,
|
|
1962
|
+
acceptedAt: null,
|
|
1963
|
+
references: null,
|
|
1964
|
+
native: native.value,
|
|
1965
|
+
provenance: labelled ? "source-labelled" : "upstream-default",
|
|
1966
|
+
deduplication: "not-guaranteed"
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
} catch {
|
|
1970
|
+
return { status: "indeterminate", operationId: operation.value, message: "Upstream submission may have been accepted before its response failed." };
|
|
1971
|
+
}
|
|
1972
|
+
},
|
|
1973
|
+
labelExternal(_author, input) {
|
|
1974
|
+
const copied = Object.freeze(input.map((item) => {
|
|
1975
|
+
const decoded = promptInputCodec.decode(item);
|
|
1976
|
+
return decoded.ok ? decoded.value : item;
|
|
1977
|
+
}));
|
|
1978
|
+
labelledExternal.add(copied);
|
|
1979
|
+
return copied;
|
|
1980
|
+
},
|
|
1981
|
+
toolCorrelation(_context) {
|
|
1982
|
+
return { status: "unknown", correlation: null, reason: "upstream-unattributed" };
|
|
1983
|
+
},
|
|
1984
|
+
async forward(context, destinationPlugin, method, input, readOptions) {
|
|
1985
|
+
const allowed = known(context);
|
|
1986
|
+
if (!allowed.ok)
|
|
1987
|
+
throw allowed.error;
|
|
1988
|
+
if (readOptions?.signal?.aborted)
|
|
1989
|
+
throw { code: "cancelled", message: "Plugin forwarding was cancelled before dispatch.", retry: "never" };
|
|
1990
|
+
const destination = idCodec("plugin").decode(destinationPlugin);
|
|
1991
|
+
if (!destination.ok || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(method) || !isJson2(input)) {
|
|
1992
|
+
throw { code: "invalid-input", message: "Invalid upstream plugin RPC destination.", retry: "never" };
|
|
1993
|
+
}
|
|
1994
|
+
return options.bb.sdk.plugins.callRpc({ pluginId: destination.value, method, input: clonePluginRpcInput(input), outputSchema: z.unknown() });
|
|
1995
|
+
},
|
|
1996
|
+
subscribe(listener) {
|
|
1997
|
+
if (!disposed)
|
|
1998
|
+
listeners.add(listener);
|
|
1999
|
+
return () => listeners.delete(listener);
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
const binding = {
|
|
2003
|
+
externalMessageRendering,
|
|
2004
|
+
driver,
|
|
2005
|
+
issue(origin) {
|
|
2006
|
+
if (disposed)
|
|
2007
|
+
throw new Error("Upstream binding is disposed.");
|
|
2008
|
+
const controller = new AbortController;
|
|
2009
|
+
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);
|
|
2013
|
+
return invocation;
|
|
2014
|
+
},
|
|
2015
|
+
release(invocation) {
|
|
2016
|
+
const record = records.get(invocation);
|
|
2017
|
+
if (record)
|
|
2018
|
+
terminate(record);
|
|
2019
|
+
},
|
|
2020
|
+
dispose() {
|
|
2021
|
+
if (disposed)
|
|
2022
|
+
return;
|
|
2023
|
+
disposed = true;
|
|
2024
|
+
for (const record of [...active])
|
|
2025
|
+
terminate(record);
|
|
2026
|
+
for (const listener of [...listeners]) {
|
|
2027
|
+
try {
|
|
2028
|
+
listener({ kind: "disposed" });
|
|
2029
|
+
} catch {}
|
|
2030
|
+
}
|
|
2031
|
+
listeners.clear();
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
2034
|
+
try {
|
|
2035
|
+
options.bb.onDispose(() => binding.dispose());
|
|
2036
|
+
} catch {
|
|
2037
|
+
binding.dispose();
|
|
2038
|
+
return err("unavailable", "Unable to register upstream binding cleanup.", "after-refresh");
|
|
2039
|
+
}
|
|
2040
|
+
return ok(binding);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
// server-runtime.ts
|
|
2044
|
+
function sameExpectation2(actor, stamp, expectedActor, expectedStamp) {
|
|
2045
|
+
return actor === expectedActor && stamp === expectedStamp ? ok(undefined) : err("stale-context", "The request actor or session changed.", "after-refresh");
|
|
2046
|
+
}
|
|
2047
|
+
function isPerson(value) {
|
|
2048
|
+
return value.kind === "person" || value.kind === "default-user";
|
|
2049
|
+
}
|
|
2050
|
+
function freezePerson(input) {
|
|
2051
|
+
return input.kind === "person" ? Object.freeze({ kind: "person", key: input.key, issuer: input.issuer, subject: input.subject }) : Object.freeze({ kind: "default-user", key: input.key, instanceId: input.instanceId });
|
|
2052
|
+
}
|
|
2053
|
+
function freezeActorReference(input) {
|
|
2054
|
+
return input.kind === "machine" ? Object.freeze({ kind: "machine", key: input.key, instanceId: input.instanceId, hostId: input.hostId }) : freezePerson(input);
|
|
2055
|
+
}
|
|
2056
|
+
function freezeActor(input) {
|
|
2057
|
+
return Object.freeze({
|
|
2058
|
+
identity: freezeActorReference(input.identity),
|
|
2059
|
+
presentation: Object.freeze({
|
|
2060
|
+
displayName: input.presentation.displayName,
|
|
2061
|
+
handle: input.presentation.handle,
|
|
2062
|
+
avatarUrl: input.presentation.avatarUrl
|
|
2063
|
+
}),
|
|
2064
|
+
evidence: input.evidence
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
function freezeExpected(input) {
|
|
2068
|
+
return Object.freeze({ actor: input.actor, session: input.session });
|
|
2069
|
+
}
|
|
2070
|
+
function createIdentityServer(options) {
|
|
2071
|
+
const { host } = options;
|
|
2072
|
+
let disposed = false;
|
|
2073
|
+
const dispose = () => {
|
|
2074
|
+
disposed = true;
|
|
2075
|
+
};
|
|
2076
|
+
const commits = {
|
|
2077
|
+
boundary: "same-process-synchronous",
|
|
2078
|
+
validate(target, scope) {
|
|
2079
|
+
if (typeof target !== "object" || target === null || target.intent !== "write" || typeof target.snapshot !== "function" || !(target.signal instanceof AbortSignal)) {
|
|
2080
|
+
return err("invalid-input", "Write target does not have write intent.");
|
|
2081
|
+
}
|
|
2082
|
+
if (disposed || target.signal.aborted)
|
|
2083
|
+
return err("expired", "The request context is no longer live.");
|
|
2084
|
+
const addressIsConcrete = scope.collection.length > 0 && scope.recordId.length > 0 && Number.isInteger(scope.schemaVersion) && scope.schemaVersion >= 0;
|
|
2085
|
+
try {
|
|
2086
|
+
const snapshot = target.snapshot();
|
|
2087
|
+
const capturedActor = snapshot.actor.identity.key;
|
|
2088
|
+
const capturedSubject = snapshot.subject.key;
|
|
2089
|
+
const capturedExpected = snapshot.expected;
|
|
2090
|
+
const capturedFactsAreConsistent = capturedExpected.actor === capturedActor;
|
|
2091
|
+
return addressIsConcrete && scope.instanceId === host.instanceId && scope.pluginId === host.pluginId && capturedFactsAreConsistent && scope.subject === capturedSubject && scope.expected.actor === capturedExpected.actor && scope.expected.session === capturedExpected.session ? ok(undefined) : err("stale-context", "Target does not match this live commit scope.", "after-refresh");
|
|
2092
|
+
} catch {
|
|
2093
|
+
return err("invalid-input", "Write target snapshot is unavailable.");
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
async function issueTarget(live, request, input) {
|
|
2098
|
+
if (request.signal.aborted)
|
|
2099
|
+
return err("expired", "The request context is no longer live.");
|
|
2100
|
+
const currentRequest = live.validate(request.expected);
|
|
2101
|
+
if (!currentRequest.ok)
|
|
2102
|
+
return currentRequest;
|
|
2103
|
+
const actor = freezeActor(request.actor);
|
|
2104
|
+
let subject = freezeActorReference(actor.identity);
|
|
2105
|
+
if (input.selection.kind === "person") {
|
|
2106
|
+
if (input.policy.kind === "self-only")
|
|
2107
|
+
return err("unsupported", "This operation only supports the acting person.");
|
|
2108
|
+
const profiles = await host.directory.getMany({ keys: [input.selection.key] });
|
|
2109
|
+
if (!profiles.ok)
|
|
2110
|
+
return profiles;
|
|
2111
|
+
const found = profiles.value[0];
|
|
2112
|
+
if (!found || found.status !== "found" || !isPerson(found.profile.identity))
|
|
2113
|
+
return err("not-found", "Selected person was not found.");
|
|
2114
|
+
subject = freezePerson(found.profile.identity);
|
|
2115
|
+
}
|
|
2116
|
+
if (input.intent === "write") {
|
|
2117
|
+
const current = sameExpectation2(request.actor.identity.key, request.expected.session, input.expected.actor, input.expected.session);
|
|
2118
|
+
if (!current.ok)
|
|
2119
|
+
return current;
|
|
2120
|
+
if (input.expectedSubject !== subject.key)
|
|
2121
|
+
return err("stale-owner", "Selected subject changed.", "after-refresh");
|
|
2122
|
+
}
|
|
2123
|
+
const expected = freezeExpected({ actor: actor.identity.key, session: request.expected.session });
|
|
2124
|
+
const snapshot = Object.freeze({
|
|
2125
|
+
actor,
|
|
2126
|
+
subject,
|
|
2127
|
+
expected
|
|
2128
|
+
});
|
|
2129
|
+
const target = Object.freeze({
|
|
2130
|
+
intent: input.intent,
|
|
2131
|
+
snapshot: () => snapshot,
|
|
2132
|
+
signal: request.signal
|
|
2133
|
+
});
|
|
2134
|
+
return ok(target);
|
|
2135
|
+
}
|
|
2136
|
+
const server = {
|
|
2137
|
+
instanceId: host.instanceId,
|
|
2138
|
+
directory: host.directory,
|
|
2139
|
+
participants: host.participants,
|
|
2140
|
+
history: host.history,
|
|
2141
|
+
commits,
|
|
2142
|
+
session: (context, readOptions) => host.session(context, readOptions),
|
|
2143
|
+
selfProfile: (context, readOptions) => host.selfProfile(context, readOptions),
|
|
2144
|
+
subscribe: (listener) => host.subscribe(listener),
|
|
2145
|
+
async personRequest(context, readOptions) {
|
|
2146
|
+
if (disposed)
|
|
2147
|
+
return err("disposed", "Identity server is disposed.");
|
|
2148
|
+
const opened = await host.openPersonRequest(context, readOptions);
|
|
2149
|
+
if (!opened.ok)
|
|
2150
|
+
return opened;
|
|
2151
|
+
const live = opened.value;
|
|
2152
|
+
let released = false;
|
|
2153
|
+
let request;
|
|
2154
|
+
async function target(input) {
|
|
2155
|
+
return issueTarget(live, request, input);
|
|
2156
|
+
}
|
|
2157
|
+
request = {
|
|
2158
|
+
actor: live.session.actor,
|
|
2159
|
+
expected: { actor: live.session.actor.identity.key, session: live.session.stamp },
|
|
2160
|
+
capabilities: live.session.capabilities,
|
|
2161
|
+
signal: live.signal,
|
|
2162
|
+
target,
|
|
2163
|
+
async send(input, sendOptions) {
|
|
2164
|
+
const valid = live.validate(input.expected);
|
|
2165
|
+
if (!valid.ok)
|
|
2166
|
+
return { status: "rejected", error: valid.error };
|
|
2167
|
+
return host.send(live, input, sendOptions);
|
|
2168
|
+
},
|
|
2169
|
+
async callPlugin(input, callOptions) {
|
|
2170
|
+
const valid = live.validate(request.expected);
|
|
2171
|
+
if (!valid.ok)
|
|
2172
|
+
return valid;
|
|
2173
|
+
try {
|
|
2174
|
+
return input.output.decode(await host.forward(live, input.pluginId, input.method, input.payload));
|
|
2175
|
+
} catch {
|
|
2176
|
+
return err("unavailable", "Destination plugin call failed.", callOptions?.signal?.aborted ? "never" : "after-reconnect");
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
dispose() {
|
|
2180
|
+
if (released)
|
|
2181
|
+
return;
|
|
2182
|
+
released = true;
|
|
2183
|
+
live.release();
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
return ok(request);
|
|
2187
|
+
},
|
|
2188
|
+
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
2189
|
+
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
2190
|
+
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
2191
|
+
dispose
|
|
2192
|
+
};
|
|
2193
|
+
return server;
|
|
2194
|
+
}
|
|
2195
|
+
function createIdentityEndpoint(options) {
|
|
2196
|
+
let disposed = false;
|
|
2197
|
+
let stop = null;
|
|
2198
|
+
const dispose = () => {
|
|
2199
|
+
disposed = true;
|
|
2200
|
+
const unsubscribe = stop;
|
|
2201
|
+
stop = null;
|
|
2202
|
+
unsubscribe?.();
|
|
2203
|
+
};
|
|
2204
|
+
const subscribed = options.server.subscribe((event) => {
|
|
2205
|
+
if (disposed)
|
|
2206
|
+
return;
|
|
2207
|
+
try {
|
|
2208
|
+
if (event.kind === "directory") {
|
|
2209
|
+
const keys = event.keys.map((key) => identityKeyCodec.decode(key));
|
|
2210
|
+
options.publish({ kind: "directory", keys: keys.every((key) => key.ok) ? keys.map((key) => key.value) : [], revision: null });
|
|
2211
|
+
} else if (event.kind === "participants") {
|
|
2212
|
+
const ids = event.threadIds.map((id) => idCodec("thread").decode(id));
|
|
2213
|
+
if (ids.every((id) => id.ok))
|
|
2214
|
+
options.publish({ kind: "participants", threadIds: ids.map((id) => id.value) });
|
|
2215
|
+
} else if (event.kind === "session")
|
|
2216
|
+
options.publish(event.reason === undefined ? { kind: "session" } : { kind: "session", reason: event.reason });
|
|
2217
|
+
else if (event.kind === "reconnected" || event.kind === "disconnected")
|
|
2218
|
+
options.publish({ kind: event.kind });
|
|
2219
|
+
else if (event.kind === "disposed") {
|
|
2220
|
+
dispose();
|
|
2221
|
+
options.publish({ kind: "disconnected" });
|
|
2222
|
+
}
|
|
2223
|
+
} catch {}
|
|
2224
|
+
});
|
|
2225
|
+
if (disposed)
|
|
2226
|
+
subscribed();
|
|
2227
|
+
else
|
|
2228
|
+
stop = subscribed;
|
|
2229
|
+
const reject = () => Promise.resolve(err("disposed", "Identity endpoint is disposed."));
|
|
2230
|
+
const fence = async (work) => {
|
|
2231
|
+
try {
|
|
2232
|
+
const result = await work;
|
|
2233
|
+
return disposed ? err("disposed", "Identity endpoint is disposed.") : result;
|
|
2234
|
+
} catch {
|
|
2235
|
+
return err(disposed ? "disposed" : "unavailable", disposed ? "Identity endpoint is disposed." : "Identity endpoint read failed.");
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
return {
|
|
2239
|
+
bootstrap(context, readOptions) {
|
|
2240
|
+
return disposed ? reject() : fence(options.server.session(context, readOptions).then(ok));
|
|
2241
|
+
},
|
|
2242
|
+
selfProfile(context, readOptions) {
|
|
2243
|
+
return disposed ? reject() : fence(options.server.selfProfile(context, readOptions));
|
|
2244
|
+
},
|
|
2245
|
+
search(context, input, readOptions) {
|
|
2246
|
+
return disposed ? reject() : fence(options.server.directory.search(input, readOptions));
|
|
2247
|
+
},
|
|
2248
|
+
profiles(context, input, readOptions) {
|
|
2249
|
+
return disposed ? reject() : fence(options.server.directory.getMany(input, readOptions));
|
|
2250
|
+
},
|
|
2251
|
+
participants(context, input, readOptions) {
|
|
2252
|
+
return disposed ? reject() : fence(options.server.participants.list(input, readOptions));
|
|
2253
|
+
},
|
|
2254
|
+
participantPreviews(context, input, readOptions) {
|
|
2255
|
+
return disposed ? reject() : fence(options.server.participants.previews(input, readOptions));
|
|
2256
|
+
},
|
|
2257
|
+
dispose
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// state-rpc-runtime.ts
|
|
2262
|
+
var methods = {
|
|
2263
|
+
load: identityStateRpcMethods[identityStateRoutes.load],
|
|
2264
|
+
save: identityStateRpcMethods[identityStateRoutes.save],
|
|
2265
|
+
reconcile: identityStateRpcMethods[identityStateRoutes.reconcile]
|
|
2266
|
+
};
|
|
2267
|
+
var inputSchema = { "~standard": { version: 1, vendor: "bb-identity", validate: (value) => ({ value }) } };
|
|
2268
|
+
function isJson3(value) {
|
|
2269
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every(isJson3) || isRecord7(value) && Object.values(value).every(isJson3);
|
|
2270
|
+
}
|
|
2271
|
+
var outputSchema = { "~standard": { version: 1, vendor: "bb-identity", validate: (value) => isJson3(value) ? { value } : { issues: [{ message: "Expected an encoded identity state result." }] } } };
|
|
2272
|
+
function isRecord7(value) {
|
|
2273
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2274
|
+
}
|
|
2275
|
+
function decodeRequest(input) {
|
|
2276
|
+
if (!isRecord7(input) || !isRecord7(input.address) || !isRecord7(input.expected))
|
|
2277
|
+
return err("invalid-input", "State request requires an address and expected session.");
|
|
2278
|
+
const address = input.address;
|
|
2279
|
+
const instance = idCodec("instance").decode(address.instanceId);
|
|
2280
|
+
const plugin = idCodec("plugin").decode(address.pluginId);
|
|
2281
|
+
const owner = identityKeyCodec.decode(address.owner);
|
|
2282
|
+
const actor = identityKeyCodec.decode(input.expected.actor);
|
|
2283
|
+
const session = idCodec("server-session").decode(input.expected.session);
|
|
2284
|
+
if (!instance.ok || !plugin.ok || !owner.ok || !actor.ok || !session.ok || typeof address.collection !== "string" || !address.collection.length || typeof address.recordId !== "string" || !address.recordId.length)
|
|
2285
|
+
return err("invalid-input", "Invalid state request metadata.");
|
|
2286
|
+
return ok({ address: { instanceId: instance.value, pluginId: plugin.value, owner: owner.value, collection: address.collection, recordId: address.recordId }, expected: { actor: actor.value, session: session.value } });
|
|
2287
|
+
}
|
|
2288
|
+
function encoded(result, codec) {
|
|
2289
|
+
return result.ok ? { ok: true, value: codec.encode(result.value) } : { ok: false, error: { ...result.error } };
|
|
2290
|
+
}
|
|
2291
|
+
function failed(code, message) {
|
|
2292
|
+
return { ok: false, error: { code, message, retry: "never" } };
|
|
2293
|
+
}
|
|
2294
|
+
function createIdentityStateRpcBridge(options) {
|
|
2295
|
+
if (options.rpc.instanceId !== options.server.instanceId)
|
|
2296
|
+
return err("incompatible", "RPC and state server instance namespaces disagree.");
|
|
2297
|
+
let disposed = false;
|
|
2298
|
+
const resources = new Map;
|
|
2299
|
+
const controllers = new Set;
|
|
2300
|
+
const dispatch = async (action, input, context) => {
|
|
2301
|
+
if (disposed)
|
|
2302
|
+
return failed("disposed", "State RPC bridge is disposed.");
|
|
2303
|
+
const decoded = decodeRequest(input);
|
|
2304
|
+
if (!decoded.ok)
|
|
2305
|
+
return { ok: false, error: { ...decoded.error } };
|
|
2306
|
+
if (decoded.value.address.instanceId !== options.server.instanceId || decoded.value.address.pluginId !== options.pluginId)
|
|
2307
|
+
return failed("invalid-input", "State address belongs to another instance or plugin.");
|
|
2308
|
+
const handler = resources.get(decoded.value.address.collection);
|
|
2309
|
+
return handler ? handler(action, input, context) : failed("not-found", "State collection is not registered.");
|
|
2310
|
+
};
|
|
2311
|
+
const route = { input: inputSchema, output: outputSchema };
|
|
2312
|
+
const registered = options.rpc.register({
|
|
2313
|
+
[methods.load]: route,
|
|
2314
|
+
[methods.save]: route,
|
|
2315
|
+
[methods.reconcile]: route
|
|
2316
|
+
}, {
|
|
2317
|
+
[methods.load]: { origin: "interactive-user", handle: (input, context) => dispatch("load", input, context.request) },
|
|
2318
|
+
[methods.save]: { origin: "interactive-user", handle: (input, context) => dispatch("save", input, context.request) },
|
|
2319
|
+
[methods.reconcile]: { origin: "interactive-user", handle: (input, context) => dispatch("reconcile", input, context.request) }
|
|
2320
|
+
});
|
|
2321
|
+
if (!registered.ok)
|
|
2322
|
+
return registered;
|
|
2323
|
+
return ok({
|
|
2324
|
+
register({ resource, storage, policy, readPolicy }) {
|
|
2325
|
+
if (disposed)
|
|
2326
|
+
return err("disposed", "State RPC bridge is disposed.");
|
|
2327
|
+
const definition = { ...resource.definition, codec: { ...resource.definition.codec } };
|
|
2328
|
+
const capturedPolicy = { ...policy };
|
|
2329
|
+
const capturedReadPolicy = { ...readPolicy ?? policy };
|
|
2330
|
+
const collection = definition.collection;
|
|
2331
|
+
if (resource.pluginId !== options.pluginId || !collection || !Number.isInteger(resource.definition.schemaVersion) || resource.definition.schemaVersion < 0 || storage.boundary !== "same-process-synchronous" || policy.kind !== "self-only" && policy.kind !== "collaborators" || capturedReadPolicy.kind !== "self-only" && capturedReadPolicy.kind !== "collaborators")
|
|
2332
|
+
return err("invalid-input", "Invalid state resource registration.");
|
|
2333
|
+
if (resources.has(collection))
|
|
2334
|
+
return err("conflict", "State collection is already registered.");
|
|
2335
|
+
const controller = new AbortController;
|
|
2336
|
+
controllers.add(controller);
|
|
2337
|
+
const codecs = stateCodecs(definition.codec);
|
|
2338
|
+
const service = createStateService({
|
|
2339
|
+
instanceId: options.server.instanceId,
|
|
2340
|
+
pluginId: options.pluginId,
|
|
2341
|
+
definition,
|
|
2342
|
+
storage,
|
|
2343
|
+
commits: { boundary: "same-process-synchronous", validate: (target, scope) => disposed || controller.signal.aborted ? err("expired", "State resource registration expired.") : options.server.commits.validate(target, scope) },
|
|
2344
|
+
publish: (event) => {
|
|
2345
|
+
if (!disposed && !controller.signal.aborted)
|
|
2346
|
+
options.publish(event);
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
const handler = async (action, input, context) => {
|
|
2350
|
+
if (disposed || controller.signal.aborted)
|
|
2351
|
+
return failed("disposed", "State resource registration expired.");
|
|
2352
|
+
const decoded = decodeRequest(input);
|
|
2353
|
+
if (!decoded.ok)
|
|
2354
|
+
return { ok: false, error: { ...decoded.error } };
|
|
2355
|
+
const captured = decoded.value;
|
|
2356
|
+
const parsedMutation = action === "save" ? codecs.mutation.decode(input) : null;
|
|
2357
|
+
const mutation = parsedMutation?.ok ? codecs.mutation.decode(structuredClone(codecs.mutation.encode(parsedMutation.value))) : parsedMutation;
|
|
2358
|
+
if (mutation && !mutation.ok)
|
|
2359
|
+
return { ok: false, error: { ...mutation.error } };
|
|
2360
|
+
const operation = action === "reconcile" ? idCodec("operation").decode(isRecord7(input) ? input.operationId : undefined) : null;
|
|
2361
|
+
if (operation && !operation.ok)
|
|
2362
|
+
return { ok: false, error: { ...operation.error } };
|
|
2363
|
+
const opened = await options.server.personRequest(context);
|
|
2364
|
+
if (!opened.ok)
|
|
2365
|
+
return { ok: false, error: { ...opened.error } };
|
|
2366
|
+
const person = opened.value;
|
|
2367
|
+
const signal = AbortSignal.any([person.signal, controller.signal]);
|
|
2368
|
+
const live = () => !disposed && !signal.aborted;
|
|
2369
|
+
try {
|
|
2370
|
+
if (!live())
|
|
2371
|
+
return failed("expired", "State request expired.");
|
|
2372
|
+
if (captured.expected.actor !== person.expected.actor || captured.expected.session !== person.expected.session)
|
|
2373
|
+
return failed("stale-context", "State request expected another actor or session.");
|
|
2374
|
+
const selection = captured.address.owner === person.actor.identity.key ? { kind: "self" } : { kind: "person", key: captured.address.owner };
|
|
2375
|
+
if (mutation?.ok) {
|
|
2376
|
+
const target2 = await person.target({ intent: "write", selection, policy: capturedPolicy, expected: captured.expected, expectedSubject: captured.address.owner });
|
|
2377
|
+
if (!target2.ok)
|
|
2378
|
+
return { ok: false, error: { ...target2.error } };
|
|
2379
|
+
if (!live())
|
|
2380
|
+
return failed("expired", "State request expired.");
|
|
2381
|
+
return encoded(await service.save(target2.value, mutation.value, { signal }), codecs.save);
|
|
2382
|
+
}
|
|
2383
|
+
const target = await person.target({ intent: "read", selection, policy: capturedReadPolicy });
|
|
2384
|
+
if (!target.ok)
|
|
2385
|
+
return { ok: false, error: { ...target.error } };
|
|
2386
|
+
if (!live())
|
|
2387
|
+
return failed("expired", "State request expired.");
|
|
2388
|
+
if (target.value.snapshot().subject.key !== captured.address.owner)
|
|
2389
|
+
return failed("incompatible", "Resolved state subject does not match the requested owner.");
|
|
2390
|
+
if (operation?.ok) {
|
|
2391
|
+
const result2 = await service.reconcile(target.value, captured.address.recordId, operation.value);
|
|
2392
|
+
return live() ? encoded(result2, codecs.lookup) : failed("expired", "State request expired.");
|
|
2393
|
+
}
|
|
2394
|
+
const result = await service.read(target.value, captured.address.recordId, { signal });
|
|
2395
|
+
return live() ? encoded(result, codecs.read) : failed("expired", "State request expired.");
|
|
2396
|
+
} catch {
|
|
2397
|
+
return failed("unavailable", "State resource operation failed.");
|
|
2398
|
+
} finally {
|
|
2399
|
+
person.dispose();
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
resources.set(collection, handler);
|
|
2403
|
+
return ok({ dispose() {
|
|
2404
|
+
if (controller.signal.aborted)
|
|
2405
|
+
return;
|
|
2406
|
+
controller.abort();
|
|
2407
|
+
controllers.delete(controller);
|
|
2408
|
+
if (resources.get(collection) === handler)
|
|
2409
|
+
resources.delete(collection);
|
|
2410
|
+
} });
|
|
2411
|
+
},
|
|
2412
|
+
dispose() {
|
|
2413
|
+
if (disposed)
|
|
2414
|
+
return;
|
|
2415
|
+
disposed = true;
|
|
2416
|
+
for (const controller of controllers)
|
|
2417
|
+
controller.abort();
|
|
2418
|
+
controllers.clear();
|
|
2419
|
+
resources.clear();
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// bb-binding-runtime.ts
|
|
2425
|
+
function isRecord8(value) {
|
|
2426
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2427
|
+
}
|
|
2428
|
+
function isJson4(value) {
|
|
2429
|
+
return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every(isJson4) || isRecord8(value) && Object.values(value).every(isJson4);
|
|
2430
|
+
}
|
|
2431
|
+
function jsonResult(result, codec) {
|
|
2432
|
+
return result.ok ? { ok: true, value: codec ? codec.encode(result.value) : result.value } : { ok: false, error: { ...result.error } };
|
|
2433
|
+
}
|
|
2434
|
+
var jsonSchema = {
|
|
2435
|
+
"~standard": { version: 1, vendor: "bb-identity", validate: (value) => isJson4(value) ? { value } : { issues: [{ message: "Expected JSON." }] } }
|
|
2436
|
+
};
|
|
2437
|
+
var inputSchema2 = {
|
|
2438
|
+
"~standard": { version: 1, vendor: "bb-identity", validate: (value) => ({ value }) }
|
|
2439
|
+
};
|
|
2440
|
+
function invalid(message) {
|
|
2441
|
+
return { ok: false, error: { code: "invalid-input", message, retry: "never" } };
|
|
2442
|
+
}
|
|
2443
|
+
function decodeEmpty(value) {
|
|
2444
|
+
return isRecord8(value) && Object.keys(value).length === 0 ? ok(undefined) : err("invalid-input", "Endpoint requires an empty input object.");
|
|
2445
|
+
}
|
|
2446
|
+
function decodeDirectory(value) {
|
|
2447
|
+
if (!isRecord8(value) || typeof value.query !== "string" || !Array.isArray(value.kinds) || !value.kinds.every((kind) => kind === "person" || kind === "default-user" || kind === "machine" || kind === "external") || value.history !== "current" && value.history !== "include-historical" || typeof value.limit !== "number" || !Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100 || value.cursor !== undefined && !idCodec("cursor").decode(value.cursor).ok)
|
|
2448
|
+
return err("invalid-input", "Invalid directory query.");
|
|
2449
|
+
const cursor = value.cursor === undefined ? undefined : idCodec("cursor").decode(value.cursor);
|
|
2450
|
+
const limit = value.limit;
|
|
2451
|
+
return cursor && !cursor.ok ? cursor : ok({ query: value.query, kinds: [...value.kinds], history: value.history, limit, ...cursor ? { cursor: cursor.value } : {} });
|
|
2452
|
+
}
|
|
2453
|
+
function decodeProfiles(value) {
|
|
2454
|
+
if (!isRecord8(value) || !Array.isArray(value.keys) || value.keys.length > 64)
|
|
2455
|
+
return err("invalid-input", "Invalid profile query.");
|
|
2456
|
+
const keys = value.keys.map((key) => identityKeyCodec.decode(key));
|
|
2457
|
+
return keys.every((key) => key.ok) ? ok({ keys: keys.map((key) => key.value) }) : err("invalid-input", "Invalid profile key.");
|
|
2458
|
+
}
|
|
2459
|
+
function decodeParticipants(value) {
|
|
2460
|
+
if (!isRecord8(value) || typeof value.limit !== "number" || !Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100)
|
|
2461
|
+
return err("invalid-input", "Invalid participant query.");
|
|
2462
|
+
const thread = idCodec("thread").decode(value.threadId);
|
|
2463
|
+
const cursor = value.cursor === undefined ? undefined : idCodec("cursor").decode(value.cursor);
|
|
2464
|
+
const limit = value.limit;
|
|
2465
|
+
return !thread.ok || cursor && !cursor.ok ? err("invalid-input", "Invalid participant query identifiers.") : ok({ threadId: thread.value, limit, ...cursor ? { cursor: cursor.value } : {} });
|
|
2466
|
+
}
|
|
2467
|
+
function decodePreviews2(value) {
|
|
2468
|
+
if (!isRecord8(value) || !Array.isArray(value.threadIds) || value.threadIds.length > 64 || typeof value.perThreadLimit !== "number" || !Number.isInteger(value.perThreadLimit) || value.perThreadLimit < 1 || value.perThreadLimit > 100)
|
|
2469
|
+
return err("invalid-input", "Invalid participant preview query.");
|
|
2470
|
+
const threads = value.threadIds.map((thread) => idCodec("thread").decode(thread));
|
|
2471
|
+
const perThreadLimit = value.perThreadLimit;
|
|
2472
|
+
return threads.every((thread) => thread.ok) ? ok({ threadIds: threads.map((thread) => thread.value), perThreadLimit }) : err("invalid-input", "Invalid participant preview thread ID.");
|
|
2473
|
+
}
|
|
2474
|
+
function createPortableFoundation(options) {
|
|
2475
|
+
if (options.extension.status === "malformed")
|
|
2476
|
+
return { ok: false, error: options.extension.error };
|
|
2477
|
+
if (options.extension.status === "unsupported-version")
|
|
2478
|
+
return err("incompatible", `Unsupported identity protocol version ${options.extension.version}.`);
|
|
2479
|
+
let disposed = false;
|
|
2480
|
+
const registrations = new Set;
|
|
2481
|
+
const releaseRegistrations = () => {
|
|
2482
|
+
for (const registration of [...registrations]) {
|
|
2483
|
+
registrations.delete(registration);
|
|
2484
|
+
try {
|
|
2485
|
+
registration.dispose();
|
|
2486
|
+
} catch {}
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
const dispose = () => {
|
|
2490
|
+
if (disposed)
|
|
2491
|
+
return;
|
|
2492
|
+
disposed = true;
|
|
2493
|
+
releaseRegistrations();
|
|
2494
|
+
};
|
|
2495
|
+
const invoke = async (descriptor, input, invocation) => {
|
|
2496
|
+
if (disposed)
|
|
2497
|
+
throw new Error("Identity binding is disposed.");
|
|
2498
|
+
return descriptor.handle(input, invocation);
|
|
2499
|
+
};
|
|
2500
|
+
const foundation = {
|
|
2501
|
+
instanceId: options.extension.status === "supported" ? options.extension.protocol.instanceId : options.upstream.driver.instanceId,
|
|
2502
|
+
register(contract, handlers) {
|
|
2503
|
+
if (disposed)
|
|
2504
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2505
|
+
const names = Object.keys(contract);
|
|
2506
|
+
if (!names.length || names.length !== Object.keys(handlers).length || names.some((name) => {
|
|
2507
|
+
if (!Object.hasOwn(handlers, name))
|
|
2508
|
+
return true;
|
|
2509
|
+
const descriptor = handlers[name];
|
|
2510
|
+
return !descriptor || typeof descriptor.handle !== "function" || !["interactive-user", "background", "external"].includes(descriptor.origin);
|
|
2511
|
+
}))
|
|
2512
|
+
return err("invalid-input", "RPC handlers must exactly match the contract and declare a valid origin.");
|
|
2513
|
+
if (options.extension.status === "absent") {
|
|
2514
|
+
let admitted2 = true;
|
|
2515
|
+
const wrapped2 = {};
|
|
2516
|
+
for (const name of names) {
|
|
2517
|
+
const method = name;
|
|
2518
|
+
const descriptor = handlers[method];
|
|
2519
|
+
wrapped2[method] = async (input) => {
|
|
2520
|
+
if (disposed || !admitted2)
|
|
2521
|
+
throw new Error("Identity RPC registration is retired.");
|
|
2522
|
+
const request = options.upstream.issue(descriptor.origin);
|
|
2523
|
+
try {
|
|
2524
|
+
return await invoke(descriptor, input, { request, signal: request.signal, origin: descriptor.origin });
|
|
2525
|
+
} finally {
|
|
2526
|
+
options.upstream.release(request);
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
try {
|
|
2531
|
+
options.bb.rpc.register(contract, wrapped2);
|
|
2532
|
+
if (disposed) {
|
|
2533
|
+
admitted2 = false;
|
|
2534
|
+
return err("disposed", "Identity binding disposed during RPC registration.");
|
|
2535
|
+
}
|
|
2536
|
+
return ok(undefined);
|
|
2537
|
+
} catch {
|
|
2538
|
+
admitted2 = false;
|
|
2539
|
+
return err("unavailable", "Upstream RPC registration failed.", "after-refresh");
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (options.extension.status !== "supported")
|
|
2543
|
+
return err("incompatible", "Identity protocol is unavailable.");
|
|
2544
|
+
const staged = [];
|
|
2545
|
+
const wrapped = {};
|
|
2546
|
+
let admitted = true;
|
|
2547
|
+
try {
|
|
2548
|
+
for (const name of names) {
|
|
2549
|
+
const method = name;
|
|
2550
|
+
const descriptor = handlers[method];
|
|
2551
|
+
const bound = options.extension.protocol.bindInvocation({
|
|
2552
|
+
routeClass: descriptor.origin === "interactive-user" ? "interactive-session" : descriptor.origin === "external" ? "external-credential" : "plugin-background",
|
|
2553
|
+
handler: async (raw, input) => {
|
|
2554
|
+
if (disposed || !admitted)
|
|
2555
|
+
throw new Error("Identity RPC registration is retired.");
|
|
2556
|
+
try {
|
|
2557
|
+
return await invoke(descriptor, input, { request: raw.request, signal: raw.scope.signal, origin: descriptor.origin });
|
|
2558
|
+
} finally {
|
|
2559
|
+
raw.scope.release();
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
staged.push(bound.registration);
|
|
2564
|
+
wrapped[method] = bound.handler;
|
|
2565
|
+
}
|
|
2566
|
+
options.bb.rpc.register(contract, wrapped);
|
|
2567
|
+
if (disposed) {
|
|
2568
|
+
for (const registration of staged)
|
|
2569
|
+
registration.dispose();
|
|
2570
|
+
return err("disposed", "Identity binding disposed during registration.");
|
|
2571
|
+
}
|
|
2572
|
+
for (const registration of staged)
|
|
2573
|
+
registrations.add(registration);
|
|
2574
|
+
return ok(undefined);
|
|
2575
|
+
} catch {
|
|
2576
|
+
admitted = false;
|
|
2577
|
+
for (const registration of staged) {
|
|
2578
|
+
try {
|
|
2579
|
+
registration.dispose();
|
|
2580
|
+
} catch {}
|
|
2581
|
+
}
|
|
2582
|
+
return err("unavailable", "Enhanced RPC registration failed.", "after-refresh");
|
|
2583
|
+
}
|
|
2584
|
+
},
|
|
2585
|
+
dispose
|
|
2586
|
+
};
|
|
2587
|
+
try {
|
|
2588
|
+
options.bb.onDispose(dispose);
|
|
2589
|
+
} catch {
|
|
2590
|
+
dispose();
|
|
2591
|
+
return err("unavailable", "Could not register RPC cleanup.", "after-refresh");
|
|
2592
|
+
}
|
|
2593
|
+
return ok(foundation);
|
|
2594
|
+
}
|
|
2595
|
+
function createBbIdentityServerBinding(options) {
|
|
2596
|
+
const upstream = createBbUpstreamDriver({ bb: options.bb, ...options.stateNamespace !== undefined ? { stateNamespace: options.stateNamespace } : {}, ...options.scheduler !== undefined ? { scheduler: options.scheduler } : {}, ...options.externalMessageRendering !== undefined ? { externalMessageRendering: options.externalMessageRendering } : {} });
|
|
2597
|
+
if (!upstream.ok)
|
|
2598
|
+
return upstream;
|
|
2599
|
+
const extension = inspectForkExtension(options.bb.experimental_p6rIdentity);
|
|
2600
|
+
if (extension.status !== "absent" && extension.status !== "supported") {
|
|
2601
|
+
upstream.value.dispose();
|
|
2602
|
+
return extension.status === "malformed" ? { ok: false, error: extension.error } : err("incompatible", `Unsupported identity protocol version ${extension.version}.`);
|
|
2603
|
+
}
|
|
2604
|
+
if (upstream.value.externalMessageRendering === "producer" && extension.status === "supported") {
|
|
2605
|
+
const enableProducerRendering = extension.protocol.experimental_useProducerMessageRendering;
|
|
2606
|
+
if (typeof enableProducerRendering !== "function") {
|
|
2607
|
+
upstream.value.dispose();
|
|
2608
|
+
return err("incompatible", "Enhanced identity host does not support producer-owned external message rendering.");
|
|
2609
|
+
}
|
|
2610
|
+
try {
|
|
2611
|
+
const hookResult = enableProducerRendering.call(extension.protocol);
|
|
2612
|
+
if (hookResult !== undefined)
|
|
2613
|
+
throw Error("Producer rendering hook must be synchronous.");
|
|
2614
|
+
} catch {
|
|
2615
|
+
upstream.value.dispose();
|
|
2616
|
+
return err("incompatible", "Enhanced identity host could not enable producer-owned external message rendering.");
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
const host = createHostAdapter({ upstream: upstream.value.driver, extension });
|
|
2620
|
+
const server = createIdentityServer({ host });
|
|
2621
|
+
let disposed = false;
|
|
2622
|
+
const publish = (event) => {
|
|
2623
|
+
if (!disposed)
|
|
2624
|
+
try {
|
|
2625
|
+
options.bb.realtime.publish("bb-identity/v1", event);
|
|
2626
|
+
} catch {}
|
|
2627
|
+
};
|
|
2628
|
+
const endpoint = createIdentityEndpoint({ server, publish });
|
|
2629
|
+
const foundation = createPortableFoundation({ bb: options.bb, extension, upstream: upstream.value });
|
|
2630
|
+
if (!foundation.ok) {
|
|
2631
|
+
endpoint.dispose();
|
|
2632
|
+
server.dispose();
|
|
2633
|
+
host.dispose();
|
|
2634
|
+
upstream.value.dispose();
|
|
2635
|
+
return foundation;
|
|
2636
|
+
}
|
|
2637
|
+
const state = createIdentityStateRpcBridge({ rpc: foundation.value, server, pluginId: host.pluginId, publish: (event) => publish({ kind: "state", event }) });
|
|
2638
|
+
if (!state.ok) {
|
|
2639
|
+
foundation.value.dispose();
|
|
2640
|
+
endpoint.dispose();
|
|
2641
|
+
server.dispose();
|
|
2642
|
+
host.dispose();
|
|
2643
|
+
upstream.value.dispose();
|
|
2644
|
+
return state;
|
|
2645
|
+
}
|
|
2646
|
+
const invocations = new WeakMap;
|
|
2647
|
+
const expired = () => err("expired", "Invocation has already settled.");
|
|
2648
|
+
const recordFor = (invocation) => {
|
|
2649
|
+
const record = invocations.get(invocation);
|
|
2650
|
+
return disposed || !record || record.settled || record.signal.aborted ? expired() : ok(record);
|
|
2651
|
+
};
|
|
2652
|
+
const unavailableSession2 = () => ({
|
|
2653
|
+
status: "unavailable",
|
|
2654
|
+
instanceId: server.instanceId,
|
|
2655
|
+
error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
|
|
2656
|
+
});
|
|
2657
|
+
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) {
|
|
2663
|
+
if (opened.ok)
|
|
2664
|
+
opened.value.dispose();
|
|
2665
|
+
return expired();
|
|
2666
|
+
}
|
|
2667
|
+
if (opened.ok)
|
|
2668
|
+
record.value.people.add(opened.value);
|
|
2669
|
+
return opened;
|
|
2670
|
+
};
|
|
2671
|
+
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;
|
|
2677
|
+
};
|
|
2678
|
+
let disposeBinding = () => {};
|
|
2679
|
+
const guardedHistory = {
|
|
2680
|
+
async contributions(query, readOptions) {
|
|
2681
|
+
if (disposed)
|
|
2682
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2683
|
+
const result = await server.history.contributions(query, readOptions);
|
|
2684
|
+
return disposed ? err("disposed", "Identity binding is disposed.") : result;
|
|
2685
|
+
},
|
|
2686
|
+
async attempts(query, readOptions) {
|
|
2687
|
+
if (disposed)
|
|
2688
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2689
|
+
const result = await server.history.attempts(query, readOptions);
|
|
2690
|
+
return disposed ? err("disposed", "Identity binding is disposed.") : result;
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
const publicServer = {
|
|
2694
|
+
instanceId: server.instanceId,
|
|
2695
|
+
commits: server.commits,
|
|
2696
|
+
async session(invocation) {
|
|
2697
|
+
const record = recordFor(invocation);
|
|
2698
|
+
if (!record.ok)
|
|
2699
|
+
return unavailableSession2();
|
|
2700
|
+
const session = await server.session(record.value.request);
|
|
2701
|
+
return record.value.settled || record.value.signal.aborted ? unavailableSession2() : session;
|
|
2702
|
+
},
|
|
2703
|
+
selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
|
|
2704
|
+
personRequest: openPerson,
|
|
2705
|
+
async sendExternal(author, input, readOptions) {
|
|
2706
|
+
if (disposed)
|
|
2707
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
|
|
2708
|
+
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
|
|
2709
|
+
return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
|
|
2710
|
+
},
|
|
2711
|
+
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
|
|
2712
|
+
history: guardedHistory
|
|
2713
|
+
};
|
|
2714
|
+
const publicEndpoint = {
|
|
2715
|
+
bootstrap: (invocation) => guardEndpoint(invocation, (request) => endpoint.bootstrap(request)),
|
|
2716
|
+
selfProfile: (invocation) => guardEndpoint(invocation, (request) => endpoint.selfProfile(request)),
|
|
2717
|
+
search: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.search(request, input)),
|
|
2718
|
+
profiles: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.profiles(request, input)),
|
|
2719
|
+
participants: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.participants(request, input)),
|
|
2720
|
+
participantPreviews: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.participantPreviews(request, input)),
|
|
2721
|
+
dispose: () => disposeBinding()
|
|
2722
|
+
};
|
|
2723
|
+
const endpointContract = {
|
|
2724
|
+
[identityRpcMethods[identityRoutes.bootstrap]]: { input: inputSchema2, output: jsonSchema },
|
|
2725
|
+
[identityRpcMethods[identityRoutes.selfProfile]]: { input: inputSchema2, output: jsonSchema },
|
|
2726
|
+
[identityRpcMethods[identityRoutes.search]]: { input: inputSchema2, output: jsonSchema },
|
|
2727
|
+
[identityRpcMethods[identityRoutes.profiles]]: { input: inputSchema2, output: jsonSchema },
|
|
2728
|
+
[identityRpcMethods[identityRoutes.participants]]: { input: inputSchema2, output: jsonSchema },
|
|
2729
|
+
[identityRpcMethods[identityRoutes.participantPreviews]]: { input: inputSchema2, output: jsonSchema }
|
|
2730
|
+
};
|
|
2731
|
+
const endpointRegistered = foundation.value.register(endpointContract, {
|
|
2732
|
+
[identityRpcMethods[identityRoutes.bootstrap]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2733
|
+
const decoded = decodeEmpty(input);
|
|
2734
|
+
return decoded.ok ? jsonResult(await endpoint.bootstrap(context.request)) : invalid(decoded.error.message);
|
|
2735
|
+
} },
|
|
2736
|
+
[identityRpcMethods[identityRoutes.selfProfile]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2737
|
+
const decoded = decodeEmpty(input);
|
|
2738
|
+
return decoded.ok ? jsonResult(await endpoint.selfProfile(context.request)) : invalid(decoded.error.message);
|
|
2739
|
+
} },
|
|
2740
|
+
[identityRpcMethods[identityRoutes.search]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2741
|
+
const decoded = decodeDirectory(input);
|
|
2742
|
+
return decoded.ok ? jsonResult(await endpoint.search(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2743
|
+
} },
|
|
2744
|
+
[identityRpcMethods[identityRoutes.profiles]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2745
|
+
const decoded = decodeProfiles(input);
|
|
2746
|
+
return decoded.ok ? jsonResult(await endpoint.profiles(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2747
|
+
} },
|
|
2748
|
+
[identityRpcMethods[identityRoutes.participants]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2749
|
+
const decoded = decodeParticipants(input);
|
|
2750
|
+
return decoded.ok ? jsonResult(await endpoint.participants(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2751
|
+
} },
|
|
2752
|
+
[identityRpcMethods[identityRoutes.participantPreviews]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2753
|
+
const decoded = decodePreviews2(input);
|
|
2754
|
+
return decoded.ok ? jsonResult(await endpoint.participantPreviews(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2755
|
+
} }
|
|
2756
|
+
});
|
|
2757
|
+
if (!endpointRegistered.ok) {
|
|
2758
|
+
state.value.dispose();
|
|
2759
|
+
foundation.value.dispose();
|
|
2760
|
+
endpoint.dispose();
|
|
2761
|
+
server.dispose();
|
|
2762
|
+
host.dispose();
|
|
2763
|
+
upstream.value.dispose();
|
|
2764
|
+
return endpointRegistered;
|
|
2765
|
+
}
|
|
2766
|
+
const createInvocation = (context) => {
|
|
2767
|
+
const record = { request: context.request, signal: context.signal, people: new Set, settled: false };
|
|
2768
|
+
const invocation = {
|
|
2769
|
+
origin: context.origin,
|
|
2770
|
+
signal: context.signal,
|
|
2771
|
+
person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
|
|
2772
|
+
};
|
|
2773
|
+
invocations.set(invocation, record);
|
|
2774
|
+
const finish = () => {
|
|
2775
|
+
record.settled = true;
|
|
2776
|
+
for (const request of record.people)
|
|
2777
|
+
request.dispose();
|
|
2778
|
+
record.people.clear();
|
|
2779
|
+
};
|
|
2780
|
+
return { invocation, finish };
|
|
2781
|
+
};
|
|
2782
|
+
const httpRegistrations = new Set;
|
|
2783
|
+
const httpReleases = new Set;
|
|
2784
|
+
const binding = {
|
|
2785
|
+
server: publicServer,
|
|
2786
|
+
endpoint: publicEndpoint,
|
|
2787
|
+
rpc: { register(contract, handlers) {
|
|
2788
|
+
const names = Object.keys(contract);
|
|
2789
|
+
if (!names.length || names.length !== Object.keys(handlers).length || names.some((name) => {
|
|
2790
|
+
if (!Object.hasOwn(handlers, name))
|
|
2791
|
+
return true;
|
|
2792
|
+
const descriptor = handlers[name];
|
|
2793
|
+
return !descriptor || typeof descriptor.handle !== "function" || !["interactive-user", "background", "external"].includes(descriptor.origin);
|
|
2794
|
+
}))
|
|
2795
|
+
return err("invalid-input", "RPC handlers must exactly match the contract and declare a valid origin.");
|
|
2796
|
+
const normalized = {};
|
|
2797
|
+
for (const name of names) {
|
|
2798
|
+
const descriptor = handlers[name];
|
|
2799
|
+
if (!descriptor)
|
|
2800
|
+
return err("invalid-input", "RPC handlers must exactly match the contract.");
|
|
2801
|
+
normalized[name] = { origin: descriptor.origin, handle: async (input, context) => {
|
|
2802
|
+
const issued = createInvocation(context);
|
|
2803
|
+
try {
|
|
2804
|
+
return await descriptor.handle(input, issued.invocation);
|
|
2805
|
+
} finally {
|
|
2806
|
+
issued.finish();
|
|
2807
|
+
}
|
|
2808
|
+
} };
|
|
2809
|
+
}
|
|
2810
|
+
return foundation.value.register(contract, normalized);
|
|
2811
|
+
} },
|
|
2812
|
+
http: { route(method, path, descriptor, routeOptions) {
|
|
2813
|
+
if (disposed)
|
|
2814
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2815
|
+
if (!options.bb.http)
|
|
2816
|
+
return err("unsupported", "This SDK has no HTTP registration.");
|
|
2817
|
+
if (!descriptor || typeof descriptor.handle !== "function" || !["interactive-user", "external"].includes(descriptor.origin) || descriptor.origin === "interactive-user" && (routeOptions?.auth ?? "local") !== "local") {
|
|
2818
|
+
return err("invalid-input", "Interactive HTTP identity requires local authentication.");
|
|
2819
|
+
}
|
|
2820
|
+
const run = async (context, raw, release, coreOwnsBody = false) => {
|
|
2821
|
+
if (disposed) {
|
|
2822
|
+
release();
|
|
2823
|
+
throw new Error("Identity HTTP registration is retired.");
|
|
2824
|
+
}
|
|
2825
|
+
const issued = createInvocation(raw);
|
|
2826
|
+
let finished = false;
|
|
2827
|
+
const finish = () => {
|
|
2828
|
+
if (finished)
|
|
2829
|
+
return;
|
|
2830
|
+
finished = true;
|
|
2831
|
+
context.req.raw.signal.removeEventListener("abort", finish);
|
|
2832
|
+
raw.signal.removeEventListener("abort", finish);
|
|
2833
|
+
httpReleases.delete(finish);
|
|
2834
|
+
issued.finish();
|
|
2835
|
+
release();
|
|
2836
|
+
};
|
|
2837
|
+
httpReleases.add(finish);
|
|
2838
|
+
context.req.raw.signal.addEventListener("abort", finish, { once: true });
|
|
2839
|
+
raw.signal.addEventListener("abort", finish, { once: true });
|
|
2840
|
+
if (context.req.raw.signal.aborted) {
|
|
2841
|
+
finish();
|
|
2842
|
+
throw new Error("HTTP request is aborted.");
|
|
2843
|
+
}
|
|
2844
|
+
try {
|
|
2845
|
+
const response = await descriptor.handle(context, issued.invocation);
|
|
2846
|
+
if (coreOwnsBody)
|
|
2847
|
+
return response;
|
|
2848
|
+
return retainIdentityHttpResponse(response, raw.signal, context.req.raw.signal, finish);
|
|
2849
|
+
} catch (cause) {
|
|
2850
|
+
finish();
|
|
2851
|
+
throw cause;
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
let registration;
|
|
2855
|
+
try {
|
|
2856
|
+
if (extension.status === "supported") {
|
|
2857
|
+
const bound = extension.protocol.bindInvocation({
|
|
2858
|
+
routeClass: descriptor.origin === "interactive-user" ? "interactive-session" : "external-credential",
|
|
2859
|
+
handler: (raw, context) => run(context, { request: raw.request, signal: raw.scope.signal, origin: descriptor.origin }, () => raw.scope.release(), true)
|
|
2860
|
+
});
|
|
2861
|
+
registration = bound.registration;
|
|
2862
|
+
options.bb.http.route(method, path, bound.handler, routeOptions);
|
|
2863
|
+
if (disposed)
|
|
2864
|
+
registration.dispose();
|
|
2865
|
+
else
|
|
2866
|
+
httpRegistrations.add(registration);
|
|
2867
|
+
} else {
|
|
2868
|
+
options.bb.http.route(method, path, (context) => {
|
|
2869
|
+
const request = upstream.value.issue(descriptor.origin);
|
|
2870
|
+
return run(context, { request, signal: request.signal, origin: descriptor.origin }, () => upstream.value.release(request));
|
|
2871
|
+
}, routeOptions);
|
|
2872
|
+
}
|
|
2873
|
+
return disposed ? err("disposed", "Identity binding is disposed.") : ok(undefined);
|
|
2874
|
+
} catch {
|
|
2875
|
+
registration?.dispose();
|
|
2876
|
+
return err("unavailable", "HTTP registration failed.");
|
|
2877
|
+
}
|
|
2878
|
+
} },
|
|
2879
|
+
state: { register: (options2) => disposed ? err("disposed", "Identity binding is disposed.") : state.value.register(options2) },
|
|
2880
|
+
async registerProvider(provider) {
|
|
2881
|
+
if (disposed)
|
|
2882
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2883
|
+
const registration = await host.registerProvider(provider);
|
|
2884
|
+
return disposed && registration.ok ? (registration.value.dispose(), err("disposed", "Identity binding is disposed.")) : registration;
|
|
2885
|
+
},
|
|
2886
|
+
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.toolProvenance(context),
|
|
2887
|
+
async background(run) {
|
|
2888
|
+
if (disposed)
|
|
2889
|
+
throw new Error("Identity binding is disposed.");
|
|
2890
|
+
const request = upstream.value.issue("background");
|
|
2891
|
+
const issued = createInvocation({ request, origin: "background", signal: request.signal });
|
|
2892
|
+
try {
|
|
2893
|
+
return await run(issued.invocation);
|
|
2894
|
+
} finally {
|
|
2895
|
+
issued.finish();
|
|
2896
|
+
upstream.value.release(request);
|
|
2897
|
+
}
|
|
2898
|
+
},
|
|
2899
|
+
dispose() {
|
|
2900
|
+
if (disposed)
|
|
2901
|
+
return;
|
|
2902
|
+
disposed = true;
|
|
2903
|
+
for (const release of [...httpReleases])
|
|
2904
|
+
release();
|
|
2905
|
+
for (const registration of httpRegistrations)
|
|
2906
|
+
registration.dispose();
|
|
2907
|
+
httpRegistrations.clear();
|
|
2908
|
+
state.value.dispose();
|
|
2909
|
+
foundation.value.dispose();
|
|
2910
|
+
endpoint.dispose();
|
|
2911
|
+
server.dispose();
|
|
2912
|
+
host.dispose();
|
|
2913
|
+
upstream.value.dispose();
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
disposeBinding = () => binding.dispose();
|
|
2917
|
+
try {
|
|
2918
|
+
options.bb.onDispose(() => binding.dispose());
|
|
2919
|
+
} catch {
|
|
2920
|
+
binding.dispose();
|
|
2921
|
+
return err("unavailable", "Could not register binding cleanup.", "after-refresh");
|
|
2922
|
+
}
|
|
2923
|
+
return ok(binding);
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
// bb-entry-runtime.ts
|
|
2927
|
+
function bindBbIdentity(bb, options) {
|
|
2928
|
+
return createBbIdentityServerBinding({
|
|
2929
|
+
bb,
|
|
2930
|
+
...options?.stateNamespace === undefined ? {} : { stateNamespace: options.stateNamespace },
|
|
2931
|
+
...options?.scheduler === undefined ? {} : { scheduler: options.scheduler },
|
|
2932
|
+
...options?.externalMessageRendering === undefined ? {} : { externalMessageRendering: options.externalMessageRendering }
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
// testing-wire-runtime.ts
|
|
2937
|
+
var CHANNEL = "bb-identity/v1";
|
|
2938
|
+
var ISSUER = "fixture";
|
|
2939
|
+
function gate() {
|
|
2940
|
+
let reach, open;
|
|
2941
|
+
const reached = new Promise((r) => {
|
|
2942
|
+
reach = r;
|
|
2943
|
+
});
|
|
2944
|
+
const released = new Promise((r) => {
|
|
2945
|
+
open = r;
|
|
2946
|
+
});
|
|
2947
|
+
return { reached, reach: () => reach(), wait: () => released, release: () => open() };
|
|
2948
|
+
}
|
|
2949
|
+
function brand(kind, value) {
|
|
2950
|
+
const decoded = idCodec(kind).decode(value);
|
|
2951
|
+
if (!decoded.ok)
|
|
2952
|
+
throw new TypeError(decoded.error.message);
|
|
2953
|
+
return decoded.value;
|
|
2954
|
+
}
|
|
2955
|
+
function json(value) {
|
|
2956
|
+
return JSON.parse(JSON.stringify(value));
|
|
2957
|
+
}
|
|
2958
|
+
function createMultiUserProtocol(instanceId, initial) {
|
|
2959
|
+
let actor = initial;
|
|
2960
|
+
let epoch = 0;
|
|
2961
|
+
let requests = 0;
|
|
2962
|
+
const people = new Map;
|
|
2963
|
+
const scopes = new Set;
|
|
2964
|
+
const listeners = new Set;
|
|
2965
|
+
const session = () => ({
|
|
2966
|
+
status: "ready",
|
|
2967
|
+
instanceId,
|
|
2968
|
+
mode: "multi-user",
|
|
2969
|
+
actor,
|
|
2970
|
+
stamp: brand("server-session", `wire-fake-session-${epoch}`),
|
|
2971
|
+
capabilities: {
|
|
2972
|
+
requestIdentity: "host-resolved",
|
|
2973
|
+
acceptance: "transactional-check",
|
|
2974
|
+
forwarding: "host-bound",
|
|
2975
|
+
directory: { search: true, lookup: true },
|
|
2976
|
+
participants: false,
|
|
2977
|
+
externalSend: "structured",
|
|
2978
|
+
toolProvenance: "causal",
|
|
2979
|
+
operationLookup: true
|
|
2980
|
+
}
|
|
2981
|
+
});
|
|
2982
|
+
const scope = () => {
|
|
2983
|
+
const controller = new AbortController;
|
|
2984
|
+
scopes.add(controller);
|
|
2985
|
+
return {
|
|
2986
|
+
signal: controller.signal,
|
|
2987
|
+
validate: () => controller.signal.aborted ? { ok: false, code: "invalidated" } : { ok: true },
|
|
2988
|
+
release: () => {
|
|
2989
|
+
scopes.delete(controller);
|
|
2990
|
+
controller.abort();
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
};
|
|
2994
|
+
const record = (subject) => {
|
|
2995
|
+
const person = people.get(subject);
|
|
2996
|
+
return person ? { issuer: ISSUER, subject, presentation: { displayName: person.name, handle: null, avatarUrl: null }, status: "current" } : null;
|
|
2997
|
+
};
|
|
2998
|
+
const revision = () => `wire-fake-directory-${people.size}`;
|
|
2999
|
+
const directory = {
|
|
3000
|
+
issuers: [ISSUER],
|
|
3001
|
+
generation: brand("provider-generation", "wire-fake-provider"),
|
|
3002
|
+
async directory(input) {
|
|
3003
|
+
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);
|
|
3005
|
+
return ok({ records, nextCursor: null, revision: revision() });
|
|
3006
|
+
},
|
|
3007
|
+
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 })) });
|
|
3009
|
+
},
|
|
3010
|
+
person(issuer, subject) {
|
|
3011
|
+
const key = identityKeyCodec.decode(`${issuer}:${subject}`);
|
|
3012
|
+
return key.ok ? ok({ kind: "person", key: key.value, issuer, subject }) : key;
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
const unsupported = async () => err("unsupported", "Not modeled by the identity wire fake.");
|
|
3016
|
+
const protocol = {
|
|
3017
|
+
version: 1,
|
|
3018
|
+
instanceId,
|
|
3019
|
+
bindInvocation({ handler }) {
|
|
3020
|
+
return {
|
|
3021
|
+
registration: { generation: "wire-fake-invocation", status: "active", dispose() {} },
|
|
3022
|
+
handler: (...args) => handler({ request: { id: `wire-fake-request-${++requests}` }, scope: scope() }, ...args)
|
|
3023
|
+
};
|
|
3024
|
+
},
|
|
3025
|
+
async session() {
|
|
3026
|
+
return session();
|
|
3027
|
+
},
|
|
3028
|
+
async selfProfile() {
|
|
3029
|
+
return ok({ identity: actor.identity, presentation: actor.presentation, revision: brand("revision", `wire-fake-profile-${epoch}`), status: "current" });
|
|
3030
|
+
},
|
|
3031
|
+
async openRequest() {
|
|
3032
|
+
const current = session();
|
|
3033
|
+
const controller = new AbortController;
|
|
3034
|
+
const opened = scope();
|
|
3035
|
+
scopes.add(controller);
|
|
3036
|
+
return ok({
|
|
3037
|
+
signal: controller.signal,
|
|
3038
|
+
session: current,
|
|
3039
|
+
scope: opened,
|
|
3040
|
+
validate: (expected) => controller.signal.aborted ? err("expired", "Wire fake request expired.") : expected.actor === current.actor.identity.key && expected.session === current.stamp ? ok(undefined) : err("stale-context", "Wire fake session changed."),
|
|
3041
|
+
release: () => {
|
|
3042
|
+
scopes.delete(controller);
|
|
3043
|
+
controller.abort();
|
|
3044
|
+
}
|
|
3045
|
+
});
|
|
3046
|
+
},
|
|
3047
|
+
async accept() {
|
|
3048
|
+
return { status: "rejected", error: { code: "unsupported", message: "Not modeled by the identity wire fake.", retry: "never" } };
|
|
3049
|
+
},
|
|
3050
|
+
async lookup() {
|
|
3051
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
3052
|
+
},
|
|
3053
|
+
async provenance() {
|
|
3054
|
+
return ok({ status: "unknown", correlation: null, reason: "wire-fake" });
|
|
3055
|
+
},
|
|
3056
|
+
async historyContributions() {
|
|
3057
|
+
return ok({ status: "unavailable", reason: "unsupported" });
|
|
3058
|
+
},
|
|
3059
|
+
async historyAttempts() {
|
|
3060
|
+
return ok({ status: "unavailable", reason: "unsupported" });
|
|
3061
|
+
},
|
|
3062
|
+
directorySources: () => [directory],
|
|
3063
|
+
participants: unsupported,
|
|
3064
|
+
async forwardRpc(_scope, _destination, input) {
|
|
3065
|
+
return input;
|
|
3066
|
+
},
|
|
3067
|
+
registerProvider: unsupported,
|
|
3068
|
+
subscribe(listener) {
|
|
3069
|
+
listeners.add(listener);
|
|
3070
|
+
return () => listeners.delete(listener);
|
|
3071
|
+
}
|
|
3072
|
+
};
|
|
3073
|
+
return {
|
|
3074
|
+
protocol,
|
|
3075
|
+
get actor() {
|
|
3076
|
+
return actor;
|
|
3077
|
+
},
|
|
3078
|
+
addPerson(subject, name) {
|
|
3079
|
+
people.set(subject, { name });
|
|
3080
|
+
},
|
|
3081
|
+
switchActor(next) {
|
|
3082
|
+
actor = next;
|
|
3083
|
+
epoch++;
|
|
3084
|
+
for (const controller of [...scopes])
|
|
3085
|
+
controller.abort();
|
|
3086
|
+
scopes.clear();
|
|
3087
|
+
for (const listener of [...listeners])
|
|
3088
|
+
listener({ kind: "session", reason: "actor" });
|
|
3089
|
+
}
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function createIdentityWireFake(options) {
|
|
3093
|
+
const clock = options.clock ?? createManualClock();
|
|
3094
|
+
const harness = createStateStorageHarness({ clock });
|
|
3095
|
+
const host = options.host;
|
|
3096
|
+
const pluginId = host.harness.inspection.pluginId;
|
|
3097
|
+
if (options.resource.pluginId !== pluginId)
|
|
3098
|
+
throw new TypeError("The state resource must belong to the fake host plugin.");
|
|
3099
|
+
const multi = options.mode === "multi-user" ? createMultiUserProtocol(brand("instance", options.instanceId ?? "wire-fake-instance"), personFixture(options.actor ?? { subject: "wire-fake-user", name: "Wire fake user" })) : null;
|
|
3100
|
+
let failLoads = 0;
|
|
3101
|
+
let loadHold = null, saveHold = null, loseSave = false;
|
|
3102
|
+
const storage = {
|
|
3103
|
+
boundary: harness.storage.boundary,
|
|
3104
|
+
get receiptRetentionMs() {
|
|
3105
|
+
return harness.storage.receiptRetentionMs;
|
|
3106
|
+
},
|
|
3107
|
+
async read(address, readOptions) {
|
|
3108
|
+
if (loadHold) {
|
|
3109
|
+
const held = loadHold;
|
|
3110
|
+
held.reach();
|
|
3111
|
+
await held.wait();
|
|
3112
|
+
}
|
|
3113
|
+
if (failLoads > 0) {
|
|
3114
|
+
failLoads--;
|
|
3115
|
+
return err("unavailable", "Controlled state read failure.", "after-reconnect");
|
|
3116
|
+
}
|
|
3117
|
+
return harness.storage.read(address, readOptions);
|
|
3118
|
+
},
|
|
3119
|
+
async commit(input) {
|
|
3120
|
+
if (saveHold) {
|
|
3121
|
+
const held = saveHold;
|
|
3122
|
+
held.reach();
|
|
3123
|
+
await held.wait();
|
|
3124
|
+
}
|
|
3125
|
+
return harness.storage.commit(input);
|
|
3126
|
+
},
|
|
3127
|
+
reconcile: (input) => harness.storage.reconcile(input)
|
|
3128
|
+
};
|
|
3129
|
+
const realtime = new Set;
|
|
3130
|
+
const publish = (channel, payload) => {
|
|
3131
|
+
host.bb.realtime.publish(channel, payload);
|
|
3132
|
+
const wire = json(payload ?? null);
|
|
3133
|
+
for (const listener of [...realtime])
|
|
3134
|
+
listener(channel, wire);
|
|
3135
|
+
};
|
|
3136
|
+
const bb = {
|
|
3137
|
+
pluginId,
|
|
3138
|
+
onDispose: (fn) => host.bb.onDispose(fn),
|
|
3139
|
+
rpc: { register: (contract, handlers) => host.bb.rpc.register(contract, handlers) },
|
|
3140
|
+
realtime: { publish },
|
|
3141
|
+
sdk: host.bb.sdk,
|
|
3142
|
+
...multi ? { experimental_p6rIdentity: multi.protocol } : {}
|
|
3143
|
+
};
|
|
3144
|
+
const bound = bindBbIdentity(bb, options.instanceId && !multi ? { stateNamespace: options.instanceId } : undefined);
|
|
3145
|
+
if (!bound.ok)
|
|
3146
|
+
throw new Error(`bindBbIdentity failed: ${bound.error.message}`);
|
|
3147
|
+
const binding = bound.value;
|
|
3148
|
+
const registered = binding.state.register({ resource: options.resource, storage, policy: options.policy ?? { kind: "self-only" }, ...options.readPolicy ? { readPolicy: options.readPolicy } : {} });
|
|
3149
|
+
if (!registered.ok)
|
|
3150
|
+
throw new Error(`State registration failed: ${registered.error.message}`);
|
|
3151
|
+
const calls = [];
|
|
3152
|
+
const saveInputs = [];
|
|
3153
|
+
const stateSave = identityRpcMethods["bb-identity/v1/state/save"];
|
|
3154
|
+
const rpc = {};
|
|
3155
|
+
for (const method of Object.values(identityRpcMethods)) {
|
|
3156
|
+
rpc[method] = async (input) => {
|
|
3157
|
+
calls.push(method);
|
|
3158
|
+
if (method === stateSave)
|
|
3159
|
+
saveInputs.push(json(input ?? null));
|
|
3160
|
+
const result = await host.harness.behavior.callRpc(method, input ?? {});
|
|
3161
|
+
if (method === stateSave && loseSave) {
|
|
3162
|
+
loseSave = false;
|
|
3163
|
+
throw new Error("Controlled post-commit response loss.");
|
|
3164
|
+
}
|
|
3165
|
+
return result;
|
|
3166
|
+
};
|
|
3167
|
+
}
|
|
3168
|
+
const nextVersion = (address) => {
|
|
3169
|
+
const current = harness.readCommitted(address);
|
|
3170
|
+
if (!current)
|
|
3171
|
+
throw new Error("No committed state at that address to invalidate.");
|
|
3172
|
+
return current;
|
|
3173
|
+
};
|
|
3174
|
+
return {
|
|
3175
|
+
rpc,
|
|
3176
|
+
channel: CHANNEL,
|
|
3177
|
+
host,
|
|
3178
|
+
storage: harness,
|
|
3179
|
+
get actor() {
|
|
3180
|
+
return multi ? multi.actor : null;
|
|
3181
|
+
},
|
|
3182
|
+
onRealtime(listener) {
|
|
3183
|
+
realtime.add(listener);
|
|
3184
|
+
return () => {
|
|
3185
|
+
realtime.delete(listener);
|
|
3186
|
+
};
|
|
3187
|
+
},
|
|
3188
|
+
switchActor(subject) {
|
|
3189
|
+
if (!multi)
|
|
3190
|
+
throw new Error("switchActor needs mode: multi-user; the singleton actor cannot change.");
|
|
3191
|
+
multi.switchActor(personFixture({ subject, name: subject }));
|
|
3192
|
+
},
|
|
3193
|
+
addPerson(person) {
|
|
3194
|
+
if (!multi)
|
|
3195
|
+
throw new Error("addPerson needs mode: multi-user; the singleton directory is empty.");
|
|
3196
|
+
multi.addPerson(person.subject, person.name);
|
|
3197
|
+
},
|
|
3198
|
+
personKey(subject) {
|
|
3199
|
+
const key = identityKeyCodec.decode(`${ISSUER}:${subject}`);
|
|
3200
|
+
if (!key.ok)
|
|
3201
|
+
throw new TypeError(key.error.message);
|
|
3202
|
+
return key.value;
|
|
3203
|
+
},
|
|
3204
|
+
invalidateExternally(address, value) {
|
|
3205
|
+
const current = nextVersion(address);
|
|
3206
|
+
const envelope = { ...current, version: { epoch: current.version.epoch, sequence: current.version.sequence + 1 }, ...value === undefined ? {} : { value } };
|
|
3207
|
+
harness.seed(envelope);
|
|
3208
|
+
publish(CHANNEL, { kind: "state", event: { address, version: envelope.version, operationId: null } });
|
|
3209
|
+
return envelope;
|
|
3210
|
+
},
|
|
3211
|
+
failNextLoads(count) {
|
|
3212
|
+
failLoads = count;
|
|
3213
|
+
},
|
|
3214
|
+
loseNextSaveResponse() {
|
|
3215
|
+
loseSave = true;
|
|
3216
|
+
},
|
|
3217
|
+
holdLoads() {
|
|
3218
|
+
const held = gate();
|
|
3219
|
+
loadHold = held;
|
|
3220
|
+
const release = held.release;
|
|
3221
|
+
return { reached: held.reached, release() {
|
|
3222
|
+
if (loadHold === held)
|
|
3223
|
+
loadHold = null;
|
|
3224
|
+
release();
|
|
3225
|
+
} };
|
|
3226
|
+
},
|
|
3227
|
+
holdSaves() {
|
|
3228
|
+
const held = gate();
|
|
3229
|
+
saveHold = held;
|
|
3230
|
+
const release = held.release;
|
|
3231
|
+
return { reached: held.reached, release() {
|
|
3232
|
+
if (saveHold === held)
|
|
3233
|
+
saveHold = null;
|
|
3234
|
+
release();
|
|
3235
|
+
} };
|
|
3236
|
+
},
|
|
3237
|
+
calls,
|
|
3238
|
+
saveInputs,
|
|
3239
|
+
committed: (address) => harness.readCommitted(address),
|
|
3240
|
+
dispose() {
|
|
3241
|
+
registered.value.dispose();
|
|
3242
|
+
binding.dispose();
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
// testing-storage-runtime.ts
|
|
3247
|
+
class StorageConformanceError extends Error {
|
|
3248
|
+
check;
|
|
3249
|
+
constructor(check, detail) {
|
|
3250
|
+
super(`[state storage ${check}] ${detail}`);
|
|
3251
|
+
this.name = "StorageConformanceError";
|
|
3252
|
+
this.check = check;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
function canonical(value) {
|
|
3256
|
+
if (value === null || typeof value !== "object")
|
|
3257
|
+
return JSON.stringify(value) ?? "undefined";
|
|
3258
|
+
if (Array.isArray(value))
|
|
3259
|
+
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(",") + "}";
|
|
3262
|
+
}
|
|
3263
|
+
function id(kind, value) {
|
|
3264
|
+
const decoded = idCodec(kind).decode(value);
|
|
3265
|
+
if (!decoded.ok)
|
|
3266
|
+
throw new TypeError(decoded.error.message);
|
|
3267
|
+
return decoded.value;
|
|
3268
|
+
}
|
|
3269
|
+
function defineStateStorageConformance(options) {
|
|
3270
|
+
const actor = options.actor ?? personFixture({ subject: "storage-conformance", name: "Storage conformance" });
|
|
3271
|
+
const schemaVersion = options.schemaVersion ?? 1;
|
|
3272
|
+
const [first, second] = options.values;
|
|
3273
|
+
const prefix = options.prefix ?? "state storage conformance: ";
|
|
3274
|
+
const accepted = () => ({ ok: true, value: actor });
|
|
3275
|
+
const mutation = (input) => {
|
|
3276
|
+
const address = options.address(input.recordId);
|
|
3277
|
+
return {
|
|
3278
|
+
kind: input.kind ?? "replace",
|
|
3279
|
+
address,
|
|
3280
|
+
expectedVersion: input.expectedVersion,
|
|
3281
|
+
expected: { actor: address.owner, session: id("server-session", "storage-conformance-session") },
|
|
3282
|
+
ownerSession: id("owner-session", "storage-conformance-owner"),
|
|
3283
|
+
localGeneration: 1,
|
|
3284
|
+
operationId: id("operation", input.operation),
|
|
3285
|
+
schemaVersion,
|
|
3286
|
+
value: input.value
|
|
3287
|
+
};
|
|
3288
|
+
};
|
|
3289
|
+
const check = (name, condition, detail) => {
|
|
3290
|
+
if (!condition)
|
|
3291
|
+
throw new StorageConformanceError(name, detail);
|
|
3292
|
+
};
|
|
3293
|
+
const same = (name, actual, expected, detail) => check(name, canonical(actual) === canonical(expected), `${detail}
|
|
3294
|
+
expected: ${canonical(expected)}
|
|
3295
|
+
actual: ${canonical(actual)}`);
|
|
3296
|
+
const value = (name, result) => {
|
|
3297
|
+
if (!result.ok)
|
|
3298
|
+
throw new StorageConformanceError(name, `unexpected error ${result.error.code}: ${result.error.message}`);
|
|
3299
|
+
return result.value;
|
|
3300
|
+
};
|
|
3301
|
+
const code = (name, result, expected) => check(name, !result.ok && result.error.code === expected, `expected error ${expected}, got ${canonical(result)}`);
|
|
3302
|
+
const versionOf = (read) => read.status === "present" ? read.envelope.version : read.status === "empty" ? read.version : null;
|
|
3303
|
+
const outcome = (name, save, status) => {
|
|
3304
|
+
const v = value(name, save);
|
|
3305
|
+
check(name, v.status === status, `expected ${status}, got ${v.status}`);
|
|
3306
|
+
return v;
|
|
3307
|
+
};
|
|
3308
|
+
const noValidation = (name) => () => {
|
|
3309
|
+
throw new StorageConformanceError(name, "validateAtCommit ran for a mutation that must not commit");
|
|
3310
|
+
};
|
|
3311
|
+
async function withStorage(name, body) {
|
|
3312
|
+
let now = 1750000000000;
|
|
3313
|
+
const opened = await options.open({ now: () => now });
|
|
3314
|
+
check(name, opened.storage.boundary === "same-process-synchronous", "storage must declare a same-process-synchronous commit boundary");
|
|
3315
|
+
try {
|
|
3316
|
+
await body(opened.storage, { advance(ms) {
|
|
3317
|
+
now += ms;
|
|
3318
|
+
} }, opened);
|
|
3319
|
+
} finally {
|
|
3320
|
+
await opened.close?.();
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
const empty = async (name, storage, recordId) => {
|
|
3324
|
+
const read = value(name, await storage.read(options.address(recordId)));
|
|
3325
|
+
check(name, read.status === "empty", `a fresh record reads as ${read.status}, not empty`);
|
|
3326
|
+
return versionOf(read);
|
|
3327
|
+
};
|
|
3328
|
+
options.test(`${prefix}empty records have a stable version`, () => withStorage("empty-version", async (storage) => {
|
|
3329
|
+
const name = "empty-version";
|
|
3330
|
+
const a = await empty(name, storage, "conformance-empty");
|
|
3331
|
+
same(name, await empty(name, storage, "conformance-empty"), a, "two reads of an empty record disagree on its version");
|
|
3332
|
+
}));
|
|
3333
|
+
options.test(`${prefix}initialize has one winner; unchanged and conflict are reported`, () => withStorage("initialize-winner", async (storage) => {
|
|
3334
|
+
const name = "initialize-winner", version = await empty(name, storage, "conformance-init");
|
|
3335
|
+
const winner = outcome(name, await storage.commit({ mutation: mutation({ recordId: "conformance-init", operation: "init-a", value: first, expectedVersion: version, kind: "initialize" }), validateAtCommit: accepted }), "saved");
|
|
3336
|
+
if (winner.status !== "saved")
|
|
3337
|
+
return;
|
|
3338
|
+
outcome(name, await storage.commit({ mutation: mutation({ recordId: "conformance-init", operation: "init-b", value: second, expectedVersion: version, kind: "initialize" }), validateAtCommit: accepted }), "already-initialized");
|
|
3339
|
+
outcome(name, await storage.commit({ mutation: mutation({ recordId: "conformance-init", operation: "same-value", value: first, expectedVersion: winner.envelope.version }), validateAtCommit: accepted }), "unchanged");
|
|
3340
|
+
outcome(name, await storage.commit({ mutation: mutation({ recordId: "conformance-init", operation: "stale-version", value: second, expectedVersion: version }), validateAtCommit: accepted }), "conflict");
|
|
3341
|
+
same(name, value(name, await storage.read(options.address("conformance-init"))), { status: "present", envelope: winner.envelope }, "losing mutations changed the record");
|
|
3342
|
+
}));
|
|
3343
|
+
options.test(`${prefix}operation ids are immutable, replayed exactly and address scoped`, () => withStorage("operation-ids", async (storage) => {
|
|
3344
|
+
const name = "operation-ids", version = await empty(name, storage, "conformance-replay");
|
|
3345
|
+
const original = mutation({ recordId: "conformance-replay", operation: "shared-operation", value: first, expectedVersion: version, kind: "initialize" });
|
|
3346
|
+
const saved = await storage.commit({ mutation: original, validateAtCommit: accepted });
|
|
3347
|
+
outcome(name, saved, "saved");
|
|
3348
|
+
same(name, await storage.commit({ mutation: structuredClone(original), validateAtCommit: noValidation(name) }), saved, "an exact replay did not return the original outcome");
|
|
3349
|
+
code(name, await storage.commit({ mutation: { ...original, value: second }, validateAtCommit: noValidation(name) }), "invalid-operation");
|
|
3350
|
+
same(name, value(name, await storage.reconcile({ address: original.address, operationId: original.operationId })), { status: "final", outcome: value(name, saved) }, "reconcile does not report the original outcome");
|
|
3351
|
+
const otherVersion = await empty(name, storage, "conformance-replay-other");
|
|
3352
|
+
outcome(name, await storage.commit({ mutation: mutation({ recordId: "conformance-replay-other", operation: "shared-operation", value: second, expectedVersion: otherVersion, kind: "initialize" }), validateAtCommit: accepted }), "saved");
|
|
3353
|
+
}));
|
|
3354
|
+
options.test(`${prefix}expired operation ids stay tombstoned and the record is retained`, () => withStorage("expiry", async (storage, clock) => {
|
|
3355
|
+
const name = "expiry", version = await empty(name, storage, "conformance-expiry");
|
|
3356
|
+
const original = mutation({ recordId: "conformance-expiry", operation: "expiring-operation", value: first, expectedVersion: version, kind: "initialize" });
|
|
3357
|
+
const saved = outcome(name, await storage.commit({ mutation: original, validateAtCommit: accepted }), "saved");
|
|
3358
|
+
clock.advance(storage.receiptRetentionMs + 1);
|
|
3359
|
+
same(name, value(name, await storage.reconcile({ address: original.address, operationId: original.operationId })), { status: "unknown", reason: "expired" }, "an expired operation must reconcile as unknown/expired");
|
|
3360
|
+
code(name, await storage.commit({ mutation: structuredClone(original), validateAtCommit: noValidation(name) }), "expired");
|
|
3361
|
+
code(name, await storage.commit({ mutation: { ...original, value: second }, validateAtCommit: noValidation(name) }), "expired");
|
|
3362
|
+
if (saved.status === "saved")
|
|
3363
|
+
same(name, value(name, await storage.read(original.address)), { status: "present", envelope: saved.envelope }, "expiry changed the record");
|
|
3364
|
+
}));
|
|
3365
|
+
options.test(`${prefix}a commit-time rejection leaves neither record nor operation outcome`, () => withStorage("commit-rejection", async (storage, _clock, opened) => {
|
|
3366
|
+
const name = "commit-rejection", before = value(name, await storage.read(options.address("conformance-rejected")));
|
|
3367
|
+
const rejected = mutation({ recordId: "conformance-rejected", operation: "rejected-operation", value: first, expectedVersion: versionOf(before), kind: before.status === "empty" ? "initialize" : "replace" });
|
|
3368
|
+
let inside = null;
|
|
3369
|
+
const result = await storage.commit({ mutation: rejected, validateAtCommit: () => {
|
|
3370
|
+
inside = opened.insideCommit?.() ?? null;
|
|
3371
|
+
return { ok: false, error: { code: "expired", message: "scope released before commit", retry: "never" } };
|
|
3372
|
+
} });
|
|
3373
|
+
code(name, result, "expired");
|
|
3374
|
+
if (opened.insideCommit)
|
|
3375
|
+
check(name, inside === true, "validateAtCommit ran outside the synchronous commit transaction");
|
|
3376
|
+
same(name, value(name, await storage.read(rejected.address)), before, "a rejected commit changed the record");
|
|
3377
|
+
same(name, value(name, await storage.reconcile({ address: rejected.address, operationId: rejected.operationId })), { status: "absent-final", retry: "same-operation-only" }, "a rejected commit left an operation outcome");
|
|
3378
|
+
}));
|
|
3379
|
+
const restart = `${prefix}records and operation outcomes survive a restart`;
|
|
3380
|
+
const gap = options.skip?.restart;
|
|
3381
|
+
if (gap !== undefined) {
|
|
3382
|
+
const skip = options.test.skip ?? options.test;
|
|
3383
|
+
skip(`${restart} \u2014 gap: ${gap}`, async () => {});
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
options.test(restart, async () => {
|
|
3387
|
+
const name = "restart";
|
|
3388
|
+
await withStorage(name, async (storage, _clock, opened) => {
|
|
3389
|
+
if (!opened.reopen)
|
|
3390
|
+
throw new StorageConformanceError(name, "open() returned no reopen(); supply reopen, or pass skip.restart with the reason restart durability is not checked.");
|
|
3391
|
+
const version = await empty(name, storage, "conformance-restart");
|
|
3392
|
+
const original = mutation({ recordId: "conformance-restart", operation: "durable-operation", value: first, expectedVersion: version, kind: "initialize" });
|
|
3393
|
+
const saved = outcome(name, await storage.commit({ mutation: original, validateAtCommit: accepted }), "saved");
|
|
3394
|
+
const restarted = await opened.reopen();
|
|
3395
|
+
if (saved.status === "saved")
|
|
3396
|
+
same(name, value(name, await restarted.read(original.address)), { status: "present", envelope: saved.envelope }, "the record did not survive a restart");
|
|
3397
|
+
same(name, value(name, await restarted.reconcile({ address: original.address, operationId: original.operationId })), { status: "final", outcome: saved }, "the operation outcome did not survive a restart");
|
|
3398
|
+
});
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// testing-runtime.ts
|
|
3403
|
+
var configuration = {
|
|
3404
|
+
version: 1,
|
|
3405
|
+
boundaryId: "fixture-boundary",
|
|
3406
|
+
pluginId: "fixture-plugin",
|
|
3407
|
+
ingressIds: ["local"],
|
|
3408
|
+
credentials: [],
|
|
3409
|
+
resolver: { timeoutMs: 1000 }
|
|
3410
|
+
};
|
|
3411
|
+
function requiredId(value, kind, fallback) {
|
|
3412
|
+
const decoded = idCodec(kind).decode(value ?? fallback);
|
|
3413
|
+
if (!decoded.ok)
|
|
3414
|
+
throw new TypeError(decoded.error.message);
|
|
3415
|
+
return decoded.value;
|
|
3416
|
+
}
|
|
3417
|
+
function submitted(input) {
|
|
3418
|
+
return {
|
|
3419
|
+
status: "submitted",
|
|
3420
|
+
receipt: {
|
|
3421
|
+
evidence: "host-accepted",
|
|
3422
|
+
operationId: input.operationId,
|
|
3423
|
+
acceptedAt: new Date(0).toISOString(),
|
|
3424
|
+
references: [],
|
|
3425
|
+
native: { deliveryId: null, queuedMessageId: null, turnId: null },
|
|
3426
|
+
provenance: "structured",
|
|
3427
|
+
deduplication: "guaranteed",
|
|
3428
|
+
retainedUntil: new Date(60000).toISOString()
|
|
3429
|
+
}
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
function wireSubmitted(input) {
|
|
3433
|
+
return {
|
|
3434
|
+
status: "submitted",
|
|
3435
|
+
receipt: {
|
|
3436
|
+
evidence: "host-accepted",
|
|
3437
|
+
operationId: input.operationId,
|
|
3438
|
+
acceptedAt: new Date(0).toISOString(),
|
|
3439
|
+
references: [],
|
|
3440
|
+
native: { deliveryId: null, queuedMessageId: null, turnId: null },
|
|
3441
|
+
provenance: "structured",
|
|
3442
|
+
deduplication: "guaranteed",
|
|
3443
|
+
retainedUntil: new Date(60000).toISOString()
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
function deferred() {
|
|
3448
|
+
let resolve;
|
|
3449
|
+
let reject;
|
|
3450
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
3451
|
+
resolve = resolvePromise;
|
|
3452
|
+
reject = rejectPromise;
|
|
3453
|
+
});
|
|
3454
|
+
return { promise, resolve, reject };
|
|
3455
|
+
}
|
|
3456
|
+
function createManualClock(initialMilliseconds = 0) {
|
|
3457
|
+
let now = initialMilliseconds;
|
|
3458
|
+
let nextId = 0;
|
|
3459
|
+
const timers = new Map;
|
|
3460
|
+
const runDue = async () => {
|
|
3461
|
+
for (;; ) {
|
|
3462
|
+
const due = [...timers.entries()].filter(([, timer]) => timer.at <= now).sort((left, right) => left[1].at - right[1].at)[0];
|
|
3463
|
+
if (!due)
|
|
3464
|
+
return;
|
|
3465
|
+
timers.delete(due[0]);
|
|
3466
|
+
due[1].callback();
|
|
3467
|
+
await Promise.resolve();
|
|
3468
|
+
}
|
|
3469
|
+
};
|
|
3470
|
+
return {
|
|
3471
|
+
now: () => now,
|
|
3472
|
+
schedule(delayMs, callback) {
|
|
3473
|
+
const id2 = nextId++;
|
|
3474
|
+
timers.set(id2, { at: now + Math.max(0, delayMs), callback });
|
|
3475
|
+
return () => {
|
|
3476
|
+
timers.delete(id2);
|
|
3477
|
+
};
|
|
3478
|
+
},
|
|
3479
|
+
async advance(milliseconds) {
|
|
3480
|
+
now += Math.max(0, milliseconds);
|
|
3481
|
+
await runDue();
|
|
3482
|
+
},
|
|
3483
|
+
flush: runDue,
|
|
3484
|
+
get pendingTimers() {
|
|
3485
|
+
return timers.size;
|
|
3486
|
+
}
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
function personFixture(input) {
|
|
3490
|
+
const key = identityKeyCodec.decode(`fixture:${input.subject}`);
|
|
3491
|
+
if (!key.ok)
|
|
3492
|
+
throw new TypeError(key.error.message);
|
|
3493
|
+
return {
|
|
3494
|
+
identity: { kind: "person", key: key.value, issuer: "fixture", subject: input.subject },
|
|
3495
|
+
presentation: { displayName: input.name, handle: null, avatarUrl: null },
|
|
3496
|
+
evidence: "integration-asserted"
|
|
3497
|
+
};
|
|
3498
|
+
}
|
|
3499
|
+
function unavailableHistory() {
|
|
3500
|
+
return {
|
|
3501
|
+
contributions: async () => ok({ status: "unavailable", reason: "unsupported" }),
|
|
3502
|
+
attempts: async () => ok({ status: "unavailable", reason: "unsupported" })
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
function unsupportedControl(name) {
|
|
3506
|
+
throw new Error(`AdapterHarness.${name} is unsupported by this fixture; use a core integration fixture for that behavior.`);
|
|
3507
|
+
}
|
|
3508
|
+
function createAdapterHarness(options) {
|
|
3509
|
+
const clock = options.clock ?? createManualClock();
|
|
3510
|
+
const instanceId = requiredId(options.instanceId, "instance", "fixture-instance");
|
|
3511
|
+
const pluginId = requiredId(options.pluginId, "plugin", "fixture-plugin");
|
|
3512
|
+
const defaultActor = {
|
|
3513
|
+
identity: { kind: "default-user", key: defaultIdentityKey(instanceId), instanceId },
|
|
3514
|
+
presentation: { displayName: "Fixture user", handle: null, avatarUrl: null },
|
|
3515
|
+
evidence: "local-user"
|
|
3516
|
+
};
|
|
3517
|
+
let actor = defaultActor;
|
|
3518
|
+
let sessionEpoch = 0;
|
|
3519
|
+
let identityStatus = "ready";
|
|
3520
|
+
let rawExtension;
|
|
3521
|
+
let providerCounter = 0;
|
|
3522
|
+
let activeProvider = null;
|
|
3523
|
+
let nextProviderResult = "succeed";
|
|
3524
|
+
const hostListeners = new Set;
|
|
3525
|
+
const requestControllers = new Map;
|
|
3526
|
+
const scopeControllers = new Set;
|
|
3527
|
+
const observations = { submissions: 0, rawExtensionReads: 0, profileReads: 0, participantReads: 0, subscriptions: 0 };
|
|
3528
|
+
const invalidateRequests = () => {
|
|
3529
|
+
for (const controller of requestControllers.values())
|
|
3530
|
+
controller.abort();
|
|
3531
|
+
for (const controller of scopeControllers)
|
|
3532
|
+
controller.abort();
|
|
3533
|
+
scopeControllers.clear();
|
|
3534
|
+
};
|
|
3535
|
+
const readySession = () => identityStatus === "ready" ? { status: "ready", instanceId, mode: options.mode, actor, stamp: requiredId(undefined, "server-session", sessionEpoch === 0 ? "fixture-session" : `fixture-session-${sessionEpoch}`), capabilities: {
|
|
3536
|
+
requestIdentity: "host-resolved",
|
|
3537
|
+
acceptance: "transactional-check",
|
|
3538
|
+
forwarding: "host-bound",
|
|
3539
|
+
directory: { search: false, lookup: false },
|
|
3540
|
+
participants: false,
|
|
3541
|
+
externalSend: "structured",
|
|
3542
|
+
toolProvenance: "causal",
|
|
3543
|
+
operationLookup: true
|
|
3544
|
+
} } : { status: identityStatus, instanceId, error: {
|
|
3545
|
+
code: identityStatus === "unauthenticated" ? "unauthenticated" : "unavailable",
|
|
3546
|
+
message: `Fixture identity is ${identityStatus}.`,
|
|
3547
|
+
retry: "after-refresh"
|
|
3548
|
+
} };
|
|
3549
|
+
const profile = () => ({
|
|
3550
|
+
identity: actor.identity,
|
|
3551
|
+
presentation: actor.presentation,
|
|
3552
|
+
revision: requiredId(undefined, "revision", "fixture-profile"),
|
|
3553
|
+
status: "current"
|
|
3554
|
+
});
|
|
3555
|
+
const newScope = () => {
|
|
3556
|
+
const controller = new AbortController;
|
|
3557
|
+
scopeControllers.add(controller);
|
|
3558
|
+
let released = false;
|
|
3559
|
+
return {
|
|
3560
|
+
signal: controller.signal,
|
|
3561
|
+
validate: () => released || controller.signal.aborted ? { ok: false, code: "invalidated" } : { ok: true },
|
|
3562
|
+
release: () => {
|
|
3563
|
+
released = true;
|
|
3564
|
+
scopeControllers.delete(controller);
|
|
3565
|
+
controller.abort();
|
|
3566
|
+
}
|
|
3567
|
+
};
|
|
3568
|
+
};
|
|
3569
|
+
const registerProvider = async (provider) => {
|
|
3570
|
+
if (nextProviderResult === "fail") {
|
|
3571
|
+
nextProviderResult = "succeed";
|
|
3572
|
+
return err("unavailable", "Fixture provider replacement failed.", "after-reconnect");
|
|
3573
|
+
}
|
|
3574
|
+
const generation = `fixture-generation-${++providerCounter}`;
|
|
3575
|
+
const controller = new AbortController;
|
|
3576
|
+
let status = "staged";
|
|
3577
|
+
const listeners = new Set;
|
|
3578
|
+
const publish = (next) => {
|
|
3579
|
+
status = next;
|
|
3580
|
+
for (const listener of listeners)
|
|
3581
|
+
listener(status);
|
|
3582
|
+
};
|
|
3583
|
+
const raw = {
|
|
3584
|
+
generation,
|
|
3585
|
+
configuration,
|
|
3586
|
+
getStatus: () => status,
|
|
3587
|
+
signal: controller.signal,
|
|
3588
|
+
subscribe(listener) {
|
|
3589
|
+
listeners.add(listener);
|
|
3590
|
+
return () => listeners.delete(listener);
|
|
3591
|
+
},
|
|
3592
|
+
invalidate: () => ok(undefined),
|
|
3593
|
+
person(issuer, subject) {
|
|
3594
|
+
const key = identityKeyCodec.decode(`fixture:${issuer}:${subject}`);
|
|
3595
|
+
return key.ok ? ok({ kind: "person", key: key.value, issuer, subject }) : key;
|
|
3596
|
+
},
|
|
3597
|
+
dispose() {
|
|
3598
|
+
if (status !== "retired") {
|
|
3599
|
+
publish("retired");
|
|
3600
|
+
controller.abort();
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
if (provider.validateReadiness) {
|
|
3605
|
+
const readiness = await provider.validateReadiness({
|
|
3606
|
+
generation,
|
|
3607
|
+
configuration,
|
|
3608
|
+
deadlineAt: clock.now() + configuration.resolver.timeoutMs,
|
|
3609
|
+
signal: controller.signal
|
|
3610
|
+
});
|
|
3611
|
+
if (!readiness.ok) {
|
|
3612
|
+
raw.dispose();
|
|
3613
|
+
return readiness;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
const previous = activeProvider;
|
|
3617
|
+
activeProvider = raw;
|
|
3618
|
+
publish("active");
|
|
3619
|
+
previous?.dispose();
|
|
3620
|
+
return ok(raw);
|
|
3621
|
+
};
|
|
3622
|
+
const rawProtocol = {
|
|
3623
|
+
version: 1,
|
|
3624
|
+
instanceId,
|
|
3625
|
+
bindInvocation({ handler }) {
|
|
3626
|
+
return { registration: { generation: "fixture-invocation", status: "active", dispose() {} }, handler: (...args) => handler({ request: { id: "bound", origin: "interactive-user" }, scope: newScope() }, ...args) };
|
|
3627
|
+
},
|
|
3628
|
+
async session() {
|
|
3629
|
+
return readySession();
|
|
3630
|
+
},
|
|
3631
|
+
async selfProfile() {
|
|
3632
|
+
observations.profileReads++;
|
|
3633
|
+
return identityStatus === "ready" ? ok(profile()) : err("unauthenticated", "No fixture identity.");
|
|
3634
|
+
},
|
|
3635
|
+
async openRequest(request) {
|
|
3636
|
+
const session = readySession();
|
|
3637
|
+
if (session.status !== "ready")
|
|
3638
|
+
return err("unauthenticated", "No fixture identity.");
|
|
3639
|
+
const controller = new AbortController;
|
|
3640
|
+
requestControllers.set(request.id, controller);
|
|
3641
|
+
const scope = newScope();
|
|
3642
|
+
return ok({
|
|
3643
|
+
signal: controller.signal,
|
|
3644
|
+
session,
|
|
3645
|
+
validate: (expected) => controller.signal.aborted ? err("expired", "Fixture request expired.") : expected.actor === session.actor.identity.key && expected.session === session.stamp ? ok(undefined) : err("stale-context", "Fixture request changed."),
|
|
3646
|
+
release: () => controller.abort(),
|
|
3647
|
+
scope
|
|
3648
|
+
});
|
|
3649
|
+
},
|
|
3650
|
+
async accept(input) {
|
|
3651
|
+
if (input.source.kind === "scope" && !input.source.scope.validate().ok) {
|
|
3652
|
+
return { status: "rejected", error: { code: "expired", message: "Fixture request context is no longer live.", retry: "never" } };
|
|
3653
|
+
}
|
|
3654
|
+
observations.submissions++;
|
|
3655
|
+
return wireSubmitted(input.input);
|
|
3656
|
+
},
|
|
3657
|
+
async lookup() {
|
|
3658
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
3659
|
+
},
|
|
3660
|
+
async provenance() {
|
|
3661
|
+
return ok({ status: "unknown", correlation: null, reason: "fixture" });
|
|
3662
|
+
},
|
|
3663
|
+
historyContributions: unavailableHistory().contributions,
|
|
3664
|
+
historyAttempts: unavailableHistory().attempts,
|
|
3665
|
+
directorySources: () => [],
|
|
3666
|
+
async participants() {
|
|
3667
|
+
observations.participantReads++;
|
|
3668
|
+
return err("unsupported", "Fixture participants are disabled.");
|
|
3669
|
+
},
|
|
3670
|
+
async forwardRpc(_scope, _destination, input) {
|
|
3671
|
+
return input;
|
|
3672
|
+
},
|
|
3673
|
+
registerProvider,
|
|
3674
|
+
subscribe(listener) {
|
|
3675
|
+
observations.subscriptions++;
|
|
3676
|
+
hostListeners.add(listener);
|
|
3677
|
+
return () => hostListeners.delete(listener);
|
|
3678
|
+
}
|
|
3679
|
+
};
|
|
3680
|
+
rawExtension = rawProtocol;
|
|
3681
|
+
const upstream = {
|
|
3682
|
+
instanceId,
|
|
3683
|
+
pluginId,
|
|
3684
|
+
inputCodec: options.inputCodec,
|
|
3685
|
+
scheduler: clock,
|
|
3686
|
+
async session() {
|
|
3687
|
+
return readySession();
|
|
3688
|
+
},
|
|
3689
|
+
async selfProfile() {
|
|
3690
|
+
observations.profileReads++;
|
|
3691
|
+
return identityStatus === "ready" ? ok(profile()) : err("unauthenticated", "No fixture identity.");
|
|
3692
|
+
},
|
|
3693
|
+
async openScope(request) {
|
|
3694
|
+
const session = readySession();
|
|
3695
|
+
if (session.status !== "ready")
|
|
3696
|
+
return err("unauthenticated", "No fixture identity.");
|
|
3697
|
+
const controller = new AbortController;
|
|
3698
|
+
requestControllers.set(request.id, controller);
|
|
3699
|
+
return ok({
|
|
3700
|
+
signal: controller.signal,
|
|
3701
|
+
session,
|
|
3702
|
+
validate: (expected) => controller.signal.aborted ? err("expired", "Fixture request expired.") : expected.actor === session.actor.identity.key && expected.session === session.stamp ? ok(undefined) : err("stale-context", "Fixture request changed."),
|
|
3703
|
+
release: () => controller.abort()
|
|
3704
|
+
});
|
|
3705
|
+
},
|
|
3706
|
+
async submit(input) {
|
|
3707
|
+
observations.submissions++;
|
|
3708
|
+
return submitted(input);
|
|
3709
|
+
},
|
|
3710
|
+
labelExternal: (_author, input) => input,
|
|
3711
|
+
toolCorrelation: () => ({ status: "unknown", correlation: null, reason: "fixture" }),
|
|
3712
|
+
async forward(_context, _plugin, _method, input) {
|
|
3713
|
+
return input;
|
|
3714
|
+
},
|
|
3715
|
+
subscribe(listener) {
|
|
3716
|
+
observations.subscriptions++;
|
|
3717
|
+
hostListeners.add(listener);
|
|
3718
|
+
return () => hostListeners.delete(listener);
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
return {
|
|
3722
|
+
upstream,
|
|
3723
|
+
clock,
|
|
3724
|
+
connect() {
|
|
3725
|
+
observations.rawExtensionReads++;
|
|
3726
|
+
return createHostAdapter({ upstream, extension: inspectForkExtension(rawExtension) });
|
|
3727
|
+
},
|
|
3728
|
+
setRawExtension(value) {
|
|
3729
|
+
rawExtension = value;
|
|
3730
|
+
},
|
|
3731
|
+
setLookup() {
|
|
3732
|
+
unsupportedControl("setLookup");
|
|
3733
|
+
},
|
|
3734
|
+
request(origin = "interactive-user") {
|
|
3735
|
+
return { id: `request-${requestControllers.size + 1}`, origin };
|
|
3736
|
+
},
|
|
3737
|
+
setActor(next) {
|
|
3738
|
+
if (options.mode === "single-user")
|
|
3739
|
+
return err("unsupported", "Singleton adapter fixtures cannot replace the reserved default actor.");
|
|
3740
|
+
actor = next;
|
|
3741
|
+
sessionEpoch++;
|
|
3742
|
+
invalidateRequests();
|
|
3743
|
+
for (const listener of hostListeners)
|
|
3744
|
+
listener({ kind: "session", reason: "actor" });
|
|
3745
|
+
return ok(undefined);
|
|
3746
|
+
},
|
|
3747
|
+
setIdentityAvailability(status) {
|
|
3748
|
+
if (identityStatus === status)
|
|
3749
|
+
return;
|
|
3750
|
+
identityStatus = status;
|
|
3751
|
+
invalidateRequests();
|
|
3752
|
+
for (const listener of hostListeners)
|
|
3753
|
+
listener({ kind: "session", reason: "provider" });
|
|
3754
|
+
},
|
|
3755
|
+
settle(request) {
|
|
3756
|
+
requestControllers.get(request.id)?.abort();
|
|
3757
|
+
},
|
|
3758
|
+
constructStreamingResponse() {
|
|
3759
|
+
unsupportedControl("constructStreamingResponse");
|
|
3760
|
+
},
|
|
3761
|
+
finishStream() {
|
|
3762
|
+
unsupportedControl("finishStream");
|
|
3763
|
+
},
|
|
3764
|
+
abort(request) {
|
|
3765
|
+
requestControllers.get(request.id)?.abort();
|
|
3766
|
+
},
|
|
3767
|
+
replaceProvider(result) {
|
|
3768
|
+
nextProviderResult = result;
|
|
3769
|
+
},
|
|
3770
|
+
disconnect() {
|
|
3771
|
+
for (const listener of hostListeners)
|
|
3772
|
+
listener({ kind: "disconnected" });
|
|
3773
|
+
},
|
|
3774
|
+
reconnect() {
|
|
3775
|
+
for (const listener of hostListeners)
|
|
3776
|
+
listener({ kind: "reconnected" });
|
|
3777
|
+
},
|
|
3778
|
+
pauseAcceptance() {
|
|
3779
|
+
return unsupportedControl("pauseAcceptance");
|
|
3780
|
+
},
|
|
3781
|
+
loseNextAcceptanceResponse() {
|
|
3782
|
+
unsupportedControl("loseNextAcceptanceResponse");
|
|
3783
|
+
},
|
|
3784
|
+
beginAttempt() {
|
|
3785
|
+
return unsupportedControl("beginAttempt");
|
|
3786
|
+
},
|
|
3787
|
+
editContribution() {
|
|
3788
|
+
unsupportedControl("editContribution");
|
|
3789
|
+
},
|
|
3790
|
+
continueAttempt() {
|
|
3791
|
+
return unsupportedControl("continueAttempt");
|
|
3792
|
+
},
|
|
3793
|
+
toolCallForAttempt() {
|
|
3794
|
+
return unsupportedControl("toolCallForAttempt");
|
|
3795
|
+
},
|
|
3796
|
+
get accepted() {
|
|
3797
|
+
return unsupportedControl("accepted");
|
|
3798
|
+
},
|
|
3799
|
+
observations,
|
|
3800
|
+
dispose() {
|
|
3801
|
+
invalidateRequests();
|
|
3802
|
+
for (const listener of hostListeners)
|
|
3803
|
+
listener({ kind: "disposed" });
|
|
3804
|
+
hostListeners.clear();
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
}
|
|
3808
|
+
function createPause() {
|
|
3809
|
+
const reached = deferred();
|
|
3810
|
+
const releaseGate = deferred();
|
|
3811
|
+
let reachedOnce = false;
|
|
3812
|
+
let released = false;
|
|
3813
|
+
return {
|
|
3814
|
+
reached: reached.promise,
|
|
3815
|
+
reach() {
|
|
3816
|
+
if (!reachedOnce) {
|
|
3817
|
+
reachedOnce = true;
|
|
3818
|
+
reached.resolve();
|
|
3819
|
+
}
|
|
3820
|
+
},
|
|
3821
|
+
wait: () => releaseGate.promise,
|
|
3822
|
+
release() {
|
|
3823
|
+
if (!released) {
|
|
3824
|
+
released = true;
|
|
3825
|
+
releaseGate.resolve();
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
function cloneFixture(value) {
|
|
3831
|
+
return structuredClone(value);
|
|
3832
|
+
}
|
|
3833
|
+
function fixtureJson(value) {
|
|
3834
|
+
const visit = (current) => {
|
|
3835
|
+
if (current === null || typeof current === "string" || typeof current === "boolean")
|
|
3836
|
+
return current;
|
|
3837
|
+
if (typeof current === "number") {
|
|
3838
|
+
if (!Number.isFinite(current))
|
|
3839
|
+
throw new TypeError("Fixture storage accepts finite JSON numbers only.");
|
|
3840
|
+
return current;
|
|
3841
|
+
}
|
|
3842
|
+
if (Array.isArray(current))
|
|
3843
|
+
return current.map(visit);
|
|
3844
|
+
if (typeof current !== "object")
|
|
3845
|
+
throw new TypeError("Fixture storage accepts JSON values only.");
|
|
3846
|
+
const prototype = Object.getPrototypeOf(current);
|
|
3847
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
3848
|
+
throw new TypeError("Fixture storage accepts plain JSON objects only.");
|
|
3849
|
+
const output = Object.create(null);
|
|
3850
|
+
for (const key of Object.keys(current).sort()) {
|
|
3851
|
+
const child = current[key];
|
|
3852
|
+
if (child === undefined)
|
|
3853
|
+
throw new TypeError("Fixture storage does not accept undefined values.");
|
|
3854
|
+
output[key] = visit(child);
|
|
3855
|
+
}
|
|
3856
|
+
return output;
|
|
3857
|
+
};
|
|
3858
|
+
return JSON.stringify(visit(value));
|
|
3859
|
+
}
|
|
3860
|
+
function sameVersion(left, right) {
|
|
3861
|
+
return left.epoch === right.epoch && left.sequence === right.sequence;
|
|
3862
|
+
}
|
|
3863
|
+
function cloneRead(read) {
|
|
3864
|
+
return cloneFixture(read);
|
|
3865
|
+
}
|
|
3866
|
+
function createConnectionHarness() {
|
|
3867
|
+
let health = {
|
|
3868
|
+
generation: 0,
|
|
3869
|
+
identity: { status: "healthy" },
|
|
3870
|
+
state: { status: "healthy" }
|
|
3871
|
+
};
|
|
3872
|
+
const replies = new Map;
|
|
3873
|
+
const paused = new Map;
|
|
3874
|
+
const events = new Set;
|
|
3875
|
+
const healthListeners = new Set;
|
|
3876
|
+
const methods2 = [];
|
|
3877
|
+
let disposeCalls = 0;
|
|
3878
|
+
const connection = createIdentityConnection({
|
|
3879
|
+
async request(method) {
|
|
3880
|
+
methods2.push(method);
|
|
3881
|
+
const gate2 = paused.get(method)?.shift();
|
|
3882
|
+
if (gate2) {
|
|
3883
|
+
gate2.reach();
|
|
3884
|
+
await gate2.wait();
|
|
3885
|
+
}
|
|
3886
|
+
const queue = replies.get(method);
|
|
3887
|
+
if (!queue?.length)
|
|
3888
|
+
throw new Error(`No fixture reply queued for ${method}.`);
|
|
3889
|
+
return cloneFixture(queue.shift());
|
|
3890
|
+
},
|
|
3891
|
+
subscribe(listener) {
|
|
3892
|
+
events.add(listener);
|
|
3893
|
+
return () => events.delete(listener);
|
|
3894
|
+
},
|
|
3895
|
+
getHealth: () => cloneFixture(health),
|
|
3896
|
+
subscribeHealth(listener) {
|
|
3897
|
+
healthListeners.add(listener);
|
|
3898
|
+
return () => healthListeners.delete(listener);
|
|
3899
|
+
},
|
|
3900
|
+
async revalidate() {
|
|
3901
|
+
return { ok: true, value: undefined };
|
|
3902
|
+
},
|
|
3903
|
+
dispose() {
|
|
3904
|
+
disposeCalls++;
|
|
3905
|
+
}
|
|
3906
|
+
});
|
|
3907
|
+
return {
|
|
3908
|
+
connection,
|
|
3909
|
+
connect() {
|
|
3910
|
+
return createIdentityClientTransport(connection);
|
|
3911
|
+
},
|
|
3912
|
+
setHealth(next) {
|
|
3913
|
+
health = cloneFixture(next);
|
|
3914
|
+
for (const listener of [...healthListeners])
|
|
3915
|
+
listener();
|
|
3916
|
+
},
|
|
3917
|
+
replyNext(method, payload) {
|
|
3918
|
+
const queue = replies.get(method) ?? [];
|
|
3919
|
+
queue.push(cloneFixture(payload));
|
|
3920
|
+
replies.set(method, queue);
|
|
3921
|
+
},
|
|
3922
|
+
emit(event) {
|
|
3923
|
+
for (const listener of [...events])
|
|
3924
|
+
listener(cloneFixture(event));
|
|
3925
|
+
},
|
|
3926
|
+
pauseNextRequest(method) {
|
|
3927
|
+
const gate2 = createPause();
|
|
3928
|
+
const queue = paused.get(method) ?? [];
|
|
3929
|
+
queue.push(gate2);
|
|
3930
|
+
paused.set(method, queue);
|
|
3931
|
+
return gate2;
|
|
3932
|
+
},
|
|
3933
|
+
observations: {
|
|
3934
|
+
methods: methods2,
|
|
3935
|
+
get disposeCalls() {
|
|
3936
|
+
return disposeCalls;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
function createStateStorageHarness(options) {
|
|
3942
|
+
const records = new Map;
|
|
3943
|
+
const receipts = new Map;
|
|
3944
|
+
const commits = [];
|
|
3945
|
+
let readFailure = null;
|
|
3946
|
+
let readPause = null;
|
|
3947
|
+
let commitPause = null;
|
|
3948
|
+
let loseCommitResponse = false;
|
|
3949
|
+
const key = (address) => fixtureJson([address.instanceId, address.pluginId, address.collection, address.recordId, address.owner]);
|
|
3950
|
+
const receiptKey = (address, operationId) => `${key(address)}\x00${operationId}`;
|
|
3951
|
+
const empty = (address) => ({ status: "empty", address: cloneFixture(address), version: { epoch: "fixture-empty-r1", sequence: 0 } });
|
|
3952
|
+
const current = (address) => cloneRead(records.get(key(address)) ?? empty(address));
|
|
3953
|
+
const storage = {
|
|
3954
|
+
boundary: "same-process-synchronous",
|
|
3955
|
+
receiptRetentionMs: 60000,
|
|
3956
|
+
async read(address) {
|
|
3957
|
+
const paused = readPause;
|
|
3958
|
+
readPause = null;
|
|
3959
|
+
const before = paused?.capture === "before-pause" ? current(address) : null;
|
|
3960
|
+
if (paused) {
|
|
3961
|
+
paused.gate.reach();
|
|
3962
|
+
await paused.gate.wait();
|
|
3963
|
+
}
|
|
3964
|
+
if (readFailure)
|
|
3965
|
+
return { ok: false, error: cloneFixture(readFailure) };
|
|
3966
|
+
return ok(before ?? current(address));
|
|
3967
|
+
},
|
|
3968
|
+
async commit(input) {
|
|
3969
|
+
const gate2 = commitPause;
|
|
3970
|
+
commitPause = null;
|
|
3971
|
+
if (gate2) {
|
|
3972
|
+
gate2.reach();
|
|
3973
|
+
await gate2.wait();
|
|
3974
|
+
}
|
|
3975
|
+
let mutation;
|
|
3976
|
+
let fingerprint;
|
|
3977
|
+
try {
|
|
3978
|
+
mutation = cloneFixture(input.mutation);
|
|
3979
|
+
fingerprint = fixtureJson(mutation);
|
|
3980
|
+
} catch (caught) {
|
|
3981
|
+
return err("invalid-input", caught instanceof Error ? caught.message : "Invalid fixture state mutation.");
|
|
3982
|
+
}
|
|
3983
|
+
const addressKey = key(mutation.address);
|
|
3984
|
+
const existingReceipt = receipts.get(receiptKey(mutation.address, mutation.operationId));
|
|
3985
|
+
if (existingReceipt) {
|
|
3986
|
+
if (existingReceipt.expired || existingReceipt.retainedUntil <= options.clock.now()) {
|
|
3987
|
+
existingReceipt.expired = true;
|
|
3988
|
+
return err("expired", "Fixture operation receipt expired.");
|
|
3989
|
+
}
|
|
3990
|
+
return existingReceipt.mutation === fingerprint ? ok(cloneFixture(existingReceipt.outcome)) : err("invalid-operation", "Operation ID was reused with a different immutable mutation.");
|
|
3991
|
+
}
|
|
3992
|
+
const actor = input.validateAtCommit();
|
|
3993
|
+
if (!actor.ok)
|
|
3994
|
+
return actor;
|
|
3995
|
+
const present = current(mutation.address);
|
|
3996
|
+
const version = present.status === "present" ? present.envelope.version : present.status === "empty" ? present.version : null;
|
|
3997
|
+
const outcome = present.status === "migration-required" ? { status: "conflict", current: present, operationId: mutation.operationId } : mutation.kind === "initialize" && present.status === "present" ? { status: "already-initialized", current: present, operationId: mutation.operationId } : version === null || !sameVersion(version, mutation.expectedVersion) ? { status: "conflict", current: present, operationId: mutation.operationId } : present.status === "present" && fixtureJson(present.envelope.value) === fixtureJson(mutation.value) && present.envelope.schemaVersion === mutation.schemaVersion ? { status: "unchanged", envelope: present.envelope, operationId: mutation.operationId } : (() => {
|
|
3998
|
+
const envelope = {
|
|
3999
|
+
address: cloneFixture(mutation.address),
|
|
4000
|
+
version: { epoch: version.epoch, sequence: version.sequence + 1 },
|
|
4001
|
+
schemaVersion: mutation.schemaVersion,
|
|
4002
|
+
value: cloneFixture(mutation.value),
|
|
4003
|
+
lastEditedBy: cloneFixture(actor.value)
|
|
4004
|
+
};
|
|
4005
|
+
records.set(addressKey, { status: "present", envelope: cloneFixture(envelope) });
|
|
4006
|
+
commits.push({ actor: actor.value.identity.key, address: cloneFixture(envelope.address), version: cloneFixture(envelope.version) });
|
|
4007
|
+
return { status: "saved", envelope, operationId: mutation.operationId };
|
|
4008
|
+
})();
|
|
4009
|
+
receipts.set(receiptKey(mutation.address, mutation.operationId), {
|
|
4010
|
+
mutation: fingerprint,
|
|
4011
|
+
outcome: cloneFixture(outcome),
|
|
4012
|
+
retainedUntil: options.clock.now() + storage.receiptRetentionMs,
|
|
4013
|
+
expired: false
|
|
4014
|
+
});
|
|
4015
|
+
if (loseCommitResponse) {
|
|
4016
|
+
loseCommitResponse = false;
|
|
4017
|
+
return ok({ status: "indeterminate", operationId: mutation.operationId });
|
|
4018
|
+
}
|
|
4019
|
+
return ok(cloneFixture(outcome));
|
|
4020
|
+
},
|
|
4021
|
+
async reconcile({ address, operationId }) {
|
|
4022
|
+
const receipt = receipts.get(receiptKey(address, operationId));
|
|
4023
|
+
if (!receipt)
|
|
4024
|
+
return ok({ status: "absent-final", retry: "same-operation-only" });
|
|
4025
|
+
if (receipt.expired || receipt.retainedUntil <= options.clock.now()) {
|
|
4026
|
+
receipt.expired = true;
|
|
4027
|
+
return ok({ status: "unknown", reason: "expired" });
|
|
4028
|
+
}
|
|
4029
|
+
return ok({ status: "final", outcome: cloneFixture(receipt.outcome) });
|
|
4030
|
+
}
|
|
4031
|
+
};
|
|
4032
|
+
return {
|
|
4033
|
+
storage,
|
|
4034
|
+
seed(envelope) {
|
|
4035
|
+
records.set(key(envelope.address), { status: "present", envelope: cloneFixture(envelope) });
|
|
4036
|
+
},
|
|
4037
|
+
seedEmpty(address, version) {
|
|
4038
|
+
records.set(key(address), { status: "empty", address: cloneFixture(address), version: cloneFixture(version) });
|
|
4039
|
+
},
|
|
4040
|
+
seedReadFailure(error) {
|
|
4041
|
+
readFailure = cloneFixture(error);
|
|
4042
|
+
},
|
|
4043
|
+
seedLegacy(address, schemaVersion) {
|
|
4044
|
+
records.set(key(address), { status: "migration-required", address: cloneFixture(address), storedSchemaVersion: schemaVersion });
|
|
4045
|
+
},
|
|
4046
|
+
readCommitted(address) {
|
|
4047
|
+
const read = records.get(key(address));
|
|
4048
|
+
return read?.status === "present" ? cloneFixture(read.envelope) : null;
|
|
4049
|
+
},
|
|
4050
|
+
pauseNextRead(options2) {
|
|
4051
|
+
const gate2 = createPause();
|
|
4052
|
+
readPause = { capture: options2.capture, gate: gate2 };
|
|
4053
|
+
return gate2;
|
|
4054
|
+
},
|
|
4055
|
+
pauseNextCommit() {
|
|
4056
|
+
const gate2 = createPause();
|
|
4057
|
+
commitPause = gate2;
|
|
4058
|
+
return gate2;
|
|
4059
|
+
},
|
|
4060
|
+
loseNextCommitResponse() {
|
|
4061
|
+
loseCommitResponse = true;
|
|
4062
|
+
},
|
|
4063
|
+
expireReceipt(operationId) {
|
|
4064
|
+
for (const [receiptKeyValue, receipt] of receipts)
|
|
4065
|
+
if (receiptKeyValue.endsWith(`\x00${operationId}`))
|
|
4066
|
+
receipt.expired = true;
|
|
4067
|
+
},
|
|
4068
|
+
commits
|
|
4069
|
+
};
|
|
4070
|
+
}
|
|
4071
|
+
function consumerProvenanceFixture(provenance) {
|
|
4072
|
+
return { id: provenance.status === "unknown" ? "unknown-provenance" : provenance.correlation?.toolCallId ?? "fixture-tool-call" };
|
|
4073
|
+
}
|
|
4074
|
+
export {
|
|
4075
|
+
personFixture,
|
|
4076
|
+
defineStateStorageConformance,
|
|
4077
|
+
deferred,
|
|
4078
|
+
createStateStorageHarness,
|
|
4079
|
+
createManualClock,
|
|
4080
|
+
createIdentityWireFake,
|
|
4081
|
+
createConnectionHarness,
|
|
4082
|
+
createAdapterHarness,
|
|
4083
|
+
consumerProvenanceFixture
|
|
4084
|
+
};
|