@adcp/sdk 16.0.0-beta.1 → 16.0.0-beta.2
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/lib/core/SingleAgentClient.d.mts +10 -1
- package/dist/lib/core/SingleAgentClient.d.ts +10 -1
- package/dist/lib/core/SingleAgentClient.js +29 -8
- package/dist/lib/core/SingleAgentClient.mjs +29 -8
- package/dist/lib/identity-3.3/generated-compat.d.mts +34 -0
- package/dist/lib/identity-3.3/generated-compat.d.ts +34 -0
- package/dist/lib/identity-3.3/generated-compat.js +16 -0
- package/dist/lib/identity-3.3/generated-compat.mjs +0 -0
- package/dist/lib/identity-3.3/index.d.mts +2 -2
- package/dist/lib/identity-3.3/index.d.ts +2 -2
- package/dist/lib/identity-3.3/index.js +4 -0
- package/dist/lib/identity-3.3/index.mjs +5 -1
- package/dist/lib/identity-3.3/lifecycle.d.mts +17 -5
- package/dist/lib/identity-3.3/lifecycle.d.ts +17 -5
- package/dist/lib/identity-3.3/lifecycle.js +32 -5
- package/dist/lib/identity-3.3/lifecycle.mjs +38 -6
- package/dist/lib/identity-3.3/revocation.d.mts +39 -3
- package/dist/lib/identity-3.3/revocation.d.ts +39 -3
- package/dist/lib/identity-3.3/revocation.js +88 -18
- package/dist/lib/identity-3.3/revocation.mjs +89 -19
- package/dist/lib/identity-3.3/server.d.mts +12 -0
- package/dist/lib/identity-3.3/server.d.ts +12 -0
- package/dist/lib/identity-3.3/server.js +103 -43
- package/dist/lib/identity-3.3/server.mjs +111 -45
- package/dist/lib/identity-3.3/trust.d.mts +62 -1
- package/dist/lib/identity-3.3/trust.d.ts +62 -1
- package/dist/lib/identity-3.3/trust.js +126 -21
- package/dist/lib/identity-3.3/trust.mjs +124 -21
- package/dist/lib/identity-3.3/types.d.mts +39 -11
- package/dist/lib/identity-3.3/types.d.ts +39 -11
- package/dist/lib/index.d.mts +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +2 -0
- package/dist/lib/index.mjs +2 -0
- package/dist/lib/protocols/index.d.mts +1 -1
- package/dist/lib/protocols/index.d.ts +1 -1
- package/dist/lib/protocols/index.js +2 -2
- package/dist/lib/protocols/index.mjs +2 -2
- package/dist/lib/reporting/bigquery/index.js +8 -0
- package/dist/lib/reporting/bigquery/index.mjs +8 -0
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/auth-signature.d.mts +6 -1
- package/dist/lib/server/auth-signature.d.ts +6 -1
- package/dist/lib/server/auth-signature.js +24 -13
- package/dist/lib/server/auth-signature.mjs +29 -13
- package/dist/lib/server/auth.d.mts +2 -2
- package/dist/lib/server/auth.d.ts +2 -2
- package/dist/lib/server/create-adcp-server.js +31 -10
- package/dist/lib/server/create-adcp-server.mjs +37 -11
- package/dist/lib/signing/crypto.js +8 -0
- package/dist/lib/signing/crypto.mjs +9 -1
- package/dist/lib/signing/fetch-async.js +1 -1
- package/dist/lib/signing/fetch-async.mjs +1 -1
- package/dist/lib/signing/fetch.js +1 -1
- package/dist/lib/signing/fetch.mjs +1 -1
- package/dist/lib/signing/middleware.js +9 -0
- package/dist/lib/signing/middleware.mjs +14 -0
- package/dist/lib/signing/parser.d.mts +8 -5
- package/dist/lib/signing/parser.d.ts +8 -5
- package/dist/lib/signing/parser.js +77 -61
- package/dist/lib/signing/parser.mjs +77 -61
- package/dist/lib/signing/revocation.js +3 -0
- package/dist/lib/signing/revocation.mjs +3 -0
- package/dist/lib/signing/verifier.js +1 -1
- package/dist/lib/signing/verifier.mjs +1 -1
- package/dist/lib/signing/web-bot-auth.d.mts +84 -10
- package/dist/lib/signing/web-bot-auth.d.ts +84 -10
- package/dist/lib/signing/web-bot-auth.js +259 -82
- package/dist/lib/signing/web-bot-auth.mjs +259 -87
- package/dist/lib/signing/webhook-verifier.js +1 -1
- package/dist/lib/signing/webhook-verifier.mjs +1 -1
- package/dist/lib/testing/storyboard/validations.d.mts +1 -1
- package/dist/lib/testing/storyboard/validations.d.ts +1 -1
- package/dist/lib/v2/projection/catalog-snapshot.js +3 -1
- package/dist/lib/v2/projection/catalog-snapshot.mjs +3 -1
- package/dist/lib/v2/projection/creative-delivery.d.mts +31 -9
- package/dist/lib/v2/projection/creative-delivery.d.ts +31 -9
- package/dist/lib/v2/projection/index.d.mts +1 -1
- package/dist/lib/v2/projection/index.d.ts +1 -1
- package/dist/lib/v2/projection/index.js +2 -0
- package/dist/lib/v2/projection/index.mjs +2 -0
- package/dist/lib/v2/projection/legacy-routes.d.mts +10 -0
- package/dist/lib/v2/projection/legacy-routes.d.ts +10 -0
- package/dist/lib/v2/projection/legacy-routes.js +7 -0
- package/dist/lib/v2/projection/legacy-routes.mjs +6 -0
- 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/REPORTING-LEDGER.md +15 -5
- package/docs/guides/REPORTING-OPERATIONS.md +17 -11
- package/docs/llms.txt +2 -2
- package/docs/migration-12-to-13.md +8 -0
- package/docs/migration-14.x-rc-worksheet.md +4 -4
- package/docs/migration-agent-identity-3.3.md +150 -11
- package/package.json +3 -1
|
@@ -30,6 +30,8 @@ var trust_exports = {};
|
|
|
30
30
|
__export(trust_exports, {
|
|
31
31
|
IdentityAuthorizationError: () => IdentityAuthorizationError,
|
|
32
32
|
TrustEvaluator: () => TrustEvaluator,
|
|
33
|
+
TrustMembershipError: () => TrustMembershipError,
|
|
34
|
+
TrustRecordUnavailableError: () => TrustRecordUnavailableError,
|
|
33
35
|
authorizeDemandGrant: () => authorizeDemandGrant,
|
|
34
36
|
createTrustRecord: () => createTrustRecord,
|
|
35
37
|
validateIdentity33Schema: () => validateIdentity33Schema,
|
|
@@ -42,6 +44,7 @@ var import_net = require('../net/index.js');
|
|
|
42
44
|
var import_canonicalize = require('../signing/agent-resolver/canonicalize.js');
|
|
43
45
|
var import_etld = require('../signing/agent-resolver/etld.js');
|
|
44
46
|
var import_strict_json = require('../signing/agent-resolver/strict-json.js');
|
|
47
|
+
var import_tldts = require("tldts");
|
|
45
48
|
var import_schemas = __toESM(require('./schemas.draft.js'));
|
|
46
49
|
const ajv = new import_ajv.default({ strict: false, allErrors: false });
|
|
47
50
|
(0, import_ajv_formats.default)(ajv);
|
|
@@ -58,6 +61,39 @@ class IdentityAuthorizationError extends Error {
|
|
|
58
61
|
this.details = reason === "grant_absent" ? { scope: "agent", reason } : void 0;
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
class TrustRecordUnavailableError extends IdentityAuthorizationError {
|
|
65
|
+
transient = true;
|
|
66
|
+
/**
|
|
67
|
+
* Set by `match()`: the last record read for this operator listed the origin
|
|
68
|
+
* with the role and is still within the seller's outage bound
|
|
69
|
+
* (`maxOutageContinuationSeconds` past its freshness). Only then may non-spend
|
|
70
|
+
* calls continue under a binding.
|
|
71
|
+
*/
|
|
72
|
+
lastKnownListed = false;
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
75
|
+
this.name = "TrustRecordUnavailableError";
|
|
76
|
+
this.message = "Agent trust record is temporarily unavailable";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
class TrustMembershipError extends IdentityAuthorizationError {
|
|
80
|
+
constructor(membership = "unlisted") {
|
|
81
|
+
super();
|
|
82
|
+
this.membership = membership;
|
|
83
|
+
this.name = "TrustMembershipError";
|
|
84
|
+
}
|
|
85
|
+
membership;
|
|
86
|
+
}
|
|
87
|
+
function unavailable(cause) {
|
|
88
|
+
const error = new TrustRecordUnavailableError();
|
|
89
|
+
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause });
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
class TrustHostCapacityError extends Error {
|
|
93
|
+
}
|
|
94
|
+
const FOREIGN_HOST_BUDGET = "\0foreign";
|
|
95
|
+
const OWN_HOST_REQUESTS_PER_MINUTE = 10;
|
|
96
|
+
const FOREIGN_HOST_REQUESTS_PER_MINUTE = 30;
|
|
61
97
|
function failure(cause) {
|
|
62
98
|
const error = new IdentityAuthorizationError();
|
|
63
99
|
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause });
|
|
@@ -82,6 +118,9 @@ class TrustEvaluator {
|
|
|
82
118
|
constructor(options = {}) {
|
|
83
119
|
this.options = options;
|
|
84
120
|
this.pollingIntervalSeconds = options.pollingIntervalSeconds ?? 300;
|
|
121
|
+
this.maxOutageContinuationSeconds = options.maxOutageContinuationSeconds ?? 4 * this.pollingIntervalSeconds;
|
|
122
|
+
if (!Number.isFinite(this.maxOutageContinuationSeconds) || this.maxOutageContinuationSeconds < 0)
|
|
123
|
+
throw new TypeError("maxOutageContinuationSeconds must be a finite, non-negative number");
|
|
85
124
|
if (!Number.isFinite(this.pollingIntervalSeconds) || this.pollingIntervalSeconds < 60 || this.pollingIntervalSeconds > 1800)
|
|
86
125
|
throw new TypeError("Trust polling interval must be 60\u20131800 seconds");
|
|
87
126
|
}
|
|
@@ -90,10 +129,14 @@ class TrustEvaluator {
|
|
|
90
129
|
negative = /* @__PURE__ */ new Map();
|
|
91
130
|
pending = /* @__PURE__ */ new Map();
|
|
92
131
|
urlRequests = /* @__PURE__ */ new Map();
|
|
132
|
+
/** Keyed by (operator, host): one operator's authoritative_location hops never spend another's budget. */
|
|
93
133
|
hostRates = /* @__PURE__ */ new Map();
|
|
94
134
|
authoritativeOperators = /* @__PURE__ */ new Set();
|
|
95
|
-
|
|
135
|
+
/** Remembered authoritative operators per budget group (see authorityGroup). */
|
|
136
|
+
authorityGroups = /* @__PURE__ */ new Map();
|
|
137
|
+
privateAuthorityOperators = 0;
|
|
96
138
|
pollingIntervalSeconds;
|
|
139
|
+
maxOutageContinuationSeconds;
|
|
97
140
|
async get(domainOrOrigin, force = false) {
|
|
98
141
|
let operator;
|
|
99
142
|
try {
|
|
@@ -104,10 +147,10 @@ class TrustEvaluator {
|
|
|
104
147
|
const now = this.now();
|
|
105
148
|
const cached = this.cache.get(operator);
|
|
106
149
|
if (!force && cached && cached.freshUntil > now) return cached;
|
|
107
|
-
if ((this.negative.get(operator) ?? 0) > now)
|
|
150
|
+
if ((this.negative.get(operator) ?? 0) > now) unavailable();
|
|
108
151
|
let pending = this.pending.get(operator);
|
|
109
152
|
if (!pending) {
|
|
110
|
-
if (this.pending.size >= (this.options.maxConcurrentFetches ?? 16))
|
|
153
|
+
if (this.pending.size >= (this.options.maxConcurrentFetches ?? 16)) unavailable();
|
|
111
154
|
pending = this.fetch(operator, now);
|
|
112
155
|
this.pending.set(operator, pending);
|
|
113
156
|
}
|
|
@@ -119,11 +162,20 @@ class TrustEvaluator {
|
|
|
119
162
|
}
|
|
120
163
|
async match(origin, role, countries) {
|
|
121
164
|
const canonical = (0, import_canonicalize.canonicalAgentOrigin)(origin);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
165
|
+
let result;
|
|
166
|
+
try {
|
|
167
|
+
result = await this.get(canonical);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (!(error instanceof TrustRecordUnavailableError)) throw error;
|
|
170
|
+
const last = this.cache.get((0, import_etld.eTldPlusOne)(canonical));
|
|
171
|
+
if (last && !listedAgent(last, canonical, role))
|
|
172
|
+
throw new TrustMembershipError(last.status === "present" ? "unlisted" : "absent");
|
|
173
|
+
error.lastKnownListed = !!last && this.now() <= last.freshUntil + this.maxOutageContinuationSeconds;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
const agent = listedAgent(result, canonical, role);
|
|
177
|
+
if (result.status !== "present") throw new TrustMembershipError("absent");
|
|
178
|
+
if (!agent) throw new TrustMembershipError();
|
|
127
179
|
if (agent.countries && countries?.length && countries.some((country) => !agent.countries.includes(country)))
|
|
128
180
|
failure();
|
|
129
181
|
return { operator: result.operator, agent, record: result.record, freshUntil: result.freshUntil };
|
|
@@ -131,20 +183,55 @@ class TrustEvaluator {
|
|
|
131
183
|
invalidate(domain) {
|
|
132
184
|
this.cache.delete((0, import_etld.eTldPlusOne)(domain));
|
|
133
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Record that `operator` served a trust record. With durable history the
|
|
188
|
+
* in-memory set is only a cache and may evict; without it, nothing is
|
|
189
|
+
* evicted and an operator over its group budget or the total is refused.
|
|
190
|
+
*/
|
|
191
|
+
rememberAuthority(operator, durable) {
|
|
192
|
+
if (this.authoritativeOperators.has(operator)) return true;
|
|
193
|
+
const max = this.options.maxEntries ?? 1e3;
|
|
194
|
+
if (durable) {
|
|
195
|
+
if (this.authoritativeOperators.size >= max)
|
|
196
|
+
this.authoritativeOperators.delete(this.authoritativeOperators.values().next().value);
|
|
197
|
+
this.authoritativeOperators.add(operator);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
const group = authorityGroup(operator);
|
|
201
|
+
const hosted = group !== operator;
|
|
202
|
+
if (this.authoritativeOperators.size >= max || hosted && (this.privateAuthorityOperators >= (this.options.maxPrivateAuthoritativeOperators ?? Math.floor(max / 2)) || (this.authorityGroups.get(group) ?? 0) >= (this.options.maxAuthoritativeOperatorsPerPrivateSuffix ?? 32)))
|
|
203
|
+
return false;
|
|
204
|
+
this.authoritativeOperators.add(operator);
|
|
205
|
+
if (hosted) {
|
|
206
|
+
this.privateAuthorityOperators++;
|
|
207
|
+
this.authorityGroups.set(group, (this.authorityGroups.get(group) ?? 0) + 1);
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Process-local view only. With durable `authorityHistory` this set is a
|
|
213
|
+
* cache and may have evicted an operator that durable history still records.
|
|
214
|
+
*/
|
|
134
215
|
hasAuthoritativeRecord(domain) {
|
|
135
216
|
return this.authoritativeOperators.has((0, import_etld.eTldPlusOne)(domain));
|
|
136
217
|
}
|
|
137
218
|
now() {
|
|
138
219
|
return (this.options.now ?? (() => Date.now() / 1e3))();
|
|
139
220
|
}
|
|
140
|
-
async request(url) {
|
|
221
|
+
async request(url, operator) {
|
|
141
222
|
const parsed = new URL(url);
|
|
142
223
|
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) failure();
|
|
143
|
-
|
|
224
|
+
let own = false;
|
|
225
|
+
try {
|
|
226
|
+
own = (0, import_etld.eTldPlusOne)(parsed.hostname) === operator;
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
const host = JSON.stringify([own ? operator : FOREIGN_HOST_BUDGET, parsed.hostname]);
|
|
230
|
+
const limit = own ? OWN_HOST_REQUESTS_PER_MINUTE : FOREIGN_HOST_REQUESTS_PER_MINUTE;
|
|
144
231
|
const pending = this.urlRequests.get(url);
|
|
145
232
|
if (pending) return pending;
|
|
146
233
|
const now = this.now(), rate = this.hostRates.get(host);
|
|
147
|
-
if (rate && now - rate.started < 60 && rate.count >=
|
|
234
|
+
if (rate && now - rate.started < 60 && rate.count >= limit) throw new TrustHostCapacityError();
|
|
148
235
|
if (!rate && this.hostRates.size >= (this.options.maxEntries ?? 1e3))
|
|
149
236
|
this.hostRates.delete(this.hostRates.keys().next().value);
|
|
150
237
|
this.hostRates.set(
|
|
@@ -166,24 +253,24 @@ class TrustEvaluator {
|
|
|
166
253
|
}
|
|
167
254
|
async fetch(operator, now) {
|
|
168
255
|
try {
|
|
169
|
-
let response = await this.request(`https://${operator}/.well-known/trust.json
|
|
256
|
+
let response = await this.request(`https://${operator}/.well-known/trust.json`, operator);
|
|
170
257
|
let result;
|
|
171
258
|
if (response.status === 404 || response.status === 410) {
|
|
172
|
-
if (this.
|
|
259
|
+
if (this.authoritativeOperators.has(operator) || await this.options.authorityHistory?.has(operator))
|
|
173
260
|
failure();
|
|
174
261
|
result = { status: "absent", operator, checkedAt: now, freshUntil: now + 60 };
|
|
175
262
|
} else {
|
|
176
263
|
if (response.status !== 200) failure();
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
this.
|
|
264
|
+
if (this.options.authorityHistory) {
|
|
265
|
+
await this.options.authorityHistory.mark(operator);
|
|
266
|
+
this.rememberAuthority(operator, true);
|
|
267
|
+
} else if (!this.rememberAuthority(operator, false)) {
|
|
268
|
+
failure();
|
|
180
269
|
}
|
|
181
|
-
await this.options.authorityHistory?.mark(operator);
|
|
182
|
-
this.authoritativeOperators.add(operator);
|
|
183
270
|
let record = (0, import_strict_json.parseStrictJson)(new TextDecoder("utf-8", { fatal: true }).decode(response.body), 32);
|
|
184
271
|
validateTrustRecord(record);
|
|
185
272
|
if (record.authoritative_location) {
|
|
186
|
-
response = await this.request(record.authoritative_location);
|
|
273
|
+
response = await this.request(record.authoritative_location, operator);
|
|
187
274
|
if (response.status !== 200) failure();
|
|
188
275
|
record = (0, import_strict_json.parseStrictJson)(new TextDecoder("utf-8", { fatal: true }).decode(response.body), 32);
|
|
189
276
|
validateTrustRecord(record);
|
|
@@ -203,13 +290,24 @@ class TrustEvaluator {
|
|
|
203
290
|
this.negative.delete(operator);
|
|
204
291
|
return result;
|
|
205
292
|
} catch (cause) {
|
|
293
|
+
if (cause instanceof TrustHostCapacityError) return unavailable(cause);
|
|
206
294
|
if (this.negative.size >= (this.options.maxEntries ?? 1e3))
|
|
207
295
|
this.negative.delete(this.negative.keys().next().value);
|
|
208
296
|
this.negative.set(operator, now + 60);
|
|
209
|
-
return
|
|
297
|
+
return unavailable(cause);
|
|
210
298
|
}
|
|
211
299
|
}
|
|
212
300
|
}
|
|
301
|
+
function listedAgent(result, origin, role) {
|
|
302
|
+
if (result.status !== "present") return void 0;
|
|
303
|
+
const entries = (result.record.agents ?? []).filter((agent) => (0, import_canonicalize.canonicalAgentOrigin)(agent.url) === origin);
|
|
304
|
+
return entries.length === 1 && entries[0].roles.includes(role) ? entries[0] : void 0;
|
|
305
|
+
}
|
|
306
|
+
function authorityGroup(operator) {
|
|
307
|
+
if (!(0, import_tldts.parse)(operator, { allowPrivateDomains: true }).isPrivate) return operator;
|
|
308
|
+
const provider = (0, import_tldts.parse)(operator).domain;
|
|
309
|
+
return `private:${provider ?? operator}`;
|
|
310
|
+
}
|
|
213
311
|
function grantMatches(grant, target, claim, now) {
|
|
214
312
|
if ((0, import_etld.eTldPlusOne)(grant.grantee) !== target) return false;
|
|
215
313
|
if (!grant.scopes.some(
|
|
@@ -251,6 +349,11 @@ async function authorizeDemandGrant(claim, options) {
|
|
|
251
349
|
}
|
|
252
350
|
const first = (record.record.grants ?? []).filter((g) => grantMatches(g, account, grantClaim, now));
|
|
253
351
|
if (account === operator && first.length) return result("trust", [brand, operator], [first[0]]);
|
|
352
|
+
const selfOperated = account === brand || account === authoritativeBrand;
|
|
353
|
+
if (selfOperated && account !== operator) {
|
|
354
|
+
const direct = (record.record.grants ?? []).find((g) => grantMatches(g, operator, grantClaim, now));
|
|
355
|
+
if (direct) return result("trust", [brand, operator], [direct]);
|
|
356
|
+
}
|
|
254
357
|
if (account !== operator && account !== brand && first.length) {
|
|
255
358
|
const intermediary = await options.evaluator.get(account);
|
|
256
359
|
if (intermediary.status === "present") {
|
|
@@ -259,7 +362,7 @@ async function authorizeDemandGrant(claim, options) {
|
|
|
259
362
|
}
|
|
260
363
|
}
|
|
261
364
|
const covered = (record.record.grants ?? []).some(
|
|
262
|
-
(g) => (0, import_etld.eTldPlusOne)(g.grantee) === account && g.scopes.some(
|
|
365
|
+
(g) => ((0, import_etld.eTldPlusOne)(g.grantee) === account || selfOperated && (0, import_etld.eTldPlusOne)(g.grantee) === operator) && g.scopes.some(
|
|
263
366
|
(scope) => scope === "*" || scope === claim.scope || scope === "adcp:all" && claim.scope.startsWith("adcp:")
|
|
264
367
|
)
|
|
265
368
|
);
|
|
@@ -271,6 +374,8 @@ async function authorizeDemandGrant(claim, options) {
|
|
|
271
374
|
0 && (module.exports = {
|
|
272
375
|
IdentityAuthorizationError,
|
|
273
376
|
TrustEvaluator,
|
|
377
|
+
TrustMembershipError,
|
|
378
|
+
TrustRecordUnavailableError,
|
|
274
379
|
authorizeDemandGrant,
|
|
275
380
|
createTrustRecord,
|
|
276
381
|
validateIdentity33Schema,
|
|
@@ -4,6 +4,7 @@ import { ssrfSafeFetch } from "../net/index.mjs";
|
|
|
4
4
|
import { canonicalAgentOrigin } from "../signing/agent-resolver/canonicalize.mjs";
|
|
5
5
|
import { eTldPlusOne } from "../signing/agent-resolver/etld.mjs";
|
|
6
6
|
import { parseStrictJson } from "../signing/agent-resolver/strict-json.mjs";
|
|
7
|
+
import { parse as parseDomain } from "tldts";
|
|
7
8
|
import schemas from "./schemas.draft.mjs";
|
|
8
9
|
const ajv = new Ajv({ strict: false, allErrors: false });
|
|
9
10
|
addFormats(ajv);
|
|
@@ -20,6 +21,39 @@ class IdentityAuthorizationError extends Error {
|
|
|
20
21
|
this.details = reason === "grant_absent" ? { scope: "agent", reason } : void 0;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
class TrustRecordUnavailableError extends IdentityAuthorizationError {
|
|
25
|
+
transient = true;
|
|
26
|
+
/**
|
|
27
|
+
* Set by `match()`: the last record read for this operator listed the origin
|
|
28
|
+
* with the role and is still within the seller's outage bound
|
|
29
|
+
* (`maxOutageContinuationSeconds` past its freshness). Only then may non-spend
|
|
30
|
+
* calls continue under a binding.
|
|
31
|
+
*/
|
|
32
|
+
lastKnownListed = false;
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
this.name = "TrustRecordUnavailableError";
|
|
36
|
+
this.message = "Agent trust record is temporarily unavailable";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
class TrustMembershipError extends IdentityAuthorizationError {
|
|
40
|
+
constructor(membership = "unlisted") {
|
|
41
|
+
super();
|
|
42
|
+
this.membership = membership;
|
|
43
|
+
this.name = "TrustMembershipError";
|
|
44
|
+
}
|
|
45
|
+
membership;
|
|
46
|
+
}
|
|
47
|
+
function unavailable(cause) {
|
|
48
|
+
const error = new TrustRecordUnavailableError();
|
|
49
|
+
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
class TrustHostCapacityError extends Error {
|
|
53
|
+
}
|
|
54
|
+
const FOREIGN_HOST_BUDGET = "\0foreign";
|
|
55
|
+
const OWN_HOST_REQUESTS_PER_MINUTE = 10;
|
|
56
|
+
const FOREIGN_HOST_REQUESTS_PER_MINUTE = 30;
|
|
23
57
|
function failure(cause) {
|
|
24
58
|
const error = new IdentityAuthorizationError();
|
|
25
59
|
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause });
|
|
@@ -44,6 +78,9 @@ class TrustEvaluator {
|
|
|
44
78
|
constructor(options = {}) {
|
|
45
79
|
this.options = options;
|
|
46
80
|
this.pollingIntervalSeconds = options.pollingIntervalSeconds ?? 300;
|
|
81
|
+
this.maxOutageContinuationSeconds = options.maxOutageContinuationSeconds ?? 4 * this.pollingIntervalSeconds;
|
|
82
|
+
if (!Number.isFinite(this.maxOutageContinuationSeconds) || this.maxOutageContinuationSeconds < 0)
|
|
83
|
+
throw new TypeError("maxOutageContinuationSeconds must be a finite, non-negative number");
|
|
47
84
|
if (!Number.isFinite(this.pollingIntervalSeconds) || this.pollingIntervalSeconds < 60 || this.pollingIntervalSeconds > 1800)
|
|
48
85
|
throw new TypeError("Trust polling interval must be 60\u20131800 seconds");
|
|
49
86
|
}
|
|
@@ -52,10 +89,14 @@ class TrustEvaluator {
|
|
|
52
89
|
negative = /* @__PURE__ */ new Map();
|
|
53
90
|
pending = /* @__PURE__ */ new Map();
|
|
54
91
|
urlRequests = /* @__PURE__ */ new Map();
|
|
92
|
+
/** Keyed by (operator, host): one operator's authoritative_location hops never spend another's budget. */
|
|
55
93
|
hostRates = /* @__PURE__ */ new Map();
|
|
56
94
|
authoritativeOperators = /* @__PURE__ */ new Set();
|
|
57
|
-
|
|
95
|
+
/** Remembered authoritative operators per budget group (see authorityGroup). */
|
|
96
|
+
authorityGroups = /* @__PURE__ */ new Map();
|
|
97
|
+
privateAuthorityOperators = 0;
|
|
58
98
|
pollingIntervalSeconds;
|
|
99
|
+
maxOutageContinuationSeconds;
|
|
59
100
|
async get(domainOrOrigin, force = false) {
|
|
60
101
|
let operator;
|
|
61
102
|
try {
|
|
@@ -66,10 +107,10 @@ class TrustEvaluator {
|
|
|
66
107
|
const now = this.now();
|
|
67
108
|
const cached = this.cache.get(operator);
|
|
68
109
|
if (!force && cached && cached.freshUntil > now) return cached;
|
|
69
|
-
if ((this.negative.get(operator) ?? 0) > now)
|
|
110
|
+
if ((this.negative.get(operator) ?? 0) > now) unavailable();
|
|
70
111
|
let pending = this.pending.get(operator);
|
|
71
112
|
if (!pending) {
|
|
72
|
-
if (this.pending.size >= (this.options.maxConcurrentFetches ?? 16))
|
|
113
|
+
if (this.pending.size >= (this.options.maxConcurrentFetches ?? 16)) unavailable();
|
|
73
114
|
pending = this.fetch(operator, now);
|
|
74
115
|
this.pending.set(operator, pending);
|
|
75
116
|
}
|
|
@@ -81,11 +122,20 @@ class TrustEvaluator {
|
|
|
81
122
|
}
|
|
82
123
|
async match(origin, role, countries) {
|
|
83
124
|
const canonical = canonicalAgentOrigin(origin);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
125
|
+
let result;
|
|
126
|
+
try {
|
|
127
|
+
result = await this.get(canonical);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (!(error instanceof TrustRecordUnavailableError)) throw error;
|
|
130
|
+
const last = this.cache.get(eTldPlusOne(canonical));
|
|
131
|
+
if (last && !listedAgent(last, canonical, role))
|
|
132
|
+
throw new TrustMembershipError(last.status === "present" ? "unlisted" : "absent");
|
|
133
|
+
error.lastKnownListed = !!last && this.now() <= last.freshUntil + this.maxOutageContinuationSeconds;
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const agent = listedAgent(result, canonical, role);
|
|
137
|
+
if (result.status !== "present") throw new TrustMembershipError("absent");
|
|
138
|
+
if (!agent) throw new TrustMembershipError();
|
|
89
139
|
if (agent.countries && countries?.length && countries.some((country) => !agent.countries.includes(country)))
|
|
90
140
|
failure();
|
|
91
141
|
return { operator: result.operator, agent, record: result.record, freshUntil: result.freshUntil };
|
|
@@ -93,20 +143,55 @@ class TrustEvaluator {
|
|
|
93
143
|
invalidate(domain) {
|
|
94
144
|
this.cache.delete(eTldPlusOne(domain));
|
|
95
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Record that `operator` served a trust record. With durable history the
|
|
148
|
+
* in-memory set is only a cache and may evict; without it, nothing is
|
|
149
|
+
* evicted and an operator over its group budget or the total is refused.
|
|
150
|
+
*/
|
|
151
|
+
rememberAuthority(operator, durable) {
|
|
152
|
+
if (this.authoritativeOperators.has(operator)) return true;
|
|
153
|
+
const max = this.options.maxEntries ?? 1e3;
|
|
154
|
+
if (durable) {
|
|
155
|
+
if (this.authoritativeOperators.size >= max)
|
|
156
|
+
this.authoritativeOperators.delete(this.authoritativeOperators.values().next().value);
|
|
157
|
+
this.authoritativeOperators.add(operator);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
const group = authorityGroup(operator);
|
|
161
|
+
const hosted = group !== operator;
|
|
162
|
+
if (this.authoritativeOperators.size >= max || hosted && (this.privateAuthorityOperators >= (this.options.maxPrivateAuthoritativeOperators ?? Math.floor(max / 2)) || (this.authorityGroups.get(group) ?? 0) >= (this.options.maxAuthoritativeOperatorsPerPrivateSuffix ?? 32)))
|
|
163
|
+
return false;
|
|
164
|
+
this.authoritativeOperators.add(operator);
|
|
165
|
+
if (hosted) {
|
|
166
|
+
this.privateAuthorityOperators++;
|
|
167
|
+
this.authorityGroups.set(group, (this.authorityGroups.get(group) ?? 0) + 1);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Process-local view only. With durable `authorityHistory` this set is a
|
|
173
|
+
* cache and may have evicted an operator that durable history still records.
|
|
174
|
+
*/
|
|
96
175
|
hasAuthoritativeRecord(domain) {
|
|
97
176
|
return this.authoritativeOperators.has(eTldPlusOne(domain));
|
|
98
177
|
}
|
|
99
178
|
now() {
|
|
100
179
|
return (this.options.now ?? (() => Date.now() / 1e3))();
|
|
101
180
|
}
|
|
102
|
-
async request(url) {
|
|
181
|
+
async request(url, operator) {
|
|
103
182
|
const parsed = new URL(url);
|
|
104
183
|
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.hash) failure();
|
|
105
|
-
|
|
184
|
+
let own = false;
|
|
185
|
+
try {
|
|
186
|
+
own = eTldPlusOne(parsed.hostname) === operator;
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
const host = JSON.stringify([own ? operator : FOREIGN_HOST_BUDGET, parsed.hostname]);
|
|
190
|
+
const limit = own ? OWN_HOST_REQUESTS_PER_MINUTE : FOREIGN_HOST_REQUESTS_PER_MINUTE;
|
|
106
191
|
const pending = this.urlRequests.get(url);
|
|
107
192
|
if (pending) return pending;
|
|
108
193
|
const now = this.now(), rate = this.hostRates.get(host);
|
|
109
|
-
if (rate && now - rate.started < 60 && rate.count >=
|
|
194
|
+
if (rate && now - rate.started < 60 && rate.count >= limit) throw new TrustHostCapacityError();
|
|
110
195
|
if (!rate && this.hostRates.size >= (this.options.maxEntries ?? 1e3))
|
|
111
196
|
this.hostRates.delete(this.hostRates.keys().next().value);
|
|
112
197
|
this.hostRates.set(
|
|
@@ -128,24 +213,24 @@ class TrustEvaluator {
|
|
|
128
213
|
}
|
|
129
214
|
async fetch(operator, now) {
|
|
130
215
|
try {
|
|
131
|
-
let response = await this.request(`https://${operator}/.well-known/trust.json
|
|
216
|
+
let response = await this.request(`https://${operator}/.well-known/trust.json`, operator);
|
|
132
217
|
let result;
|
|
133
218
|
if (response.status === 404 || response.status === 410) {
|
|
134
|
-
if (this.
|
|
219
|
+
if (this.authoritativeOperators.has(operator) || await this.options.authorityHistory?.has(operator))
|
|
135
220
|
failure();
|
|
136
221
|
result = { status: "absent", operator, checkedAt: now, freshUntil: now + 60 };
|
|
137
222
|
} else {
|
|
138
223
|
if (response.status !== 200) failure();
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
this.
|
|
224
|
+
if (this.options.authorityHistory) {
|
|
225
|
+
await this.options.authorityHistory.mark(operator);
|
|
226
|
+
this.rememberAuthority(operator, true);
|
|
227
|
+
} else if (!this.rememberAuthority(operator, false)) {
|
|
228
|
+
failure();
|
|
142
229
|
}
|
|
143
|
-
await this.options.authorityHistory?.mark(operator);
|
|
144
|
-
this.authoritativeOperators.add(operator);
|
|
145
230
|
let record = parseStrictJson(new TextDecoder("utf-8", { fatal: true }).decode(response.body), 32);
|
|
146
231
|
validateTrustRecord(record);
|
|
147
232
|
if (record.authoritative_location) {
|
|
148
|
-
response = await this.request(record.authoritative_location);
|
|
233
|
+
response = await this.request(record.authoritative_location, operator);
|
|
149
234
|
if (response.status !== 200) failure();
|
|
150
235
|
record = parseStrictJson(new TextDecoder("utf-8", { fatal: true }).decode(response.body), 32);
|
|
151
236
|
validateTrustRecord(record);
|
|
@@ -165,13 +250,24 @@ class TrustEvaluator {
|
|
|
165
250
|
this.negative.delete(operator);
|
|
166
251
|
return result;
|
|
167
252
|
} catch (cause) {
|
|
253
|
+
if (cause instanceof TrustHostCapacityError) return unavailable(cause);
|
|
168
254
|
if (this.negative.size >= (this.options.maxEntries ?? 1e3))
|
|
169
255
|
this.negative.delete(this.negative.keys().next().value);
|
|
170
256
|
this.negative.set(operator, now + 60);
|
|
171
|
-
return
|
|
257
|
+
return unavailable(cause);
|
|
172
258
|
}
|
|
173
259
|
}
|
|
174
260
|
}
|
|
261
|
+
function listedAgent(result, origin, role) {
|
|
262
|
+
if (result.status !== "present") return void 0;
|
|
263
|
+
const entries = (result.record.agents ?? []).filter((agent) => canonicalAgentOrigin(agent.url) === origin);
|
|
264
|
+
return entries.length === 1 && entries[0].roles.includes(role) ? entries[0] : void 0;
|
|
265
|
+
}
|
|
266
|
+
function authorityGroup(operator) {
|
|
267
|
+
if (!parseDomain(operator, { allowPrivateDomains: true }).isPrivate) return operator;
|
|
268
|
+
const provider = parseDomain(operator).domain;
|
|
269
|
+
return `private:${provider ?? operator}`;
|
|
270
|
+
}
|
|
175
271
|
function grantMatches(grant, target, claim, now) {
|
|
176
272
|
if (eTldPlusOne(grant.grantee) !== target) return false;
|
|
177
273
|
if (!grant.scopes.some(
|
|
@@ -213,6 +309,11 @@ async function authorizeDemandGrant(claim, options) {
|
|
|
213
309
|
}
|
|
214
310
|
const first = (record.record.grants ?? []).filter((g) => grantMatches(g, account, grantClaim, now));
|
|
215
311
|
if (account === operator && first.length) return result("trust", [brand, operator], [first[0]]);
|
|
312
|
+
const selfOperated = account === brand || account === authoritativeBrand;
|
|
313
|
+
if (selfOperated && account !== operator) {
|
|
314
|
+
const direct = (record.record.grants ?? []).find((g) => grantMatches(g, operator, grantClaim, now));
|
|
315
|
+
if (direct) return result("trust", [brand, operator], [direct]);
|
|
316
|
+
}
|
|
216
317
|
if (account !== operator && account !== brand && first.length) {
|
|
217
318
|
const intermediary = await options.evaluator.get(account);
|
|
218
319
|
if (intermediary.status === "present") {
|
|
@@ -221,7 +322,7 @@ async function authorizeDemandGrant(claim, options) {
|
|
|
221
322
|
}
|
|
222
323
|
}
|
|
223
324
|
const covered = (record.record.grants ?? []).some(
|
|
224
|
-
(g) => eTldPlusOne(g.grantee) === account && g.scopes.some(
|
|
325
|
+
(g) => (eTldPlusOne(g.grantee) === account || selfOperated && eTldPlusOne(g.grantee) === operator) && g.scopes.some(
|
|
225
326
|
(scope) => scope === "*" || scope === claim.scope || scope === "adcp:all" && claim.scope.startsWith("adcp:")
|
|
226
327
|
)
|
|
227
328
|
);
|
|
@@ -232,6 +333,8 @@ async function authorizeDemandGrant(claim, options) {
|
|
|
232
333
|
export {
|
|
233
334
|
IdentityAuthorizationError,
|
|
234
335
|
TrustEvaluator,
|
|
336
|
+
TrustMembershipError,
|
|
337
|
+
TrustRecordUnavailableError,
|
|
235
338
|
authorizeDemandGrant,
|
|
236
339
|
createTrustRecord,
|
|
237
340
|
validateIdentity33Schema,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Experimental drafts; the released SDK wire/schema pin remains 3.2.4. */
|
|
2
|
-
|
|
2
|
+
import type { GeneratedAccountCapabilities, ResolveAgentOnboarding } from './generated-compat.mjs';
|
|
3
3
|
export type BillingParty = 'operator' | 'agent' | 'advertiser';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
kind:
|
|
4
|
+
type DraftAgentOnboardingRequirementKind = 'agent_identity' | 'grant' | 'billing_entity' | 'contract' | 'credit_application' | 'aao_verified' | 'storyboard' | 'sandbox' | 'experimental_feature';
|
|
5
|
+
interface DraftAgentOnboardingRequirement {
|
|
6
|
+
kind: DraftAgentOnboardingRequirementKind;
|
|
7
7
|
role?: string;
|
|
8
8
|
verification_modes?: string[];
|
|
9
9
|
specialisms?: string[];
|
|
@@ -11,23 +11,50 @@ export interface AgentOnboardingRequirement {
|
|
|
11
11
|
id?: string;
|
|
12
12
|
ext?: Record<string, unknown>;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
mode:
|
|
14
|
+
interface DraftAgentOnboardingTier {
|
|
15
|
+
mode: 'open' | 'application' | 'invite_only';
|
|
16
16
|
url?: string;
|
|
17
|
-
requirements?:
|
|
17
|
+
requirements?: DraftAgentOnboardingRequirement[];
|
|
18
18
|
ext?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
sandbox?:
|
|
22
|
-
billing?: Partial<Record<BillingParty,
|
|
20
|
+
interface DraftAgentOnboarding {
|
|
21
|
+
sandbox?: DraftAgentOnboardingTier;
|
|
22
|
+
billing?: Partial<Record<BillingParty, DraftAgentOnboardingTier>>;
|
|
23
23
|
url?: string;
|
|
24
24
|
documentation_url?: string;
|
|
25
25
|
message?: string;
|
|
26
26
|
ext?: Record<string, unknown>;
|
|
27
27
|
}
|
|
28
|
+
/** `get_adcp_capabilities.account.agent_onboarding`: how a seller onboards buyer agents. */
|
|
29
|
+
export type AgentOnboarding = ResolveAgentOnboarding<GeneratedAccountCapabilities, DraftAgentOnboarding>;
|
|
30
|
+
type AgentOnboardingBilling = NonNullable<AgentOnboarding['billing']>;
|
|
31
|
+
/** The onboarding mode for sandbox access or for one billing party. */
|
|
32
|
+
export type AgentOnboardingTier = NonNullable<AgentOnboarding['sandbox']> | NonNullable<AgentOnboardingBilling[keyof AgentOnboardingBilling]>;
|
|
33
|
+
export type AgentOnboardingMode = AgentOnboardingTier['mode'];
|
|
34
|
+
/** One requirement an onboarding mode imposes, discriminated by `kind`. */
|
|
35
|
+
export type AgentOnboardingRequirement = NonNullable<AgentOnboardingTier['requirements']>[number];
|
|
36
|
+
export type AgentOnboardingRequirementKind = AgentOnboardingRequirement['kind'];
|
|
37
|
+
/**
|
|
38
|
+
* A requirement as an evaluator sees it. `kind` accepts any string because a
|
|
39
|
+
* counterparty on a newer revision may publish kinds this SDK does not know,
|
|
40
|
+
* and readers MUST treat those as unsatisfiable rather than reject the
|
|
41
|
+
* posture. Structural on purpose: every {@link AgentOnboardingRequirement} is
|
|
42
|
+
* assignable to it, whichever shape the pinned schemas give that type.
|
|
43
|
+
* `verified_specialisms` is the revised #8170 name for `specialisms`.
|
|
44
|
+
*/
|
|
45
|
+
export interface ObservedAgentOnboardingRequirement {
|
|
46
|
+
kind: AgentOnboardingRequirementKind | (string & {});
|
|
47
|
+
role?: string;
|
|
48
|
+
verification_modes?: readonly string[];
|
|
49
|
+
specialisms?: readonly string[];
|
|
50
|
+
verified_specialisms?: readonly string[];
|
|
51
|
+
adcp_version?: string;
|
|
52
|
+
id?: string;
|
|
53
|
+
ext?: Record<string, unknown>;
|
|
54
|
+
}
|
|
28
55
|
export interface BuyerOnboardingPosture {
|
|
29
56
|
mode: 'considers_unsolicited' | 'application' | 'invite_only';
|
|
30
|
-
requirements?:
|
|
57
|
+
requirements?: ObservedAgentOnboardingRequirement[];
|
|
31
58
|
url?: string;
|
|
32
59
|
documentation_url?: string;
|
|
33
60
|
message?: string;
|
|
@@ -125,3 +152,4 @@ export type Identity33GetPrincipalResponse = Omit<import('../types/tools.generat
|
|
|
125
152
|
kind: 'failed';
|
|
126
153
|
}>;
|
|
127
154
|
};
|
|
155
|
+
export {};
|