@phosphorco/bb-cross-references 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bb.js ADDED
@@ -0,0 +1,665 @@
1
+ // src/canonical.ts
2
+ import { sha256 } from "@noble/hashes/sha256";
3
+ import { bytesToHex } from "@noble/hashes/utils";
4
+ var PROTOCOL_VERSION = 1;
5
+ var MAX_RESOURCE_KEYS = 32;
6
+ var MAX_KEY_VALUE_BYTES = 512;
7
+ var MAX_KEY_VALUE_MATERIAL_BYTES = 8192;
8
+ var MAX_CANONICAL_IDENTITY_BYTES = 16 * 1024;
9
+ var MAX_PRESENTATION_BYTES = 4 * 1024;
10
+ var MAX_PRESENTATION_LABEL_BYTES = 256;
11
+ var MAX_PRESENTATION_DETAIL_BYTES = 1024;
12
+ var MAX_PRESENTATION_URL_BYTES = 2048;
13
+ var MAX_TARGETS = 256;
14
+ var MAX_PROJECTION_PAYLOAD_BYTES = 256 * 1024;
15
+ var MAX_MUTATION_ID_BYTES = 128;
16
+ var MACHINE_MONITOR_ROUTE = "/plugins/machine-monitor/machine-monitor";
17
+ var NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
18
+ var ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
19
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
20
+ var CONTROL_PATTERN = /\p{Cc}/u;
21
+ var encoder = new TextEncoder;
22
+ var SENSITIVE_URL_PARAMETER_NAMES = new Set([
23
+ "access_token",
24
+ "api_key",
25
+ "apikey",
26
+ "authorization",
27
+ "code",
28
+ "cookie",
29
+ "id_token",
30
+ "password",
31
+ "passwd",
32
+ "refresh_token",
33
+ "secret",
34
+ "session",
35
+ "sessionid",
36
+ "sid",
37
+ "sig",
38
+ "signature",
39
+ "token",
40
+ "x-amz-credential",
41
+ "x-amz-security-token",
42
+ "x-amz-signature",
43
+ "x-goog-credential",
44
+ "x-goog-signature",
45
+ "x-ms-signature"
46
+ ]);
47
+
48
+ class CrossReferenceValidationError extends Error {
49
+ constructor(message) {
50
+ super(message);
51
+ this.name = "CrossReferenceValidationError";
52
+ }
53
+ }
54
+ function utf8ByteLength(value) {
55
+ return encoder.encode(value).byteLength;
56
+ }
57
+ function fail(message) {
58
+ throw new CrossReferenceValidationError(message);
59
+ }
60
+ function isPlainObject(value) {
61
+ if (typeof value !== "object" || value === null || Array.isArray(value))
62
+ return false;
63
+ const prototype = Object.getPrototypeOf(value);
64
+ return prototype === Object.prototype || prototype === null;
65
+ }
66
+ function assertPlainObject(value, label) {
67
+ if (!isPlainObject(value))
68
+ fail(`${label} must be a plain object.`);
69
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string")) {
70
+ fail(`${label} must not contain symbol keys.`);
71
+ }
72
+ }
73
+ function assertExactObjectKeys(value, allowed, label) {
74
+ const allowedSet = new Set(allowed);
75
+ for (const key of Object.keys(value)) {
76
+ if (!allowedSet.has(key))
77
+ fail(`${label} contains unknown field ${key}.`);
78
+ }
79
+ }
80
+ function assertWellFormedUnicode(value, label) {
81
+ for (let index = 0;index < value.length; index += 1) {
82
+ const codeUnit = value.charCodeAt(index);
83
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
84
+ const next = value.charCodeAt(index + 1);
85
+ if (next < 56320 || next > 57343)
86
+ fail(`${label} contains malformed Unicode.`);
87
+ index += 1;
88
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
89
+ fail(`${label} contains malformed Unicode.`);
90
+ }
91
+ }
92
+ }
93
+ function assertSafeText(value, label, maxBytes, options = {}) {
94
+ assertWellFormedUnicode(value, label);
95
+ if (CONTROL_PATTERN.test(value))
96
+ fail(`${label} contains a Unicode control character.`);
97
+ if (options.nonblank === true && value.trim().length === 0)
98
+ fail(`${label} must not be blank.`);
99
+ if (utf8ByteLength(value) > maxBytes)
100
+ fail(`${label} exceeds its ${maxBytes}-byte limit.`);
101
+ return value;
102
+ }
103
+ function assertName(value, label) {
104
+ if (typeof value !== "string" || !NAME_PATTERN.test(value)) {
105
+ fail(`${label} must match ^[a-z][a-z0-9._-]{0,63}$.`);
106
+ }
107
+ return value;
108
+ }
109
+ function validateBbId(value, label) {
110
+ if (typeof value !== "string" || !ID_PATTERN.test(value)) {
111
+ fail(`${label} must match ^[A-Za-z0-9_-]{1,128}$.`);
112
+ }
113
+ return value;
114
+ }
115
+ function validateBbIdentity(provider, keys) {
116
+ if (provider !== "bb")
117
+ return;
118
+ const names = Object.keys(keys);
119
+ if (names.length === 1 && names[0] === "project") {
120
+ validateBbId(keys.project, "projectId");
121
+ return;
122
+ }
123
+ if (names.length === 2 && names[0] === "project" && names[1] === "thread") {
124
+ validateBbId(keys.project, "projectId");
125
+ validateBbId(keys.thread, "threadId");
126
+ return;
127
+ }
128
+ if (names.length === 2 && names[0] === "page" && names[1] === "plugin" && keys.page === "machine-monitor" && keys.plugin === "machine-monitor") {
129
+ return;
130
+ }
131
+ fail("provider bb must use a v1 project, thread, or Machine Monitor identity.");
132
+ }
133
+ function hasSensitiveParameterName(params) {
134
+ for (const name of params.keys()) {
135
+ if (SENSITIVE_URL_PARAMETER_NAMES.has(name.toLowerCase()))
136
+ return true;
137
+ }
138
+ return false;
139
+ }
140
+ function validateUrlIdentity(provider, keys) {
141
+ if (provider !== "url")
142
+ return;
143
+ const names = Object.keys(keys);
144
+ if (names.length !== 1 || names[0] !== "href")
145
+ fail("provider url must use exactly the href key.");
146
+ const href = keys.href;
147
+ let parsed;
148
+ try {
149
+ parsed = new URL(href);
150
+ } catch {
151
+ fail("url href must be a valid HTTP(S) URL.");
152
+ }
153
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
154
+ fail("url href must use HTTP(S).");
155
+ if (parsed.username !== "" || parsed.password !== "")
156
+ fail("url href must not contain credentials.");
157
+ if (hasSensitiveParameterName(parsed.searchParams) || parsed.hash.startsWith("#") && hasSensitiveParameterName(new URLSearchParams(parsed.hash.slice(1)))) {
158
+ fail("url href must not contain credential-shaped query or fragment parameters.");
159
+ }
160
+ if (parsed.href !== href)
161
+ fail("url href must use canonical URL serialization.");
162
+ }
163
+ function validateProducerPluginId(value) {
164
+ return assertName(value, "producerPluginId");
165
+ }
166
+ function validateMutationId(value) {
167
+ if (typeof value !== "string" || utf8ByteLength(value) > MAX_MUTATION_ID_BYTES || !UUID_PATTERN.test(value)) {
168
+ fail("mutationId must be a lower-case UUID string.");
169
+ }
170
+ return value;
171
+ }
172
+ function validateRevision(value, label, minimum) {
173
+ if (!Number.isSafeInteger(value) || value < minimum) {
174
+ fail(`${label} must be a safe integer >= ${minimum}.`);
175
+ }
176
+ return value;
177
+ }
178
+ function validateDigest(value, label = "payloadDigest") {
179
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/.test(value)) {
180
+ fail(`${label} must be lower-case SHA-256 hex.`);
181
+ }
182
+ return value;
183
+ }
184
+ function canonicalizeKeyMap(value) {
185
+ assertPlainObject(value, "keys");
186
+ const keyNames = Object.keys(value);
187
+ if (keyNames.length < 1 || keyNames.length > MAX_RESOURCE_KEYS) {
188
+ fail(`keys must contain between 1 and ${MAX_RESOURCE_KEYS} entries.`);
189
+ }
190
+ let materialBytes = 0;
191
+ const entries = keyNames.map((key) => {
192
+ assertName(key, "key name");
193
+ const rawValue = value[key];
194
+ if (typeof rawValue !== "string")
195
+ fail(`key ${key} value must be a string.`);
196
+ const normalizedValue = rawValue.normalize("NFC");
197
+ const safeValue = assertSafeText(normalizedValue, `key ${key} value`, MAX_KEY_VALUE_BYTES, { nonblank: true });
198
+ materialBytes += utf8ByteLength(key) + utf8ByteLength(safeValue);
199
+ return [key, safeValue];
200
+ });
201
+ if (materialBytes > MAX_KEY_VALUE_MATERIAL_BYTES) {
202
+ fail(`combined key/value material exceeds its ${MAX_KEY_VALUE_MATERIAL_BYTES}-byte limit.`);
203
+ }
204
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
205
+ const keys = Object.fromEntries(entries);
206
+ return { keys, canonicalKeysJson: JSON.stringify(keys) };
207
+ }
208
+ function canonicalizeIdentity(value) {
209
+ assertPlainObject(value, "resource identity");
210
+ assertExactObjectKeys(value, ["provider", "keys"], "resource identity");
211
+ const provider = assertName(value.provider, "provider");
212
+ const { keys, canonicalKeysJson } = canonicalizeKeyMap(value.keys);
213
+ validateBbIdentity(provider, keys);
214
+ validateUrlIdentity(provider, keys);
215
+ const identityObject = { provider, keys };
216
+ const canonicalIdentityJson = JSON.stringify(identityObject);
217
+ if (utf8ByteLength(canonicalIdentityJson) > MAX_CANONICAL_IDENTITY_BYTES) {
218
+ fail(`canonical identity exceeds its ${MAX_CANONICAL_IDENTITY_BYTES}-byte limit.`);
219
+ }
220
+ return {
221
+ provider,
222
+ keys,
223
+ canonicalKeysJson,
224
+ canonicalIdentityJson,
225
+ identityDigest: sha256Hex(canonicalIdentityJson)
226
+ };
227
+ }
228
+ function canonicalizePresentation(value) {
229
+ assertPlainObject(value, "presentation");
230
+ assertExactObjectKeys(value, ["label", "detail", "url"], "presentation");
231
+ if (typeof value.label !== "string")
232
+ fail("presentation.label must be a string.");
233
+ const label = assertSafeText(value.label, "presentation.label", MAX_PRESENTATION_LABEL_BYTES, { nonblank: true });
234
+ const presentation = { label };
235
+ if (value.detail !== undefined) {
236
+ if (typeof value.detail !== "string")
237
+ fail("presentation.detail must be a string when supplied.");
238
+ presentation.detail = assertSafeText(value.detail, "presentation.detail", MAX_PRESENTATION_DETAIL_BYTES, { nonblank: true });
239
+ }
240
+ if (value.url !== undefined) {
241
+ if (typeof value.url !== "string")
242
+ fail("presentation.url must be a string when supplied.");
243
+ const url = assertSafeText(value.url, "presentation.url", MAX_PRESENTATION_URL_BYTES, { nonblank: true });
244
+ if (url.startsWith("/") && !url.startsWith("//") && !url.includes("\\")) {
245
+ presentation.url = url;
246
+ } else if (url.startsWith("http://") || url.startsWith("https://")) {
247
+ try {
248
+ const parsed = new URL(url);
249
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
250
+ fail("presentation.url must use http or https.");
251
+ presentation.url = url;
252
+ } catch {
253
+ fail("presentation.url must be a valid http or https URL.");
254
+ }
255
+ } else {
256
+ fail("presentation.url must be a same-origin route or an http(s) URL.");
257
+ }
258
+ }
259
+ const presentationJson = JSON.stringify(presentation);
260
+ if (utf8ByteLength(presentationJson) > MAX_PRESENTATION_BYTES) {
261
+ fail(`presentation exceeds its ${MAX_PRESENTATION_BYTES}-byte limit.`);
262
+ }
263
+ return { presentation, presentationJson };
264
+ }
265
+ function canonicalizeResource(value) {
266
+ assertPlainObject(value, "resource");
267
+ assertExactObjectKeys(value, ["provider", "keys", "presentation"], "resource");
268
+ const identity = canonicalizeIdentity({ provider: value.provider, keys: value.keys });
269
+ const presentation = canonicalizePresentation(value.presentation);
270
+ if (identity.provider === "url" && presentation.presentation.url !== identity.keys.href) {
271
+ fail("provider url presentation.url must equal its canonical href identity.");
272
+ }
273
+ return { ...identity, ...presentation };
274
+ }
275
+ function resourceIdentityOf(resource) {
276
+ return { provider: resource.provider, keys: resource.keys };
277
+ }
278
+ function projectIdentity(projectId) {
279
+ return { provider: "bb", keys: { project: validateBbId(projectId, "projectId") } };
280
+ }
281
+ function threadIdentity(projectId, threadId) {
282
+ return {
283
+ provider: "bb",
284
+ keys: {
285
+ project: validateBbId(projectId, "projectId"),
286
+ thread: validateBbId(threadId, "threadId")
287
+ }
288
+ };
289
+ }
290
+ function machineMonitorIdentity() {
291
+ return { provider: "bb", keys: { page: "machine-monitor", plugin: "machine-monitor" } };
292
+ }
293
+ function projectResource(projectId, presentation) {
294
+ return { ...projectIdentity(projectId), presentation };
295
+ }
296
+ function threadResource(projectId, threadId, presentation) {
297
+ return { ...threadIdentity(projectId, threadId), presentation };
298
+ }
299
+ function machineMonitorResource(presentation = {
300
+ label: "Machine Monitor",
301
+ url: MACHINE_MONITOR_ROUTE
302
+ }) {
303
+ return { ...machineMonitorIdentity(), presentation };
304
+ }
305
+ function serializeResource(resource) {
306
+ return {
307
+ provider: resource.provider,
308
+ keys: resource.keys,
309
+ presentation: resource.presentation
310
+ };
311
+ }
312
+ function sha256Hex(value) {
313
+ return bytesToHex(sha256(encoder.encode(value)));
314
+ }
315
+ function projectionPayloadJson(producerPluginId, source, tombstone, targets) {
316
+ const payload = {
317
+ protocolVersion: PROTOCOL_VERSION,
318
+ producerPluginId,
319
+ source: serializeResource(source),
320
+ tombstone,
321
+ targets: targets.map(serializeResource)
322
+ };
323
+ const json = JSON.stringify(payload);
324
+ if (utf8ByteLength(json) > MAX_PROJECTION_PAYLOAD_BYTES) {
325
+ fail(`projection payload exceeds its ${MAX_PROJECTION_PAYLOAD_BYTES}-byte limit.`);
326
+ }
327
+ return json;
328
+ }
329
+ function projectionPayloadDigest(producerPluginId, source, tombstone, targets) {
330
+ return sha256Hex(projectionPayloadJson(producerPluginId, source, tombstone, targets));
331
+ }
332
+ function isExactBbThreadResource(resource) {
333
+ const keys = Object.keys(resource.keys);
334
+ return resource.provider === "bb" && keys.length === 2 && keys[0] === "project" && keys[1] === "thread" && ID_PATTERN.test(resource.keys.project) && ID_PATTERN.test(resource.keys.thread);
335
+ }
336
+ function isUrlResource(resource) {
337
+ return resource.provider === "url" && Object.keys(resource.keys).length === 1 && resource.keys.href != null && resource.presentation.url === resource.keys.href;
338
+ }
339
+ function normalizeThreadOrUrlReference(target) {
340
+ const resource = canonicalizeResource(target);
341
+ if (!isExactBbThreadResource(resource) && !isUrlResource(resource)) {
342
+ throw new CrossReferenceValidationError("References must be exact BB threads or safe HTTP(S) URLs.");
343
+ }
344
+ if (isExactBbThreadResource(resource) && resource.presentation.url !== undefined && resource.presentation.url !== `/projects/${resource.keys.project}/threads/${resource.keys.thread}` && resource.presentation.url !== `/threads/${resource.keys.thread}`) {
345
+ throw new CrossReferenceValidationError("Native thread presentation URL must match its thread identity.");
346
+ }
347
+ return resource;
348
+ }
349
+ function urlResource(href, presentation) {
350
+ const canonicalHref = new URL(href).href;
351
+ return { provider: "url", keys: { href: canonicalHref }, presentation: { ...presentation, url: canonicalHref } };
352
+ }
353
+ var MAX_REFERENCE_URL_BYTES = 512;
354
+ var MAX_REFERENCE_LABEL_BYTES = 256;
355
+ function hasSensitiveUrlParameter(url) {
356
+ return hasSensitiveParameterName(url.searchParams) || url.hash.startsWith("#") && hasSensitiveParameterName(new URLSearchParams(url.hash.slice(1)));
357
+ }
358
+ function referenceLabelError(value) {
359
+ if (value.trim().length === 0)
360
+ return "Give this external reference a name.";
361
+ if (utf8ByteLength(value) > MAX_REFERENCE_LABEL_BYTES) {
362
+ return `Reference names are limited to ${MAX_REFERENCE_LABEL_BYTES} UTF-8 bytes.`;
363
+ }
364
+ return null;
365
+ }
366
+
367
+ // src/index.ts
368
+ import { z } from "zod";
369
+ var FORWARD_REFERENCE_CHECK_LIMIT = 10;
370
+ function assertInputRecord(value) {
371
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
372
+ throw new CrossReferenceValidationError("projection input must be an object.");
373
+ }
374
+ const fields = Object.keys(value).sort();
375
+ if (fields.join(",") !== "expectedRevision,mutationId,payloadDigest,producerPluginId,protocolVersion,revision,source,targets,tombstone") {
376
+ throw new CrossReferenceValidationError("projection input fields are invalid.");
377
+ }
378
+ }
379
+ function normalizeProjectionCommand(input) {
380
+ assertInputRecord(input);
381
+ if (input.protocolVersion !== PROTOCOL_VERSION) {
382
+ throw new CrossReferenceValidationError(`protocolVersion must be ${PROTOCOL_VERSION}.`);
383
+ }
384
+ const producerPluginId = validateProducerPluginId(input.producerPluginId);
385
+ const mutationId = validateMutationId(input.mutationId);
386
+ const revision = validateRevision(input.revision, "revision", 1);
387
+ const expectedRevision = validateRevision(input.expectedRevision, "expectedRevision", 0);
388
+ if (typeof input.tombstone !== "boolean") {
389
+ throw new CrossReferenceValidationError("tombstone must be a boolean.");
390
+ }
391
+ if (!Array.isArray(input.targets) || input.targets.length > MAX_TARGETS) {
392
+ throw new CrossReferenceValidationError(`targets must contain at most ${MAX_TARGETS} resources.`);
393
+ }
394
+ const source = canonicalizeResource(input.source);
395
+ const targets = input.targets.map((target) => canonicalizeResource(target));
396
+ const identitySet = new Set;
397
+ for (const target of targets) {
398
+ if (identitySet.has(target.canonicalIdentityJson)) {
399
+ throw new CrossReferenceValidationError("targets must not contain duplicate resource identities.");
400
+ }
401
+ identitySet.add(target.canonicalIdentityJson);
402
+ }
403
+ if (input.tombstone && targets.length !== 0) {
404
+ throw new CrossReferenceValidationError("a tombstone must have an empty target list.");
405
+ }
406
+ const payloadJson = projectionPayloadJson(producerPluginId, source, input.tombstone, targets);
407
+ const computedPayloadDigest = sha256Hex(payloadJson);
408
+ const payloadDigest = validateDigest(input.payloadDigest);
409
+ if (payloadDigest !== computedPayloadDigest) {
410
+ throw new CrossReferenceValidationError("payloadDigest does not match the canonical projection payload.");
411
+ }
412
+ return {
413
+ protocolVersion: PROTOCOL_VERSION,
414
+ producerPluginId,
415
+ mutationId,
416
+ source,
417
+ revision,
418
+ expectedRevision,
419
+ payloadDigest,
420
+ tombstone: input.tombstone,
421
+ targets,
422
+ payloadJson,
423
+ computedPayloadDigest
424
+ };
425
+ }
426
+ function normalizeIdentityInput(input) {
427
+ return canonicalizeIdentity(input);
428
+ }
429
+ function defaultPageSize(pageSize) {
430
+ if (pageSize === undefined)
431
+ return 25;
432
+ const validated = validateRevision(pageSize, "pageSize", 1);
433
+ if (validated > 100) {
434
+ throw new CrossReferenceValidationError("pageSize must be between 1 and 100.");
435
+ }
436
+ return validated;
437
+ }
438
+ var namePattern = /^[a-z][a-z0-9._-]{0,63}$/;
439
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
440
+ var digestPattern = /^[0-9a-f]{64}$/;
441
+ var safeInteger = (schema) => schema.refine(Number.isSafeInteger, { message: "must be a safe integer" });
442
+ var presentationSchema = z.object({
443
+ label: z.string().max(MAX_PRESENTATION_LABEL_BYTES),
444
+ detail: z.string().max(MAX_PRESENTATION_DETAIL_BYTES).optional(),
445
+ url: z.string().max(MAX_PRESENTATION_URL_BYTES).optional()
446
+ }).strict().superRefine((value, context) => {
447
+ try {
448
+ canonicalizeResource({ provider: "test", keys: { resource: "placeholder" }, presentation: value });
449
+ } catch (cause) {
450
+ context.addIssue({ code: "custom", message: cause instanceof Error ? cause.message : String(cause) });
451
+ }
452
+ });
453
+ var resourceSchema = z.object({
454
+ provider: z.string().max(64).regex(namePattern),
455
+ keys: z.record(z.string().max(64), z.string().max(MAX_KEY_VALUE_BYTES)),
456
+ presentation: presentationSchema
457
+ }).strict().superRefine((value, context) => {
458
+ try {
459
+ canonicalizeResource(value);
460
+ } catch (cause) {
461
+ context.addIssue({ code: "custom", message: cause instanceof Error ? cause.message : String(cause) });
462
+ }
463
+ });
464
+ var resourceIdentitySchema = z.object({
465
+ provider: z.string().max(64).regex(namePattern),
466
+ keys: z.record(z.string().max(64), z.string().max(MAX_KEY_VALUE_BYTES))
467
+ }).strict().superRefine((value, context) => {
468
+ try {
469
+ canonicalizeIdentity(value);
470
+ } catch (cause) {
471
+ context.addIssue({ code: "custom", message: cause instanceof Error ? cause.message : String(cause) });
472
+ }
473
+ });
474
+ var producerPluginIdSchema = z.string().max(64).regex(namePattern);
475
+ var mutationIdSchema = z.string().max(MAX_MUTATION_ID_BYTES).regex(uuidPattern);
476
+ var revisionSchema = safeInteger(z.number().int());
477
+ var applyProjectionInputSchema = z.object({
478
+ protocolVersion: z.literal(PROTOCOL_VERSION),
479
+ producerPluginId: producerPluginIdSchema,
480
+ mutationId: mutationIdSchema,
481
+ source: resourceSchema,
482
+ revision: revisionSchema.positive(),
483
+ expectedRevision: revisionSchema.nonnegative(),
484
+ payloadDigest: z.string().regex(digestPattern),
485
+ tombstone: z.boolean(),
486
+ targets: z.array(resourceSchema).max(MAX_TARGETS)
487
+ }).strict().superRefine((value, context) => {
488
+ try {
489
+ normalizeProjectionCommand(value);
490
+ } catch (cause) {
491
+ context.addIssue({ code: "custom", message: cause instanceof Error ? cause.message : String(cause) });
492
+ }
493
+ });
494
+ var applyProjectionResponseSchema = z.object({
495
+ outcome: z.enum(["applied", "duplicate", "equal", "stale", "conflict", "cas-mismatch"]),
496
+ currentRevision: revisionSchema.nonnegative(),
497
+ currentDigest: z.string().regex(digestPattern).nullable()
498
+ }).strict();
499
+ var projectionSchema = z.object({
500
+ producerPluginId: producerPluginIdSchema,
501
+ source: resourceSchema,
502
+ revision: revisionSchema.nonnegative(),
503
+ mutationId: mutationIdSchema,
504
+ payloadDigest: z.string().regex(digestPattern),
505
+ tombstone: z.boolean(),
506
+ targets: z.array(resourceSchema).max(MAX_TARGETS)
507
+ }).strict();
508
+ var getProjectionInputSchema = z.object({
509
+ producerPluginId: producerPluginIdSchema,
510
+ source: resourceIdentitySchema
511
+ }).strict();
512
+ var getProjectionOutputSchema = z.object({ projection: projectionSchema.nullable() }).strict();
513
+ var backlinkRowSchema = z.object({
514
+ source: resourceSchema,
515
+ producerPluginId: producerPluginIdSchema,
516
+ revision: revisionSchema.nonnegative(),
517
+ targetPresentation: presentationSchema,
518
+ position: z.number().int().min(0).max(MAX_TARGETS - 1).refine(Number.isSafeInteger),
519
+ lastSeenAt: revisionSchema.nonnegative().nullable().optional()
520
+ }).strict();
521
+ var listBacklinksInputSchema = z.object({
522
+ target: resourceIdentitySchema,
523
+ pageSize: z.number().int().min(1).max(100).refine(Number.isSafeInteger).optional(),
524
+ cursor: z.string().max(4096).optional()
525
+ }).strict();
526
+ var listBacklinksOutputSchema = z.object({
527
+ rows: z.array(backlinkRowSchema).max(100),
528
+ total: revisionSchema.nonnegative(),
529
+ nextCursor: z.string().max(4096).nullable()
530
+ }).strict();
531
+ var forwardReferenceRowSchema = z.object({
532
+ target: resourceSchema,
533
+ producerPluginId: producerPluginIdSchema,
534
+ revision: revisionSchema.nonnegative(),
535
+ position: z.number().int().min(0).max(MAX_TARGETS - 1).refine(Number.isSafeInteger),
536
+ lastSeenAt: revisionSchema.nonnegative().nullable().optional()
537
+ }).strict();
538
+ var listForwardReferencesInputSchema = z.object({
539
+ source: resourceIdentitySchema,
540
+ producerPluginId: producerPluginIdSchema.optional(),
541
+ pageSize: z.number().int().min(1).max(100).refine(Number.isSafeInteger).optional(),
542
+ cursor: z.string().max(4096).optional()
543
+ }).strict();
544
+ var listForwardReferencesOutputSchema = z.object({
545
+ rows: z.array(forwardReferenceRowSchema).max(100),
546
+ total: revisionSchema.nonnegative(),
547
+ nextCursor: z.string().max(4096).nullable()
548
+ }).strict();
549
+ var checkForwardReferencesInputSchema = z.object({
550
+ source: resourceIdentitySchema,
551
+ producerPluginId: producerPluginIdSchema.optional()
552
+ }).strict();
553
+ var forwardReferenceStatusSchema = z.object({
554
+ url: z.string().max(MAX_PRESENTATION_URL_BYTES),
555
+ status: z.number().int().min(100).max(599).nullable(),
556
+ label: z.string().min(1).max(128)
557
+ }).strict();
558
+ var checkForwardReferencesOutputSchema = z.array(forwardReferenceStatusSchema).max(FORWARD_REFERENCE_CHECK_LIMIT);
559
+ var CROSS_REFERENCES_PLUGIN_ID = "cross-references";
560
+ var CROSS_REFERENCES_PROTOCOL = "cross-references";
561
+ var LIMITS = Object.freeze({ callMs: 5000, describeMs: 1000, responseBytes: 1024 * 1024 });
562
+ var describeInputSchema = z.null();
563
+ var describeOutputSchema = z.object({
564
+ protocol: z.literal(CROSS_REFERENCES_PROTOCOL),
565
+ versions: z.array(z.number().int().positive().refine(Number.isSafeInteger)).max(16)
566
+ });
567
+ var crossReferencesRpcSchemas = {
568
+ "crossReferences.describe": { input: describeInputSchema, output: describeOutputSchema },
569
+ applyProjection: { input: applyProjectionInputSchema, output: applyProjectionResponseSchema },
570
+ getProjection: { input: getProjectionInputSchema, output: getProjectionOutputSchema },
571
+ listBacklinks: { input: listBacklinksInputSchema, output: listBacklinksOutputSchema },
572
+ listForwardReferences: { input: listForwardReferencesInputSchema, output: listForwardReferencesOutputSchema },
573
+ checkForwardReferences: { input: checkForwardReferencesInputSchema, output: checkForwardReferencesOutputSchema }
574
+ };
575
+ var referenceSnapshotSchema = z.object({
576
+ revision: revisionSchema.nonnegative(),
577
+ targets: z.array(resourceSchema).max(MAX_TARGETS),
578
+ status: z.object({ state: z.enum(["synced", "pending", "degraded", "blocked"]), error: z.string().max(4096).nullable() })
579
+ });
580
+
581
+ // src/bb.ts
582
+ import { z as z2 } from "zod";
583
+ class CrossReferencesProtocolError extends Error {
584
+ code = "incompatible_protocol";
585
+ constructor(message) {
586
+ super(message);
587
+ this.name = "CrossReferencesProtocolError";
588
+ }
589
+ }
590
+ function budget(value, maximum) {
591
+ if (value === undefined)
592
+ return maximum;
593
+ if (!Number.isFinite(value) || value <= 0 || value > maximum)
594
+ throw new RangeError(`Budget must be > 0 and <= ${maximum}.`);
595
+ return value;
596
+ }
597
+ function createCrossReferencesClient(sdk, targetPluginId = CROSS_REFERENCES_PLUGIN_ID, options = {}) {
598
+ const pluginId = validateProducerPluginId(targetPluginId);
599
+ const callMs = budget(options.callMs, LIMITS.callMs);
600
+ const describeMs = budget(options.describeMs, LIMITS.describeMs);
601
+ async function call(method, input, inputSchema, outputSchema, signal) {
602
+ const parsedInput = inputSchema.parse(input);
603
+ if (signal?.aborted)
604
+ throw signal.reason ?? new DOMException("Request aborted", "AbortError");
605
+ const controller = new AbortController;
606
+ let rejectCancellation = () => {};
607
+ const cancelled = new Promise((_, reject) => {
608
+ rejectCancellation = reject;
609
+ });
610
+ const abort = () => {
611
+ const reason = signal?.reason ?? new DOMException("Request aborted", "AbortError");
612
+ rejectCancellation(reason);
613
+ controller.abort(reason);
614
+ };
615
+ signal?.addEventListener("abort", abort, { once: true });
616
+ const timer = setTimeout(() => {
617
+ const reason = Object.assign(new Error(`Cross References ${method} timed out`), { name: "TimeoutError", code: "timeout" });
618
+ rejectCancellation(reason);
619
+ controller.abort(reason);
620
+ }, method === "crossReferences.describe" ? describeMs : callMs);
621
+ try {
622
+ const value = await Promise.race([
623
+ sdk.plugins.callRpc({ pluginId, method, input: JSON.parse(JSON.stringify(parsedInput)), outputSchema: z2.unknown(), signal: controller.signal }),
624
+ cancelled
625
+ ]);
626
+ const json = JSON.stringify(value);
627
+ if (json === undefined || utf8ByteLength(json) > LIMITS.responseBytes)
628
+ throw new CrossReferencesProtocolError("RPC result exceeds its canonical JSON byte limit or is not JSON.");
629
+ return outputSchema.parse(value);
630
+ } finally {
631
+ clearTimeout(timer);
632
+ signal?.removeEventListener("abort", abort);
633
+ }
634
+ }
635
+ function operation(method, input, signal) {
636
+ const schemas = crossReferencesRpcSchemas[method];
637
+ return call(method, input, schemas.input, schemas.output, signal);
638
+ }
639
+ return {
640
+ pluginId,
641
+ async describe(signal) {
642
+ const schemas = crossReferencesRpcSchemas["crossReferences.describe"];
643
+ try {
644
+ const description = await call("crossReferences.describe", null, schemas.input, schemas.output, signal);
645
+ if (!description.versions.includes(1))
646
+ throw new CrossReferencesProtocolError("Cross References does not support wire version 1.");
647
+ return { protocol: CROSS_REFERENCES_PROTOCOL, version: 1, legacy: false };
648
+ } catch (error) {
649
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "unknown_method") {
650
+ return { protocol: CROSS_REFERENCES_PROTOCOL, version: 1, legacy: true };
651
+ }
652
+ throw error;
653
+ }
654
+ },
655
+ applyProjection: (input, signal) => operation("applyProjection", input, signal),
656
+ getProjection: (input, signal) => operation("getProjection", input, signal),
657
+ listBacklinks: (input, signal) => operation("listBacklinks", input, signal),
658
+ listForwardReferences: (input, signal) => operation("listForwardReferences", input, signal),
659
+ checkForwardReferences: (input, signal) => operation("checkForwardReferences", input, signal)
660
+ };
661
+ }
662
+ export {
663
+ createCrossReferencesClient,
664
+ CrossReferencesProtocolError
665
+ };