@adcp/sdk 16.0.0-beta.6 → 16.0.0-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/lib/conformance/index.d.mts +2 -0
- package/dist/lib/conformance/index.d.ts +2 -0
- package/dist/lib/conformance/index.js +3 -0
- package/dist/lib/conformance/index.mjs +2 -0
- package/dist/lib/conformance/property-lists.d.mts +14 -0
- package/dist/lib/conformance/property-lists.d.ts +14 -0
- package/dist/lib/conformance/property-lists.js +45 -0
- package/dist/lib/conformance/property-lists.mjs +21 -0
- package/dist/lib/conformance/runConformance.js +20 -1
- package/dist/lib/conformance/runConformance.mjs +20 -1
- package/dist/lib/conformance/types.d.mts +3 -0
- package/dist/lib/conformance/types.d.ts +3 -0
- package/dist/lib/core/SingleAgentClient.d.mts +13 -3
- package/dist/lib/core/SingleAgentClient.d.ts +13 -3
- package/dist/lib/core/SingleAgentClient.js +109 -21
- package/dist/lib/core/SingleAgentClient.mjs +112 -22
- package/dist/lib/core/TaskExecutor.d.mts +1 -0
- package/dist/lib/core/TaskExecutor.d.ts +1 -0
- package/dist/lib/core/TaskExecutor.js +48 -34
- package/dist/lib/core/TaskExecutor.mjs +49 -35
- package/dist/lib/identifiers.d.mts +53 -0
- package/dist/lib/identifiers.d.ts +53 -0
- package/dist/lib/identifiers.js +212 -0
- package/dist/lib/identifiers.mjs +163 -0
- package/dist/lib/identity-3.3/revocation.js +5 -4
- package/dist/lib/identity-3.3/revocation.mjs +5 -4
- package/dist/lib/index.d.mts +3 -1
- package/dist/lib/index.d.ts +3 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.mjs +1 -0
- package/dist/lib/media-buy/index.d.mts +1 -1
- package/dist/lib/media-buy/index.d.ts +1 -1
- package/dist/lib/media-buy/property-policy.d.mts +15 -1
- package/dist/lib/media-buy/property-policy.d.ts +15 -1
- package/dist/lib/media-buy/property-policy.js +135 -3
- package/dist/lib/media-buy/property-policy.mjs +134 -3
- package/dist/lib/registry/index.d.mts +12 -0
- package/dist/lib/registry/index.d.ts +12 -0
- package/dist/lib/registry/index.js +159 -5
- package/dist/lib/registry/index.mjs +156 -4
- package/dist/lib/registry/types.d.mts +23 -0
- package/dist/lib/registry/types.d.ts +23 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/auth-signature.js +23 -2
- package/dist/lib/server/auth-signature.mjs +26 -2
- package/dist/lib/server/auth.d.mts +6 -0
- package/dist/lib/server/auth.d.ts +6 -0
- package/dist/lib/server/auth.js +6 -0
- package/dist/lib/server/auth.mjs +6 -0
- package/dist/lib/server/create-adcp-server.js +19 -3
- package/dist/lib/server/create-adcp-server.mjs +23 -4
- package/dist/lib/server/decisioning/runtime/from-platform.js +6 -2
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +6 -2
- package/dist/lib/server/decisioning/specialisms/lists.d.mts +4 -4
- package/dist/lib/server/decisioning/specialisms/lists.d.ts +4 -4
- package/dist/lib/server/index.d.mts +3 -0
- package/dist/lib/server/index.d.ts +3 -0
- package/dist/lib/server/index.js +8 -1
- package/dist/lib/server/index.mjs +4 -0
- package/dist/lib/server/property-lists.d.mts +36 -0
- package/dist/lib/server/property-lists.d.ts +36 -0
- package/dist/lib/server/property-lists.js +154 -0
- package/dist/lib/server/property-lists.mjs +129 -0
- package/dist/lib/server/serve.js +2 -1
- package/dist/lib/server/serve.mjs +2 -1
- package/dist/lib/server/targeting-helpers.d.mts +2 -0
- package/dist/lib/server/targeting-helpers.d.ts +2 -0
- package/dist/lib/server/targeting-helpers.js +28 -1
- package/dist/lib/server/targeting-helpers.mjs +18 -1
- package/dist/lib/server/webhook-emitter.js +1 -0
- package/dist/lib/server/webhook-emitter.mjs +2 -1
- package/dist/lib/signing/crypto.d.mts +1 -1
- package/dist/lib/signing/crypto.d.ts +1 -1
- package/dist/lib/signing/crypto.js +37 -2
- package/dist/lib/signing/crypto.mjs +37 -2
- package/dist/lib/signing/parser.d.mts +6 -0
- package/dist/lib/signing/parser.d.ts +6 -0
- package/dist/lib/signing/parser.js +7 -3
- package/dist/lib/signing/parser.mjs +6 -3
- package/dist/lib/signing/server.d.mts +1 -1
- package/dist/lib/signing/server.d.ts +1 -1
- package/dist/lib/signing/server.mjs +5 -1
- package/dist/lib/signing/web-bot-auth.d.mts +61 -22
- package/dist/lib/signing/web-bot-auth.d.ts +61 -22
- package/dist/lib/signing/web-bot-auth.js +286 -266
- package/dist/lib/signing/web-bot-auth.mjs +288 -277
- package/dist/lib/signing/webhook-verifier.d.mts +11 -0
- package/dist/lib/signing/webhook-verifier.d.ts +11 -0
- package/dist/lib/signing/webhook-verifier.js +11 -3
- package/dist/lib/signing/webhook-verifier.mjs +11 -3
- package/dist/lib/supply-path/verify.js +12 -2
- package/dist/lib/supply-path/verify.mjs +2 -2
- package/dist/lib/testing/client.js +3 -5
- package/dist/lib/testing/client.mjs +4 -6
- package/dist/lib/testing/compliance/comply.js +7 -2
- package/dist/lib/testing/compliance/comply.mjs +8 -3
- package/dist/lib/testing/index.d.mts +1 -1
- package/dist/lib/testing/index.d.ts +1 -1
- package/dist/lib/testing/index.js +4 -2
- package/dist/lib/testing/index.mjs +8 -2
- package/dist/lib/testing/storyboard/runner.js +12 -12
- package/dist/lib/testing/storyboard/runner.mjs +14 -13
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/testing/storyboard/validations.js +23 -1
- package/dist/lib/testing/storyboard/validations.mjs +28 -2
- package/dist/lib/testing/types.d.mts +39 -0
- package/dist/lib/testing/types.d.ts +39 -0
- package/dist/lib/v2/projection/augment-response.js +6 -3
- package/dist/lib/v2/projection/augment-response.mjs +8 -5
- package/dist/lib/v2/projection/creative-delivery.js +29 -17
- package/dist/lib/v2/projection/creative-delivery.mjs +31 -18
- package/dist/lib/v2/projection/v1-to-v2.d.mts +7 -0
- package/dist/lib/v2/projection/v1-to-v2.d.ts +7 -0
- package/dist/lib/v2/projection/v1-to-v2.js +70 -2
- package/dist/lib/v2/projection/v1-to-v2.mjs +72 -1
- package/dist/lib/validation/index.d.mts +1 -1
- package/dist/lib/validation/index.d.ts +1 -1
- package/dist/lib/validation/index.js +4 -2
- package/dist/lib/validation/index.mjs +4 -2
- package/dist/lib/validation/schema-loader.d.mts +26 -0
- package/dist/lib/validation/schema-loader.d.ts +26 -0
- package/dist/lib/validation/schema-loader.js +98 -14
- package/dist/lib/validation/schema-loader.mjs +95 -13
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.mjs +3 -3
- package/docs/TYPE-SUMMARY.md +2 -2
- package/docs/guides/PROPERTY-LISTS.md +264 -0
- package/docs/guides/SIGNING-GUIDE.md +25 -0
- package/docs/guides/VALIDATE-YOUR-AGENT.md +34 -0
- package/docs/llms.txt +2 -2
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/migration-agent-identity-3.3.md +60 -23
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
15
|
}
|
|
16
16
|
return to;
|
|
17
17
|
};
|
|
18
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
18
19
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
20
|
var registry_exports = {};
|
|
20
21
|
__export(registry_exports, {
|
|
@@ -37,6 +38,8 @@ __export(registry_exports, {
|
|
|
37
38
|
sanitizeStreamText: () => import_feed_stream2.sanitizeStreamText
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(registry_exports);
|
|
41
|
+
var import_identifiers = require('../identifiers.js');
|
|
42
|
+
__reExport(registry_exports, require('../identifiers.js'), module.exports);
|
|
40
43
|
var import_validation = require('../supply-path/validation.js');
|
|
41
44
|
var import_feed_stream = require('./feed-stream.js');
|
|
42
45
|
var import_sync = require('./sync.js');
|
|
@@ -51,6 +54,9 @@ const ERROR_BODY_PREVIEW_CHARS = 200;
|
|
|
51
54
|
const MAX_BRAND_BULK_DOMAINS = 25;
|
|
52
55
|
const MAX_BULK_DOMAINS = 100;
|
|
53
56
|
const MAX_CHECK_DOMAINS = 1e4;
|
|
57
|
+
const MAX_RESOLVE_IDENTIFIERS = 1e4;
|
|
58
|
+
const DEFAULT_RESOLVE_REQUEST_BYTES = 1024 * 1024;
|
|
59
|
+
const DEFAULT_RESOLVE_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
54
60
|
const COMMUNITY_MIRROR_PLATFORM_RE = /^[a-z0-9_-]{1,64}$/;
|
|
55
61
|
const MAX_REGISTRY_ERROR_DETAILS_BYTES = 64 * 1024;
|
|
56
62
|
const MAX_RETRY_AFTER_MS = 2147483647;
|
|
@@ -303,7 +309,15 @@ class RegistryClient {
|
|
|
303
309
|
}
|
|
304
310
|
/** Save or update a hosted property. Requires authentication. */
|
|
305
311
|
async saveProperty(property) {
|
|
306
|
-
if (
|
|
312
|
+
if (typeof property?.publisher_domain !== "string" || !property.publisher_domain.trim()) {
|
|
313
|
+
throw new import_identifiers.IdentifierValidationError(
|
|
314
|
+
"invalid_publisher_domain",
|
|
315
|
+
"publisher_domain",
|
|
316
|
+
"publisher_domain is required"
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
const domain = (0, import_identifiers.validatePublisherDomain)(property.publisher_domain);
|
|
320
|
+
if (!domain.ok) throw domain.error;
|
|
307
321
|
if (!this.apiKey) throw new Error("apiKey is required for save operations");
|
|
308
322
|
const payload = this.normalizeSavePropertyRequest(property);
|
|
309
323
|
return this.post(`${this.baseUrl}/api/properties/save`, payload);
|
|
@@ -335,17 +349,19 @@ class RegistryClient {
|
|
|
335
349
|
*/
|
|
336
350
|
async saveProperties(properties, options) {
|
|
337
351
|
const concurrency = options?.concurrency ?? 5;
|
|
338
|
-
|
|
352
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1)
|
|
353
|
+
throw new RangeError("concurrency must be a positive integer");
|
|
354
|
+
const results = /* @__PURE__ */ Object.create(null);
|
|
339
355
|
for (let i = 0; i < properties.length; i += concurrency) {
|
|
340
356
|
const batch = properties.slice(i, i + concurrency);
|
|
341
357
|
const settled = await Promise.allSettled(batch.map((p) => this.saveProperty(p)));
|
|
342
358
|
for (let j = 0; j < batch.length; j++) {
|
|
343
359
|
const domain = batch[j].publisher_domain;
|
|
344
360
|
const s = settled[j];
|
|
345
|
-
results[domain] = s.status === "fulfilled" ? s.value : { error: String(s.reason) };
|
|
361
|
+
results[domain] = s.status === "fulfilled" ? s.value : { error: String(s.reason), ...s.reason instanceof Error ? { cause: s.reason } : {} };
|
|
346
362
|
}
|
|
347
363
|
}
|
|
348
|
-
return results;
|
|
364
|
+
return Object.fromEntries(Object.entries(results));
|
|
349
365
|
}
|
|
350
366
|
/**
|
|
351
367
|
* Resolve catalog identifiers to stable property_rids.
|
|
@@ -354,11 +370,147 @@ class RegistryClient {
|
|
|
354
370
|
*/
|
|
355
371
|
async resolveIdentifiers(request) {
|
|
356
372
|
if (!request?.identifiers?.length) throw new Error("identifiers are required");
|
|
373
|
+
if (request.identifiers.length > MAX_RESOLVE_IDENTIFIERS) {
|
|
374
|
+
throw new RangeError("At most 10,000 identifiers are allowed; use resolveIdentifiersAll for larger lists");
|
|
375
|
+
}
|
|
357
376
|
if ((request.mode ?? "resolve") !== "lookup" && !this.apiKey) {
|
|
358
377
|
throw new Error("apiKey is required for resolveIdentifiers in resolve mode");
|
|
359
378
|
}
|
|
360
379
|
return this.post(`${this.baseUrl}/api/registry/resolve`, request);
|
|
361
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* Bulk catalog registration (`resolve`, the default) or read-only `lookup`.
|
|
383
|
+
* Splits by identifier count and UTF-8 request bytes, bounds concurrency,
|
|
384
|
+
* and honors Retry-After on 429. Results retain input order and duplicates.
|
|
385
|
+
* Network/5xx failures are not retried: resolve contributions have additive
|
|
386
|
+
* activity logs even though identifier-to-RID mapping is idempotent.
|
|
387
|
+
*/
|
|
388
|
+
async resolveIdentifiersAll(request, options = {}) {
|
|
389
|
+
const {
|
|
390
|
+
batchSize = MAX_RESOLVE_IDENTIFIERS,
|
|
391
|
+
maxRequestBytes = DEFAULT_RESOLVE_REQUEST_BYTES,
|
|
392
|
+
concurrency = 2,
|
|
393
|
+
maxRetries = 3,
|
|
394
|
+
maxRetryDelayMs = 6e4
|
|
395
|
+
} = options;
|
|
396
|
+
for (const [name, value, minimum] of [
|
|
397
|
+
["batchSize", batchSize, 1],
|
|
398
|
+
["maxRequestBytes", maxRequestBytes, 1],
|
|
399
|
+
["concurrency", concurrency, 1],
|
|
400
|
+
["maxRetries", maxRetries, 0],
|
|
401
|
+
["maxRetryDelayMs", maxRetryDelayMs, 0]
|
|
402
|
+
]) {
|
|
403
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
404
|
+
throw new RangeError(`${name} must be an integer >= ${minimum}`);
|
|
405
|
+
}
|
|
406
|
+
if (batchSize > MAX_RESOLVE_IDENTIFIERS) throw new RangeError("batchSize must not exceed 10,000");
|
|
407
|
+
if (!Array.isArray(request?.identifiers)) throw new TypeError("identifiers are required");
|
|
408
|
+
if ((request.mode ?? "resolve") !== "lookup" && !this.apiKey)
|
|
409
|
+
throw new Error("apiKey is required for resolveIdentifiers in resolve mode");
|
|
410
|
+
const { identifiers, ...envelope } = request;
|
|
411
|
+
const envelopeBytes = Buffer.byteLength(JSON.stringify({ ...envelope, identifiers: [] }), "utf8");
|
|
412
|
+
const outcomes = new Array(identifiers.length);
|
|
413
|
+
const batches = [];
|
|
414
|
+
let batch = { indexes: [], identifiers: [] };
|
|
415
|
+
let bytes = envelopeBytes;
|
|
416
|
+
for (const [index, identifier] of identifiers.entries()) {
|
|
417
|
+
if (!identifier || typeof identifier !== "object" || typeof identifier.type !== "string" || typeof identifier.value !== "string") {
|
|
418
|
+
outcomes[index] = {
|
|
419
|
+
index,
|
|
420
|
+
identifier,
|
|
421
|
+
success: false,
|
|
422
|
+
error: new TypeError("Identifier must contain string type and value")
|
|
423
|
+
};
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const size = Buffer.byteLength(JSON.stringify(identifier), "utf8");
|
|
427
|
+
if (envelopeBytes + size > maxRequestBytes) {
|
|
428
|
+
outcomes[index] = {
|
|
429
|
+
index,
|
|
430
|
+
identifier,
|
|
431
|
+
success: false,
|
|
432
|
+
error: new RangeError("Identifier and provenance exceed maxRequestBytes")
|
|
433
|
+
};
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (batch.indexes.length && (batch.indexes.length >= batchSize || bytes + size + 1 > maxRequestBytes)) {
|
|
437
|
+
batches.push(batch);
|
|
438
|
+
batch = { indexes: [], identifiers: [] };
|
|
439
|
+
bytes = envelopeBytes;
|
|
440
|
+
}
|
|
441
|
+
bytes += size + (batch.indexes.length ? 1 : 0);
|
|
442
|
+
batch.indexes.push(index);
|
|
443
|
+
batch.identifiers.push(identifier);
|
|
444
|
+
}
|
|
445
|
+
if (batch.indexes.length) batches.push(batch);
|
|
446
|
+
let nextBatch = 0;
|
|
447
|
+
let retryNotBefore = 0;
|
|
448
|
+
let rateLimitFailure;
|
|
449
|
+
const worker = async () => {
|
|
450
|
+
while (nextBatch < batches.length) {
|
|
451
|
+
const current = batches[nextBatch++];
|
|
452
|
+
let response;
|
|
453
|
+
let failure;
|
|
454
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
455
|
+
if (rateLimitFailure) {
|
|
456
|
+
failure = rateLimitFailure;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
while (!rateLimitFailure && retryNotBefore > Date.now())
|
|
460
|
+
await new Promise(
|
|
461
|
+
(resolve) => setTimeout(resolve, Math.min(retryNotBefore - Date.now(), MAX_RETRY_AFTER_MS))
|
|
462
|
+
);
|
|
463
|
+
if (rateLimitFailure) {
|
|
464
|
+
failure = rateLimitFailure;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
try {
|
|
468
|
+
response = await this.resolveIdentifiers({ ...envelope, identifiers: current.identifiers });
|
|
469
|
+
if (!response || !Array.isArray(response.resolved)) {
|
|
470
|
+
throw new TypeError("Registry returned an invalid identifier resolution response");
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
response = void 0;
|
|
475
|
+
failure = error instanceof Error ? error : new Error("Registry resolution failed");
|
|
476
|
+
if (!isRegistryRequestError(error) || error.status !== 429) break;
|
|
477
|
+
const delay = typeof error.retryAfterMs === "number" && Number.isFinite(error.retryAfterMs) && error.retryAfterMs >= 0 ? error.retryAfterMs : Math.min(1e3 * 2 ** attempt, 3e4);
|
|
478
|
+
if (delay > maxRetryDelayMs) {
|
|
479
|
+
rateLimitFailure = error;
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
retryNotBefore = Math.max(retryNotBefore, Date.now() + delay);
|
|
483
|
+
if (attempt === maxRetries) break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const entries = /* @__PURE__ */ new Map();
|
|
487
|
+
let malformedEntries = false;
|
|
488
|
+
for (const entry of response?.resolved ?? []) {
|
|
489
|
+
if (!entry || typeof entry !== "object" || !entry.identifier || typeof entry.identifier.type !== "string" || typeof entry.identifier.value !== "string" || entry.property_rid !== null && typeof entry.property_rid !== "string" || !["existing", "created", "excluded"].includes(entry.status)) {
|
|
490
|
+
malformedEntries = true;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const key = JSON.stringify([entry.identifier.type, entry.identifier.value]);
|
|
494
|
+
const group = entries.get(key) ?? [];
|
|
495
|
+
group.push(entry);
|
|
496
|
+
entries.set(key, group);
|
|
497
|
+
}
|
|
498
|
+
for (const index of current.indexes) {
|
|
499
|
+
const identifier = identifiers[index];
|
|
500
|
+
const group = entries.get(JSON.stringify([identifier.type, identifier.value]));
|
|
501
|
+
const entry = group && (group.length > 1 ? group.shift() : group[0]);
|
|
502
|
+
outcomes[index] = entry ? { index, identifier, success: true, result: entry } : {
|
|
503
|
+
index,
|
|
504
|
+
identifier,
|
|
505
|
+
success: false,
|
|
506
|
+
error: response ? malformedEntries ? new TypeError("Registry returned an invalid identifier resolution response") : new Error("Registry response omitted identifier") : failure
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, batches.length) }, worker));
|
|
512
|
+
return outcomes;
|
|
513
|
+
}
|
|
362
514
|
/** File a catalog fact dispute. Requires authentication. */
|
|
363
515
|
async fileCatalogDispute(request) {
|
|
364
516
|
if (!request?.subject_type?.trim()) throw new Error("subject_type is required");
|
|
@@ -1130,6 +1282,7 @@ class RegistryClient {
|
|
|
1130
1282
|
bodyLimitForUrl(url) {
|
|
1131
1283
|
if (this.hasCustomMaxBodyBytes) return this.maxBodyBytes;
|
|
1132
1284
|
const path = new URL(url).pathname;
|
|
1285
|
+
if (path === "/api/registry/resolve") return DEFAULT_RESOLVE_RESPONSE_BYTES;
|
|
1133
1286
|
if (path === "/api/brands/registry" || path === "/api/brands/resolve/bulk" || path === "/api/properties/registry" || path === "/api/properties/resolve/bulk" || path === "/api/registry/agents" || path === "/api/registry/publishers" || path === "/api/registry/feed" || path === "/api/registry/mirrors" || path === "/api/registry/agents/search" || path === "/api/registry/authorizations" || path === "/api/registry/authorizations/snapshot" || path === "/api/search" || path === "/api/policies/registry" || path === "/api/policies/resolve/bulk" || path === "/api/public/discover-agent" || path === "/api/public/agent-formats" || path === "/api/public/agent-products" || path === "/api/public/validate-publisher" || path === "/api/registry/agents/storyboard-status" || path.startsWith("/api/registry/agents/") && path.endsWith("/storyboard-status") || path.startsWith("/api/registry/mirrors/") || path.startsWith("/api/properties/check")) {
|
|
1134
1287
|
return DEFAULT_LARGE_RESPONSE_MAX_BODY_BYTES;
|
|
1135
1288
|
}
|
|
@@ -1239,5 +1392,6 @@ class RegistryClient {
|
|
|
1239
1392
|
isRegistryRequestError,
|
|
1240
1393
|
openFeedStream,
|
|
1241
1394
|
parseSseStream,
|
|
1242
|
-
sanitizeStreamText
|
|
1395
|
+
sanitizeStreamText,
|
|
1396
|
+
...require('../identifiers.js')
|
|
1243
1397
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validatePublisherDomain, IdentifierValidationError } from "../identifiers.mjs";
|
|
2
|
+
export * from "../identifiers.mjs";
|
|
1
3
|
import { validateSupplyPathRequest, assertRegistrySupplyPathResult } from "../supply-path/validation.mjs";
|
|
2
4
|
import { openFeedStream } from "./feed-stream.mjs";
|
|
3
5
|
import { RegistrySync } from "./sync.mjs";
|
|
@@ -22,6 +24,9 @@ const ERROR_BODY_PREVIEW_CHARS = 200;
|
|
|
22
24
|
const MAX_BRAND_BULK_DOMAINS = 25;
|
|
23
25
|
const MAX_BULK_DOMAINS = 100;
|
|
24
26
|
const MAX_CHECK_DOMAINS = 1e4;
|
|
27
|
+
const MAX_RESOLVE_IDENTIFIERS = 1e4;
|
|
28
|
+
const DEFAULT_RESOLVE_REQUEST_BYTES = 1024 * 1024;
|
|
29
|
+
const DEFAULT_RESOLVE_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
25
30
|
const COMMUNITY_MIRROR_PLATFORM_RE = /^[a-z0-9_-]{1,64}$/;
|
|
26
31
|
const MAX_REGISTRY_ERROR_DETAILS_BYTES = 64 * 1024;
|
|
27
32
|
const MAX_RETRY_AFTER_MS = 2147483647;
|
|
@@ -274,7 +279,15 @@ class RegistryClient {
|
|
|
274
279
|
}
|
|
275
280
|
/** Save or update a hosted property. Requires authentication. */
|
|
276
281
|
async saveProperty(property) {
|
|
277
|
-
if (
|
|
282
|
+
if (typeof property?.publisher_domain !== "string" || !property.publisher_domain.trim()) {
|
|
283
|
+
throw new IdentifierValidationError(
|
|
284
|
+
"invalid_publisher_domain",
|
|
285
|
+
"publisher_domain",
|
|
286
|
+
"publisher_domain is required"
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const domain = validatePublisherDomain(property.publisher_domain);
|
|
290
|
+
if (!domain.ok) throw domain.error;
|
|
278
291
|
if (!this.apiKey) throw new Error("apiKey is required for save operations");
|
|
279
292
|
const payload = this.normalizeSavePropertyRequest(property);
|
|
280
293
|
return this.post(`${this.baseUrl}/api/properties/save`, payload);
|
|
@@ -306,17 +319,19 @@ class RegistryClient {
|
|
|
306
319
|
*/
|
|
307
320
|
async saveProperties(properties, options) {
|
|
308
321
|
const concurrency = options?.concurrency ?? 5;
|
|
309
|
-
|
|
322
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1)
|
|
323
|
+
throw new RangeError("concurrency must be a positive integer");
|
|
324
|
+
const results = /* @__PURE__ */ Object.create(null);
|
|
310
325
|
for (let i = 0; i < properties.length; i += concurrency) {
|
|
311
326
|
const batch = properties.slice(i, i + concurrency);
|
|
312
327
|
const settled = await Promise.allSettled(batch.map((p) => this.saveProperty(p)));
|
|
313
328
|
for (let j = 0; j < batch.length; j++) {
|
|
314
329
|
const domain = batch[j].publisher_domain;
|
|
315
330
|
const s = settled[j];
|
|
316
|
-
results[domain] = s.status === "fulfilled" ? s.value : { error: String(s.reason) };
|
|
331
|
+
results[domain] = s.status === "fulfilled" ? s.value : { error: String(s.reason), ...s.reason instanceof Error ? { cause: s.reason } : {} };
|
|
317
332
|
}
|
|
318
333
|
}
|
|
319
|
-
return results;
|
|
334
|
+
return Object.fromEntries(Object.entries(results));
|
|
320
335
|
}
|
|
321
336
|
/**
|
|
322
337
|
* Resolve catalog identifiers to stable property_rids.
|
|
@@ -325,11 +340,147 @@ class RegistryClient {
|
|
|
325
340
|
*/
|
|
326
341
|
async resolveIdentifiers(request) {
|
|
327
342
|
if (!request?.identifiers?.length) throw new Error("identifiers are required");
|
|
343
|
+
if (request.identifiers.length > MAX_RESOLVE_IDENTIFIERS) {
|
|
344
|
+
throw new RangeError("At most 10,000 identifiers are allowed; use resolveIdentifiersAll for larger lists");
|
|
345
|
+
}
|
|
328
346
|
if ((request.mode ?? "resolve") !== "lookup" && !this.apiKey) {
|
|
329
347
|
throw new Error("apiKey is required for resolveIdentifiers in resolve mode");
|
|
330
348
|
}
|
|
331
349
|
return this.post(`${this.baseUrl}/api/registry/resolve`, request);
|
|
332
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Bulk catalog registration (`resolve`, the default) or read-only `lookup`.
|
|
353
|
+
* Splits by identifier count and UTF-8 request bytes, bounds concurrency,
|
|
354
|
+
* and honors Retry-After on 429. Results retain input order and duplicates.
|
|
355
|
+
* Network/5xx failures are not retried: resolve contributions have additive
|
|
356
|
+
* activity logs even though identifier-to-RID mapping is idempotent.
|
|
357
|
+
*/
|
|
358
|
+
async resolveIdentifiersAll(request, options = {}) {
|
|
359
|
+
const {
|
|
360
|
+
batchSize = MAX_RESOLVE_IDENTIFIERS,
|
|
361
|
+
maxRequestBytes = DEFAULT_RESOLVE_REQUEST_BYTES,
|
|
362
|
+
concurrency = 2,
|
|
363
|
+
maxRetries = 3,
|
|
364
|
+
maxRetryDelayMs = 6e4
|
|
365
|
+
} = options;
|
|
366
|
+
for (const [name, value, minimum] of [
|
|
367
|
+
["batchSize", batchSize, 1],
|
|
368
|
+
["maxRequestBytes", maxRequestBytes, 1],
|
|
369
|
+
["concurrency", concurrency, 1],
|
|
370
|
+
["maxRetries", maxRetries, 0],
|
|
371
|
+
["maxRetryDelayMs", maxRetryDelayMs, 0]
|
|
372
|
+
]) {
|
|
373
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
374
|
+
throw new RangeError(`${name} must be an integer >= ${minimum}`);
|
|
375
|
+
}
|
|
376
|
+
if (batchSize > MAX_RESOLVE_IDENTIFIERS) throw new RangeError("batchSize must not exceed 10,000");
|
|
377
|
+
if (!Array.isArray(request?.identifiers)) throw new TypeError("identifiers are required");
|
|
378
|
+
if ((request.mode ?? "resolve") !== "lookup" && !this.apiKey)
|
|
379
|
+
throw new Error("apiKey is required for resolveIdentifiers in resolve mode");
|
|
380
|
+
const { identifiers, ...envelope } = request;
|
|
381
|
+
const envelopeBytes = Buffer.byteLength(JSON.stringify({ ...envelope, identifiers: [] }), "utf8");
|
|
382
|
+
const outcomes = new Array(identifiers.length);
|
|
383
|
+
const batches = [];
|
|
384
|
+
let batch = { indexes: [], identifiers: [] };
|
|
385
|
+
let bytes = envelopeBytes;
|
|
386
|
+
for (const [index, identifier] of identifiers.entries()) {
|
|
387
|
+
if (!identifier || typeof identifier !== "object" || typeof identifier.type !== "string" || typeof identifier.value !== "string") {
|
|
388
|
+
outcomes[index] = {
|
|
389
|
+
index,
|
|
390
|
+
identifier,
|
|
391
|
+
success: false,
|
|
392
|
+
error: new TypeError("Identifier must contain string type and value")
|
|
393
|
+
};
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const size = Buffer.byteLength(JSON.stringify(identifier), "utf8");
|
|
397
|
+
if (envelopeBytes + size > maxRequestBytes) {
|
|
398
|
+
outcomes[index] = {
|
|
399
|
+
index,
|
|
400
|
+
identifier,
|
|
401
|
+
success: false,
|
|
402
|
+
error: new RangeError("Identifier and provenance exceed maxRequestBytes")
|
|
403
|
+
};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (batch.indexes.length && (batch.indexes.length >= batchSize || bytes + size + 1 > maxRequestBytes)) {
|
|
407
|
+
batches.push(batch);
|
|
408
|
+
batch = { indexes: [], identifiers: [] };
|
|
409
|
+
bytes = envelopeBytes;
|
|
410
|
+
}
|
|
411
|
+
bytes += size + (batch.indexes.length ? 1 : 0);
|
|
412
|
+
batch.indexes.push(index);
|
|
413
|
+
batch.identifiers.push(identifier);
|
|
414
|
+
}
|
|
415
|
+
if (batch.indexes.length) batches.push(batch);
|
|
416
|
+
let nextBatch = 0;
|
|
417
|
+
let retryNotBefore = 0;
|
|
418
|
+
let rateLimitFailure;
|
|
419
|
+
const worker = async () => {
|
|
420
|
+
while (nextBatch < batches.length) {
|
|
421
|
+
const current = batches[nextBatch++];
|
|
422
|
+
let response;
|
|
423
|
+
let failure;
|
|
424
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
425
|
+
if (rateLimitFailure) {
|
|
426
|
+
failure = rateLimitFailure;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
while (!rateLimitFailure && retryNotBefore > Date.now())
|
|
430
|
+
await new Promise(
|
|
431
|
+
(resolve) => setTimeout(resolve, Math.min(retryNotBefore - Date.now(), MAX_RETRY_AFTER_MS))
|
|
432
|
+
);
|
|
433
|
+
if (rateLimitFailure) {
|
|
434
|
+
failure = rateLimitFailure;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
response = await this.resolveIdentifiers({ ...envelope, identifiers: current.identifiers });
|
|
439
|
+
if (!response || !Array.isArray(response.resolved)) {
|
|
440
|
+
throw new TypeError("Registry returned an invalid identifier resolution response");
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
response = void 0;
|
|
445
|
+
failure = error instanceof Error ? error : new Error("Registry resolution failed");
|
|
446
|
+
if (!isRegistryRequestError(error) || error.status !== 429) break;
|
|
447
|
+
const delay = typeof error.retryAfterMs === "number" && Number.isFinite(error.retryAfterMs) && error.retryAfterMs >= 0 ? error.retryAfterMs : Math.min(1e3 * 2 ** attempt, 3e4);
|
|
448
|
+
if (delay > maxRetryDelayMs) {
|
|
449
|
+
rateLimitFailure = error;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
retryNotBefore = Math.max(retryNotBefore, Date.now() + delay);
|
|
453
|
+
if (attempt === maxRetries) break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const entries = /* @__PURE__ */ new Map();
|
|
457
|
+
let malformedEntries = false;
|
|
458
|
+
for (const entry of response?.resolved ?? []) {
|
|
459
|
+
if (!entry || typeof entry !== "object" || !entry.identifier || typeof entry.identifier.type !== "string" || typeof entry.identifier.value !== "string" || entry.property_rid !== null && typeof entry.property_rid !== "string" || !["existing", "created", "excluded"].includes(entry.status)) {
|
|
460
|
+
malformedEntries = true;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const key = JSON.stringify([entry.identifier.type, entry.identifier.value]);
|
|
464
|
+
const group = entries.get(key) ?? [];
|
|
465
|
+
group.push(entry);
|
|
466
|
+
entries.set(key, group);
|
|
467
|
+
}
|
|
468
|
+
for (const index of current.indexes) {
|
|
469
|
+
const identifier = identifiers[index];
|
|
470
|
+
const group = entries.get(JSON.stringify([identifier.type, identifier.value]));
|
|
471
|
+
const entry = group && (group.length > 1 ? group.shift() : group[0]);
|
|
472
|
+
outcomes[index] = entry ? { index, identifier, success: true, result: entry } : {
|
|
473
|
+
index,
|
|
474
|
+
identifier,
|
|
475
|
+
success: false,
|
|
476
|
+
error: response ? malformedEntries ? new TypeError("Registry returned an invalid identifier resolution response") : new Error("Registry response omitted identifier") : failure
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, batches.length) }, worker));
|
|
482
|
+
return outcomes;
|
|
483
|
+
}
|
|
333
484
|
/** File a catalog fact dispute. Requires authentication. */
|
|
334
485
|
async fileCatalogDispute(request) {
|
|
335
486
|
if (!request?.subject_type?.trim()) throw new Error("subject_type is required");
|
|
@@ -1101,6 +1252,7 @@ class RegistryClient {
|
|
|
1101
1252
|
bodyLimitForUrl(url) {
|
|
1102
1253
|
if (this.hasCustomMaxBodyBytes) return this.maxBodyBytes;
|
|
1103
1254
|
const path = new URL(url).pathname;
|
|
1255
|
+
if (path === "/api/registry/resolve") return DEFAULT_RESOLVE_RESPONSE_BYTES;
|
|
1104
1256
|
if (path === "/api/brands/registry" || path === "/api/brands/resolve/bulk" || path === "/api/properties/registry" || path === "/api/properties/resolve/bulk" || path === "/api/registry/agents" || path === "/api/registry/publishers" || path === "/api/registry/feed" || path === "/api/registry/mirrors" || path === "/api/registry/agents/search" || path === "/api/registry/authorizations" || path === "/api/registry/authorizations/snapshot" || path === "/api/search" || path === "/api/policies/registry" || path === "/api/policies/resolve/bulk" || path === "/api/public/discover-agent" || path === "/api/public/agent-formats" || path === "/api/public/agent-products" || path === "/api/public/validate-publisher" || path === "/api/registry/agents/storyboard-status" || path.startsWith("/api/registry/agents/") && path.endsWith("/storyboard-status") || path.startsWith("/api/registry/mirrors/") || path.startsWith("/api/properties/check")) {
|
|
1105
1257
|
return DEFAULT_LARGE_RESPONSE_MAX_BODY_BYTES;
|
|
1106
1258
|
}
|
|
@@ -150,6 +150,29 @@ export type ResolveIdentifiersRequest = Omit<RegistryResolveIdentifiersRequest,
|
|
|
150
150
|
};
|
|
151
151
|
/** Response from POST /api/registry/resolve (200) */
|
|
152
152
|
export type ResolveIdentifiersResponse = operations['resolveIdentifiers']['responses']['200']['content']['application/json'];
|
|
153
|
+
export interface ResolveIdentifiersAllOptions {
|
|
154
|
+
/** Per-request identifier cap, from 1 through the registry limit of 10,000. */
|
|
155
|
+
batchSize?: number;
|
|
156
|
+
/** UTF-8 JSON request cap including provenance and envelope. Default: 1 MiB. */
|
|
157
|
+
maxRequestBytes?: number;
|
|
158
|
+
/** Maximum simultaneous requests. Default: 2. */
|
|
159
|
+
concurrency?: number;
|
|
160
|
+
/** Retries for HTTP 429 only. Other failures are returned without retrying contributions. Default: 3. */
|
|
161
|
+
maxRetries?: number;
|
|
162
|
+
/** Maximum server-requested retry delay. Default: 60 seconds; longer rate limits return errors without early retries. */
|
|
163
|
+
maxRetryDelayMs?: number;
|
|
164
|
+
}
|
|
165
|
+
/** One outcome per input, in input order (including duplicates). */
|
|
166
|
+
export type ResolveIdentifierOutcome = {
|
|
167
|
+
index: number;
|
|
168
|
+
identifier: ResolveIdentifiersRequest['identifiers'][number];
|
|
169
|
+
} & ({
|
|
170
|
+
success: true;
|
|
171
|
+
result: ResolveIdentifiersResponse['resolved'][number];
|
|
172
|
+
} | {
|
|
173
|
+
success: false;
|
|
174
|
+
error: Error;
|
|
175
|
+
});
|
|
153
176
|
/** Request body for POST /api/registry/catalog/disputes */
|
|
154
177
|
export type FileCatalogDisputeRequest = NonNullable<operations['fileCatalogDispute']['requestBody']>['content']['application/json'];
|
|
155
178
|
/** Response from POST /api/registry/catalog/disputes (200) */
|
|
@@ -150,6 +150,29 @@ export type ResolveIdentifiersRequest = Omit<RegistryResolveIdentifiersRequest,
|
|
|
150
150
|
};
|
|
151
151
|
/** Response from POST /api/registry/resolve (200) */
|
|
152
152
|
export type ResolveIdentifiersResponse = operations['resolveIdentifiers']['responses']['200']['content']['application/json'];
|
|
153
|
+
export interface ResolveIdentifiersAllOptions {
|
|
154
|
+
/** Per-request identifier cap, from 1 through the registry limit of 10,000. */
|
|
155
|
+
batchSize?: number;
|
|
156
|
+
/** UTF-8 JSON request cap including provenance and envelope. Default: 1 MiB. */
|
|
157
|
+
maxRequestBytes?: number;
|
|
158
|
+
/** Maximum simultaneous requests. Default: 2. */
|
|
159
|
+
concurrency?: number;
|
|
160
|
+
/** Retries for HTTP 429 only. Other failures are returned without retrying contributions. Default: 3. */
|
|
161
|
+
maxRetries?: number;
|
|
162
|
+
/** Maximum server-requested retry delay. Default: 60 seconds; longer rate limits return errors without early retries. */
|
|
163
|
+
maxRetryDelayMs?: number;
|
|
164
|
+
}
|
|
165
|
+
/** One outcome per input, in input order (including duplicates). */
|
|
166
|
+
export type ResolveIdentifierOutcome = {
|
|
167
|
+
index: number;
|
|
168
|
+
identifier: ResolveIdentifiersRequest['identifiers'][number];
|
|
169
|
+
} & ({
|
|
170
|
+
success: true;
|
|
171
|
+
result: ResolveIdentifiersResponse['resolved'][number];
|
|
172
|
+
} | {
|
|
173
|
+
success: false;
|
|
174
|
+
error: Error;
|
|
175
|
+
});
|
|
153
176
|
/** Request body for POST /api/registry/catalog/disputes */
|
|
154
177
|
export type FileCatalogDisputeRequest = NonNullable<operations['fileCatalogDispute']['requestBody']>['content']['application/json'];
|
|
155
178
|
/** Response from POST /api/registry/catalog/disputes (200) */
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
"source_sha": "4e553ad955f83b49c7d221ab5c3ff78237ad02e3",
|
|
5
5
|
"source_tarball_sha256": "580656d6466ef9f0d1119985e6726c2efea718dc671e2ad30957fcb2fd54af0f",
|
|
6
6
|
"upstream_adcp_version": "2.5.3",
|
|
7
|
-
"synced_at": "2026-10-
|
|
7
|
+
"synced_at": "2026-10-18T02:15:53.357Z"
|
|
8
8
|
}
|
|
@@ -149,8 +149,29 @@ function verifySignatureAsAuthenticator(options) {
|
|
|
149
149
|
return principal;
|
|
150
150
|
};
|
|
151
151
|
const tagged = (0, import_auth.tagAuthenticatorNeedsRawBody)(authenticator);
|
|
152
|
+
if (options.webBotAuth) webBotAuthProfiles.set(tagged, options.capability.profiles ?? []);
|
|
152
153
|
return options.webBotAuth ? (0, import_auth.tagAuthenticatorPresenceGated)(tagged) : tagged;
|
|
153
154
|
}
|
|
155
|
+
const webBotAuthProfiles = /* @__PURE__ */ new WeakMap();
|
|
156
|
+
function unsignedRequiredError(err, signatureAuth, body) {
|
|
157
|
+
const profiles = webBotAuthProfiles.get(signatureAuth);
|
|
158
|
+
if (!profiles?.includes("web-bot-auth") || !(err instanceof import_errors.RequestSignatureError) || err.code !== "request_signature_required")
|
|
159
|
+
return asSignatureAuthError(err);
|
|
160
|
+
const hasBody = body !== void 0 && body !== "";
|
|
161
|
+
if ((0, import_web_bot_auth2.advertisesOnlyWebBotAuth)(profiles)) {
|
|
162
|
+
const refused = (0, import_web_bot_auth2.unsignedWebBotAuthError)(hasBody);
|
|
163
|
+
return new import_auth.AuthError(refused.message, {
|
|
164
|
+
cause: refused,
|
|
165
|
+
status: refused.status,
|
|
166
|
+
headers: refused.headers,
|
|
167
|
+
body: refused.body
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return new import_auth.AuthError(`Signature rejected (${err.code}).`, {
|
|
171
|
+
cause: err,
|
|
172
|
+
headers: { "Accept-Signature": (0, import_web_bot_auth2.webBotAuthAcceptSignature)("sig1", hasBody) }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
154
175
|
function principalForVerifiedSigner(signer, makePrincipal) {
|
|
155
176
|
if (!SAFE_KEYID.test(signer.keyid)) {
|
|
156
177
|
throw new import_auth.AuthError("Signature key id contains unsupported characters.", {
|
|
@@ -207,7 +228,7 @@ function requireSignatureWhenPresent(signatureAuth, fallbackAuth, options = {})
|
|
|
207
228
|
try {
|
|
208
229
|
(0, import_verifier.assertUnsignedWebhookAuthenticationAbsent)(body);
|
|
209
230
|
} catch (err) {
|
|
210
|
-
throw
|
|
231
|
+
throw unsignedRequiredError(err, signatureAuth, body);
|
|
211
232
|
}
|
|
212
233
|
let fallbackResult = null;
|
|
213
234
|
let fallbackError;
|
|
@@ -223,7 +244,7 @@ function requireSignatureWhenPresent(signatureAuth, fallbackAuth, options = {})
|
|
|
223
244
|
(0, import_verifier.assertUnsignedOperationNotRequired)(operation, unsignedCapability);
|
|
224
245
|
(0, import_verifier.assertUnsignedProtocolMethodNotRequired)(body, unsignedCapability);
|
|
225
246
|
} catch (err) {
|
|
226
|
-
throw
|
|
247
|
+
throw unsignedRequiredError(err, signatureAuth, body);
|
|
227
248
|
}
|
|
228
249
|
if (fallbackThrew) throw fallbackError;
|
|
229
250
|
return null;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { getWebBotAuthLegacyOriginGate } from "../signing/web-bot-auth.mjs";
|
|
2
2
|
import { RequestSignatureError } from "../signing/errors.mjs";
|
|
3
3
|
import {
|
|
4
|
+
advertisesOnlyWebBotAuth,
|
|
4
5
|
selectSignatureProfile,
|
|
6
|
+
unsignedWebBotAuthError,
|
|
5
7
|
verifyWebBotAuth,
|
|
8
|
+
webBotAuthAcceptSignature,
|
|
6
9
|
isWebBotAuthRevocationChecked,
|
|
7
10
|
webBotAuthUnavailableResponse,
|
|
8
11
|
WebBotAuthUnavailableError,
|
|
@@ -146,8 +149,29 @@ function verifySignatureAsAuthenticator(options) {
|
|
|
146
149
|
return principal;
|
|
147
150
|
};
|
|
148
151
|
const tagged = tagAuthenticatorNeedsRawBody(authenticator);
|
|
152
|
+
if (options.webBotAuth) webBotAuthProfiles.set(tagged, options.capability.profiles ?? []);
|
|
149
153
|
return options.webBotAuth ? tagAuthenticatorPresenceGated(tagged) : tagged;
|
|
150
154
|
}
|
|
155
|
+
const webBotAuthProfiles = /* @__PURE__ */ new WeakMap();
|
|
156
|
+
function unsignedRequiredError(err, signatureAuth, body) {
|
|
157
|
+
const profiles = webBotAuthProfiles.get(signatureAuth);
|
|
158
|
+
if (!profiles?.includes("web-bot-auth") || !(err instanceof RequestSignatureError) || err.code !== "request_signature_required")
|
|
159
|
+
return asSignatureAuthError(err);
|
|
160
|
+
const hasBody = body !== void 0 && body !== "";
|
|
161
|
+
if (advertisesOnlyWebBotAuth(profiles)) {
|
|
162
|
+
const refused = unsignedWebBotAuthError(hasBody);
|
|
163
|
+
return new AuthError(refused.message, {
|
|
164
|
+
cause: refused,
|
|
165
|
+
status: refused.status,
|
|
166
|
+
headers: refused.headers,
|
|
167
|
+
body: refused.body
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return new AuthError(`Signature rejected (${err.code}).`, {
|
|
171
|
+
cause: err,
|
|
172
|
+
headers: { "Accept-Signature": webBotAuthAcceptSignature("sig1", hasBody) }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
151
175
|
function principalForVerifiedSigner(signer, makePrincipal) {
|
|
152
176
|
if (!SAFE_KEYID.test(signer.keyid)) {
|
|
153
177
|
throw new AuthError("Signature key id contains unsupported characters.", {
|
|
@@ -204,7 +228,7 @@ function requireSignatureWhenPresent(signatureAuth, fallbackAuth, options = {})
|
|
|
204
228
|
try {
|
|
205
229
|
assertUnsignedWebhookAuthenticationAbsent(body);
|
|
206
230
|
} catch (err) {
|
|
207
|
-
throw
|
|
231
|
+
throw unsignedRequiredError(err, signatureAuth, body);
|
|
208
232
|
}
|
|
209
233
|
let fallbackResult = null;
|
|
210
234
|
let fallbackError;
|
|
@@ -220,7 +244,7 @@ function requireSignatureWhenPresent(signatureAuth, fallbackAuth, options = {})
|
|
|
220
244
|
assertUnsignedOperationNotRequired(operation, unsignedCapability);
|
|
221
245
|
assertUnsignedProtocolMethodNotRequired(body, unsignedCapability);
|
|
222
246
|
} catch (err) {
|
|
223
|
-
throw
|
|
247
|
+
throw unsignedRequiredError(err, signatureAuth, body);
|
|
224
248
|
}
|
|
225
249
|
if (fallbackThrew) throw fallbackError;
|
|
226
250
|
return null;
|
|
@@ -346,6 +346,12 @@ export interface RespondUnauthorizedOptions {
|
|
|
346
346
|
* directly.
|
|
347
347
|
*/
|
|
348
348
|
signatureError?: RequestSignatureErrorCode;
|
|
349
|
+
/**
|
|
350
|
+
* Additional response headers, such as the Web Bot Auth `Accept-Signature`
|
|
351
|
+
* challenge added to an AdCP RFC 9421 `request_signature_required` answer.
|
|
352
|
+
* Cannot replace `WWW-Authenticate` or `Content-Type`.
|
|
353
|
+
*/
|
|
354
|
+
headers?: Record<string, string>;
|
|
349
355
|
}
|
|
350
356
|
/**
|
|
351
357
|
* Send an RFC 6750-compliant 401/403 response with the `WWW-Authenticate`
|
|
@@ -346,6 +346,12 @@ export interface RespondUnauthorizedOptions {
|
|
|
346
346
|
* directly.
|
|
347
347
|
*/
|
|
348
348
|
signatureError?: RequestSignatureErrorCode;
|
|
349
|
+
/**
|
|
350
|
+
* Additional response headers, such as the Web Bot Auth `Accept-Signature`
|
|
351
|
+
* challenge added to an AdCP RFC 9421 `request_signature_required` answer.
|
|
352
|
+
* Cannot replace `WWW-Authenticate` or `Content-Type`.
|
|
353
|
+
*/
|
|
354
|
+
headers?: Record<string, string>;
|
|
349
355
|
}
|
|
350
356
|
/**
|
|
351
357
|
* Send an RFC 6750-compliant 401/403 response with the `WWW-Authenticate`
|
package/dist/lib/server/auth.js
CHANGED
|
@@ -247,7 +247,13 @@ function respondUnauthorized(_req, res, options = {}) {
|
|
|
247
247
|
if (options.errorDescription) parts.push(`error_description="${escapeQuotes(options.errorDescription)}"`);
|
|
248
248
|
if (options.resourceMetadata) parts.push(`resource_metadata="${escapeQuotes(options.resourceMetadata)}"`);
|
|
249
249
|
const scheme = useSignatureChallenge ? "Signature" : "Bearer";
|
|
250
|
+
const extra = Object.fromEntries(
|
|
251
|
+
Object.entries(options.headers ?? {}).filter(
|
|
252
|
+
([name]) => !["content-type", "www-authenticate"].includes(name.toLowerCase())
|
|
253
|
+
)
|
|
254
|
+
);
|
|
250
255
|
res.writeHead(options.status ?? 401, {
|
|
256
|
+
...extra,
|
|
251
257
|
"Content-Type": "application/json",
|
|
252
258
|
"WWW-Authenticate": `${scheme} ${parts.join(", ")}`
|
|
253
259
|
});
|