@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,2574 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// http-lifetime-runtime.ts
|
|
3
|
+
function retainIdentityHttpResponse(response, invocationSignal, requestSignal, release) {
|
|
4
|
+
let settled = false;
|
|
5
|
+
const finish = () => {
|
|
6
|
+
if (settled)
|
|
7
|
+
return;
|
|
8
|
+
settled = true;
|
|
9
|
+
invocationSignal.removeEventListener("abort", abort);
|
|
10
|
+
requestSignal.removeEventListener("abort", abort);
|
|
11
|
+
release();
|
|
12
|
+
};
|
|
13
|
+
let reader;
|
|
14
|
+
let stream;
|
|
15
|
+
const abort = () => {
|
|
16
|
+
if (settled)
|
|
17
|
+
return;
|
|
18
|
+
finish();
|
|
19
|
+
stream?.error(new Error("Identity HTTP invocation expired."));
|
|
20
|
+
reader?.cancel().catch(() => {
|
|
21
|
+
return;
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
if (!response.body) {
|
|
25
|
+
finish();
|
|
26
|
+
return response;
|
|
27
|
+
}
|
|
28
|
+
reader = response.body.getReader();
|
|
29
|
+
const body = new ReadableStream({
|
|
30
|
+
start(controller) {
|
|
31
|
+
stream = controller;
|
|
32
|
+
invocationSignal.addEventListener("abort", abort, { once: true });
|
|
33
|
+
requestSignal.addEventListener("abort", abort, { once: true });
|
|
34
|
+
if (invocationSignal.aborted || requestSignal.aborted)
|
|
35
|
+
abort();
|
|
36
|
+
},
|
|
37
|
+
async pull(controller) {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const item = await reader.read();
|
|
42
|
+
if (settled)
|
|
43
|
+
return;
|
|
44
|
+
if (item.done) {
|
|
45
|
+
finish();
|
|
46
|
+
controller.close();
|
|
47
|
+
} else
|
|
48
|
+
controller.enqueue(item.value);
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
if (!settled) {
|
|
51
|
+
finish();
|
|
52
|
+
controller.error(cause);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
cancel(reason) {
|
|
57
|
+
finish();
|
|
58
|
+
return reader.cancel(reason);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// bb-upstream-runtime.ts
|
|
65
|
+
import { z } from "zod";
|
|
66
|
+
|
|
67
|
+
// model-runtime.ts
|
|
68
|
+
var ID_MAX_LENGTH = 256;
|
|
69
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
70
|
+
function failure(code, message, retry = "never") {
|
|
71
|
+
return { code, message, retry };
|
|
72
|
+
}
|
|
73
|
+
function ok(value) {
|
|
74
|
+
return { ok: true, value };
|
|
75
|
+
}
|
|
76
|
+
function err(code, message, retry = "never") {
|
|
77
|
+
return { ok: false, error: failure(code, message, retry) };
|
|
78
|
+
}
|
|
79
|
+
function isRecord(value) {
|
|
80
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
function isBoundedString(value, maximum = ID_MAX_LENGTH) {
|
|
83
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum;
|
|
84
|
+
}
|
|
85
|
+
function idCodec(kind) {
|
|
86
|
+
return {
|
|
87
|
+
decode(input) {
|
|
88
|
+
const valid = kind === "server-session" ? isBoundedString(input, 4096) && !/[\u0000-\u0020\u007f]/.test(input) : isBoundedString(input) && ID_PATTERN.test(input);
|
|
89
|
+
if (!valid) {
|
|
90
|
+
return err("invalid-input", `Invalid ${kind} identifier.`);
|
|
91
|
+
}
|
|
92
|
+
return ok(input);
|
|
93
|
+
},
|
|
94
|
+
encode(value) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
var identityKeyCodec = {
|
|
100
|
+
decode(input) {
|
|
101
|
+
if (!isBoundedString(input, 512) || /[\u0000-\u001f\u007f]/.test(input)) {
|
|
102
|
+
return err("invalid-input", "Invalid identity key.");
|
|
103
|
+
}
|
|
104
|
+
return ok(input);
|
|
105
|
+
},
|
|
106
|
+
encode(value) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
function decodePresentation(value) {
|
|
111
|
+
return isRecord(value) && isBoundedString(value.displayName, 512) && (value.handle === null || isBoundedString(value.handle, 512)) && (value.avatarUrl === null || isBoundedString(value.avatarUrl, 2048));
|
|
112
|
+
}
|
|
113
|
+
function decodeIdentity(value) {
|
|
114
|
+
if (!isRecord(value) || !identityKeyCodec.decode(value.key).ok)
|
|
115
|
+
return false;
|
|
116
|
+
if (value.kind === "person")
|
|
117
|
+
return isBoundedString(value.issuer, 512) && isBoundedString(value.subject, 512);
|
|
118
|
+
if (value.kind === "default-user")
|
|
119
|
+
return isBoundedString(value.instanceId);
|
|
120
|
+
if (value.kind === "machine") {
|
|
121
|
+
return isBoundedString(value.instanceId) && (value.hostId === null || isBoundedString(value.hostId, 512));
|
|
122
|
+
}
|
|
123
|
+
return value.kind === "external" && isBoundedString(value.pluginId) && isBoundedString(value.subject, 512);
|
|
124
|
+
}
|
|
125
|
+
var profileCodec = {
|
|
126
|
+
decode(input) {
|
|
127
|
+
if (!isRecord(input) || !decodeIdentity(input.identity) || !decodePresentation(input.presentation) || !isBoundedString(input.revision) || input.status !== "current" && input.status !== "historical") {
|
|
128
|
+
return err("invalid-input", "Invalid identity profile.");
|
|
129
|
+
}
|
|
130
|
+
return ok(input);
|
|
131
|
+
},
|
|
132
|
+
encode(value) {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
function isContributionReference(value) {
|
|
137
|
+
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && idCodec("contribution").decode(value.contributionId).ok;
|
|
138
|
+
}
|
|
139
|
+
function isPersonReference(value) {
|
|
140
|
+
return isRecord(value) && (value.kind === "person" || value.kind === "default-user") && decodeIdentity(value);
|
|
141
|
+
}
|
|
142
|
+
function isMachineReference(value) {
|
|
143
|
+
return isRecord(value) && value.kind === "machine" && decodeIdentity(value);
|
|
144
|
+
}
|
|
145
|
+
function isActorSnapshot(value) {
|
|
146
|
+
if (!isRecord(value) || !decodeIdentity(value.identity) || !decodePresentation(value.presentation))
|
|
147
|
+
return false;
|
|
148
|
+
const evidence = value.evidence;
|
|
149
|
+
if (evidence !== "provider-verified" && evidence !== "local-user" && evidence !== "upstream-default" && evidence !== "integration-asserted" && evidence !== "legacy" && evidence !== "machine")
|
|
150
|
+
return false;
|
|
151
|
+
return isMachineReference(value.identity) ? evidence === "machine" : evidence !== "machine";
|
|
152
|
+
}
|
|
153
|
+
function isOrigin(value) {
|
|
154
|
+
if (!isRecord(value))
|
|
155
|
+
return false;
|
|
156
|
+
if (value.kind === "person") {
|
|
157
|
+
const actor = value.actor;
|
|
158
|
+
return isActorSnapshot(actor) && isRecord(actor) && isPersonReference(actor.identity);
|
|
159
|
+
}
|
|
160
|
+
if (value.kind === "machine") {
|
|
161
|
+
const actor = value.actor;
|
|
162
|
+
return isActorSnapshot(actor) && isRecord(actor) && isMachineReference(actor.identity) && isRecord(actor) && actor.evidence === "machine";
|
|
163
|
+
}
|
|
164
|
+
if (value.kind === "external") {
|
|
165
|
+
const actor = value.actor;
|
|
166
|
+
return isActorSnapshot(actor) && isRecord(actor) && isRecord(actor.identity) && actor.identity.kind === "external" && actor.evidence !== "machine";
|
|
167
|
+
}
|
|
168
|
+
if (value.kind === "agent")
|
|
169
|
+
return value.agentId === null || isBoundedString(value.agentId);
|
|
170
|
+
if (value.kind === "system")
|
|
171
|
+
return isBoundedString(value.reason, 1024);
|
|
172
|
+
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
173
|
+
}
|
|
174
|
+
function isContribution(value) {
|
|
175
|
+
return isRecord(value) && isContributionReference(value.reference) && isOrigin(value.author) && (value.latestEditor === null || isActorSnapshot(value.latestEditor)) && isBoundedString(value.acceptedAt, 256) && Array.isArray(value.mentionedPeople) && value.mentionedPeople.every(isPersonReference);
|
|
176
|
+
}
|
|
177
|
+
function isCorrelation(value) {
|
|
178
|
+
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
179
|
+
}
|
|
180
|
+
function referenceKey(value) {
|
|
181
|
+
return `${value.threadId}\x00${value.contributionId}`;
|
|
182
|
+
}
|
|
183
|
+
function isInputSource(value) {
|
|
184
|
+
if (!isRecord(value))
|
|
185
|
+
return false;
|
|
186
|
+
if (value.kind === "contribution")
|
|
187
|
+
return isContributionReference(value.reference);
|
|
188
|
+
if (value.kind === "interaction")
|
|
189
|
+
return isBoundedString(value.interactionId) && isOrigin(value.resolver);
|
|
190
|
+
return value.kind === "generated" && (value.purpose === "continuation" || value.purpose === "resolved-resource" || value.purpose === "system-context") && Array.isArray(value.basedOn) && value.basedOn.every(isContributionReference);
|
|
191
|
+
}
|
|
192
|
+
function causalReferences(groups) {
|
|
193
|
+
const references = [];
|
|
194
|
+
for (const group of groups) {
|
|
195
|
+
if (!isRecord(group) || !Array.isArray(group.sources) || !group.sources.every(isInputSource))
|
|
196
|
+
return null;
|
|
197
|
+
for (const source of group.sources) {
|
|
198
|
+
if (!isRecord(source))
|
|
199
|
+
return null;
|
|
200
|
+
if (source.kind === "contribution" && isContributionReference(source.reference))
|
|
201
|
+
references.push(source.reference);
|
|
202
|
+
if (source.kind === "generated" && Array.isArray(source.basedOn) && source.basedOn.every(isContributionReference))
|
|
203
|
+
references.push(...source.basedOn);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return references;
|
|
207
|
+
}
|
|
208
|
+
function hasCompleteCausality(groups, contributions, missing) {
|
|
209
|
+
const references = causalReferences(groups);
|
|
210
|
+
if (references === null || !contributions.every(isContribution) || !missing.every(isContributionReference))
|
|
211
|
+
return false;
|
|
212
|
+
const required = new Set(references.map(referenceKey));
|
|
213
|
+
const found = new Set;
|
|
214
|
+
for (const contribution of contributions) {
|
|
215
|
+
if (!isRecord(contribution) || !isContributionReference(contribution.reference))
|
|
216
|
+
return false;
|
|
217
|
+
const key = referenceKey(contribution.reference);
|
|
218
|
+
if (found.has(key))
|
|
219
|
+
return false;
|
|
220
|
+
found.add(key);
|
|
221
|
+
}
|
|
222
|
+
const absent = new Set;
|
|
223
|
+
for (const reference of missing) {
|
|
224
|
+
const key = referenceKey(reference);
|
|
225
|
+
if (absent.has(key) || found.has(key))
|
|
226
|
+
return false;
|
|
227
|
+
absent.add(key);
|
|
228
|
+
}
|
|
229
|
+
return required.size === found.size + absent.size && [...required].every((key) => found.has(key) || absent.has(key));
|
|
230
|
+
}
|
|
231
|
+
var provenanceCodec = {
|
|
232
|
+
decode(input) {
|
|
233
|
+
if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
|
|
234
|
+
return err("invalid-input", "Invalid execution provenance.");
|
|
235
|
+
}
|
|
236
|
+
if (input.status === "unknown")
|
|
237
|
+
return (input.correlation === null || isCorrelation(input.correlation)) && isBoundedString(input.reason, 1024) ? ok(input) : err("invalid-input", "Unknown provenance needs a valid correlation and reason.");
|
|
238
|
+
if (!Array.isArray(input.inputGroups) || !Array.isArray(input.contributions))
|
|
239
|
+
return err("invalid-input", "Provenance needs input groups and contributions.");
|
|
240
|
+
if (input.status === "known") {
|
|
241
|
+
return isCorrelation(input.correlation) && hasCompleteCausality(input.inputGroups, input.contributions, []) ? ok(input) : err("invalid-input", "Known provenance needs complete correlated evidence.");
|
|
242
|
+
}
|
|
243
|
+
if ((input.correlation === null || isCorrelation(input.correlation)) && Array.isArray(input.missing) && isBoundedString(input.reason, 1024) && hasCompleteCausality(input.inputGroups, input.contributions, input.missing)) {
|
|
244
|
+
return ok(input);
|
|
245
|
+
}
|
|
246
|
+
return err("invalid-input", "Partial provenance needs complete causal accounting.");
|
|
247
|
+
},
|
|
248
|
+
encode(value) {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
function defaultIdentityKey(instance) {
|
|
253
|
+
return `local:${instance}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// bb-upstream-runtime.ts
|
|
257
|
+
function isRecord2(value) {
|
|
258
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
259
|
+
}
|
|
260
|
+
function isJson(value) {
|
|
261
|
+
return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every(isJson) || isRecord2(value) && Object.values(value).every(isJson);
|
|
262
|
+
}
|
|
263
|
+
function cloneJson(value) {
|
|
264
|
+
return structuredClone(value);
|
|
265
|
+
}
|
|
266
|
+
function clonePluginRpcInput(value) {
|
|
267
|
+
return structuredClone(value);
|
|
268
|
+
}
|
|
269
|
+
function freeze(value) {
|
|
270
|
+
if (value && typeof value === "object") {
|
|
271
|
+
Object.freeze(value);
|
|
272
|
+
for (const child of Object.values(value))
|
|
273
|
+
freeze(child);
|
|
274
|
+
}
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
function escapeExternalEnvelopeValue(value) {
|
|
278
|
+
return value.replace(/[\[\]\r\n%<>]/g, (character) => encodeURIComponent(character));
|
|
279
|
+
}
|
|
280
|
+
function renderExternalPrompt(pluginId, author, input) {
|
|
281
|
+
const displayName = author.presentation.handle ?? author.presentation.displayName;
|
|
282
|
+
const label = escapeExternalEnvelopeValue(`${pluginId}:${displayName.length ? displayName : "External source"}`);
|
|
283
|
+
return Object.freeze([
|
|
284
|
+
{ type: "text", text: `[message received from external source]
|
|
285
|
+
[sender=${label}]
|
|
286
|
+
`, mentions: [], visibility: "agent-only" },
|
|
287
|
+
...input,
|
|
288
|
+
{ type: "text", text: `
|
|
289
|
+
[/sender=${label}]`, mentions: [], visibility: "agent-only" }
|
|
290
|
+
]);
|
|
291
|
+
}
|
|
292
|
+
function defaultScheduler() {
|
|
293
|
+
return {
|
|
294
|
+
now: () => Date.now(),
|
|
295
|
+
schedule(delayMs, callback) {
|
|
296
|
+
const timeout = setTimeout(callback, delayMs);
|
|
297
|
+
return () => clearTimeout(timeout);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
var promptInputCodec = {
|
|
302
|
+
decode(value) {
|
|
303
|
+
if (!isRecord2(value) || !isJson(value) || typeof value.type !== "string")
|
|
304
|
+
return err("invalid-input", "Prompt input must be a JSON SDK prompt part.");
|
|
305
|
+
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;
|
|
306
|
+
return valid ? ok(freeze(structuredClone(value))) : err("invalid-input", "Invalid installed-SDK prompt input shape.");
|
|
307
|
+
},
|
|
308
|
+
encode(value) {
|
|
309
|
+
const encoded = value;
|
|
310
|
+
if (!isJson(encoded))
|
|
311
|
+
throw new TypeError("Prompt input codec received non-JSON data.");
|
|
312
|
+
return cloneJson(encoded);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
function unavailableSession(instanceId, code, message) {
|
|
316
|
+
return { status: "unauthenticated", instanceId, error: { code, message, retry: "never" } };
|
|
317
|
+
}
|
|
318
|
+
function nativeIds(value) {
|
|
319
|
+
if (!isRecord2(value) || value.ok !== true)
|
|
320
|
+
return err("incompatible", "Malformed upstream dispatch response.");
|
|
321
|
+
if (!Object.hasOwn(value, "delivery"))
|
|
322
|
+
return ok({ deliveryId: null, queuedMessageId: null, turnId: null });
|
|
323
|
+
if (value.delivery === "sent")
|
|
324
|
+
return ok({ deliveryId: null, queuedMessageId: null, turnId: null });
|
|
325
|
+
if (value.delivery === "queued" && isRecord2(value.queuedMessage) && typeof value.queuedMessage.id === "string" && value.queuedMessage.id.length) {
|
|
326
|
+
return ok({ deliveryId: null, queuedMessageId: value.queuedMessage.id, turnId: null });
|
|
327
|
+
}
|
|
328
|
+
return err("incompatible", "Malformed upstream dispatch response.");
|
|
329
|
+
}
|
|
330
|
+
function createBbUpstreamDriver(options) {
|
|
331
|
+
const externalMessageRendering = options.externalMessageRendering ?? "host";
|
|
332
|
+
if (externalMessageRendering !== "host" && externalMessageRendering !== "producer") {
|
|
333
|
+
return err("invalid-input", "Invalid external message rendering mode.");
|
|
334
|
+
}
|
|
335
|
+
const instance = idCodec("instance").decode(options.stateNamespace ?? "local");
|
|
336
|
+
const plugin = idCodec("plugin").decode(options.bb.pluginId);
|
|
337
|
+
if (!instance.ok || !plugin.ok)
|
|
338
|
+
return err("invalid-input", "The upstream binding requires valid plugin and state namespace IDs.");
|
|
339
|
+
const instanceId = instance.value;
|
|
340
|
+
const pluginId = plugin.value;
|
|
341
|
+
const singletonKey = defaultIdentityKey(instanceId);
|
|
342
|
+
const machineKey = identityKeyCodec.decode("p6r-machine:v1:" + encodeURIComponent(instanceId) + ":server");
|
|
343
|
+
const stamp = idCodec("server-session").decode("singleton:" + instanceId);
|
|
344
|
+
const revision = idCodec("revision").decode("singleton:" + instanceId);
|
|
345
|
+
if (!machineKey.ok || !stamp.ok || !revision.ok)
|
|
346
|
+
return err("incompatible", "Unable to construct stable upstream identity identifiers.");
|
|
347
|
+
const machineIdentity = { kind: "machine", key: machineKey.value, instanceId, hostId: null };
|
|
348
|
+
const capabilities = {
|
|
349
|
+
requestIdentity: "singleton",
|
|
350
|
+
acceptance: "pre-dispatch-check",
|
|
351
|
+
forwarding: "singleton-convention",
|
|
352
|
+
directory: { search: false, lookup: false },
|
|
353
|
+
participants: false,
|
|
354
|
+
externalSend: "source-labelled",
|
|
355
|
+
toolProvenance: "unknown",
|
|
356
|
+
operationLookup: false
|
|
357
|
+
};
|
|
358
|
+
const readySession = () => freeze({
|
|
359
|
+
status: "ready",
|
|
360
|
+
instanceId,
|
|
361
|
+
mode: "single-user",
|
|
362
|
+
actor: {
|
|
363
|
+
identity: { kind: "default-user", key: singletonKey, instanceId },
|
|
364
|
+
presentation: { displayName: "Local user", handle: null, avatarUrl: null },
|
|
365
|
+
evidence: "upstream-default"
|
|
366
|
+
},
|
|
367
|
+
stamp: stamp.value,
|
|
368
|
+
capabilities
|
|
369
|
+
});
|
|
370
|
+
const machineSession = () => freeze({
|
|
371
|
+
status: "ready",
|
|
372
|
+
instanceId,
|
|
373
|
+
mode: "single-user",
|
|
374
|
+
actor: {
|
|
375
|
+
identity: machineIdentity,
|
|
376
|
+
presentation: { displayName: "BB machine", handle: null, avatarUrl: null },
|
|
377
|
+
evidence: "machine"
|
|
378
|
+
},
|
|
379
|
+
stamp: stamp.value,
|
|
380
|
+
capabilities
|
|
381
|
+
});
|
|
382
|
+
const selfProfile = () => freeze({
|
|
383
|
+
identity: { kind: "default-user", key: singletonKey, instanceId },
|
|
384
|
+
presentation: { displayName: "Local user", handle: null, avatarUrl: null },
|
|
385
|
+
revision: revision.value,
|
|
386
|
+
status: "current"
|
|
387
|
+
});
|
|
388
|
+
const machineProfile = () => freeze({
|
|
389
|
+
identity: machineIdentity,
|
|
390
|
+
presentation: { displayName: "BB machine", handle: null, avatarUrl: null },
|
|
391
|
+
revision: revision.value,
|
|
392
|
+
status: "current"
|
|
393
|
+
});
|
|
394
|
+
let disposed = false;
|
|
395
|
+
const records = new WeakMap;
|
|
396
|
+
const active = new Set;
|
|
397
|
+
const listeners = new Set;
|
|
398
|
+
const labelledExternal = new WeakSet;
|
|
399
|
+
const terminateScope = (record, scope) => {
|
|
400
|
+
if (!scope || scope.released)
|
|
401
|
+
return;
|
|
402
|
+
scope.released = true;
|
|
403
|
+
record.controller.signal.removeEventListener("abort", scope.parentAbort);
|
|
404
|
+
scope.controller.abort();
|
|
405
|
+
record.scopes.delete(scope);
|
|
406
|
+
};
|
|
407
|
+
const terminate = (record) => {
|
|
408
|
+
for (const scope of [...record.scopes])
|
|
409
|
+
terminateScope(record, scope);
|
|
410
|
+
record.controller.abort();
|
|
411
|
+
active.delete(record);
|
|
412
|
+
};
|
|
413
|
+
const known = (context) => {
|
|
414
|
+
const record = records.get(context);
|
|
415
|
+
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);
|
|
416
|
+
};
|
|
417
|
+
const admission = (context) => {
|
|
418
|
+
const record = known(context);
|
|
419
|
+
if (!record.ok)
|
|
420
|
+
return record;
|
|
421
|
+
if (record.value.origin === "external")
|
|
422
|
+
return err("unauthenticated", "External upstream work uses an explicit external source.");
|
|
423
|
+
return record;
|
|
424
|
+
};
|
|
425
|
+
const session = (context) => {
|
|
426
|
+
const record = known(context);
|
|
427
|
+
if (!record.ok)
|
|
428
|
+
return unavailableSession(instanceId, record.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record.error.message);
|
|
429
|
+
if (record.value.origin === "interactive-user")
|
|
430
|
+
return readySession();
|
|
431
|
+
return record.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
|
|
432
|
+
};
|
|
433
|
+
const driver = {
|
|
434
|
+
instanceId,
|
|
435
|
+
pluginId,
|
|
436
|
+
inputCodec: promptInputCodec,
|
|
437
|
+
scheduler: options.scheduler ?? defaultScheduler(),
|
|
438
|
+
async session(context) {
|
|
439
|
+
return session(context);
|
|
440
|
+
},
|
|
441
|
+
async selfProfile(context) {
|
|
442
|
+
const allowed = admission(context);
|
|
443
|
+
return allowed.ok ? ok(allowed.value.origin === "background" ? machineProfile() : selfProfile()) : allowed;
|
|
444
|
+
},
|
|
445
|
+
async openScope(context) {
|
|
446
|
+
const allowed = admission(context);
|
|
447
|
+
if (!allowed.ok)
|
|
448
|
+
return allowed;
|
|
449
|
+
const record = allowed.value;
|
|
450
|
+
let scope;
|
|
451
|
+
const parentAbort = () => terminateScope(record, scope);
|
|
452
|
+
scope = { controller: new AbortController, parentAbort, released: false };
|
|
453
|
+
record.scopes.add(scope);
|
|
454
|
+
const expire = () => terminateScope(record, scope);
|
|
455
|
+
record.controller.signal.addEventListener("abort", parentAbort, { once: true });
|
|
456
|
+
const exposedSession = record.origin === "background" ? machineSession() : readySession();
|
|
457
|
+
const expectedKey = record.origin === "background" ? machineKey.value : singletonKey;
|
|
458
|
+
return ok({
|
|
459
|
+
signal: scope.controller.signal,
|
|
460
|
+
session: exposedSession,
|
|
461
|
+
validate(expected) {
|
|
462
|
+
if (scope.released || scope.controller.signal.aborted || record.controller.signal.aborted || disposed) {
|
|
463
|
+
return err("expired", "Upstream request context is no longer live.");
|
|
464
|
+
}
|
|
465
|
+
return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
|
|
466
|
+
},
|
|
467
|
+
release: expire
|
|
468
|
+
});
|
|
469
|
+
},
|
|
470
|
+
async submit(input, readOptions) {
|
|
471
|
+
if (disposed)
|
|
472
|
+
return { status: "rejected", error: { code: "disposed", message: "Upstream binding is disposed.", retry: "never" } };
|
|
473
|
+
if (readOptions?.signal?.aborted)
|
|
474
|
+
return { status: "rejected", error: { code: "cancelled", message: "Submission was cancelled before dispatch.", retry: "never" } };
|
|
475
|
+
const operation = idCodec("operation").decode(input.operationId);
|
|
476
|
+
const thread = idCodec("thread").decode(input.threadId);
|
|
477
|
+
if (!operation.ok || !thread.ok || !["auto", "start", "steer-if-active", "queue-if-active"].includes(input.mode)) {
|
|
478
|
+
return { status: "rejected", error: { code: "invalid-input", message: "Invalid upstream submission metadata.", retry: "never" } };
|
|
479
|
+
}
|
|
480
|
+
const labelled = labelledExternal.has(input.input);
|
|
481
|
+
const prompts = [];
|
|
482
|
+
for (const item of input.input) {
|
|
483
|
+
const decoded = promptInputCodec.decode(item);
|
|
484
|
+
if (!decoded.ok)
|
|
485
|
+
return { status: "rejected", error: decoded.error };
|
|
486
|
+
prompts.push(decoded.value);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const response = await options.bb.sdk.threads.send({ threadId: thread.value, mode: input.mode, input: prompts });
|
|
490
|
+
const native = nativeIds(response);
|
|
491
|
+
if (!native.ok)
|
|
492
|
+
return { status: "indeterminate", operationId: operation.value, message: "Upstream dispatch returned an unrecognized response after submission." };
|
|
493
|
+
return {
|
|
494
|
+
status: "submitted",
|
|
495
|
+
receipt: {
|
|
496
|
+
evidence: "upstream-response",
|
|
497
|
+
operationId: operation.value,
|
|
498
|
+
acceptedAt: null,
|
|
499
|
+
references: null,
|
|
500
|
+
native: native.value,
|
|
501
|
+
provenance: labelled ? "source-labelled" : "upstream-default",
|
|
502
|
+
deduplication: "not-guaranteed"
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
} catch {
|
|
506
|
+
return { status: "indeterminate", operationId: operation.value, message: "Upstream submission may have been accepted before its response failed." };
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
labelExternal(_author, input) {
|
|
510
|
+
const copied = Object.freeze(input.map((item) => {
|
|
511
|
+
const decoded = promptInputCodec.decode(item);
|
|
512
|
+
return decoded.ok ? decoded.value : item;
|
|
513
|
+
}));
|
|
514
|
+
labelledExternal.add(copied);
|
|
515
|
+
return copied;
|
|
516
|
+
},
|
|
517
|
+
toolCorrelation(_context) {
|
|
518
|
+
return { status: "unknown", correlation: null, reason: "upstream-unattributed" };
|
|
519
|
+
},
|
|
520
|
+
async forward(context, destinationPlugin, method, input, readOptions) {
|
|
521
|
+
const allowed = known(context);
|
|
522
|
+
if (!allowed.ok)
|
|
523
|
+
throw allowed.error;
|
|
524
|
+
if (readOptions?.signal?.aborted)
|
|
525
|
+
throw { code: "cancelled", message: "Plugin forwarding was cancelled before dispatch.", retry: "never" };
|
|
526
|
+
const destination = idCodec("plugin").decode(destinationPlugin);
|
|
527
|
+
if (!destination.ok || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(method) || !isJson(input)) {
|
|
528
|
+
throw { code: "invalid-input", message: "Invalid upstream plugin RPC destination.", retry: "never" };
|
|
529
|
+
}
|
|
530
|
+
return options.bb.sdk.plugins.callRpc({ pluginId: destination.value, method, input: clonePluginRpcInput(input), outputSchema: z.unknown() });
|
|
531
|
+
},
|
|
532
|
+
subscribe(listener) {
|
|
533
|
+
if (!disposed)
|
|
534
|
+
listeners.add(listener);
|
|
535
|
+
return () => listeners.delete(listener);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
const binding = {
|
|
539
|
+
externalMessageRendering,
|
|
540
|
+
driver,
|
|
541
|
+
issue(origin) {
|
|
542
|
+
if (disposed)
|
|
543
|
+
throw new Error("Upstream binding is disposed.");
|
|
544
|
+
const controller = new AbortController;
|
|
545
|
+
const invocation = Object.freeze({ origin, signal: controller.signal });
|
|
546
|
+
const record = { origin, controller, scopes: new Set };
|
|
547
|
+
records.set(invocation, record);
|
|
548
|
+
active.add(record);
|
|
549
|
+
return invocation;
|
|
550
|
+
},
|
|
551
|
+
release(invocation) {
|
|
552
|
+
const record = records.get(invocation);
|
|
553
|
+
if (record)
|
|
554
|
+
terminate(record);
|
|
555
|
+
},
|
|
556
|
+
dispose() {
|
|
557
|
+
if (disposed)
|
|
558
|
+
return;
|
|
559
|
+
disposed = true;
|
|
560
|
+
for (const record of [...active])
|
|
561
|
+
terminate(record);
|
|
562
|
+
for (const listener of [...listeners]) {
|
|
563
|
+
try {
|
|
564
|
+
listener({ kind: "disposed" });
|
|
565
|
+
} catch {}
|
|
566
|
+
}
|
|
567
|
+
listeners.clear();
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
try {
|
|
571
|
+
options.bb.onDispose(() => binding.dispose());
|
|
572
|
+
} catch {
|
|
573
|
+
binding.dispose();
|
|
574
|
+
return err("unavailable", "Unable to register upstream binding cleanup.", "after-refresh");
|
|
575
|
+
}
|
|
576
|
+
return ok(binding);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// host-runtime.ts
|
|
580
|
+
function isRecord3(value) {
|
|
581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
582
|
+
}
|
|
583
|
+
function unavailable(message) {
|
|
584
|
+
return { code: "unavailable", message, retry: "after-reconnect" };
|
|
585
|
+
}
|
|
586
|
+
function isPresentation(value) {
|
|
587
|
+
return isRecord3(value) && typeof value.displayName === "string" && (value.handle === null || typeof value.handle === "string") && (value.avatarUrl === null || typeof value.avatarUrl === "string");
|
|
588
|
+
}
|
|
589
|
+
function isPersonIdentity(value) {
|
|
590
|
+
if (!isRecord3(value) || !identityKeyCodec.decode(value.key).ok)
|
|
591
|
+
return false;
|
|
592
|
+
return value.kind === "person" ? typeof value.issuer === "string" && typeof value.subject === "string" : value.kind === "default-user" && idCodec("instance").decode(value.instanceId).ok;
|
|
593
|
+
}
|
|
594
|
+
function isMachineIdentity(value) {
|
|
595
|
+
return isRecord3(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);
|
|
596
|
+
}
|
|
597
|
+
function isActorIdentity(value) {
|
|
598
|
+
return isPersonIdentity(value) || isMachineIdentity(value);
|
|
599
|
+
}
|
|
600
|
+
function isCapabilities(value) {
|
|
601
|
+
return isRecord3(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") && isRecord3(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";
|
|
602
|
+
}
|
|
603
|
+
function asResult(value) {
|
|
604
|
+
if (!isRecord3(value) || typeof value.ok !== "boolean")
|
|
605
|
+
return err("incompatible", "Host returned a malformed Result.");
|
|
606
|
+
if (value.ok)
|
|
607
|
+
return "value" in value ? ok(value.value) : err("incompatible", "Host returned a successful Result without a value.");
|
|
608
|
+
const failure2 = decodeError(value.error);
|
|
609
|
+
return failure2.ok ? { ok: false, error: failure2.value } : failure2;
|
|
610
|
+
}
|
|
611
|
+
function decodeError(value) {
|
|
612
|
+
const codes = new Set([
|
|
613
|
+
"unavailable",
|
|
614
|
+
"unauthenticated",
|
|
615
|
+
"unsupported",
|
|
616
|
+
"incompatible",
|
|
617
|
+
"invalid-input",
|
|
618
|
+
"not-found",
|
|
619
|
+
"ambiguous",
|
|
620
|
+
"stale-owner",
|
|
621
|
+
"stale-context",
|
|
622
|
+
"conflict",
|
|
623
|
+
"cancelled",
|
|
624
|
+
"disposed",
|
|
625
|
+
"limit-exceeded",
|
|
626
|
+
"invalid-operation",
|
|
627
|
+
"expired"
|
|
628
|
+
]);
|
|
629
|
+
const retries = new Set(["never", "after-refresh", "after-reconnect", "same-operation"]);
|
|
630
|
+
return isRecord3(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.");
|
|
631
|
+
}
|
|
632
|
+
function isReference(value) {
|
|
633
|
+
return isRecord3(value) && idCodec("thread").decode(value.threadId).ok && idCodec("contribution").decode(value.contributionId).ok;
|
|
634
|
+
}
|
|
635
|
+
function isNativeIds(value) {
|
|
636
|
+
return isRecord3(value) && ["deliveryId", "queuedMessageId", "turnId"].every((key) => value[key] === null || typeof value[key] === "string");
|
|
637
|
+
}
|
|
638
|
+
function decodeAcceptance(value) {
|
|
639
|
+
if (!isRecord3(value) || typeof value.status !== "string")
|
|
640
|
+
return err("incompatible", "Host returned a malformed acceptance outcome.");
|
|
641
|
+
if (value.status === "rejected") {
|
|
642
|
+
const error = decodeError(value.error);
|
|
643
|
+
return error.ok ? ok({ status: "rejected", error: error.value }) : error;
|
|
644
|
+
}
|
|
645
|
+
if (value.status === "indeterminate") {
|
|
646
|
+
const operationId2 = idCodec("operation").decode(value.operationId);
|
|
647
|
+
return operationId2.ok && typeof value.message === "string" ? ok({ status: "indeterminate", operationId: operationId2.value, message: value.message }) : err("incompatible", "Host returned malformed indeterminate acceptance metadata.");
|
|
648
|
+
}
|
|
649
|
+
if (value.status !== "submitted" || !isRecord3(value.receipt))
|
|
650
|
+
return err("incompatible", "Host returned an unknown acceptance outcome.");
|
|
651
|
+
const receipt = value.receipt;
|
|
652
|
+
const operationId = idCodec("operation").decode(receipt.operationId);
|
|
653
|
+
if (!operationId.ok || !isNativeIds(receipt.native))
|
|
654
|
+
return err("incompatible", "Host returned malformed acceptance receipt identifiers.");
|
|
655
|
+
if (receipt.evidence === "host-accepted") {
|
|
656
|
+
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.");
|
|
657
|
+
}
|
|
658
|
+
if (receipt.evidence === "upstream-response") {
|
|
659
|
+
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.");
|
|
660
|
+
}
|
|
661
|
+
return err("incompatible", "Host returned an unknown acceptance receipt evidence value.");
|
|
662
|
+
}
|
|
663
|
+
function decodeLookup(value) {
|
|
664
|
+
if (!isRecord3(value) || typeof value.status !== "string")
|
|
665
|
+
return err("incompatible", "Host returned a malformed operation lookup.");
|
|
666
|
+
if (value.status === "final") {
|
|
667
|
+
const outcome = decodeAcceptance(value.outcome);
|
|
668
|
+
return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
|
|
669
|
+
}
|
|
670
|
+
if (value.status === "pending")
|
|
671
|
+
return ok({ status: "pending" });
|
|
672
|
+
if (value.status === "absent-final" && value.retry === "same-operation-only")
|
|
673
|
+
return ok({ status: "absent-final", retry: "same-operation-only" });
|
|
674
|
+
if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable")) {
|
|
675
|
+
return ok({ status: "unknown", reason: value.reason });
|
|
676
|
+
}
|
|
677
|
+
return err("incompatible", "Host returned malformed operation lookup metadata.");
|
|
678
|
+
}
|
|
679
|
+
var serverSessionCodec = {
|
|
680
|
+
decode(input) {
|
|
681
|
+
if (!isRecord3(input) || typeof input.status !== "string" || !identityKeyCodec.decode(input.instanceId).ok) {
|
|
682
|
+
return err("invalid-input", "Invalid server session.");
|
|
683
|
+
}
|
|
684
|
+
if (input.status !== "ready") {
|
|
685
|
+
if ((input.status === "unavailable" || input.status === "unauthenticated" || input.status === "incompatible") && isRecord3(input.error)) {
|
|
686
|
+
return ok(input);
|
|
687
|
+
}
|
|
688
|
+
return err("invalid-input", "Invalid unavailable server session.");
|
|
689
|
+
}
|
|
690
|
+
if (!isRecord3(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") {
|
|
691
|
+
return err("invalid-input", "Invalid ready server session.");
|
|
692
|
+
}
|
|
693
|
+
return ok(input);
|
|
694
|
+
},
|
|
695
|
+
encode(value) {
|
|
696
|
+
return value;
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
function unsupportedDirectory() {
|
|
700
|
+
return {
|
|
701
|
+
search: async () => err("unsupported", "Directory search is unavailable."),
|
|
702
|
+
getMany: async () => err("unsupported", "Directory lookup is unavailable."),
|
|
703
|
+
subscribe: () => () => {}
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function unsupportedParticipants() {
|
|
707
|
+
return {
|
|
708
|
+
list: async () => err("unsupported", "Participant history is unavailable."),
|
|
709
|
+
previews: async () => err("unsupported", "Participant previews are unavailable.")
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function unsupportedHistory() {
|
|
713
|
+
return {
|
|
714
|
+
contributions: async (_query) => ok({ status: "unavailable", reason: "unsupported" }),
|
|
715
|
+
attempts: async (_query) => ok({ status: "unavailable", reason: "unsupported" })
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function isProfilePresentation(value) {
|
|
719
|
+
if (!isRecord3(value))
|
|
720
|
+
return false;
|
|
721
|
+
const displayName = value.displayName;
|
|
722
|
+
const handle = value.handle;
|
|
723
|
+
const avatarUrl = value.avatarUrl;
|
|
724
|
+
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);
|
|
725
|
+
}
|
|
726
|
+
function isExternalIdentity(value) {
|
|
727
|
+
return isRecord3(value) && value.kind === "external" && identityKeyCodec.decode(value.key).ok && idCodec("plugin").decode(value.pluginId).ok && idCodec("subject").decode(value.subject).ok;
|
|
728
|
+
}
|
|
729
|
+
function isParticipant(value) {
|
|
730
|
+
if (!isRecord3(value))
|
|
731
|
+
return false;
|
|
732
|
+
const roles = value.roles;
|
|
733
|
+
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");
|
|
734
|
+
}
|
|
735
|
+
function decodeParticipantPage(value) {
|
|
736
|
+
if (!isRecord3(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") {
|
|
737
|
+
return err("incompatible", "Host returned a malformed participant page.");
|
|
738
|
+
}
|
|
739
|
+
const items = [];
|
|
740
|
+
for (const item of value.items) {
|
|
741
|
+
if (!isParticipant(item) || !isRecord3(item) || !Array.isArray(item.roles))
|
|
742
|
+
return err("incompatible", "Host returned a malformed participant item.");
|
|
743
|
+
const identity = profileCodec.decode({ identity: item.identity, presentation: item.presentation, revision: "participant", status: "current" });
|
|
744
|
+
if (!identity.ok)
|
|
745
|
+
return err("incompatible", "Host returned a malformed participant identity.");
|
|
746
|
+
items.push({ identity: identity.value.identity, presentation: identity.value.presentation, roles: [...item.roles] });
|
|
747
|
+
}
|
|
748
|
+
const cursor = value.nextCursor === null ? null : idCodec("cursor").decode(value.nextCursor);
|
|
749
|
+
const revision = idCodec("revision").decode(value.revision);
|
|
750
|
+
if (cursor !== null && !cursor.ok || !revision.ok)
|
|
751
|
+
return err("incompatible", "Host returned malformed participant identifiers.");
|
|
752
|
+
return ok({ items, nextCursor: cursor === null ? null : cursor.value, revision: revision.value, coverage: value.coverage });
|
|
753
|
+
}
|
|
754
|
+
function isContribution2(value) {
|
|
755
|
+
if (!isRecord3(value) || !isReference(value.reference) || typeof value.acceptedAt !== "string" || value.acceptedAt.length === 0 || value.acceptedAt.length > 256 || !Array.isArray(value.mentionedPeople) || !value.mentionedPeople.every(isPersonIdentity))
|
|
756
|
+
return false;
|
|
757
|
+
const snapshot = (candidate) => {
|
|
758
|
+
if (!isRecord3(candidate) || !(isActorIdentity(candidate.identity) || isExternalIdentity(candidate.identity)) || !isProfilePresentation(candidate.presentation))
|
|
759
|
+
return false;
|
|
760
|
+
const evidence = candidate.evidence;
|
|
761
|
+
if (!["provider-verified", "local-user", "upstream-default", "integration-asserted", "legacy", "machine"].includes(evidence))
|
|
762
|
+
return false;
|
|
763
|
+
return isMachineIdentity(candidate.identity) ? evidence === "machine" : evidence !== "machine";
|
|
764
|
+
};
|
|
765
|
+
const origin = value.author;
|
|
766
|
+
if (!isRecord3(origin))
|
|
767
|
+
return false;
|
|
768
|
+
const validOrigin = origin.kind === "person" ? snapshot(origin.actor) && isRecord3(origin.actor) && isPersonIdentity(origin.actor.identity) : origin.kind === "machine" ? snapshot(origin.actor) && isRecord3(origin.actor) && isMachineIdentity(origin.actor.identity) : origin.kind === "external" ? snapshot(origin.actor) && isRecord3(origin.actor) && isExternalIdentity(origin.actor.identity) : origin.kind === "agent" ? origin.agentId === null || idCodec("agent").decode(origin.agentId).ok : origin.kind === "system" ? typeof origin.reason === "string" && origin.reason.length > 0 && origin.reason.length <= 1024 : origin.kind === "unknown" && (origin.reason === "legacy" || origin.reason === "upstream-unattributed" || origin.reason === "missing-source");
|
|
769
|
+
return validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
770
|
+
}
|
|
771
|
+
function decodeEvidencePage(value, decodeItem) {
|
|
772
|
+
if (!isRecord3(value) || typeof value.status !== "string")
|
|
773
|
+
return err("incompatible", "Host returned a malformed evidence page.");
|
|
774
|
+
if (value.status === "pending")
|
|
775
|
+
return ok({ status: "pending" });
|
|
776
|
+
if (value.status === "unavailable")
|
|
777
|
+
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.");
|
|
778
|
+
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)
|
|
779
|
+
return err("incompatible", "Host returned malformed evidence metadata.");
|
|
780
|
+
return ok(value);
|
|
781
|
+
}
|
|
782
|
+
function validContributionQuery(query) {
|
|
783
|
+
const cursor = query.cursor === undefined || idCodec("cursor").decode(query.cursor).ok;
|
|
784
|
+
const limit = query.limit === undefined || Number.isInteger(query.limit) && query.limit >= 1 && query.limit <= 200;
|
|
785
|
+
if (!cursor || !limit)
|
|
786
|
+
return false;
|
|
787
|
+
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;
|
|
788
|
+
}
|
|
789
|
+
function validAttemptQuery(query) {
|
|
790
|
+
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));
|
|
791
|
+
}
|
|
792
|
+
function decodeP6rPersonKey(key) {
|
|
793
|
+
const prefix = "p6r-person:v1:";
|
|
794
|
+
if (!key.startsWith(prefix))
|
|
795
|
+
return null;
|
|
796
|
+
const encoded = key.slice(prefix.length);
|
|
797
|
+
const divider = encoded.indexOf(":");
|
|
798
|
+
if (divider < 1 || divider === encoded.length - 1)
|
|
799
|
+
return null;
|
|
800
|
+
const encodedIssuer = encoded.slice(0, divider);
|
|
801
|
+
const encodedSubject = encoded.slice(divider + 1);
|
|
802
|
+
try {
|
|
803
|
+
const issuer = decodeURIComponent(encodedIssuer);
|
|
804
|
+
const subject = decodeURIComponent(encodedSubject);
|
|
805
|
+
return encodeURIComponent(issuer) === encodedIssuer && encodeURIComponent(subject) === encodedSubject && idCodec("issuer").decode(issuer).ok && idCodec("subject").decode(subject).ok ? { issuer, subject } : null;
|
|
806
|
+
} catch {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function createEnhancedDirectory(protocol) {
|
|
811
|
+
const mappings = new Map;
|
|
812
|
+
let activeGeneration = null;
|
|
813
|
+
const sources = () => {
|
|
814
|
+
let raw;
|
|
815
|
+
try {
|
|
816
|
+
raw = protocol.directorySources();
|
|
817
|
+
} catch {
|
|
818
|
+
return err("unavailable", "Enhanced provider directory is unavailable.", "after-reconnect");
|
|
819
|
+
}
|
|
820
|
+
if (!Array.isArray(raw))
|
|
821
|
+
return err("incompatible", "Enhanced provider directory sources are malformed.");
|
|
822
|
+
if (raw.length > 1)
|
|
823
|
+
return err("incompatible", "Enhanced protocol exposed multiple active provider directories.");
|
|
824
|
+
for (const source of raw) {
|
|
825
|
+
if (!isRecord3(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")
|
|
826
|
+
return err("incompatible", "Enhanced provider directory source is malformed.");
|
|
827
|
+
}
|
|
828
|
+
const generation = raw[0]?.generation ?? null;
|
|
829
|
+
if (activeGeneration !== generation) {
|
|
830
|
+
mappings.clear();
|
|
831
|
+
activeGeneration = generation;
|
|
832
|
+
}
|
|
833
|
+
return ok(raw);
|
|
834
|
+
};
|
|
835
|
+
const sameGeneration = (generation) => {
|
|
836
|
+
const current = sources();
|
|
837
|
+
if (!current.ok)
|
|
838
|
+
return current;
|
|
839
|
+
const source = current.value[0];
|
|
840
|
+
return source && source.generation === generation ? ok(source) : err("unavailable", "Provider directory generation changed during the read.", "after-reconnect");
|
|
841
|
+
};
|
|
842
|
+
const remember = (key, mapping) => {
|
|
843
|
+
if (mappings.size >= 512 && !mappings.has(key)) {
|
|
844
|
+
const oldest = mappings.keys().next().value;
|
|
845
|
+
if (oldest !== undefined)
|
|
846
|
+
mappings.delete(oldest);
|
|
847
|
+
}
|
|
848
|
+
mappings.set(key, mapping);
|
|
849
|
+
};
|
|
850
|
+
const profile = (source, record, revision) => {
|
|
851
|
+
if (!isRecord3(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)
|
|
852
|
+
return err("incompatible", "Provider directory record is malformed.");
|
|
853
|
+
const issuer = record.issuer;
|
|
854
|
+
const subject = record.subject;
|
|
855
|
+
if (typeof issuer !== "string" || typeof subject !== "string")
|
|
856
|
+
return err("incompatible", "Provider directory record is malformed.");
|
|
857
|
+
let person;
|
|
858
|
+
try {
|
|
859
|
+
person = asResult(source.person(issuer, subject));
|
|
860
|
+
} catch {
|
|
861
|
+
return err("unavailable", "Provider person normalization failed.", "after-reconnect");
|
|
862
|
+
}
|
|
863
|
+
if (!person.ok)
|
|
864
|
+
return person;
|
|
865
|
+
if (person.value.kind !== "person" || person.value.issuer !== record.issuer || person.value.subject !== record.subject || !identityKeyCodec.decode(person.value.key).ok) {
|
|
866
|
+
return err("incompatible", "Provider directory returned an invalid person reference.");
|
|
867
|
+
}
|
|
868
|
+
return profileCodec.decode({ identity: person.value, presentation: record.presentation, revision, status: record.status });
|
|
869
|
+
};
|
|
870
|
+
return {
|
|
871
|
+
async search(input, options) {
|
|
872
|
+
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")
|
|
873
|
+
return err("invalid-input", "Invalid directory search request.");
|
|
874
|
+
const available = sources();
|
|
875
|
+
if (!available.ok)
|
|
876
|
+
return available;
|
|
877
|
+
const source = available.value[0];
|
|
878
|
+
if (!source || !source.directory || !input.kinds.includes("person"))
|
|
879
|
+
return err("unsupported", "Provider directory search is unavailable.");
|
|
880
|
+
let result;
|
|
881
|
+
try {
|
|
882
|
+
result = asResult(await source.directory(input, options));
|
|
883
|
+
} catch {
|
|
884
|
+
return err("unavailable", "Provider directory search failed.", "after-reconnect");
|
|
885
|
+
}
|
|
886
|
+
if (!result.ok)
|
|
887
|
+
return result;
|
|
888
|
+
const current = sameGeneration(source.generation);
|
|
889
|
+
if (!current.ok)
|
|
890
|
+
return current;
|
|
891
|
+
if (!Array.isArray(result.value.records) || result.value.records.length > input.limit || result.value.nextCursor !== null && !idCodec("cursor").decode(result.value.nextCursor).ok || !idCodec("revision").decode(result.value.revision).ok)
|
|
892
|
+
return err("incompatible", "Provider directory page is malformed.");
|
|
893
|
+
const profiles = [];
|
|
894
|
+
for (const record of result.value.records) {
|
|
895
|
+
const decoded = profile(current.value, record, result.value.revision);
|
|
896
|
+
if (!decoded.ok)
|
|
897
|
+
return decoded;
|
|
898
|
+
if (input.history === "current" && decoded.value.status !== "current")
|
|
899
|
+
return err("incompatible", "Provider directory returned historical data for a current query.");
|
|
900
|
+
if (decoded.value.identity.kind !== "person")
|
|
901
|
+
return err("incompatible", "Provider directory returned a non-person profile.");
|
|
902
|
+
remember(decoded.value.identity.key, { generation: current.value.generation, issuer: decoded.value.identity.issuer, subject: decoded.value.identity.subject });
|
|
903
|
+
profiles.push(decoded.value);
|
|
904
|
+
}
|
|
905
|
+
const nextCursor = result.value.nextCursor === null ? null : idCodec("cursor").decode(result.value.nextCursor);
|
|
906
|
+
const revision = idCodec("revision").decode(result.value.revision);
|
|
907
|
+
if (nextCursor !== null && !nextCursor.ok || !revision.ok)
|
|
908
|
+
return err("incompatible", "Provider directory page identifiers are malformed.");
|
|
909
|
+
return ok({ items: profiles, nextCursor: nextCursor === null ? null : nextCursor.value, revision: revision.value });
|
|
910
|
+
},
|
|
911
|
+
async getMany(input, options) {
|
|
912
|
+
if (!Array.isArray(input.keys) || input.keys.length > 64 || !input.keys.every((key) => identityKeyCodec.decode(key).ok))
|
|
913
|
+
return err("invalid-input", "Invalid profile lookup request.");
|
|
914
|
+
const available = sources();
|
|
915
|
+
if (!available.ok)
|
|
916
|
+
return available;
|
|
917
|
+
const source = available.value[0];
|
|
918
|
+
if (!source || !source.lookup)
|
|
919
|
+
return err("unsupported", "Provider directory lookup is unavailable.");
|
|
920
|
+
const selected = [];
|
|
921
|
+
for (const key of input.keys) {
|
|
922
|
+
const mapped = mappings.get(key);
|
|
923
|
+
if (mapped?.generation === source.generation) {
|
|
924
|
+
selected.push(mapped);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const decoded = decodeP6rPersonKey(key);
|
|
928
|
+
if (!decoded || !source.issuers.includes(decoded.issuer)) {
|
|
929
|
+
selected.push(undefined);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
let person;
|
|
933
|
+
try {
|
|
934
|
+
person = asResult(source.person(decoded.issuer, decoded.subject));
|
|
935
|
+
} catch {
|
|
936
|
+
return err("unavailable", "Provider person normalization failed.", "after-reconnect");
|
|
937
|
+
}
|
|
938
|
+
if (!person.ok)
|
|
939
|
+
return person;
|
|
940
|
+
if (person.value.kind !== "person" || person.value.key !== key || person.value.issuer !== decoded.issuer || person.value.subject !== decoded.subject) {
|
|
941
|
+
return err("incompatible", "Provider directory rejected the canonical profile key.");
|
|
942
|
+
}
|
|
943
|
+
const entry = { generation: source.generation, issuer: decoded.issuer, subject: decoded.subject };
|
|
944
|
+
remember(key, entry);
|
|
945
|
+
selected.push(entry);
|
|
946
|
+
}
|
|
947
|
+
const unresolved = [...new Map(selected.filter((entry) => entry !== undefined).map((entry) => [JSON.stringify([entry.issuer, entry.subject]), entry])).values()];
|
|
948
|
+
if (unresolved.length === 0)
|
|
949
|
+
return ok(input.keys.map((key) => ({ key, status: "missing" })));
|
|
950
|
+
let result;
|
|
951
|
+
try {
|
|
952
|
+
result = asResult(await source.lookup(unresolved.map(({ issuer, subject }) => ({ issuer, subject })), options));
|
|
953
|
+
} catch {
|
|
954
|
+
return err("unavailable", "Provider directory lookup failed.", "after-reconnect");
|
|
955
|
+
}
|
|
956
|
+
if (!result.ok)
|
|
957
|
+
return result;
|
|
958
|
+
const current = sameGeneration(source.generation);
|
|
959
|
+
if (!current.ok)
|
|
960
|
+
return current;
|
|
961
|
+
if (!isRecord3(result.value) || !idCodec("revision").decode(result.value.revision).ok || !Array.isArray(result.value.records) || result.value.records.length !== unresolved.length)
|
|
962
|
+
return err("incompatible", "Provider profile lookup page is malformed.");
|
|
963
|
+
const found = new Map;
|
|
964
|
+
const seen = new Set;
|
|
965
|
+
for (const entry of result.value.records) {
|
|
966
|
+
if (!isRecord3(entry) || typeof entry.issuer !== "string" || typeof entry.subject !== "string")
|
|
967
|
+
return err("incompatible", "Provider profile lookup entry is malformed.");
|
|
968
|
+
const mapping = unresolved.find((candidate) => candidate.issuer === entry.issuer && candidate.subject === entry.subject);
|
|
969
|
+
if (!mapping || seen.has(`${entry.issuer}\x00${entry.subject}`))
|
|
970
|
+
return err("incompatible", "Provider profile lookup response is misaligned.");
|
|
971
|
+
seen.add(`${entry.issuer}\x00${entry.subject}`);
|
|
972
|
+
if (entry.record !== null) {
|
|
973
|
+
if (!isRecord3(entry.record) || entry.record.issuer !== entry.issuer || entry.record.subject !== entry.subject)
|
|
974
|
+
return err("incompatible", "Provider profile lookup identity does not match its requested subject.");
|
|
975
|
+
const decoded = profile(current.value, entry.record, result.value.revision);
|
|
976
|
+
if (!decoded.ok)
|
|
977
|
+
return decoded;
|
|
978
|
+
if (decoded.value.identity.kind !== "person")
|
|
979
|
+
return err("incompatible", "Provider lookup returned a non-person profile.");
|
|
980
|
+
remember(decoded.value.identity.key, { generation: current.value.generation, issuer: decoded.value.identity.issuer, subject: decoded.value.identity.subject });
|
|
981
|
+
found.set(`${entry.issuer}\x00${entry.subject}`, decoded.value);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return ok(input.keys.map((key, index) => {
|
|
985
|
+
const mapping = selected[index];
|
|
986
|
+
const profileValue = mapping === undefined ? undefined : found.get(`${mapping.issuer}\x00${mapping.subject}`);
|
|
987
|
+
return profileValue === undefined ? { key, status: "missing" } : { key, status: "found", profile: profileValue };
|
|
988
|
+
}));
|
|
989
|
+
},
|
|
990
|
+
subscribe: () => () => {}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function sameExpectation(session, expected) {
|
|
994
|
+
return session.actor.identity.key === expected.actor && session.stamp === expected.session ? ok(undefined) : err("stale-context", "The request actor or session changed.", "after-refresh");
|
|
995
|
+
}
|
|
996
|
+
function toLiveRequest(handle) {
|
|
997
|
+
const decoded = serverSessionCodec.decode(handle.session);
|
|
998
|
+
if (!decoded.ok)
|
|
999
|
+
return decoded;
|
|
1000
|
+
const session = decoded.value;
|
|
1001
|
+
if (session.status !== "ready")
|
|
1002
|
+
return err("unauthenticated", "No ready identity session is available.", "after-refresh");
|
|
1003
|
+
const request = {
|
|
1004
|
+
signal: handle.signal,
|
|
1005
|
+
session,
|
|
1006
|
+
validate(expected) {
|
|
1007
|
+
const local = sameExpectation(session, expected);
|
|
1008
|
+
if (!local.ok)
|
|
1009
|
+
return local;
|
|
1010
|
+
return asResult(handle.validate(expected));
|
|
1011
|
+
},
|
|
1012
|
+
release: () => handle.release()
|
|
1013
|
+
};
|
|
1014
|
+
return ok(request);
|
|
1015
|
+
}
|
|
1016
|
+
function toForkRequest(handle) {
|
|
1017
|
+
if (!isRecord3(handle.scope) || typeof handle.scope.release !== "function" || typeof handle.scope.validate !== "function" || !(handle.scope.signal instanceof AbortSignal)) {
|
|
1018
|
+
return err("incompatible", "Host returned an invalid enhanced request scope.");
|
|
1019
|
+
}
|
|
1020
|
+
const request = toLiveRequest(handle);
|
|
1021
|
+
return request.ok ? ok({ request: request.value, scope: handle.scope }) : request;
|
|
1022
|
+
}
|
|
1023
|
+
function normalizeRegistration(raw) {
|
|
1024
|
+
const generation = idCodec("provider-generation").decode(raw.generation);
|
|
1025
|
+
if (!generation.ok)
|
|
1026
|
+
return err("incompatible", "Host returned an invalid provider generation.");
|
|
1027
|
+
let disposed = false;
|
|
1028
|
+
return ok({
|
|
1029
|
+
generation: generation.value,
|
|
1030
|
+
configuration: raw.configuration,
|
|
1031
|
+
getStatus: () => raw.getStatus(),
|
|
1032
|
+
signal: raw.signal,
|
|
1033
|
+
subscribe: (listener) => raw.subscribe(listener),
|
|
1034
|
+
invalidate: (change) => raw.invalidate(change),
|
|
1035
|
+
person(issuer, subject) {
|
|
1036
|
+
const result = asResult(raw.person(issuer, subject));
|
|
1037
|
+
if (!result.ok)
|
|
1038
|
+
return result;
|
|
1039
|
+
if (result.value.kind === "default-user" || !identityKeyCodec.decode(result.value.key).ok || result.value.issuer !== issuer || result.value.subject !== subject) {
|
|
1040
|
+
return err("incompatible", "Host returned an invalid provider person reference.");
|
|
1041
|
+
}
|
|
1042
|
+
return ok(result.value);
|
|
1043
|
+
},
|
|
1044
|
+
dispose() {
|
|
1045
|
+
if (disposed)
|
|
1046
|
+
return;
|
|
1047
|
+
disposed = true;
|
|
1048
|
+
raw.dispose();
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function toForkProvider(provider) {
|
|
1053
|
+
const validateReadiness = provider.validateReadiness;
|
|
1054
|
+
const directory = provider.directory;
|
|
1055
|
+
const lookup = provider.lookup;
|
|
1056
|
+
return {
|
|
1057
|
+
issuers: provider.issuers,
|
|
1058
|
+
...validateReadiness ? { validateReadiness: async (input) => {
|
|
1059
|
+
const generation = idCodec("provider-generation").decode(input.generation);
|
|
1060
|
+
if (!generation.ok)
|
|
1061
|
+
return generation;
|
|
1062
|
+
return validateReadiness({ ...input, generation: generation.value });
|
|
1063
|
+
} } : {},
|
|
1064
|
+
resolve: (evidence) => provider.resolve(evidence),
|
|
1065
|
+
...directory ? { directory: (input, options) => directory(input, options) } : {},
|
|
1066
|
+
...lookup ? { lookup: (subjects, options) => lookup(subjects, options) } : {}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function wireInput(input, codec) {
|
|
1070
|
+
return {
|
|
1071
|
+
...input,
|
|
1072
|
+
input: input.input.map((value) => codec.encode(value))
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function protocolError(extension) {
|
|
1076
|
+
if (extension.status === "unsupported-version")
|
|
1077
|
+
return { code: "incompatible", message: `Unsupported identity protocol version ${extension.version}.`, retry: "never" };
|
|
1078
|
+
if (extension.status === "malformed")
|
|
1079
|
+
return extension.error;
|
|
1080
|
+
return unavailable("The configured identity boundary is unavailable.");
|
|
1081
|
+
}
|
|
1082
|
+
function createHostAdapter(options) {
|
|
1083
|
+
const { upstream, extension } = options;
|
|
1084
|
+
const directory = options.directory ?? unsupportedDirectory();
|
|
1085
|
+
const participants = unsupportedParticipants();
|
|
1086
|
+
const history = unsupportedHistory();
|
|
1087
|
+
const listeners = new Set;
|
|
1088
|
+
let disposed = false;
|
|
1089
|
+
const close = () => {
|
|
1090
|
+
if (disposed)
|
|
1091
|
+
return;
|
|
1092
|
+
disposed = true;
|
|
1093
|
+
for (const listener of listeners)
|
|
1094
|
+
listener({ kind: "disposed" });
|
|
1095
|
+
listeners.clear();
|
|
1096
|
+
};
|
|
1097
|
+
if (extension.status === "absent") {
|
|
1098
|
+
const subscriptions = upstream.subscribe((event) => {
|
|
1099
|
+
for (const listener of listeners)
|
|
1100
|
+
listener(event);
|
|
1101
|
+
});
|
|
1102
|
+
const contexts = new WeakMap;
|
|
1103
|
+
return {
|
|
1104
|
+
pluginId: upstream.pluginId,
|
|
1105
|
+
instanceId: upstream.instanceId,
|
|
1106
|
+
session: (context) => upstream.session(context),
|
|
1107
|
+
selfProfile: (context, readOptions) => upstream.selfProfile(context, readOptions),
|
|
1108
|
+
async openPersonRequest(context) {
|
|
1109
|
+
const raw = await upstream.openScope(context);
|
|
1110
|
+
if (!raw.ok)
|
|
1111
|
+
return raw;
|
|
1112
|
+
const request = toLiveRequest(raw.value);
|
|
1113
|
+
if (request.ok)
|
|
1114
|
+
contexts.set(request.value, context);
|
|
1115
|
+
return request;
|
|
1116
|
+
},
|
|
1117
|
+
readToolProvenance: async (context) => ok(upstream.toolCorrelation(context)),
|
|
1118
|
+
directory,
|
|
1119
|
+
participants,
|
|
1120
|
+
history,
|
|
1121
|
+
async send(request, input, readOptions) {
|
|
1122
|
+
if (request.signal.aborted)
|
|
1123
|
+
return { status: "rejected", error: { code: "expired", message: "The request context is no longer live.", retry: "never" } };
|
|
1124
|
+
return upstream.submit(input, readOptions);
|
|
1125
|
+
},
|
|
1126
|
+
sendExternal: (author, input, readOptions) => upstream.submit({ ...input, input: upstream.labelExternal(author, input.input) }, readOptions),
|
|
1127
|
+
lookupOperation: async () => ok({ status: "unknown", reason: "unsupported" }),
|
|
1128
|
+
async forward(request, destinationPlugin, method, input) {
|
|
1129
|
+
const context = contexts.get(request);
|
|
1130
|
+
if (!context)
|
|
1131
|
+
throw { code: "stale-context", message: "The request scope is no longer live.", retry: "after-refresh" };
|
|
1132
|
+
return upstream.forward(context, destinationPlugin, method, input);
|
|
1133
|
+
},
|
|
1134
|
+
registerProvider: async () => err("unsupported", "Identity provider registration requires the enhanced host."),
|
|
1135
|
+
subscribe(listener) {
|
|
1136
|
+
listeners.add(listener);
|
|
1137
|
+
return () => listeners.delete(listener);
|
|
1138
|
+
},
|
|
1139
|
+
dispose() {
|
|
1140
|
+
subscriptions();
|
|
1141
|
+
close();
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
if (extension.status !== "supported") {
|
|
1146
|
+
const error = protocolError(extension);
|
|
1147
|
+
const unavailableResult = async () => ({ ok: false, error });
|
|
1148
|
+
return {
|
|
1149
|
+
pluginId: upstream.pluginId,
|
|
1150
|
+
instanceId: upstream.instanceId,
|
|
1151
|
+
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
1152
|
+
selfProfile: unavailableResult,
|
|
1153
|
+
openPersonRequest: unavailableResult,
|
|
1154
|
+
readToolProvenance: unavailableResult,
|
|
1155
|
+
directory,
|
|
1156
|
+
participants,
|
|
1157
|
+
history,
|
|
1158
|
+
send: async () => ({ status: "rejected", error }),
|
|
1159
|
+
sendExternal: async () => ({ status: "rejected", error }),
|
|
1160
|
+
lookupOperation: unavailableResult,
|
|
1161
|
+
forward: async () => {
|
|
1162
|
+
throw error;
|
|
1163
|
+
},
|
|
1164
|
+
registerProvider: unavailableResult,
|
|
1165
|
+
subscribe(listener) {
|
|
1166
|
+
listeners.add(listener);
|
|
1167
|
+
return () => listeners.delete(listener);
|
|
1168
|
+
},
|
|
1169
|
+
dispose: close
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const protocol = extension.protocol;
|
|
1173
|
+
const enhancedDirectory = createEnhancedDirectory(protocol);
|
|
1174
|
+
const enhancedParticipants = {
|
|
1175
|
+
async list(input, readOptions) {
|
|
1176
|
+
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)
|
|
1177
|
+
return err("invalid-input", "Invalid participant request.");
|
|
1178
|
+
if (disposed)
|
|
1179
|
+
return err("disposed", "Identity host is disposed.");
|
|
1180
|
+
let result;
|
|
1181
|
+
try {
|
|
1182
|
+
result = asResult(await protocol.participants(input, readOptions));
|
|
1183
|
+
} catch {
|
|
1184
|
+
return err("unavailable", "Participant history is unavailable.", "after-reconnect");
|
|
1185
|
+
}
|
|
1186
|
+
return result.ok ? decodeParticipantPage(result.value) : result;
|
|
1187
|
+
},
|
|
1188
|
+
previews: async () => err("unsupported", "Participant previews are unavailable from this enhanced protocol.")
|
|
1189
|
+
};
|
|
1190
|
+
const enhancedHistory = {
|
|
1191
|
+
async contributions(query, readOptions) {
|
|
1192
|
+
if (!validContributionQuery(query))
|
|
1193
|
+
return err("invalid-input", "Invalid contribution history request.");
|
|
1194
|
+
if (disposed)
|
|
1195
|
+
return err("disposed", "Identity host is disposed.");
|
|
1196
|
+
let result;
|
|
1197
|
+
try {
|
|
1198
|
+
result = asResult(await protocol.historyContributions(query, readOptions));
|
|
1199
|
+
} catch {
|
|
1200
|
+
return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
|
|
1201
|
+
}
|
|
1202
|
+
return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
|
|
1203
|
+
},
|
|
1204
|
+
async attempts(query, readOptions) {
|
|
1205
|
+
if (!validAttemptQuery(query))
|
|
1206
|
+
return err("invalid-input", "Invalid execution history request.");
|
|
1207
|
+
if (disposed)
|
|
1208
|
+
return err("disposed", "Identity host is disposed.");
|
|
1209
|
+
let result;
|
|
1210
|
+
try {
|
|
1211
|
+
result = asResult(await protocol.historyAttempts(query, readOptions));
|
|
1212
|
+
} catch {
|
|
1213
|
+
return err("unavailable", "Execution history is unavailable.", "after-reconnect");
|
|
1214
|
+
}
|
|
1215
|
+
return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
const subscription = protocol.subscribe((event) => {
|
|
1219
|
+
for (const listener of listeners)
|
|
1220
|
+
listener(event);
|
|
1221
|
+
});
|
|
1222
|
+
const scopes = new WeakMap;
|
|
1223
|
+
return {
|
|
1224
|
+
pluginId: upstream.pluginId,
|
|
1225
|
+
instanceId: protocol.instanceId,
|
|
1226
|
+
async session(context) {
|
|
1227
|
+
const decoded = serverSessionCodec.decode(await protocol.session(context));
|
|
1228
|
+
if (!decoded.ok)
|
|
1229
|
+
return { status: "incompatible", instanceId: protocol.instanceId, error: decoded.error };
|
|
1230
|
+
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" } };
|
|
1231
|
+
},
|
|
1232
|
+
async selfProfile(context, readOptions) {
|
|
1233
|
+
const result = asResult(await protocol.selfProfile(context, readOptions));
|
|
1234
|
+
if (!result.ok)
|
|
1235
|
+
return result;
|
|
1236
|
+
return profileCodec.decode(result.value);
|
|
1237
|
+
},
|
|
1238
|
+
async openPersonRequest(context) {
|
|
1239
|
+
const raw = await protocol.openRequest(context);
|
|
1240
|
+
if (!raw.ok)
|
|
1241
|
+
return raw;
|
|
1242
|
+
const enhanced = toForkRequest(raw.value);
|
|
1243
|
+
if (!enhanced.ok) {
|
|
1244
|
+
raw.value.release();
|
|
1245
|
+
return enhanced;
|
|
1246
|
+
}
|
|
1247
|
+
if (enhanced.value.request.session.instanceId !== protocol.instanceId) {
|
|
1248
|
+
raw.value.release();
|
|
1249
|
+
return err("incompatible", "Enhanced request instance does not match persisted host instance.");
|
|
1250
|
+
}
|
|
1251
|
+
scopes.set(enhanced.value.request, enhanced.value.scope);
|
|
1252
|
+
return ok(enhanced.value.request);
|
|
1253
|
+
},
|
|
1254
|
+
async readToolProvenance(context) {
|
|
1255
|
+
const result = asResult(await protocol.provenance(context));
|
|
1256
|
+
return result.ok ? provenanceCodec.decode(result.value) : result;
|
|
1257
|
+
},
|
|
1258
|
+
directory: {
|
|
1259
|
+
search: (input, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity host is disposed.")) : enhancedDirectory.search(input, readOptions),
|
|
1260
|
+
getMany: (input, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity host is disposed.")) : enhancedDirectory.getMany(input, readOptions),
|
|
1261
|
+
subscribe: enhancedDirectory.subscribe
|
|
1262
|
+
},
|
|
1263
|
+
participants: enhancedParticipants,
|
|
1264
|
+
history: enhancedHistory,
|
|
1265
|
+
async send(request, input, readOptions) {
|
|
1266
|
+
const scope = scopes.get(request);
|
|
1267
|
+
if (!scope)
|
|
1268
|
+
return { status: "rejected", error: { code: "stale-context", message: "The request scope is no longer live.", retry: "after-refresh" } };
|
|
1269
|
+
const result = decodeAcceptance(await protocol.accept({ source: { kind: "scope", scope }, input: wireInput(input, upstream.inputCodec) }, readOptions));
|
|
1270
|
+
return result.ok ? result.value : { status: "rejected", error: result.error };
|
|
1271
|
+
},
|
|
1272
|
+
async sendExternal(author, input, readOptions) {
|
|
1273
|
+
if (disposed)
|
|
1274
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity host is disposed.", retry: "never" } };
|
|
1275
|
+
const result = decodeAcceptance(await protocol.accept({ source: { kind: "external", author }, input: wireInput(input, upstream.inputCodec) }, readOptions));
|
|
1276
|
+
return result.ok ? result.value : { status: "rejected", error: result.error };
|
|
1277
|
+
},
|
|
1278
|
+
async lookupOperation(operationId, readOptions) {
|
|
1279
|
+
const result = asResult(await protocol.lookup(operationId, readOptions));
|
|
1280
|
+
return result.ok ? decodeLookup(result.value) : result;
|
|
1281
|
+
},
|
|
1282
|
+
async forward(request, destinationPlugin, method, input) {
|
|
1283
|
+
const scope = scopes.get(request);
|
|
1284
|
+
if (!scope)
|
|
1285
|
+
throw { code: "stale-context", message: "The request scope is no longer live.", retry: "after-refresh" };
|
|
1286
|
+
return protocol.forwardRpc(scope, { pluginId: destinationPlugin, method }, input);
|
|
1287
|
+
},
|
|
1288
|
+
async registerProvider(provider) {
|
|
1289
|
+
if (disposed)
|
|
1290
|
+
return err("disposed", "Identity host is disposed.");
|
|
1291
|
+
const result = await protocol.registerProvider(toForkProvider(provider));
|
|
1292
|
+
if (!result.ok)
|
|
1293
|
+
return result;
|
|
1294
|
+
if (disposed) {
|
|
1295
|
+
result.value.dispose();
|
|
1296
|
+
return err("disposed", "Identity host is disposed.");
|
|
1297
|
+
}
|
|
1298
|
+
return normalizeRegistration(result.value);
|
|
1299
|
+
},
|
|
1300
|
+
subscribe(listener) {
|
|
1301
|
+
listeners.add(listener);
|
|
1302
|
+
return () => listeners.delete(listener);
|
|
1303
|
+
},
|
|
1304
|
+
dispose() {
|
|
1305
|
+
subscription();
|
|
1306
|
+
close();
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function inspectForkExtension(value) {
|
|
1311
|
+
if (value === undefined)
|
|
1312
|
+
return { status: "absent" };
|
|
1313
|
+
if (!isRecord3(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") {
|
|
1314
|
+
const version = isRecord3(value) && typeof value.version === "number" ? value.version : null;
|
|
1315
|
+
return version === null ? { status: "malformed", error: { code: "incompatible", message: "Malformed experimental_p6rIdentity extension.", retry: "never" } } : { status: "unsupported-version", version };
|
|
1316
|
+
}
|
|
1317
|
+
return { status: "supported", protocol: value };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// server-runtime.ts
|
|
1321
|
+
function sameExpectation2(actor, stamp, expectedActor, expectedStamp) {
|
|
1322
|
+
return actor === expectedActor && stamp === expectedStamp ? ok(undefined) : err("stale-context", "The request actor or session changed.", "after-refresh");
|
|
1323
|
+
}
|
|
1324
|
+
function isPerson(value) {
|
|
1325
|
+
return value.kind === "person" || value.kind === "default-user";
|
|
1326
|
+
}
|
|
1327
|
+
function freezePerson(input) {
|
|
1328
|
+
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 });
|
|
1329
|
+
}
|
|
1330
|
+
function freezeActorReference(input) {
|
|
1331
|
+
return input.kind === "machine" ? Object.freeze({ kind: "machine", key: input.key, instanceId: input.instanceId, hostId: input.hostId }) : freezePerson(input);
|
|
1332
|
+
}
|
|
1333
|
+
function freezeActor(input) {
|
|
1334
|
+
return Object.freeze({
|
|
1335
|
+
identity: freezeActorReference(input.identity),
|
|
1336
|
+
presentation: Object.freeze({
|
|
1337
|
+
displayName: input.presentation.displayName,
|
|
1338
|
+
handle: input.presentation.handle,
|
|
1339
|
+
avatarUrl: input.presentation.avatarUrl
|
|
1340
|
+
}),
|
|
1341
|
+
evidence: input.evidence
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
function freezeExpected(input) {
|
|
1345
|
+
return Object.freeze({ actor: input.actor, session: input.session });
|
|
1346
|
+
}
|
|
1347
|
+
function createIdentityServer(options) {
|
|
1348
|
+
const { host } = options;
|
|
1349
|
+
let disposed = false;
|
|
1350
|
+
const dispose = () => {
|
|
1351
|
+
disposed = true;
|
|
1352
|
+
};
|
|
1353
|
+
const commits = {
|
|
1354
|
+
boundary: "same-process-synchronous",
|
|
1355
|
+
validate(target, scope) {
|
|
1356
|
+
if (typeof target !== "object" || target === null || target.intent !== "write" || typeof target.snapshot !== "function" || !(target.signal instanceof AbortSignal)) {
|
|
1357
|
+
return err("invalid-input", "Write target does not have write intent.");
|
|
1358
|
+
}
|
|
1359
|
+
if (disposed || target.signal.aborted)
|
|
1360
|
+
return err("expired", "The request context is no longer live.");
|
|
1361
|
+
const addressIsConcrete = scope.collection.length > 0 && scope.recordId.length > 0 && Number.isInteger(scope.schemaVersion) && scope.schemaVersion >= 0;
|
|
1362
|
+
try {
|
|
1363
|
+
const snapshot = target.snapshot();
|
|
1364
|
+
const capturedActor = snapshot.actor.identity.key;
|
|
1365
|
+
const capturedSubject = snapshot.subject.key;
|
|
1366
|
+
const capturedExpected = snapshot.expected;
|
|
1367
|
+
const capturedFactsAreConsistent = capturedExpected.actor === capturedActor;
|
|
1368
|
+
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");
|
|
1369
|
+
} catch {
|
|
1370
|
+
return err("invalid-input", "Write target snapshot is unavailable.");
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
async function issueTarget(live, request, input) {
|
|
1375
|
+
if (request.signal.aborted)
|
|
1376
|
+
return err("expired", "The request context is no longer live.");
|
|
1377
|
+
const currentRequest = live.validate(request.expected);
|
|
1378
|
+
if (!currentRequest.ok)
|
|
1379
|
+
return currentRequest;
|
|
1380
|
+
const actor = freezeActor(request.actor);
|
|
1381
|
+
let subject = freezeActorReference(actor.identity);
|
|
1382
|
+
if (input.selection.kind === "person") {
|
|
1383
|
+
if (input.policy.kind === "self-only")
|
|
1384
|
+
return err("unsupported", "This operation only supports the acting person.");
|
|
1385
|
+
const profiles = await host.directory.getMany({ keys: [input.selection.key] });
|
|
1386
|
+
if (!profiles.ok)
|
|
1387
|
+
return profiles;
|
|
1388
|
+
const found = profiles.value[0];
|
|
1389
|
+
if (!found || found.status !== "found" || !isPerson(found.profile.identity))
|
|
1390
|
+
return err("not-found", "Selected person was not found.");
|
|
1391
|
+
subject = freezePerson(found.profile.identity);
|
|
1392
|
+
}
|
|
1393
|
+
if (input.intent === "write") {
|
|
1394
|
+
const current = sameExpectation2(request.actor.identity.key, request.expected.session, input.expected.actor, input.expected.session);
|
|
1395
|
+
if (!current.ok)
|
|
1396
|
+
return current;
|
|
1397
|
+
if (input.expectedSubject !== subject.key)
|
|
1398
|
+
return err("stale-owner", "Selected subject changed.", "after-refresh");
|
|
1399
|
+
}
|
|
1400
|
+
const expected = freezeExpected({ actor: actor.identity.key, session: request.expected.session });
|
|
1401
|
+
const snapshot = Object.freeze({
|
|
1402
|
+
actor,
|
|
1403
|
+
subject,
|
|
1404
|
+
expected
|
|
1405
|
+
});
|
|
1406
|
+
const target = Object.freeze({
|
|
1407
|
+
intent: input.intent,
|
|
1408
|
+
snapshot: () => snapshot,
|
|
1409
|
+
signal: request.signal
|
|
1410
|
+
});
|
|
1411
|
+
return ok(target);
|
|
1412
|
+
}
|
|
1413
|
+
const server = {
|
|
1414
|
+
instanceId: host.instanceId,
|
|
1415
|
+
directory: host.directory,
|
|
1416
|
+
participants: host.participants,
|
|
1417
|
+
history: host.history,
|
|
1418
|
+
commits,
|
|
1419
|
+
session: (context, readOptions) => host.session(context, readOptions),
|
|
1420
|
+
selfProfile: (context, readOptions) => host.selfProfile(context, readOptions),
|
|
1421
|
+
subscribe: (listener) => host.subscribe(listener),
|
|
1422
|
+
async personRequest(context, readOptions) {
|
|
1423
|
+
if (disposed)
|
|
1424
|
+
return err("disposed", "Identity server is disposed.");
|
|
1425
|
+
const opened = await host.openPersonRequest(context, readOptions);
|
|
1426
|
+
if (!opened.ok)
|
|
1427
|
+
return opened;
|
|
1428
|
+
const live = opened.value;
|
|
1429
|
+
let released = false;
|
|
1430
|
+
let request;
|
|
1431
|
+
async function target(input) {
|
|
1432
|
+
return issueTarget(live, request, input);
|
|
1433
|
+
}
|
|
1434
|
+
request = {
|
|
1435
|
+
actor: live.session.actor,
|
|
1436
|
+
expected: { actor: live.session.actor.identity.key, session: live.session.stamp },
|
|
1437
|
+
capabilities: live.session.capabilities,
|
|
1438
|
+
signal: live.signal,
|
|
1439
|
+
target,
|
|
1440
|
+
async send(input, sendOptions) {
|
|
1441
|
+
const valid = live.validate(input.expected);
|
|
1442
|
+
if (!valid.ok)
|
|
1443
|
+
return { status: "rejected", error: valid.error };
|
|
1444
|
+
return host.send(live, input, sendOptions);
|
|
1445
|
+
},
|
|
1446
|
+
async callPlugin(input, callOptions) {
|
|
1447
|
+
const valid = live.validate(request.expected);
|
|
1448
|
+
if (!valid.ok)
|
|
1449
|
+
return valid;
|
|
1450
|
+
try {
|
|
1451
|
+
return input.output.decode(await host.forward(live, input.pluginId, input.method, input.payload));
|
|
1452
|
+
} catch {
|
|
1453
|
+
return err("unavailable", "Destination plugin call failed.", callOptions?.signal?.aborted ? "never" : "after-reconnect");
|
|
1454
|
+
}
|
|
1455
|
+
},
|
|
1456
|
+
dispose() {
|
|
1457
|
+
if (released)
|
|
1458
|
+
return;
|
|
1459
|
+
released = true;
|
|
1460
|
+
live.release();
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
return ok(request);
|
|
1464
|
+
},
|
|
1465
|
+
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
1466
|
+
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
1467
|
+
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
1468
|
+
dispose
|
|
1469
|
+
};
|
|
1470
|
+
return server;
|
|
1471
|
+
}
|
|
1472
|
+
function createIdentityEndpoint(options) {
|
|
1473
|
+
let disposed = false;
|
|
1474
|
+
let stop = null;
|
|
1475
|
+
const dispose = () => {
|
|
1476
|
+
disposed = true;
|
|
1477
|
+
const unsubscribe = stop;
|
|
1478
|
+
stop = null;
|
|
1479
|
+
unsubscribe?.();
|
|
1480
|
+
};
|
|
1481
|
+
const subscribed = options.server.subscribe((event) => {
|
|
1482
|
+
if (disposed)
|
|
1483
|
+
return;
|
|
1484
|
+
try {
|
|
1485
|
+
if (event.kind === "directory") {
|
|
1486
|
+
const keys = event.keys.map((key) => identityKeyCodec.decode(key));
|
|
1487
|
+
options.publish({ kind: "directory", keys: keys.every((key) => key.ok) ? keys.map((key) => key.value) : [], revision: null });
|
|
1488
|
+
} else if (event.kind === "participants") {
|
|
1489
|
+
const ids = event.threadIds.map((id) => idCodec("thread").decode(id));
|
|
1490
|
+
if (ids.every((id) => id.ok))
|
|
1491
|
+
options.publish({ kind: "participants", threadIds: ids.map((id) => id.value) });
|
|
1492
|
+
} else if (event.kind === "session")
|
|
1493
|
+
options.publish(event.reason === undefined ? { kind: "session" } : { kind: "session", reason: event.reason });
|
|
1494
|
+
else if (event.kind === "reconnected" || event.kind === "disconnected")
|
|
1495
|
+
options.publish({ kind: event.kind });
|
|
1496
|
+
else if (event.kind === "disposed") {
|
|
1497
|
+
dispose();
|
|
1498
|
+
options.publish({ kind: "disconnected" });
|
|
1499
|
+
}
|
|
1500
|
+
} catch {}
|
|
1501
|
+
});
|
|
1502
|
+
if (disposed)
|
|
1503
|
+
subscribed();
|
|
1504
|
+
else
|
|
1505
|
+
stop = subscribed;
|
|
1506
|
+
const reject = () => Promise.resolve(err("disposed", "Identity endpoint is disposed."));
|
|
1507
|
+
const fence = async (work) => {
|
|
1508
|
+
try {
|
|
1509
|
+
const result = await work;
|
|
1510
|
+
return disposed ? err("disposed", "Identity endpoint is disposed.") : result;
|
|
1511
|
+
} catch {
|
|
1512
|
+
return err(disposed ? "disposed" : "unavailable", disposed ? "Identity endpoint is disposed." : "Identity endpoint read failed.");
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
return {
|
|
1516
|
+
bootstrap(context, readOptions) {
|
|
1517
|
+
return disposed ? reject() : fence(options.server.session(context, readOptions).then(ok));
|
|
1518
|
+
},
|
|
1519
|
+
selfProfile(context, readOptions) {
|
|
1520
|
+
return disposed ? reject() : fence(options.server.selfProfile(context, readOptions));
|
|
1521
|
+
},
|
|
1522
|
+
search(context, input, readOptions) {
|
|
1523
|
+
return disposed ? reject() : fence(options.server.directory.search(input, readOptions));
|
|
1524
|
+
},
|
|
1525
|
+
profiles(context, input, readOptions) {
|
|
1526
|
+
return disposed ? reject() : fence(options.server.directory.getMany(input, readOptions));
|
|
1527
|
+
},
|
|
1528
|
+
participants(context, input, readOptions) {
|
|
1529
|
+
return disposed ? reject() : fence(options.server.participants.list(input, readOptions));
|
|
1530
|
+
},
|
|
1531
|
+
participantPreviews(context, input, readOptions) {
|
|
1532
|
+
return disposed ? reject() : fence(options.server.participants.previews(input, readOptions));
|
|
1533
|
+
},
|
|
1534
|
+
dispose
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// state-service-runtime.ts
|
|
1539
|
+
function isRecord4(value) {
|
|
1540
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1541
|
+
}
|
|
1542
|
+
function nonEmpty(value) {
|
|
1543
|
+
return typeof value === "string" && value.length > 0;
|
|
1544
|
+
}
|
|
1545
|
+
function decodeAddress(value) {
|
|
1546
|
+
if (!isRecord4(value) || !nonEmpty(value.collection) || !nonEmpty(value.recordId))
|
|
1547
|
+
return err("invalid-input", "Invalid state address.");
|
|
1548
|
+
const instanceId = idCodec("instance").decode(value.instanceId);
|
|
1549
|
+
const pluginId = idCodec("plugin").decode(value.pluginId);
|
|
1550
|
+
const owner = identityKeyCodec.decode(value.owner);
|
|
1551
|
+
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.");
|
|
1552
|
+
}
|
|
1553
|
+
function decodeVersion(value) {
|
|
1554
|
+
return isRecord4(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.");
|
|
1555
|
+
}
|
|
1556
|
+
function decodeActor(value) {
|
|
1557
|
+
if (value === null)
|
|
1558
|
+
return ok(null);
|
|
1559
|
+
if (!isRecord4(value) || !isRecord4(value.identity) || !isRecord4(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)) {
|
|
1560
|
+
return err("invalid-input", "Invalid state editor snapshot.");
|
|
1561
|
+
}
|
|
1562
|
+
const key = identityKeyCodec.decode(value.identity.key);
|
|
1563
|
+
if (!key.ok)
|
|
1564
|
+
return key;
|
|
1565
|
+
const identity = value.identity;
|
|
1566
|
+
const presentation = { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl };
|
|
1567
|
+
if (identity.kind === "machine") {
|
|
1568
|
+
const instanceId = idCodec("instance").decode(identity.instanceId);
|
|
1569
|
+
if (!instanceId.ok || value.evidence !== "machine" || identity.hostId !== null && (typeof identity.hostId !== "string" || identity.hostId.length === 0 || identity.hostId.length > 512)) {
|
|
1570
|
+
return err("invalid-input", "Invalid machine editor identity.");
|
|
1571
|
+
}
|
|
1572
|
+
return ok({ identity: { kind: "machine", key: key.value, instanceId: instanceId.value, hostId: identity.hostId }, presentation, evidence: "machine" });
|
|
1573
|
+
}
|
|
1574
|
+
if (value.evidence === "machine")
|
|
1575
|
+
return err("invalid-input", "Machine evidence requires a machine identity.");
|
|
1576
|
+
if (identity.kind === "person" && nonEmpty(identity.issuer) && nonEmpty(identity.subject)) {
|
|
1577
|
+
return ok({ identity: { kind: "person", key: key.value, issuer: identity.issuer, subject: identity.subject }, presentation, evidence: value.evidence });
|
|
1578
|
+
}
|
|
1579
|
+
if (identity.kind === "default-user") {
|
|
1580
|
+
const instanceId = idCodec("instance").decode(identity.instanceId);
|
|
1581
|
+
return instanceId.ok ? ok({ identity: { kind: "default-user", key: key.value, instanceId: instanceId.value }, presentation, evidence: value.evidence }) : instanceId;
|
|
1582
|
+
}
|
|
1583
|
+
if (identity.kind === "external") {
|
|
1584
|
+
const pluginId = idCodec("plugin").decode(identity.pluginId);
|
|
1585
|
+
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.");
|
|
1586
|
+
}
|
|
1587
|
+
return err("invalid-input", "Invalid editor identity.");
|
|
1588
|
+
}
|
|
1589
|
+
function sameAddress(left, right) {
|
|
1590
|
+
return left.instanceId === right.instanceId && left.pluginId === right.pluginId && left.collection === right.collection && left.recordId === right.recordId && left.owner === right.owner;
|
|
1591
|
+
}
|
|
1592
|
+
function decodeEnvelope(value, codec) {
|
|
1593
|
+
if (!isRecord4(value) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0) {
|
|
1594
|
+
return err("invalid-input", "Invalid state envelope.");
|
|
1595
|
+
}
|
|
1596
|
+
const address = decodeAddress(value.address);
|
|
1597
|
+
const version = decodeVersion(value.version);
|
|
1598
|
+
const decoded = codec.decode(value.value);
|
|
1599
|
+
const editor = decodeActor(value.lastEditedBy);
|
|
1600
|
+
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.");
|
|
1601
|
+
}
|
|
1602
|
+
function decodeRead(value, codec) {
|
|
1603
|
+
if (!isRecord4(value) || typeof value.status !== "string")
|
|
1604
|
+
return err("invalid-input", "Invalid state read.");
|
|
1605
|
+
if (value.status === "present") {
|
|
1606
|
+
const envelope = decodeEnvelope(value.envelope, codec);
|
|
1607
|
+
return envelope.ok ? ok({ status: "present", envelope: envelope.value }) : envelope;
|
|
1608
|
+
}
|
|
1609
|
+
if (value.status === "empty") {
|
|
1610
|
+
const address = decodeAddress(value.address);
|
|
1611
|
+
const version = decodeVersion(value.version);
|
|
1612
|
+
return address.ok && version.ok ? ok({ status: "empty", address: address.value, version: version.value }) : err("invalid-input", "Invalid empty state read.");
|
|
1613
|
+
}
|
|
1614
|
+
if (value.status === "migration-required") {
|
|
1615
|
+
const address = decodeAddress(value.address);
|
|
1616
|
+
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.");
|
|
1617
|
+
}
|
|
1618
|
+
return err("invalid-input", "Unknown state read status.");
|
|
1619
|
+
}
|
|
1620
|
+
function decodeMutation(value, codec) {
|
|
1621
|
+
if (!isRecord4(value) || value.kind !== "initialize" && value.kind !== "replace" || !isRecord4(value.expected) || typeof value.schemaVersion !== "number" || !Number.isInteger(value.schemaVersion) || value.schemaVersion < 0 || typeof value.localGeneration !== "number" || !Number.isInteger(value.localGeneration) || value.localGeneration < 0) {
|
|
1622
|
+
return err("invalid-input", "Invalid state mutation.");
|
|
1623
|
+
}
|
|
1624
|
+
const address = decodeAddress(value.address);
|
|
1625
|
+
const expectedVersion = decodeVersion(value.expectedVersion);
|
|
1626
|
+
const actor = identityKeyCodec.decode(value.expected.actor);
|
|
1627
|
+
const session = idCodec("server-session").decode(value.expected.session);
|
|
1628
|
+
const ownerSession = idCodec("owner-session").decode(value.ownerSession);
|
|
1629
|
+
const operationId = idCodec("operation").decode(value.operationId);
|
|
1630
|
+
const decoded = codec.decode(value.value);
|
|
1631
|
+
return address.ok && expectedVersion.ok && actor.ok && session.ok && ownerSession.ok && operationId.ok && decoded.ok ? ok({
|
|
1632
|
+
kind: value.kind,
|
|
1633
|
+
address: address.value,
|
|
1634
|
+
expectedVersion: expectedVersion.value,
|
|
1635
|
+
expected: { actor: actor.value, session: session.value },
|
|
1636
|
+
ownerSession: ownerSession.value,
|
|
1637
|
+
localGeneration: value.localGeneration,
|
|
1638
|
+
operationId: operationId.value,
|
|
1639
|
+
schemaVersion: value.schemaVersion,
|
|
1640
|
+
value: decoded.value
|
|
1641
|
+
}) : err("invalid-input", "Invalid state mutation fields.");
|
|
1642
|
+
}
|
|
1643
|
+
function freezeDeep(value) {
|
|
1644
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1645
|
+
for (const child of Object.values(value))
|
|
1646
|
+
freezeDeep(child);
|
|
1647
|
+
Object.freeze(value);
|
|
1648
|
+
}
|
|
1649
|
+
return value;
|
|
1650
|
+
}
|
|
1651
|
+
function cloneTyped(input, codec) {
|
|
1652
|
+
try {
|
|
1653
|
+
return codec.decode(structuredClone(codec.encode(input)));
|
|
1654
|
+
} catch {
|
|
1655
|
+
return err("invalid-input", "State value could not be encoded.");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
function immutableMutation(input, codec) {
|
|
1659
|
+
const decoded = cloneTyped(input, stateCodecs(codec).mutation);
|
|
1660
|
+
return decoded.ok ? ok(freezeDeep(decoded.value)) : decoded;
|
|
1661
|
+
}
|
|
1662
|
+
function decodeOutcome(value, codec) {
|
|
1663
|
+
if (!isRecord4(value) || typeof value.status !== "string")
|
|
1664
|
+
return err("invalid-input", "Invalid state outcome.");
|
|
1665
|
+
const operationId = idCodec("operation").decode(value.operationId);
|
|
1666
|
+
if (!operationId.ok)
|
|
1667
|
+
return operationId;
|
|
1668
|
+
if (value.status === "saved" || value.status === "unchanged") {
|
|
1669
|
+
const envelope = decodeEnvelope(value.envelope, codec);
|
|
1670
|
+
return envelope.ok ? ok({ status: value.status, envelope: envelope.value, operationId: operationId.value }) : envelope;
|
|
1671
|
+
}
|
|
1672
|
+
if (value.status === "already-initialized" || value.status === "conflict") {
|
|
1673
|
+
const current = decodeRead(value.current, codec);
|
|
1674
|
+
return current.ok ? ok({ status: value.status, current: current.value, operationId: operationId.value }) : current;
|
|
1675
|
+
}
|
|
1676
|
+
return err("invalid-input", "Unknown state outcome status.");
|
|
1677
|
+
}
|
|
1678
|
+
function decodeSave(value, codec) {
|
|
1679
|
+
if (isRecord4(value) && value.status === "indeterminate") {
|
|
1680
|
+
const operationId = idCodec("operation").decode(value.operationId);
|
|
1681
|
+
return operationId.ok ? ok({ status: "indeterminate", operationId: operationId.value }) : operationId;
|
|
1682
|
+
}
|
|
1683
|
+
return decodeOutcome(value, codec);
|
|
1684
|
+
}
|
|
1685
|
+
function assertRead(read, address, schemaVersion) {
|
|
1686
|
+
const actual = read.status === "present" ? read.envelope.address : read.address;
|
|
1687
|
+
if (!sameAddress(actual, address))
|
|
1688
|
+
return err("incompatible", "Storage returned a state record for a different address.");
|
|
1689
|
+
if (read.status === "present" && read.envelope.schemaVersion !== schemaVersion) {
|
|
1690
|
+
return err("incompatible", "Storage returned an unexpected state schema.");
|
|
1691
|
+
}
|
|
1692
|
+
return ok(read);
|
|
1693
|
+
}
|
|
1694
|
+
function assertOutcome(outcome, address, schemaVersion, operationId) {
|
|
1695
|
+
if (outcome.operationId !== operationId)
|
|
1696
|
+
return err("incompatible", "Storage returned an outcome for a different operation.");
|
|
1697
|
+
const read = "envelope" in outcome ? { status: "present", envelope: outcome.envelope } : outcome.current;
|
|
1698
|
+
const checked = assertRead(read, address, schemaVersion);
|
|
1699
|
+
return checked.ok ? ok(outcome) : checked;
|
|
1700
|
+
}
|
|
1701
|
+
function assertSave(save, address, schemaVersion, operationId) {
|
|
1702
|
+
if (save.status === "indeterminate")
|
|
1703
|
+
return save.operationId === operationId ? ok(save) : err("incompatible", "Storage returned an indeterminate result for a different operation.");
|
|
1704
|
+
return assertOutcome(save, address, schemaVersion, operationId);
|
|
1705
|
+
}
|
|
1706
|
+
function addressFor(target, recordId, resource, instanceId) {
|
|
1707
|
+
if (!nonEmpty(recordId))
|
|
1708
|
+
return err("invalid-input", "State record id is required.");
|
|
1709
|
+
const snapshot = target.snapshot();
|
|
1710
|
+
return ok({ instanceId, pluginId: resource.pluginId, collection: resource.definition.collection, recordId, owner: snapshot.subject.key });
|
|
1711
|
+
}
|
|
1712
|
+
function createStateService(options) {
|
|
1713
|
+
const resource = { pluginId: options.pluginId, definition: options.definition };
|
|
1714
|
+
const codecs = stateCodecs(options.definition.codec);
|
|
1715
|
+
const staticMutation = (target, mutation) => {
|
|
1716
|
+
const expected = addressFor(target, mutation.address.recordId, resource, options.instanceId);
|
|
1717
|
+
if (!expected.ok)
|
|
1718
|
+
return expected;
|
|
1719
|
+
const snapshot = target.snapshot();
|
|
1720
|
+
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.");
|
|
1721
|
+
};
|
|
1722
|
+
return {
|
|
1723
|
+
async read(target, recordId, readOptions) {
|
|
1724
|
+
if (target.signal.aborted)
|
|
1725
|
+
return err("expired", "The request context is no longer live.");
|
|
1726
|
+
const address = addressFor(target, recordId, resource, options.instanceId);
|
|
1727
|
+
if (!address.ok)
|
|
1728
|
+
return address;
|
|
1729
|
+
const result = await options.storage.read(address.value, readOptions);
|
|
1730
|
+
if (!result.ok)
|
|
1731
|
+
return result;
|
|
1732
|
+
const decoded = cloneTyped(result.value, codecs.read);
|
|
1733
|
+
return decoded.ok ? assertRead(decoded.value, address.value, options.definition.schemaVersion) : decoded;
|
|
1734
|
+
},
|
|
1735
|
+
async save(target, mutation, readOptions) {
|
|
1736
|
+
const submitted = immutableMutation(mutation, options.definition.codec);
|
|
1737
|
+
if (!submitted.ok)
|
|
1738
|
+
return submitted;
|
|
1739
|
+
const valid = staticMutation(target, submitted.value);
|
|
1740
|
+
if (!valid.ok)
|
|
1741
|
+
return valid;
|
|
1742
|
+
const targetSnapshot = target.snapshot();
|
|
1743
|
+
const expected = Object.freeze({ actor: targetSnapshot.expected.actor, session: targetSnapshot.expected.session });
|
|
1744
|
+
const subject = targetSnapshot.subject.key;
|
|
1745
|
+
const editor = freezeDeep({
|
|
1746
|
+
identity: { ...targetSnapshot.actor.identity },
|
|
1747
|
+
presentation: { ...targetSnapshot.actor.presentation },
|
|
1748
|
+
evidence: targetSnapshot.actor.evidence
|
|
1749
|
+
});
|
|
1750
|
+
const committed = await options.storage.commit({
|
|
1751
|
+
mutation: submitted.value,
|
|
1752
|
+
validateAtCommit: () => {
|
|
1753
|
+
if (readOptions?.signal?.aborted)
|
|
1754
|
+
return err("cancelled", "State save was cancelled before durable acceptance.");
|
|
1755
|
+
const scope = {
|
|
1756
|
+
instanceId: options.instanceId,
|
|
1757
|
+
pluginId: options.pluginId,
|
|
1758
|
+
collection: options.definition.collection,
|
|
1759
|
+
recordId: submitted.value.address.recordId,
|
|
1760
|
+
subject,
|
|
1761
|
+
expected,
|
|
1762
|
+
schemaVersion: options.definition.schemaVersion
|
|
1763
|
+
};
|
|
1764
|
+
const live = options.commits.validate(target, scope);
|
|
1765
|
+
return live.ok ? ok(editor) : live;
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
if (!committed.ok)
|
|
1769
|
+
return committed;
|
|
1770
|
+
const decoded = cloneTyped(committed.value, codecs.save);
|
|
1771
|
+
if (!decoded.ok)
|
|
1772
|
+
return decoded;
|
|
1773
|
+
const checked = assertSave(decoded.value, submitted.value.address, options.definition.schemaVersion, submitted.value.operationId);
|
|
1774
|
+
if (!checked.ok)
|
|
1775
|
+
return checked;
|
|
1776
|
+
const outcome = checked.value;
|
|
1777
|
+
const publishedEnvelope = outcome.status === "saved" ? outcome.envelope : outcome.status === "already-initialized" && outcome.current.status === "present" ? outcome.current.envelope : null;
|
|
1778
|
+
if (publishedEnvelope) {
|
|
1779
|
+
try {
|
|
1780
|
+
options.publish({ address: publishedEnvelope.address, version: publishedEnvelope.version, operationId: outcome.operationId });
|
|
1781
|
+
} catch {}
|
|
1782
|
+
}
|
|
1783
|
+
return ok(outcome);
|
|
1784
|
+
},
|
|
1785
|
+
async reconcile(target, recordId, operationId) {
|
|
1786
|
+
const address = addressFor(target, recordId, resource, options.instanceId);
|
|
1787
|
+
if (!address.ok)
|
|
1788
|
+
return address;
|
|
1789
|
+
const result = await options.storage.reconcile({ address: address.value, operationId });
|
|
1790
|
+
if (!result.ok)
|
|
1791
|
+
return result;
|
|
1792
|
+
const decoded = cloneTyped(result.value, codecs.lookup);
|
|
1793
|
+
if (!decoded.ok || decoded.value.status !== "final")
|
|
1794
|
+
return decoded;
|
|
1795
|
+
const checked = assertOutcome(decoded.value.outcome, address.value, options.definition.schemaVersion, operationId);
|
|
1796
|
+
return checked.ok ? ok({ status: "final", outcome: checked.value }) : checked;
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function decodeLookup2(value, codec) {
|
|
1801
|
+
if (!isRecord4(value) || typeof value.status !== "string")
|
|
1802
|
+
return err("invalid-input", "Invalid state lookup.");
|
|
1803
|
+
if (value.status === "final") {
|
|
1804
|
+
const outcome = decodeOutcome(value.outcome, codec);
|
|
1805
|
+
return outcome.ok ? ok({ status: "final", outcome: outcome.value }) : outcome;
|
|
1806
|
+
}
|
|
1807
|
+
if (value.status === "pending")
|
|
1808
|
+
return ok({ status: "pending" });
|
|
1809
|
+
if (value.status === "absent-final" && value.retry === "same-operation-only")
|
|
1810
|
+
return ok({ status: "absent-final", retry: "same-operation-only" });
|
|
1811
|
+
if (value.status === "unknown" && (value.reason === "unsupported" || value.reason === "expired" || value.reason === "unavailable"))
|
|
1812
|
+
return ok({ status: "unknown", reason: value.reason });
|
|
1813
|
+
return err("invalid-input", "Invalid state lookup metadata.");
|
|
1814
|
+
}
|
|
1815
|
+
function encodeAddress(address) {
|
|
1816
|
+
return { instanceId: address.instanceId, pluginId: address.pluginId, collection: address.collection, recordId: address.recordId, owner: address.owner };
|
|
1817
|
+
}
|
|
1818
|
+
function encodeVersion(version) {
|
|
1819
|
+
return { epoch: version.epoch, sequence: version.sequence };
|
|
1820
|
+
}
|
|
1821
|
+
function encodeActor(actor) {
|
|
1822
|
+
return actor === null ? null : { identity: actor.identity, presentation: actor.presentation, evidence: actor.evidence };
|
|
1823
|
+
}
|
|
1824
|
+
function encodeEnvelope(envelope, codec) {
|
|
1825
|
+
return { address: encodeAddress(envelope.address), version: encodeVersion(envelope.version), schemaVersion: envelope.schemaVersion, value: codec.encode(envelope.value), lastEditedBy: encodeActor(envelope.lastEditedBy) };
|
|
1826
|
+
}
|
|
1827
|
+
function encodeRead(read, codec) {
|
|
1828
|
+
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 } : (() => {
|
|
1829
|
+
throw new TypeError("Invalid state read status.");
|
|
1830
|
+
})();
|
|
1831
|
+
}
|
|
1832
|
+
function encodeMutation(mutation, codec) {
|
|
1833
|
+
return { ...mutation, address: encodeAddress(mutation.address), expectedVersion: encodeVersion(mutation.expectedVersion), value: codec.encode(mutation.value) };
|
|
1834
|
+
}
|
|
1835
|
+
function encodeOutcome(outcome, codec) {
|
|
1836
|
+
if (outcome.status === "saved" || outcome.status === "unchanged") {
|
|
1837
|
+
return { status: outcome.status, envelope: encodeEnvelope(outcome.envelope, codec), operationId: outcome.operationId };
|
|
1838
|
+
}
|
|
1839
|
+
if ("current" in outcome)
|
|
1840
|
+
return { status: outcome.status, current: encodeRead(outcome.current, codec), operationId: outcome.operationId };
|
|
1841
|
+
throw new TypeError("Invalid state outcome.");
|
|
1842
|
+
}
|
|
1843
|
+
function encodeSave(save, codec) {
|
|
1844
|
+
return save.status === "indeterminate" ? { status: "indeterminate", operationId: save.operationId } : encodeOutcome(save, codec);
|
|
1845
|
+
}
|
|
1846
|
+
function stateCodecs(value) {
|
|
1847
|
+
return {
|
|
1848
|
+
read: { decode: (input) => decodeRead(input, value), encode: (input) => encodeRead(input, value) },
|
|
1849
|
+
mutation: { decode: (input) => decodeMutation(input, value), encode: (input) => encodeMutation(input, value) },
|
|
1850
|
+
save: { decode: (input) => decodeSave(input, value), encode: (input) => encodeSave(input, value) },
|
|
1851
|
+
invalidation: {
|
|
1852
|
+
decode(input) {
|
|
1853
|
+
if (!isRecord4(input))
|
|
1854
|
+
return err("invalid-input", "Invalid state invalidation.");
|
|
1855
|
+
const address = decodeAddress(input.address);
|
|
1856
|
+
const version = decodeVersion(input.version);
|
|
1857
|
+
const operationId = input.operationId === null ? ok(null) : idCodec("operation").decode(input.operationId);
|
|
1858
|
+
return address.ok && version.ok && operationId.ok ? ok({ address: address.value, version: version.value, operationId: operationId.value }) : err("invalid-input", "Invalid state invalidation fields.");
|
|
1859
|
+
},
|
|
1860
|
+
encode: (input) => ({ address: encodeAddress(input.address), version: encodeVersion(input.version), operationId: input.operationId })
|
|
1861
|
+
},
|
|
1862
|
+
lookup: {
|
|
1863
|
+
decode: (input) => decodeLookup2(input, value),
|
|
1864
|
+
encode: (input) => input.status === "final" ? { status: "final", outcome: encodeOutcome(input.outcome, value) } : input
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// rpc-routes-runtime.ts
|
|
1870
|
+
var identityRoutes = Object.freeze({
|
|
1871
|
+
bootstrap: "bb-identity/v1/bootstrap",
|
|
1872
|
+
selfProfile: "bb-identity/v1/selfProfile",
|
|
1873
|
+
search: "bb-identity/v1/search",
|
|
1874
|
+
profiles: "bb-identity/v1/profiles",
|
|
1875
|
+
participants: "bb-identity/v1/participants",
|
|
1876
|
+
participantPreviews: "bb-identity/v1/participantPreviews"
|
|
1877
|
+
});
|
|
1878
|
+
var identityStateRoutes = Object.freeze({
|
|
1879
|
+
load: "bb-identity/v1/state/load",
|
|
1880
|
+
save: "bb-identity/v1/state/save",
|
|
1881
|
+
reconcile: "bb-identity/v1/state/reconcile"
|
|
1882
|
+
});
|
|
1883
|
+
var identityStateRpcMethods = Object.freeze({
|
|
1884
|
+
[identityStateRoutes.load]: "bb-identity.v1.state.load",
|
|
1885
|
+
[identityStateRoutes.save]: "bb-identity.v1.state.save",
|
|
1886
|
+
[identityStateRoutes.reconcile]: "bb-identity.v1.state.reconcile"
|
|
1887
|
+
});
|
|
1888
|
+
var identityRpcMethods = Object.freeze({
|
|
1889
|
+
[identityRoutes.bootstrap]: "bb-identity.v1.bootstrap",
|
|
1890
|
+
[identityRoutes.selfProfile]: "bb-identity.v1.selfProfile",
|
|
1891
|
+
[identityRoutes.search]: "bb-identity.v1.search",
|
|
1892
|
+
[identityRoutes.profiles]: "bb-identity.v1.profiles",
|
|
1893
|
+
[identityRoutes.participants]: "bb-identity.v1.participants",
|
|
1894
|
+
[identityRoutes.participantPreviews]: "bb-identity.v1.participantPreviews",
|
|
1895
|
+
...identityStateRpcMethods
|
|
1896
|
+
});
|
|
1897
|
+
|
|
1898
|
+
// state-rpc-runtime.ts
|
|
1899
|
+
var methods = {
|
|
1900
|
+
load: identityStateRpcMethods[identityStateRoutes.load],
|
|
1901
|
+
save: identityStateRpcMethods[identityStateRoutes.save],
|
|
1902
|
+
reconcile: identityStateRpcMethods[identityStateRoutes.reconcile]
|
|
1903
|
+
};
|
|
1904
|
+
var inputSchema = { "~standard": { version: 1, vendor: "bb-identity", validate: (value) => ({ value }) } };
|
|
1905
|
+
function isJson2(value) {
|
|
1906
|
+
return value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every(isJson2) || isRecord5(value) && Object.values(value).every(isJson2);
|
|
1907
|
+
}
|
|
1908
|
+
var outputSchema = { "~standard": { version: 1, vendor: "bb-identity", validate: (value) => isJson2(value) ? { value } : { issues: [{ message: "Expected an encoded identity state result." }] } } };
|
|
1909
|
+
function isRecord5(value) {
|
|
1910
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1911
|
+
}
|
|
1912
|
+
function decodeRequest(input) {
|
|
1913
|
+
if (!isRecord5(input) || !isRecord5(input.address) || !isRecord5(input.expected))
|
|
1914
|
+
return err("invalid-input", "State request requires an address and expected session.");
|
|
1915
|
+
const address = input.address;
|
|
1916
|
+
const instance = idCodec("instance").decode(address.instanceId);
|
|
1917
|
+
const plugin = idCodec("plugin").decode(address.pluginId);
|
|
1918
|
+
const owner = identityKeyCodec.decode(address.owner);
|
|
1919
|
+
const actor = identityKeyCodec.decode(input.expected.actor);
|
|
1920
|
+
const session = idCodec("server-session").decode(input.expected.session);
|
|
1921
|
+
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)
|
|
1922
|
+
return err("invalid-input", "Invalid state request metadata.");
|
|
1923
|
+
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 } });
|
|
1924
|
+
}
|
|
1925
|
+
function encoded(result, codec) {
|
|
1926
|
+
return result.ok ? { ok: true, value: codec.encode(result.value) } : { ok: false, error: { ...result.error } };
|
|
1927
|
+
}
|
|
1928
|
+
function failed(code, message) {
|
|
1929
|
+
return { ok: false, error: { code, message, retry: "never" } };
|
|
1930
|
+
}
|
|
1931
|
+
function createIdentityStateRpcBridge(options) {
|
|
1932
|
+
if (options.rpc.instanceId !== options.server.instanceId)
|
|
1933
|
+
return err("incompatible", "RPC and state server instance namespaces disagree.");
|
|
1934
|
+
let disposed = false;
|
|
1935
|
+
const resources = new Map;
|
|
1936
|
+
const controllers = new Set;
|
|
1937
|
+
const dispatch = async (action, input, context) => {
|
|
1938
|
+
if (disposed)
|
|
1939
|
+
return failed("disposed", "State RPC bridge is disposed.");
|
|
1940
|
+
const decoded = decodeRequest(input);
|
|
1941
|
+
if (!decoded.ok)
|
|
1942
|
+
return { ok: false, error: { ...decoded.error } };
|
|
1943
|
+
if (decoded.value.address.instanceId !== options.server.instanceId || decoded.value.address.pluginId !== options.pluginId)
|
|
1944
|
+
return failed("invalid-input", "State address belongs to another instance or plugin.");
|
|
1945
|
+
const handler = resources.get(decoded.value.address.collection);
|
|
1946
|
+
return handler ? handler(action, input, context) : failed("not-found", "State collection is not registered.");
|
|
1947
|
+
};
|
|
1948
|
+
const route = { input: inputSchema, output: outputSchema };
|
|
1949
|
+
const registered = options.rpc.register({
|
|
1950
|
+
[methods.load]: route,
|
|
1951
|
+
[methods.save]: route,
|
|
1952
|
+
[methods.reconcile]: route
|
|
1953
|
+
}, {
|
|
1954
|
+
[methods.load]: { origin: "interactive-user", handle: (input, context) => dispatch("load", input, context.request) },
|
|
1955
|
+
[methods.save]: { origin: "interactive-user", handle: (input, context) => dispatch("save", input, context.request) },
|
|
1956
|
+
[methods.reconcile]: { origin: "interactive-user", handle: (input, context) => dispatch("reconcile", input, context.request) }
|
|
1957
|
+
});
|
|
1958
|
+
if (!registered.ok)
|
|
1959
|
+
return registered;
|
|
1960
|
+
return ok({
|
|
1961
|
+
register({ resource, storage, policy, readPolicy }) {
|
|
1962
|
+
if (disposed)
|
|
1963
|
+
return err("disposed", "State RPC bridge is disposed.");
|
|
1964
|
+
const definition = { ...resource.definition, codec: { ...resource.definition.codec } };
|
|
1965
|
+
const capturedPolicy = { ...policy };
|
|
1966
|
+
const capturedReadPolicy = { ...readPolicy ?? policy };
|
|
1967
|
+
const collection = definition.collection;
|
|
1968
|
+
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")
|
|
1969
|
+
return err("invalid-input", "Invalid state resource registration.");
|
|
1970
|
+
if (resources.has(collection))
|
|
1971
|
+
return err("conflict", "State collection is already registered.");
|
|
1972
|
+
const controller = new AbortController;
|
|
1973
|
+
controllers.add(controller);
|
|
1974
|
+
const codecs = stateCodecs(definition.codec);
|
|
1975
|
+
const service = createStateService({
|
|
1976
|
+
instanceId: options.server.instanceId,
|
|
1977
|
+
pluginId: options.pluginId,
|
|
1978
|
+
definition,
|
|
1979
|
+
storage,
|
|
1980
|
+
commits: { boundary: "same-process-synchronous", validate: (target, scope) => disposed || controller.signal.aborted ? err("expired", "State resource registration expired.") : options.server.commits.validate(target, scope) },
|
|
1981
|
+
publish: (event) => {
|
|
1982
|
+
if (!disposed && !controller.signal.aborted)
|
|
1983
|
+
options.publish(event);
|
|
1984
|
+
}
|
|
1985
|
+
});
|
|
1986
|
+
const handler = async (action, input, context) => {
|
|
1987
|
+
if (disposed || controller.signal.aborted)
|
|
1988
|
+
return failed("disposed", "State resource registration expired.");
|
|
1989
|
+
const decoded = decodeRequest(input);
|
|
1990
|
+
if (!decoded.ok)
|
|
1991
|
+
return { ok: false, error: { ...decoded.error } };
|
|
1992
|
+
const captured = decoded.value;
|
|
1993
|
+
const parsedMutation = action === "save" ? codecs.mutation.decode(input) : null;
|
|
1994
|
+
const mutation = parsedMutation?.ok ? codecs.mutation.decode(structuredClone(codecs.mutation.encode(parsedMutation.value))) : parsedMutation;
|
|
1995
|
+
if (mutation && !mutation.ok)
|
|
1996
|
+
return { ok: false, error: { ...mutation.error } };
|
|
1997
|
+
const operation = action === "reconcile" ? idCodec("operation").decode(isRecord5(input) ? input.operationId : undefined) : null;
|
|
1998
|
+
if (operation && !operation.ok)
|
|
1999
|
+
return { ok: false, error: { ...operation.error } };
|
|
2000
|
+
const opened = await options.server.personRequest(context);
|
|
2001
|
+
if (!opened.ok)
|
|
2002
|
+
return { ok: false, error: { ...opened.error } };
|
|
2003
|
+
const person = opened.value;
|
|
2004
|
+
const signal = AbortSignal.any([person.signal, controller.signal]);
|
|
2005
|
+
const live = () => !disposed && !signal.aborted;
|
|
2006
|
+
try {
|
|
2007
|
+
if (!live())
|
|
2008
|
+
return failed("expired", "State request expired.");
|
|
2009
|
+
if (captured.expected.actor !== person.expected.actor || captured.expected.session !== person.expected.session)
|
|
2010
|
+
return failed("stale-context", "State request expected another actor or session.");
|
|
2011
|
+
const selection = captured.address.owner === person.actor.identity.key ? { kind: "self" } : { kind: "person", key: captured.address.owner };
|
|
2012
|
+
if (mutation?.ok) {
|
|
2013
|
+
const target2 = await person.target({ intent: "write", selection, policy: capturedPolicy, expected: captured.expected, expectedSubject: captured.address.owner });
|
|
2014
|
+
if (!target2.ok)
|
|
2015
|
+
return { ok: false, error: { ...target2.error } };
|
|
2016
|
+
if (!live())
|
|
2017
|
+
return failed("expired", "State request expired.");
|
|
2018
|
+
return encoded(await service.save(target2.value, mutation.value, { signal }), codecs.save);
|
|
2019
|
+
}
|
|
2020
|
+
const target = await person.target({ intent: "read", selection, policy: capturedReadPolicy });
|
|
2021
|
+
if (!target.ok)
|
|
2022
|
+
return { ok: false, error: { ...target.error } };
|
|
2023
|
+
if (!live())
|
|
2024
|
+
return failed("expired", "State request expired.");
|
|
2025
|
+
if (target.value.snapshot().subject.key !== captured.address.owner)
|
|
2026
|
+
return failed("incompatible", "Resolved state subject does not match the requested owner.");
|
|
2027
|
+
if (operation?.ok) {
|
|
2028
|
+
const result2 = await service.reconcile(target.value, captured.address.recordId, operation.value);
|
|
2029
|
+
return live() ? encoded(result2, codecs.lookup) : failed("expired", "State request expired.");
|
|
2030
|
+
}
|
|
2031
|
+
const result = await service.read(target.value, captured.address.recordId, { signal });
|
|
2032
|
+
return live() ? encoded(result, codecs.read) : failed("expired", "State request expired.");
|
|
2033
|
+
} catch {
|
|
2034
|
+
return failed("unavailable", "State resource operation failed.");
|
|
2035
|
+
} finally {
|
|
2036
|
+
person.dispose();
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
resources.set(collection, handler);
|
|
2040
|
+
return ok({ dispose() {
|
|
2041
|
+
if (controller.signal.aborted)
|
|
2042
|
+
return;
|
|
2043
|
+
controller.abort();
|
|
2044
|
+
controllers.delete(controller);
|
|
2045
|
+
if (resources.get(collection) === handler)
|
|
2046
|
+
resources.delete(collection);
|
|
2047
|
+
} });
|
|
2048
|
+
},
|
|
2049
|
+
dispose() {
|
|
2050
|
+
if (disposed)
|
|
2051
|
+
return;
|
|
2052
|
+
disposed = true;
|
|
2053
|
+
for (const controller of controllers)
|
|
2054
|
+
controller.abort();
|
|
2055
|
+
controllers.clear();
|
|
2056
|
+
resources.clear();
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// bb-binding-runtime.ts
|
|
2062
|
+
function isRecord6(value) {
|
|
2063
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2064
|
+
}
|
|
2065
|
+
function isJson3(value) {
|
|
2066
|
+
return value === null || typeof value === "boolean" || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every(isJson3) || isRecord6(value) && Object.values(value).every(isJson3);
|
|
2067
|
+
}
|
|
2068
|
+
function jsonResult(result, codec) {
|
|
2069
|
+
return result.ok ? { ok: true, value: codec ? codec.encode(result.value) : result.value } : { ok: false, error: { ...result.error } };
|
|
2070
|
+
}
|
|
2071
|
+
var jsonSchema = {
|
|
2072
|
+
"~standard": { version: 1, vendor: "bb-identity", validate: (value) => isJson3(value) ? { value } : { issues: [{ message: "Expected JSON." }] } }
|
|
2073
|
+
};
|
|
2074
|
+
var inputSchema2 = {
|
|
2075
|
+
"~standard": { version: 1, vendor: "bb-identity", validate: (value) => ({ value }) }
|
|
2076
|
+
};
|
|
2077
|
+
function invalid(message) {
|
|
2078
|
+
return { ok: false, error: { code: "invalid-input", message, retry: "never" } };
|
|
2079
|
+
}
|
|
2080
|
+
function decodeEmpty(value) {
|
|
2081
|
+
return isRecord6(value) && Object.keys(value).length === 0 ? ok(undefined) : err("invalid-input", "Endpoint requires an empty input object.");
|
|
2082
|
+
}
|
|
2083
|
+
function decodeDirectory(value) {
|
|
2084
|
+
if (!isRecord6(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)
|
|
2085
|
+
return err("invalid-input", "Invalid directory query.");
|
|
2086
|
+
const cursor = value.cursor === undefined ? undefined : idCodec("cursor").decode(value.cursor);
|
|
2087
|
+
const limit = value.limit;
|
|
2088
|
+
return cursor && !cursor.ok ? cursor : ok({ query: value.query, kinds: [...value.kinds], history: value.history, limit, ...cursor ? { cursor: cursor.value } : {} });
|
|
2089
|
+
}
|
|
2090
|
+
function decodeProfiles(value) {
|
|
2091
|
+
if (!isRecord6(value) || !Array.isArray(value.keys) || value.keys.length > 64)
|
|
2092
|
+
return err("invalid-input", "Invalid profile query.");
|
|
2093
|
+
const keys = value.keys.map((key) => identityKeyCodec.decode(key));
|
|
2094
|
+
return keys.every((key) => key.ok) ? ok({ keys: keys.map((key) => key.value) }) : err("invalid-input", "Invalid profile key.");
|
|
2095
|
+
}
|
|
2096
|
+
function decodeParticipants(value) {
|
|
2097
|
+
if (!isRecord6(value) || typeof value.limit !== "number" || !Number.isInteger(value.limit) || value.limit < 1 || value.limit > 100)
|
|
2098
|
+
return err("invalid-input", "Invalid participant query.");
|
|
2099
|
+
const thread = idCodec("thread").decode(value.threadId);
|
|
2100
|
+
const cursor = value.cursor === undefined ? undefined : idCodec("cursor").decode(value.cursor);
|
|
2101
|
+
const limit = value.limit;
|
|
2102
|
+
return !thread.ok || cursor && !cursor.ok ? err("invalid-input", "Invalid participant query identifiers.") : ok({ threadId: thread.value, limit, ...cursor ? { cursor: cursor.value } : {} });
|
|
2103
|
+
}
|
|
2104
|
+
function decodePreviews(value) {
|
|
2105
|
+
if (!isRecord6(value) || !Array.isArray(value.threadIds) || value.threadIds.length > 64 || typeof value.perThreadLimit !== "number" || !Number.isInteger(value.perThreadLimit) || value.perThreadLimit < 1 || value.perThreadLimit > 100)
|
|
2106
|
+
return err("invalid-input", "Invalid participant preview query.");
|
|
2107
|
+
const threads = value.threadIds.map((thread) => idCodec("thread").decode(thread));
|
|
2108
|
+
const perThreadLimit = value.perThreadLimit;
|
|
2109
|
+
return threads.every((thread) => thread.ok) ? ok({ threadIds: threads.map((thread) => thread.value), perThreadLimit }) : err("invalid-input", "Invalid participant preview thread ID.");
|
|
2110
|
+
}
|
|
2111
|
+
function createPortableFoundation(options) {
|
|
2112
|
+
if (options.extension.status === "malformed")
|
|
2113
|
+
return { ok: false, error: options.extension.error };
|
|
2114
|
+
if (options.extension.status === "unsupported-version")
|
|
2115
|
+
return err("incompatible", `Unsupported identity protocol version ${options.extension.version}.`);
|
|
2116
|
+
let disposed = false;
|
|
2117
|
+
const registrations = new Set;
|
|
2118
|
+
const releaseRegistrations = () => {
|
|
2119
|
+
for (const registration of [...registrations]) {
|
|
2120
|
+
registrations.delete(registration);
|
|
2121
|
+
try {
|
|
2122
|
+
registration.dispose();
|
|
2123
|
+
} catch {}
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
const dispose = () => {
|
|
2127
|
+
if (disposed)
|
|
2128
|
+
return;
|
|
2129
|
+
disposed = true;
|
|
2130
|
+
releaseRegistrations();
|
|
2131
|
+
};
|
|
2132
|
+
const invoke = async (descriptor, input, invocation) => {
|
|
2133
|
+
if (disposed)
|
|
2134
|
+
throw new Error("Identity binding is disposed.");
|
|
2135
|
+
return descriptor.handle(input, invocation);
|
|
2136
|
+
};
|
|
2137
|
+
const foundation = {
|
|
2138
|
+
instanceId: options.extension.status === "supported" ? options.extension.protocol.instanceId : options.upstream.driver.instanceId,
|
|
2139
|
+
register(contract, handlers) {
|
|
2140
|
+
if (disposed)
|
|
2141
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2142
|
+
const names = Object.keys(contract);
|
|
2143
|
+
if (!names.length || names.length !== Object.keys(handlers).length || names.some((name) => {
|
|
2144
|
+
if (!Object.hasOwn(handlers, name))
|
|
2145
|
+
return true;
|
|
2146
|
+
const descriptor = handlers[name];
|
|
2147
|
+
return !descriptor || typeof descriptor.handle !== "function" || !["interactive-user", "background", "external"].includes(descriptor.origin);
|
|
2148
|
+
}))
|
|
2149
|
+
return err("invalid-input", "RPC handlers must exactly match the contract and declare a valid origin.");
|
|
2150
|
+
if (options.extension.status === "absent") {
|
|
2151
|
+
let admitted2 = true;
|
|
2152
|
+
const wrapped2 = {};
|
|
2153
|
+
for (const name of names) {
|
|
2154
|
+
const method = name;
|
|
2155
|
+
const descriptor = handlers[method];
|
|
2156
|
+
wrapped2[method] = async (input) => {
|
|
2157
|
+
if (disposed || !admitted2)
|
|
2158
|
+
throw new Error("Identity RPC registration is retired.");
|
|
2159
|
+
const request = options.upstream.issue(descriptor.origin);
|
|
2160
|
+
try {
|
|
2161
|
+
return await invoke(descriptor, input, { request, signal: request.signal, origin: descriptor.origin });
|
|
2162
|
+
} finally {
|
|
2163
|
+
options.upstream.release(request);
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
try {
|
|
2168
|
+
options.bb.rpc.register(contract, wrapped2);
|
|
2169
|
+
if (disposed) {
|
|
2170
|
+
admitted2 = false;
|
|
2171
|
+
return err("disposed", "Identity binding disposed during RPC registration.");
|
|
2172
|
+
}
|
|
2173
|
+
return ok(undefined);
|
|
2174
|
+
} catch {
|
|
2175
|
+
admitted2 = false;
|
|
2176
|
+
return err("unavailable", "Upstream RPC registration failed.", "after-refresh");
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
if (options.extension.status !== "supported")
|
|
2180
|
+
return err("incompatible", "Identity protocol is unavailable.");
|
|
2181
|
+
const staged = [];
|
|
2182
|
+
const wrapped = {};
|
|
2183
|
+
let admitted = true;
|
|
2184
|
+
try {
|
|
2185
|
+
for (const name of names) {
|
|
2186
|
+
const method = name;
|
|
2187
|
+
const descriptor = handlers[method];
|
|
2188
|
+
const bound = options.extension.protocol.bindInvocation({
|
|
2189
|
+
routeClass: descriptor.origin === "interactive-user" ? "interactive-session" : descriptor.origin === "external" ? "external-credential" : "plugin-background",
|
|
2190
|
+
handler: async (raw, input) => {
|
|
2191
|
+
if (disposed || !admitted)
|
|
2192
|
+
throw new Error("Identity RPC registration is retired.");
|
|
2193
|
+
try {
|
|
2194
|
+
return await invoke(descriptor, input, { request: raw.request, signal: raw.scope.signal, origin: descriptor.origin });
|
|
2195
|
+
} finally {
|
|
2196
|
+
raw.scope.release();
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
staged.push(bound.registration);
|
|
2201
|
+
wrapped[method] = bound.handler;
|
|
2202
|
+
}
|
|
2203
|
+
options.bb.rpc.register(contract, wrapped);
|
|
2204
|
+
if (disposed) {
|
|
2205
|
+
for (const registration of staged)
|
|
2206
|
+
registration.dispose();
|
|
2207
|
+
return err("disposed", "Identity binding disposed during registration.");
|
|
2208
|
+
}
|
|
2209
|
+
for (const registration of staged)
|
|
2210
|
+
registrations.add(registration);
|
|
2211
|
+
return ok(undefined);
|
|
2212
|
+
} catch {
|
|
2213
|
+
admitted = false;
|
|
2214
|
+
for (const registration of staged) {
|
|
2215
|
+
try {
|
|
2216
|
+
registration.dispose();
|
|
2217
|
+
} catch {}
|
|
2218
|
+
}
|
|
2219
|
+
return err("unavailable", "Enhanced RPC registration failed.", "after-refresh");
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2222
|
+
dispose
|
|
2223
|
+
};
|
|
2224
|
+
try {
|
|
2225
|
+
options.bb.onDispose(dispose);
|
|
2226
|
+
} catch {
|
|
2227
|
+
dispose();
|
|
2228
|
+
return err("unavailable", "Could not register RPC cleanup.", "after-refresh");
|
|
2229
|
+
}
|
|
2230
|
+
return ok(foundation);
|
|
2231
|
+
}
|
|
2232
|
+
function createBbIdentityServerBinding(options) {
|
|
2233
|
+
const upstream = createBbUpstreamDriver({ bb: options.bb, ...options.stateNamespace !== undefined ? { stateNamespace: options.stateNamespace } : {}, ...options.scheduler !== undefined ? { scheduler: options.scheduler } : {}, ...options.externalMessageRendering !== undefined ? { externalMessageRendering: options.externalMessageRendering } : {} });
|
|
2234
|
+
if (!upstream.ok)
|
|
2235
|
+
return upstream;
|
|
2236
|
+
const extension = inspectForkExtension(options.bb.experimental_p6rIdentity);
|
|
2237
|
+
if (extension.status !== "absent" && extension.status !== "supported") {
|
|
2238
|
+
upstream.value.dispose();
|
|
2239
|
+
return extension.status === "malformed" ? { ok: false, error: extension.error } : err("incompatible", `Unsupported identity protocol version ${extension.version}.`);
|
|
2240
|
+
}
|
|
2241
|
+
if (upstream.value.externalMessageRendering === "producer" && extension.status === "supported") {
|
|
2242
|
+
const enableProducerRendering = extension.protocol.experimental_useProducerMessageRendering;
|
|
2243
|
+
if (typeof enableProducerRendering !== "function") {
|
|
2244
|
+
upstream.value.dispose();
|
|
2245
|
+
return err("incompatible", "Enhanced identity host does not support producer-owned external message rendering.");
|
|
2246
|
+
}
|
|
2247
|
+
try {
|
|
2248
|
+
const hookResult = enableProducerRendering.call(extension.protocol);
|
|
2249
|
+
if (hookResult !== undefined)
|
|
2250
|
+
throw Error("Producer rendering hook must be synchronous.");
|
|
2251
|
+
} catch {
|
|
2252
|
+
upstream.value.dispose();
|
|
2253
|
+
return err("incompatible", "Enhanced identity host could not enable producer-owned external message rendering.");
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const host = createHostAdapter({ upstream: upstream.value.driver, extension });
|
|
2257
|
+
const server = createIdentityServer({ host });
|
|
2258
|
+
let disposed = false;
|
|
2259
|
+
const publish = (event) => {
|
|
2260
|
+
if (!disposed)
|
|
2261
|
+
try {
|
|
2262
|
+
options.bb.realtime.publish("bb-identity/v1", event);
|
|
2263
|
+
} catch {}
|
|
2264
|
+
};
|
|
2265
|
+
const endpoint = createIdentityEndpoint({ server, publish });
|
|
2266
|
+
const foundation = createPortableFoundation({ bb: options.bb, extension, upstream: upstream.value });
|
|
2267
|
+
if (!foundation.ok) {
|
|
2268
|
+
endpoint.dispose();
|
|
2269
|
+
server.dispose();
|
|
2270
|
+
host.dispose();
|
|
2271
|
+
upstream.value.dispose();
|
|
2272
|
+
return foundation;
|
|
2273
|
+
}
|
|
2274
|
+
const state = createIdentityStateRpcBridge({ rpc: foundation.value, server, pluginId: host.pluginId, publish: (event) => publish({ kind: "state", event }) });
|
|
2275
|
+
if (!state.ok) {
|
|
2276
|
+
foundation.value.dispose();
|
|
2277
|
+
endpoint.dispose();
|
|
2278
|
+
server.dispose();
|
|
2279
|
+
host.dispose();
|
|
2280
|
+
upstream.value.dispose();
|
|
2281
|
+
return state;
|
|
2282
|
+
}
|
|
2283
|
+
const invocations = new WeakMap;
|
|
2284
|
+
const expired = () => err("expired", "Invocation has already settled.");
|
|
2285
|
+
const recordFor = (invocation) => {
|
|
2286
|
+
const record = invocations.get(invocation);
|
|
2287
|
+
return disposed || !record || record.settled || record.signal.aborted ? expired() : ok(record);
|
|
2288
|
+
};
|
|
2289
|
+
const unavailableSession2 = () => ({
|
|
2290
|
+
status: "unavailable",
|
|
2291
|
+
instanceId: server.instanceId,
|
|
2292
|
+
error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
|
|
2293
|
+
});
|
|
2294
|
+
const openPerson = async (invocation) => {
|
|
2295
|
+
const record = recordFor(invocation);
|
|
2296
|
+
if (!record.ok)
|
|
2297
|
+
return record;
|
|
2298
|
+
const opened = await server.personRequest(record.value.request);
|
|
2299
|
+
if (record.value.settled || record.value.signal.aborted) {
|
|
2300
|
+
if (opened.ok)
|
|
2301
|
+
opened.value.dispose();
|
|
2302
|
+
return expired();
|
|
2303
|
+
}
|
|
2304
|
+
if (opened.ok)
|
|
2305
|
+
record.value.people.add(opened.value);
|
|
2306
|
+
return opened;
|
|
2307
|
+
};
|
|
2308
|
+
const guardEndpoint = async (invocation, operation) => {
|
|
2309
|
+
const record = recordFor(invocation);
|
|
2310
|
+
if (!record.ok)
|
|
2311
|
+
return record;
|
|
2312
|
+
const result = await operation(record.value.request);
|
|
2313
|
+
return record.value.settled || record.value.signal.aborted ? expired() : result;
|
|
2314
|
+
};
|
|
2315
|
+
let disposeBinding = () => {};
|
|
2316
|
+
const guardedHistory = {
|
|
2317
|
+
async contributions(query, readOptions) {
|
|
2318
|
+
if (disposed)
|
|
2319
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2320
|
+
const result = await server.history.contributions(query, readOptions);
|
|
2321
|
+
return disposed ? err("disposed", "Identity binding is disposed.") : result;
|
|
2322
|
+
},
|
|
2323
|
+
async attempts(query, readOptions) {
|
|
2324
|
+
if (disposed)
|
|
2325
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2326
|
+
const result = await server.history.attempts(query, readOptions);
|
|
2327
|
+
return disposed ? err("disposed", "Identity binding is disposed.") : result;
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
const publicServer = {
|
|
2331
|
+
instanceId: server.instanceId,
|
|
2332
|
+
commits: server.commits,
|
|
2333
|
+
async session(invocation) {
|
|
2334
|
+
const record = recordFor(invocation);
|
|
2335
|
+
if (!record.ok)
|
|
2336
|
+
return unavailableSession2();
|
|
2337
|
+
const session = await server.session(record.value.request);
|
|
2338
|
+
return record.value.settled || record.value.signal.aborted ? unavailableSession2() : session;
|
|
2339
|
+
},
|
|
2340
|
+
selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
|
|
2341
|
+
personRequest: openPerson,
|
|
2342
|
+
async sendExternal(author, input, readOptions) {
|
|
2343
|
+
if (disposed)
|
|
2344
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
|
|
2345
|
+
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
|
|
2346
|
+
return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
|
|
2347
|
+
},
|
|
2348
|
+
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
|
|
2349
|
+
history: guardedHistory
|
|
2350
|
+
};
|
|
2351
|
+
const publicEndpoint = {
|
|
2352
|
+
bootstrap: (invocation) => guardEndpoint(invocation, (request) => endpoint.bootstrap(request)),
|
|
2353
|
+
selfProfile: (invocation) => guardEndpoint(invocation, (request) => endpoint.selfProfile(request)),
|
|
2354
|
+
search: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.search(request, input)),
|
|
2355
|
+
profiles: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.profiles(request, input)),
|
|
2356
|
+
participants: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.participants(request, input)),
|
|
2357
|
+
participantPreviews: (invocation, input) => guardEndpoint(invocation, (request) => endpoint.participantPreviews(request, input)),
|
|
2358
|
+
dispose: () => disposeBinding()
|
|
2359
|
+
};
|
|
2360
|
+
const endpointContract = {
|
|
2361
|
+
[identityRpcMethods[identityRoutes.bootstrap]]: { input: inputSchema2, output: jsonSchema },
|
|
2362
|
+
[identityRpcMethods[identityRoutes.selfProfile]]: { input: inputSchema2, output: jsonSchema },
|
|
2363
|
+
[identityRpcMethods[identityRoutes.search]]: { input: inputSchema2, output: jsonSchema },
|
|
2364
|
+
[identityRpcMethods[identityRoutes.profiles]]: { input: inputSchema2, output: jsonSchema },
|
|
2365
|
+
[identityRpcMethods[identityRoutes.participants]]: { input: inputSchema2, output: jsonSchema },
|
|
2366
|
+
[identityRpcMethods[identityRoutes.participantPreviews]]: { input: inputSchema2, output: jsonSchema }
|
|
2367
|
+
};
|
|
2368
|
+
const endpointRegistered = foundation.value.register(endpointContract, {
|
|
2369
|
+
[identityRpcMethods[identityRoutes.bootstrap]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2370
|
+
const decoded = decodeEmpty(input);
|
|
2371
|
+
return decoded.ok ? jsonResult(await endpoint.bootstrap(context.request)) : invalid(decoded.error.message);
|
|
2372
|
+
} },
|
|
2373
|
+
[identityRpcMethods[identityRoutes.selfProfile]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2374
|
+
const decoded = decodeEmpty(input);
|
|
2375
|
+
return decoded.ok ? jsonResult(await endpoint.selfProfile(context.request)) : invalid(decoded.error.message);
|
|
2376
|
+
} },
|
|
2377
|
+
[identityRpcMethods[identityRoutes.search]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2378
|
+
const decoded = decodeDirectory(input);
|
|
2379
|
+
return decoded.ok ? jsonResult(await endpoint.search(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2380
|
+
} },
|
|
2381
|
+
[identityRpcMethods[identityRoutes.profiles]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2382
|
+
const decoded = decodeProfiles(input);
|
|
2383
|
+
return decoded.ok ? jsonResult(await endpoint.profiles(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2384
|
+
} },
|
|
2385
|
+
[identityRpcMethods[identityRoutes.participants]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2386
|
+
const decoded = decodeParticipants(input);
|
|
2387
|
+
return decoded.ok ? jsonResult(await endpoint.participants(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2388
|
+
} },
|
|
2389
|
+
[identityRpcMethods[identityRoutes.participantPreviews]]: { origin: "interactive-user", handle: async (input, context) => {
|
|
2390
|
+
const decoded = decodePreviews(input);
|
|
2391
|
+
return decoded.ok ? jsonResult(await endpoint.participantPreviews(context.request, decoded.value)) : invalid(decoded.error.message);
|
|
2392
|
+
} }
|
|
2393
|
+
});
|
|
2394
|
+
if (!endpointRegistered.ok) {
|
|
2395
|
+
state.value.dispose();
|
|
2396
|
+
foundation.value.dispose();
|
|
2397
|
+
endpoint.dispose();
|
|
2398
|
+
server.dispose();
|
|
2399
|
+
host.dispose();
|
|
2400
|
+
upstream.value.dispose();
|
|
2401
|
+
return endpointRegistered;
|
|
2402
|
+
}
|
|
2403
|
+
const createInvocation = (context) => {
|
|
2404
|
+
const record = { request: context.request, signal: context.signal, people: new Set, settled: false };
|
|
2405
|
+
const invocation = {
|
|
2406
|
+
origin: context.origin,
|
|
2407
|
+
signal: context.signal,
|
|
2408
|
+
person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
|
|
2409
|
+
};
|
|
2410
|
+
invocations.set(invocation, record);
|
|
2411
|
+
const finish = () => {
|
|
2412
|
+
record.settled = true;
|
|
2413
|
+
for (const request of record.people)
|
|
2414
|
+
request.dispose();
|
|
2415
|
+
record.people.clear();
|
|
2416
|
+
};
|
|
2417
|
+
return { invocation, finish };
|
|
2418
|
+
};
|
|
2419
|
+
const httpRegistrations = new Set;
|
|
2420
|
+
const httpReleases = new Set;
|
|
2421
|
+
const binding = {
|
|
2422
|
+
server: publicServer,
|
|
2423
|
+
endpoint: publicEndpoint,
|
|
2424
|
+
rpc: { register(contract, handlers) {
|
|
2425
|
+
const names = Object.keys(contract);
|
|
2426
|
+
if (!names.length || names.length !== Object.keys(handlers).length || names.some((name) => {
|
|
2427
|
+
if (!Object.hasOwn(handlers, name))
|
|
2428
|
+
return true;
|
|
2429
|
+
const descriptor = handlers[name];
|
|
2430
|
+
return !descriptor || typeof descriptor.handle !== "function" || !["interactive-user", "background", "external"].includes(descriptor.origin);
|
|
2431
|
+
}))
|
|
2432
|
+
return err("invalid-input", "RPC handlers must exactly match the contract and declare a valid origin.");
|
|
2433
|
+
const normalized = {};
|
|
2434
|
+
for (const name of names) {
|
|
2435
|
+
const descriptor = handlers[name];
|
|
2436
|
+
if (!descriptor)
|
|
2437
|
+
return err("invalid-input", "RPC handlers must exactly match the contract.");
|
|
2438
|
+
normalized[name] = { origin: descriptor.origin, handle: async (input, context) => {
|
|
2439
|
+
const issued = createInvocation(context);
|
|
2440
|
+
try {
|
|
2441
|
+
return await descriptor.handle(input, issued.invocation);
|
|
2442
|
+
} finally {
|
|
2443
|
+
issued.finish();
|
|
2444
|
+
}
|
|
2445
|
+
} };
|
|
2446
|
+
}
|
|
2447
|
+
return foundation.value.register(contract, normalized);
|
|
2448
|
+
} },
|
|
2449
|
+
http: { route(method, path, descriptor, routeOptions) {
|
|
2450
|
+
if (disposed)
|
|
2451
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2452
|
+
if (!options.bb.http)
|
|
2453
|
+
return err("unsupported", "This SDK has no HTTP registration.");
|
|
2454
|
+
if (!descriptor || typeof descriptor.handle !== "function" || !["interactive-user", "external"].includes(descriptor.origin) || descriptor.origin === "interactive-user" && (routeOptions?.auth ?? "local") !== "local") {
|
|
2455
|
+
return err("invalid-input", "Interactive HTTP identity requires local authentication.");
|
|
2456
|
+
}
|
|
2457
|
+
const run = async (context, raw, release, coreOwnsBody = false) => {
|
|
2458
|
+
if (disposed) {
|
|
2459
|
+
release();
|
|
2460
|
+
throw new Error("Identity HTTP registration is retired.");
|
|
2461
|
+
}
|
|
2462
|
+
const issued = createInvocation(raw);
|
|
2463
|
+
let finished = false;
|
|
2464
|
+
const finish = () => {
|
|
2465
|
+
if (finished)
|
|
2466
|
+
return;
|
|
2467
|
+
finished = true;
|
|
2468
|
+
context.req.raw.signal.removeEventListener("abort", finish);
|
|
2469
|
+
raw.signal.removeEventListener("abort", finish);
|
|
2470
|
+
httpReleases.delete(finish);
|
|
2471
|
+
issued.finish();
|
|
2472
|
+
release();
|
|
2473
|
+
};
|
|
2474
|
+
httpReleases.add(finish);
|
|
2475
|
+
context.req.raw.signal.addEventListener("abort", finish, { once: true });
|
|
2476
|
+
raw.signal.addEventListener("abort", finish, { once: true });
|
|
2477
|
+
if (context.req.raw.signal.aborted) {
|
|
2478
|
+
finish();
|
|
2479
|
+
throw new Error("HTTP request is aborted.");
|
|
2480
|
+
}
|
|
2481
|
+
try {
|
|
2482
|
+
const response = await descriptor.handle(context, issued.invocation);
|
|
2483
|
+
if (coreOwnsBody)
|
|
2484
|
+
return response;
|
|
2485
|
+
return retainIdentityHttpResponse(response, raw.signal, context.req.raw.signal, finish);
|
|
2486
|
+
} catch (cause) {
|
|
2487
|
+
finish();
|
|
2488
|
+
throw cause;
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
let registration;
|
|
2492
|
+
try {
|
|
2493
|
+
if (extension.status === "supported") {
|
|
2494
|
+
const bound = extension.protocol.bindInvocation({
|
|
2495
|
+
routeClass: descriptor.origin === "interactive-user" ? "interactive-session" : "external-credential",
|
|
2496
|
+
handler: (raw, context) => run(context, { request: raw.request, signal: raw.scope.signal, origin: descriptor.origin }, () => raw.scope.release(), true)
|
|
2497
|
+
});
|
|
2498
|
+
registration = bound.registration;
|
|
2499
|
+
options.bb.http.route(method, path, bound.handler, routeOptions);
|
|
2500
|
+
if (disposed)
|
|
2501
|
+
registration.dispose();
|
|
2502
|
+
else
|
|
2503
|
+
httpRegistrations.add(registration);
|
|
2504
|
+
} else {
|
|
2505
|
+
options.bb.http.route(method, path, (context) => {
|
|
2506
|
+
const request = upstream.value.issue(descriptor.origin);
|
|
2507
|
+
return run(context, { request, signal: request.signal, origin: descriptor.origin }, () => upstream.value.release(request));
|
|
2508
|
+
}, routeOptions);
|
|
2509
|
+
}
|
|
2510
|
+
return disposed ? err("disposed", "Identity binding is disposed.") : ok(undefined);
|
|
2511
|
+
} catch {
|
|
2512
|
+
registration?.dispose();
|
|
2513
|
+
return err("unavailable", "HTTP registration failed.");
|
|
2514
|
+
}
|
|
2515
|
+
} },
|
|
2516
|
+
state: { register: (options2) => disposed ? err("disposed", "Identity binding is disposed.") : state.value.register(options2) },
|
|
2517
|
+
async registerProvider(provider) {
|
|
2518
|
+
if (disposed)
|
|
2519
|
+
return err("disposed", "Identity binding is disposed.");
|
|
2520
|
+
const registration = await host.registerProvider(provider);
|
|
2521
|
+
return disposed && registration.ok ? (registration.value.dispose(), err("disposed", "Identity binding is disposed.")) : registration;
|
|
2522
|
+
},
|
|
2523
|
+
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.toolProvenance(context),
|
|
2524
|
+
async background(run) {
|
|
2525
|
+
if (disposed)
|
|
2526
|
+
throw new Error("Identity binding is disposed.");
|
|
2527
|
+
const request = upstream.value.issue("background");
|
|
2528
|
+
const issued = createInvocation({ request, origin: "background", signal: request.signal });
|
|
2529
|
+
try {
|
|
2530
|
+
return await run(issued.invocation);
|
|
2531
|
+
} finally {
|
|
2532
|
+
issued.finish();
|
|
2533
|
+
upstream.value.release(request);
|
|
2534
|
+
}
|
|
2535
|
+
},
|
|
2536
|
+
dispose() {
|
|
2537
|
+
if (disposed)
|
|
2538
|
+
return;
|
|
2539
|
+
disposed = true;
|
|
2540
|
+
for (const release of [...httpReleases])
|
|
2541
|
+
release();
|
|
2542
|
+
for (const registration of httpRegistrations)
|
|
2543
|
+
registration.dispose();
|
|
2544
|
+
httpRegistrations.clear();
|
|
2545
|
+
state.value.dispose();
|
|
2546
|
+
foundation.value.dispose();
|
|
2547
|
+
endpoint.dispose();
|
|
2548
|
+
server.dispose();
|
|
2549
|
+
host.dispose();
|
|
2550
|
+
upstream.value.dispose();
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
disposeBinding = () => binding.dispose();
|
|
2554
|
+
try {
|
|
2555
|
+
options.bb.onDispose(() => binding.dispose());
|
|
2556
|
+
} catch {
|
|
2557
|
+
binding.dispose();
|
|
2558
|
+
return err("unavailable", "Could not register binding cleanup.", "after-refresh");
|
|
2559
|
+
}
|
|
2560
|
+
return ok(binding);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// bb-entry-runtime.ts
|
|
2564
|
+
function bindBbIdentity(bb, options) {
|
|
2565
|
+
return createBbIdentityServerBinding({
|
|
2566
|
+
bb,
|
|
2567
|
+
...options?.stateNamespace === undefined ? {} : { stateNamespace: options.stateNamespace },
|
|
2568
|
+
...options?.scheduler === undefined ? {} : { scheduler: options.scheduler },
|
|
2569
|
+
...options?.externalMessageRendering === undefined ? {} : { externalMessageRendering: options.externalMessageRendering }
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
export {
|
|
2573
|
+
bindBbIdentity
|
|
2574
|
+
};
|