@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
|
@@ -14,17 +14,33 @@ export interface MonotonicOriginRevocationStore {
|
|
|
14
14
|
merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
|
|
15
15
|
get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Process-local revocation history for development and tests. Public
|
|
19
|
+
* deployments need durable history (`createIdentity33StateStores`) so a
|
|
20
|
+
* restart never forgets a revocation.
|
|
21
|
+
*
|
|
22
|
+
* Capacity is bounded per origin, per operator (registrable domain) and in
|
|
23
|
+
* total. The operator budget stops one registrable domain's wildcard
|
|
24
|
+
* subdomains from filling the store alone; it does not reserve capacity for
|
|
25
|
+
* anyone else, so several operators together can still exhaust the total.
|
|
26
|
+
* Facts are never evicted to regain capacity; a refused merge fails closed.
|
|
27
|
+
*/
|
|
17
28
|
export declare class InMemoryOriginRevocationStore implements MonotonicOriginRevocationStore {
|
|
18
|
-
private readonly limits;
|
|
19
29
|
readonly durability: "process-local";
|
|
20
30
|
private readonly revoked;
|
|
31
|
+
private readonly originKeys;
|
|
32
|
+
private readonly operatorKeys;
|
|
33
|
+
private readonly limits;
|
|
21
34
|
constructor(limits?: {
|
|
22
|
-
perOrigin
|
|
23
|
-
|
|
35
|
+
perOrigin?: number;
|
|
36
|
+
perOperator?: number;
|
|
37
|
+
total?: number;
|
|
24
38
|
});
|
|
25
39
|
merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
|
|
26
40
|
get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
|
|
27
41
|
}
|
|
42
|
+
/** See {@link OriginRevocationResolver.status}. */
|
|
43
|
+
export type OriginRevocationStatus = 'revoked' | 'current' | 'absent' | 'unknown';
|
|
28
44
|
export declare class OriginRevocationResolver {
|
|
29
45
|
private readonly options;
|
|
30
46
|
private readonly freshness;
|
|
@@ -33,6 +49,7 @@ export declare class OriginRevocationResolver {
|
|
|
33
49
|
private readonly pending;
|
|
34
50
|
private readonly failures;
|
|
35
51
|
private readonly persistenceFailures;
|
|
52
|
+
private readonly absentUntil;
|
|
36
53
|
constructor(options: {
|
|
37
54
|
directory: import('../signing/web-bot-auth.mjs').SignatureDirectoryResolver;
|
|
38
55
|
store: MonotonicOriginRevocationStore;
|
|
@@ -41,7 +58,26 @@ export declare class OriginRevocationResolver {
|
|
|
41
58
|
fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
|
|
42
59
|
maxOrigins?: number;
|
|
43
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* Compatibility wrapper over {@link status}: `true` when revoked, `false`
|
|
63
|
+
* when the list is current, and `revocation_unavailable` when the identity
|
|
64
|
+
* publishes no list (`absent`) or its status is not known (`unknown`).
|
|
65
|
+
*/
|
|
44
66
|
isRevoked(originUrl: string, keyid: string): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Revocation status of `keyid` at `origin`:
|
|
69
|
+
* - `revoked`: a stored (permanent) revocation covers the key.
|
|
70
|
+
* - `current`: a signed list is current, or within `next_update` + grace.
|
|
71
|
+
* - `absent`: the origin serves no list (404/410). The identity is still
|
|
72
|
+
* established but cannot be bound for spend.
|
|
73
|
+
* - `unknown`: no list could be verified and grace has passed (or there was
|
|
74
|
+
* never a list). The identity is not established.
|
|
75
|
+
* Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
|
|
76
|
+
* when the history store fails: an infrastructure outage, never a
|
|
77
|
+
* revocation outcome.
|
|
78
|
+
*/
|
|
79
|
+
status(originUrl: string, keyid: string): Promise<OriginRevocationStatus>;
|
|
80
|
+
private currentStatus;
|
|
45
81
|
private readStored;
|
|
46
82
|
private refresh;
|
|
47
83
|
private now;
|
|
@@ -14,17 +14,33 @@ export interface MonotonicOriginRevocationStore {
|
|
|
14
14
|
merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
|
|
15
15
|
get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Process-local revocation history for development and tests. Public
|
|
19
|
+
* deployments need durable history (`createIdentity33StateStores`) so a
|
|
20
|
+
* restart never forgets a revocation.
|
|
21
|
+
*
|
|
22
|
+
* Capacity is bounded per origin, per operator (registrable domain) and in
|
|
23
|
+
* total. The operator budget stops one registrable domain's wildcard
|
|
24
|
+
* subdomains from filling the store alone; it does not reserve capacity for
|
|
25
|
+
* anyone else, so several operators together can still exhaust the total.
|
|
26
|
+
* Facts are never evicted to regain capacity; a refused merge fails closed.
|
|
27
|
+
*/
|
|
17
28
|
export declare class InMemoryOriginRevocationStore implements MonotonicOriginRevocationStore {
|
|
18
|
-
private readonly limits;
|
|
19
29
|
readonly durability: "process-local";
|
|
20
30
|
private readonly revoked;
|
|
31
|
+
private readonly originKeys;
|
|
32
|
+
private readonly operatorKeys;
|
|
33
|
+
private readonly limits;
|
|
21
34
|
constructor(limits?: {
|
|
22
|
-
perOrigin
|
|
23
|
-
|
|
35
|
+
perOrigin?: number;
|
|
36
|
+
perOperator?: number;
|
|
37
|
+
total?: number;
|
|
24
38
|
});
|
|
25
39
|
merge(origin: string, snapshot: OriginRevocationSnapshot): Promise<void>;
|
|
26
40
|
get(origin: string, keyid: string): Promise<RevocationDetail | undefined>;
|
|
27
41
|
}
|
|
42
|
+
/** See {@link OriginRevocationResolver.status}. */
|
|
43
|
+
export type OriginRevocationStatus = 'revoked' | 'current' | 'absent' | 'unknown';
|
|
28
44
|
export declare class OriginRevocationResolver {
|
|
29
45
|
private readonly options;
|
|
30
46
|
private readonly freshness;
|
|
@@ -33,6 +49,7 @@ export declare class OriginRevocationResolver {
|
|
|
33
49
|
private readonly pending;
|
|
34
50
|
private readonly failures;
|
|
35
51
|
private readonly persistenceFailures;
|
|
52
|
+
private readonly absentUntil;
|
|
36
53
|
constructor(options: {
|
|
37
54
|
directory: import('../signing/web-bot-auth').SignatureDirectoryResolver;
|
|
38
55
|
store: MonotonicOriginRevocationStore;
|
|
@@ -41,7 +58,26 @@ export declare class OriginRevocationResolver {
|
|
|
41
58
|
fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
|
|
42
59
|
maxOrigins?: number;
|
|
43
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* Compatibility wrapper over {@link status}: `true` when revoked, `false`
|
|
63
|
+
* when the list is current, and `revocation_unavailable` when the identity
|
|
64
|
+
* publishes no list (`absent`) or its status is not known (`unknown`).
|
|
65
|
+
*/
|
|
44
66
|
isRevoked(originUrl: string, keyid: string): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Revocation status of `keyid` at `origin`:
|
|
69
|
+
* - `revoked`: a stored (permanent) revocation covers the key.
|
|
70
|
+
* - `current`: a signed list is current, or within `next_update` + grace.
|
|
71
|
+
* - `absent`: the origin serves no list (404/410). The identity is still
|
|
72
|
+
* established but cannot be bound for spend.
|
|
73
|
+
* - `unknown`: no list could be verified and grace has passed (or there was
|
|
74
|
+
* never a list). The identity is not established.
|
|
75
|
+
* Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
|
|
76
|
+
* when the history store fails: an infrastructure outage, never a
|
|
77
|
+
* revocation outcome.
|
|
78
|
+
*/
|
|
79
|
+
status(originUrl: string, keyid: string): Promise<OriginRevocationStatus>;
|
|
80
|
+
private currentStatus;
|
|
45
81
|
private readStored;
|
|
46
82
|
private refresh;
|
|
47
83
|
private now;
|
|
@@ -45,28 +45,51 @@ const REASONS = [
|
|
|
45
45
|
"keyCompromise"
|
|
46
46
|
];
|
|
47
47
|
class InMemoryOriginRevocationStore {
|
|
48
|
-
constructor(limits = { perOrigin: 16384, total: 1e5 }) {
|
|
49
|
-
this.limits = limits;
|
|
50
|
-
}
|
|
51
|
-
limits;
|
|
52
48
|
durability = "process-local";
|
|
53
49
|
revoked = /* @__PURE__ */ new Map();
|
|
50
|
+
originKeys = /* @__PURE__ */ new Map();
|
|
51
|
+
operatorKeys = /* @__PURE__ */ new Map();
|
|
52
|
+
limits;
|
|
53
|
+
constructor(limits = {}) {
|
|
54
|
+
this.limits = {
|
|
55
|
+
perOrigin: limits.perOrigin ?? 16384,
|
|
56
|
+
perOperator: limits.perOperator ?? 16384,
|
|
57
|
+
total: limits.total ?? 1e5
|
|
58
|
+
};
|
|
59
|
+
if (Object.values(this.limits).some((value) => !Number.isSafeInteger(value) || value < 1))
|
|
60
|
+
throw new TypeError("Revocation limits must be positive safe integers");
|
|
61
|
+
}
|
|
54
62
|
async merge(origin, snapshot) {
|
|
55
63
|
origin = (0, import_canonicalize.canonicalAgentOrigin)(origin);
|
|
64
|
+
const operator = operatorOf(origin);
|
|
56
65
|
const additions = new Set(snapshot.revoked_kids.filter((kid) => !this.revoked.has(JSON.stringify([origin, kid]))));
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
66
|
+
const originCount = this.originKeys.get(origin) ?? 0, operatorCount = this.operatorKeys.get(operator) ?? 0;
|
|
67
|
+
if (originCount + additions.size > this.limits.perOrigin || operatorCount + additions.size > this.limits.perOperator || this.revoked.size + additions.size > this.limits.total)
|
|
59
68
|
throw new import_web_bot_auth.WebBotAuthError(403, "revocation_storage_limit");
|
|
60
69
|
for (const kid of snapshot.revoked_kids) {
|
|
61
70
|
const key = JSON.stringify([origin, kid]), prior = this.revoked.get(key), detail = snapshot.revocation_details?.[kid] ?? {};
|
|
62
71
|
this.revoked.set(key, (0, import_revocation2.mergeRevocationDetails)(prior, detail));
|
|
63
72
|
}
|
|
73
|
+
if (additions.size) {
|
|
74
|
+
this.originKeys.set(origin, originCount + additions.size);
|
|
75
|
+
this.operatorKeys.set(operator, operatorCount + additions.size);
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
async get(origin, keyid) {
|
|
66
79
|
const entry = this.revoked.get(JSON.stringify([(0, import_canonicalize.canonicalAgentOrigin)(origin), keyid]));
|
|
67
80
|
return entry && { ...entry };
|
|
68
81
|
}
|
|
69
82
|
}
|
|
83
|
+
function operatorOf(origin) {
|
|
84
|
+
try {
|
|
85
|
+
return (0, import_etld.eTldPlusOne)(origin);
|
|
86
|
+
} catch {
|
|
87
|
+
return new URL(origin).hostname;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const MAX_POLLING_SECONDS = 1800;
|
|
91
|
+
class RevocationListAbsentError extends Error {
|
|
92
|
+
}
|
|
70
93
|
class OriginRevocationResolver {
|
|
71
94
|
constructor(options) {
|
|
72
95
|
this.options = options;
|
|
@@ -82,31 +105,73 @@ class OriginRevocationResolver {
|
|
|
82
105
|
pending = /* @__PURE__ */ new Map();
|
|
83
106
|
failures = /* @__PURE__ */ new Map();
|
|
84
107
|
persistenceFailures = /* @__PURE__ */ new Set();
|
|
108
|
+
absentUntil = /* @__PURE__ */ new Map();
|
|
109
|
+
/**
|
|
110
|
+
* Compatibility wrapper over {@link status}: `true` when revoked, `false`
|
|
111
|
+
* when the list is current, and `revocation_unavailable` when the identity
|
|
112
|
+
* publishes no list (`absent`) or its status is not known (`unknown`).
|
|
113
|
+
*/
|
|
85
114
|
async isRevoked(originUrl, keyid) {
|
|
115
|
+
const state = await this.status(originUrl, keyid);
|
|
116
|
+
if (state === "revoked") return true;
|
|
117
|
+
if (state === "current") return false;
|
|
118
|
+
throw new import_web_bot_auth.WebBotAuthError(403, state === "absent" ? "revocation_absent" : "revocation_unavailable");
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Revocation status of `keyid` at `origin`:
|
|
122
|
+
* - `revoked`: a stored (permanent) revocation covers the key.
|
|
123
|
+
* - `current`: a signed list is current, or within `next_update` + grace.
|
|
124
|
+
* - `absent`: the origin serves no list (404/410). The identity is still
|
|
125
|
+
* established but cannot be bound for spend.
|
|
126
|
+
* - `unknown`: no list could be verified and grace has passed (or there was
|
|
127
|
+
* never a list). The identity is not established.
|
|
128
|
+
* Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
|
|
129
|
+
* when the history store fails: an infrastructure outage, never a
|
|
130
|
+
* revocation outcome.
|
|
131
|
+
*/
|
|
132
|
+
async status(originUrl, keyid) {
|
|
133
|
+
try {
|
|
134
|
+
return await this.currentStatus(originUrl, keyid);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof import_web_bot_auth.WebBotAuthError && error.reason === "revocation_persistence_failed")
|
|
137
|
+
throw new import_web_bot_auth.WebBotAuthUnavailableError("revocation_persistence_failed", error);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async currentStatus(originUrl, keyid) {
|
|
86
142
|
const origin = (0, import_canonicalize.canonicalAgentOrigin)(originUrl), now = this.now();
|
|
87
|
-
if (await this.readStored(origin, keyid)) return
|
|
143
|
+
if (await this.readStored(origin, keyid)) return "revoked";
|
|
144
|
+
const settled = async () => {
|
|
145
|
+
if (await this.readStored(origin, keyid)) return "revoked";
|
|
146
|
+
if ((this.absentUntil.get(origin) ?? 0) > now) return "absent";
|
|
147
|
+
return (this.freshness.get(origin) ?? 0) > now || (this.graceUntil.get(origin) ?? 0) > now ? "current" : "unknown";
|
|
148
|
+
};
|
|
88
149
|
if ((this.freshness.get(origin) ?? 0) <= now) {
|
|
89
150
|
if ((this.failures.get(origin) ?? 0) > now) {
|
|
90
151
|
if (this.persistenceFailures.has(origin)) throw new import_web_bot_auth.WebBotAuthError(403, "revocation_persistence_failed");
|
|
91
|
-
|
|
92
|
-
throw new import_web_bot_auth.WebBotAuthError(403, "revocation_unavailable");
|
|
152
|
+
return settled();
|
|
93
153
|
}
|
|
94
154
|
let promise = this.pending.get(origin);
|
|
95
155
|
if (!promise) {
|
|
96
|
-
if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => (
|
|
97
|
-
|
|
156
|
+
if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => operatorOf(pendingOrigin) === operatorOf(origin)).length >= 2) {
|
|
157
|
+
return settled();
|
|
98
158
|
}
|
|
99
159
|
promise = this.refresh(origin).catch((error) => {
|
|
100
160
|
if (this.failures.size >= (this.options.maxOrigins ?? 1e3)) {
|
|
101
161
|
const oldest = this.failures.keys().next().value;
|
|
102
162
|
this.failures.delete(oldest);
|
|
103
163
|
this.persistenceFailures.delete(oldest);
|
|
164
|
+
this.absentUntil.delete(oldest);
|
|
104
165
|
}
|
|
105
166
|
this.failures.set(origin, now + 60);
|
|
106
167
|
if (error instanceof import_web_bot_auth.WebBotAuthError && error.reason === "revocation_persistence_failed") {
|
|
107
168
|
this.persistenceFailures.add(origin);
|
|
108
169
|
this.graceUntil.delete(origin);
|
|
109
170
|
}
|
|
171
|
+
if (error.cause instanceof RevocationListAbsentError) {
|
|
172
|
+
this.absentUntil.set(origin, now + 60);
|
|
173
|
+
this.graceUntil.delete(origin);
|
|
174
|
+
}
|
|
110
175
|
throw error;
|
|
111
176
|
});
|
|
112
177
|
this.pending.set(origin, promise);
|
|
@@ -115,12 +180,11 @@ class OriginRevocationResolver {
|
|
|
115
180
|
await promise;
|
|
116
181
|
} catch (error) {
|
|
117
182
|
if (error instanceof import_web_bot_auth.WebBotAuthError && error.reason === "revocation_persistence_failed") throw error;
|
|
118
|
-
if ((this.graceUntil.get(origin) ?? 0) <= now) throw error;
|
|
119
183
|
} finally {
|
|
120
184
|
if (this.pending.get(origin) === promise) this.pending.delete(origin);
|
|
121
185
|
}
|
|
122
186
|
}
|
|
123
|
-
return
|
|
187
|
+
return settled();
|
|
124
188
|
}
|
|
125
189
|
async readStored(origin, keyid) {
|
|
126
190
|
try {
|
|
@@ -138,6 +202,7 @@ class OriginRevocationResolver {
|
|
|
138
202
|
maxBodyBytes: 256 * 1024,
|
|
139
203
|
timeoutMs: 1e4
|
|
140
204
|
});
|
|
205
|
+
if (response.status === 404 || response.status === 410) throw new RevocationListAbsentError();
|
|
141
206
|
if (response.status !== 200) throw new Error("Unavailable revocation list");
|
|
142
207
|
const body = new TextDecoder("utf-8", { fatal: true }).decode(response.body);
|
|
143
208
|
let tokens;
|
|
@@ -163,9 +228,9 @@ class OriginRevocationResolver {
|
|
|
163
228
|
if (header.typ !== "adcp-gov-revocation+jws" || typeof header.kid !== "string" || typeof header.alg !== "string" || !["EdDSA", "ES256"].includes(header.alg))
|
|
164
229
|
continue;
|
|
165
230
|
await this.options.directory.resolve(header.kid, origin);
|
|
166
|
-
const
|
|
167
|
-
if (!
|
|
168
|
-
|
|
231
|
+
const checked = this.options.directory.getVerifiedDirectory(origin);
|
|
232
|
+
if (!checked || new URL(checked.proof.request.url).origin !== origin || checked.expires <= this.now())
|
|
233
|
+
continue;
|
|
169
234
|
const jwk = checked.keys.find((key) => key.kid === header.kid);
|
|
170
235
|
if (!jwk) continue;
|
|
171
236
|
const result = await (0, import_jose.compactVerify)(token, await (0, import_jose.importJWK)(jwk, header.alg), {
|
|
@@ -182,7 +247,8 @@ class OriginRevocationResolver {
|
|
|
182
247
|
32
|
|
183
248
|
);
|
|
184
249
|
const updated = Date.parse(snapshot.updated) / 1e3, next = Date.parse(snapshot.next_update) / 1e3, now = this.now();
|
|
185
|
-
if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) ||
|
|
250
|
+
if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) || // Tolerate a publisher clock slightly ahead, as for request `created`.
|
|
251
|
+
updated > now + 60 || next <= now || next <= updated || !Array.isArray(snapshot.revoked_kids) || snapshot.revoked_kids.length > 4096 || snapshot.revoked_kids.some((k) => typeof k !== "string" || k.length > 256) || updated < (this.updated.get(origin) ?? 0))
|
|
186
252
|
throw new Error("Invalid revocation snapshot");
|
|
187
253
|
for (const [kid, detail] of Object.entries(snapshot.revocation_details ?? {}))
|
|
188
254
|
if (!snapshot.revoked_kids.includes(kid) || !detail || detail.reason && !REASONS.includes(detail.reason) || detail.invalidity_date && !Number.isFinite(Date.parse(detail.invalidity_date)))
|
|
@@ -200,11 +266,15 @@ class OriginRevocationResolver {
|
|
|
200
266
|
this.updated.delete(oldest);
|
|
201
267
|
this.graceUntil.delete(oldest);
|
|
202
268
|
}
|
|
203
|
-
this.graceUntil.set(
|
|
269
|
+
this.graceUntil.set(
|
|
270
|
+
origin,
|
|
271
|
+
Math.min(next, now + MAX_POLLING_SECONDS) + 4 * (this.options.pollingIntervalSeconds ?? 300)
|
|
272
|
+
);
|
|
204
273
|
this.updated.set(origin, updated);
|
|
205
274
|
this.freshness.set(origin, Math.min(next, now + (this.options.pollingIntervalSeconds ?? 300)));
|
|
206
275
|
this.failures.delete(origin);
|
|
207
276
|
this.persistenceFailures.delete(origin);
|
|
277
|
+
this.absentUntil.delete(origin);
|
|
208
278
|
} catch (cause) {
|
|
209
279
|
if (cause instanceof import_web_bot_auth.WebBotAuthError && cause.reason === "revocation_persistence_failed") throw cause;
|
|
210
280
|
const error = new import_web_bot_auth.WebBotAuthError(403, "revocation_unavailable");
|
|
@@ -6,7 +6,7 @@ import { parseStrictJson } from "../signing/agent-resolver/strict-json.mjs";
|
|
|
6
6
|
import {
|
|
7
7
|
SignatureDirectoryResolver,
|
|
8
8
|
WebBotAuthError,
|
|
9
|
-
|
|
9
|
+
WebBotAuthUnavailableError,
|
|
10
10
|
jwkThumbprint
|
|
11
11
|
} from "../signing/web-bot-auth.mjs";
|
|
12
12
|
const REASONS = [
|
|
@@ -24,28 +24,51 @@ const REASONS = [
|
|
|
24
24
|
import { mergeRevocationDetails } from "../signing/revocation.mjs";
|
|
25
25
|
import { mergeRevocationDetails as mergeRevocationDetail } from "../signing/revocation.mjs";
|
|
26
26
|
class InMemoryOriginRevocationStore {
|
|
27
|
-
constructor(limits = { perOrigin: 16384, total: 1e5 }) {
|
|
28
|
-
this.limits = limits;
|
|
29
|
-
}
|
|
30
|
-
limits;
|
|
31
27
|
durability = "process-local";
|
|
32
28
|
revoked = /* @__PURE__ */ new Map();
|
|
29
|
+
originKeys = /* @__PURE__ */ new Map();
|
|
30
|
+
operatorKeys = /* @__PURE__ */ new Map();
|
|
31
|
+
limits;
|
|
32
|
+
constructor(limits = {}) {
|
|
33
|
+
this.limits = {
|
|
34
|
+
perOrigin: limits.perOrigin ?? 16384,
|
|
35
|
+
perOperator: limits.perOperator ?? 16384,
|
|
36
|
+
total: limits.total ?? 1e5
|
|
37
|
+
};
|
|
38
|
+
if (Object.values(this.limits).some((value) => !Number.isSafeInteger(value) || value < 1))
|
|
39
|
+
throw new TypeError("Revocation limits must be positive safe integers");
|
|
40
|
+
}
|
|
33
41
|
async merge(origin, snapshot) {
|
|
34
42
|
origin = canonicalAgentOrigin(origin);
|
|
43
|
+
const operator = operatorOf(origin);
|
|
35
44
|
const additions = new Set(snapshot.revoked_kids.filter((kid) => !this.revoked.has(JSON.stringify([origin, kid]))));
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
45
|
+
const originCount = this.originKeys.get(origin) ?? 0, operatorCount = this.operatorKeys.get(operator) ?? 0;
|
|
46
|
+
if (originCount + additions.size > this.limits.perOrigin || operatorCount + additions.size > this.limits.perOperator || this.revoked.size + additions.size > this.limits.total)
|
|
38
47
|
throw new WebBotAuthError(403, "revocation_storage_limit");
|
|
39
48
|
for (const kid of snapshot.revoked_kids) {
|
|
40
49
|
const key = JSON.stringify([origin, kid]), prior = this.revoked.get(key), detail = snapshot.revocation_details?.[kid] ?? {};
|
|
41
50
|
this.revoked.set(key, mergeRevocationDetail(prior, detail));
|
|
42
51
|
}
|
|
52
|
+
if (additions.size) {
|
|
53
|
+
this.originKeys.set(origin, originCount + additions.size);
|
|
54
|
+
this.operatorKeys.set(operator, operatorCount + additions.size);
|
|
55
|
+
}
|
|
43
56
|
}
|
|
44
57
|
async get(origin, keyid) {
|
|
45
58
|
const entry = this.revoked.get(JSON.stringify([canonicalAgentOrigin(origin), keyid]));
|
|
46
59
|
return entry && { ...entry };
|
|
47
60
|
}
|
|
48
61
|
}
|
|
62
|
+
function operatorOf(origin) {
|
|
63
|
+
try {
|
|
64
|
+
return eTldPlusOne(origin);
|
|
65
|
+
} catch {
|
|
66
|
+
return new URL(origin).hostname;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const MAX_POLLING_SECONDS = 1800;
|
|
70
|
+
class RevocationListAbsentError extends Error {
|
|
71
|
+
}
|
|
49
72
|
class OriginRevocationResolver {
|
|
50
73
|
constructor(options) {
|
|
51
74
|
this.options = options;
|
|
@@ -61,31 +84,73 @@ class OriginRevocationResolver {
|
|
|
61
84
|
pending = /* @__PURE__ */ new Map();
|
|
62
85
|
failures = /* @__PURE__ */ new Map();
|
|
63
86
|
persistenceFailures = /* @__PURE__ */ new Set();
|
|
87
|
+
absentUntil = /* @__PURE__ */ new Map();
|
|
88
|
+
/**
|
|
89
|
+
* Compatibility wrapper over {@link status}: `true` when revoked, `false`
|
|
90
|
+
* when the list is current, and `revocation_unavailable` when the identity
|
|
91
|
+
* publishes no list (`absent`) or its status is not known (`unknown`).
|
|
92
|
+
*/
|
|
64
93
|
async isRevoked(originUrl, keyid) {
|
|
94
|
+
const state = await this.status(originUrl, keyid);
|
|
95
|
+
if (state === "revoked") return true;
|
|
96
|
+
if (state === "current") return false;
|
|
97
|
+
throw new WebBotAuthError(403, state === "absent" ? "revocation_absent" : "revocation_unavailable");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Revocation status of `keyid` at `origin`:
|
|
101
|
+
* - `revoked`: a stored (permanent) revocation covers the key.
|
|
102
|
+
* - `current`: a signed list is current, or within `next_update` + grace.
|
|
103
|
+
* - `absent`: the origin serves no list (404/410). The identity is still
|
|
104
|
+
* established but cannot be bound for spend.
|
|
105
|
+
* - `unknown`: no list could be verified and grace has passed (or there was
|
|
106
|
+
* never a list). The identity is not established.
|
|
107
|
+
* Throws {@link WebBotAuthUnavailableError} (`revocation_persistence_failed`)
|
|
108
|
+
* when the history store fails: an infrastructure outage, never a
|
|
109
|
+
* revocation outcome.
|
|
110
|
+
*/
|
|
111
|
+
async status(originUrl, keyid) {
|
|
112
|
+
try {
|
|
113
|
+
return await this.currentStatus(originUrl, keyid);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed")
|
|
116
|
+
throw new WebBotAuthUnavailableError("revocation_persistence_failed", error);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async currentStatus(originUrl, keyid) {
|
|
65
121
|
const origin = canonicalAgentOrigin(originUrl), now = this.now();
|
|
66
|
-
if (await this.readStored(origin, keyid)) return
|
|
122
|
+
if (await this.readStored(origin, keyid)) return "revoked";
|
|
123
|
+
const settled = async () => {
|
|
124
|
+
if (await this.readStored(origin, keyid)) return "revoked";
|
|
125
|
+
if ((this.absentUntil.get(origin) ?? 0) > now) return "absent";
|
|
126
|
+
return (this.freshness.get(origin) ?? 0) > now || (this.graceUntil.get(origin) ?? 0) > now ? "current" : "unknown";
|
|
127
|
+
};
|
|
67
128
|
if ((this.freshness.get(origin) ?? 0) <= now) {
|
|
68
129
|
if ((this.failures.get(origin) ?? 0) > now) {
|
|
69
130
|
if (this.persistenceFailures.has(origin)) throw new WebBotAuthError(403, "revocation_persistence_failed");
|
|
70
|
-
|
|
71
|
-
throw new WebBotAuthError(403, "revocation_unavailable");
|
|
131
|
+
return settled();
|
|
72
132
|
}
|
|
73
133
|
let promise = this.pending.get(origin);
|
|
74
134
|
if (!promise) {
|
|
75
|
-
if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) =>
|
|
76
|
-
|
|
135
|
+
if (this.pending.size >= 16 || [...this.pending.keys()].filter((pendingOrigin) => operatorOf(pendingOrigin) === operatorOf(origin)).length >= 2) {
|
|
136
|
+
return settled();
|
|
77
137
|
}
|
|
78
138
|
promise = this.refresh(origin).catch((error) => {
|
|
79
139
|
if (this.failures.size >= (this.options.maxOrigins ?? 1e3)) {
|
|
80
140
|
const oldest = this.failures.keys().next().value;
|
|
81
141
|
this.failures.delete(oldest);
|
|
82
142
|
this.persistenceFailures.delete(oldest);
|
|
143
|
+
this.absentUntil.delete(oldest);
|
|
83
144
|
}
|
|
84
145
|
this.failures.set(origin, now + 60);
|
|
85
146
|
if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed") {
|
|
86
147
|
this.persistenceFailures.add(origin);
|
|
87
148
|
this.graceUntil.delete(origin);
|
|
88
149
|
}
|
|
150
|
+
if (error.cause instanceof RevocationListAbsentError) {
|
|
151
|
+
this.absentUntil.set(origin, now + 60);
|
|
152
|
+
this.graceUntil.delete(origin);
|
|
153
|
+
}
|
|
89
154
|
throw error;
|
|
90
155
|
});
|
|
91
156
|
this.pending.set(origin, promise);
|
|
@@ -94,12 +159,11 @@ class OriginRevocationResolver {
|
|
|
94
159
|
await promise;
|
|
95
160
|
} catch (error) {
|
|
96
161
|
if (error instanceof WebBotAuthError && error.reason === "revocation_persistence_failed") throw error;
|
|
97
|
-
if ((this.graceUntil.get(origin) ?? 0) <= now) throw error;
|
|
98
162
|
} finally {
|
|
99
163
|
if (this.pending.get(origin) === promise) this.pending.delete(origin);
|
|
100
164
|
}
|
|
101
165
|
}
|
|
102
|
-
return
|
|
166
|
+
return settled();
|
|
103
167
|
}
|
|
104
168
|
async readStored(origin, keyid) {
|
|
105
169
|
try {
|
|
@@ -117,6 +181,7 @@ class OriginRevocationResolver {
|
|
|
117
181
|
maxBodyBytes: 256 * 1024,
|
|
118
182
|
timeoutMs: 1e4
|
|
119
183
|
});
|
|
184
|
+
if (response.status === 404 || response.status === 410) throw new RevocationListAbsentError();
|
|
120
185
|
if (response.status !== 200) throw new Error("Unavailable revocation list");
|
|
121
186
|
const body = new TextDecoder("utf-8", { fatal: true }).decode(response.body);
|
|
122
187
|
let tokens;
|
|
@@ -142,9 +207,9 @@ class OriginRevocationResolver {
|
|
|
142
207
|
if (header.typ !== "adcp-gov-revocation+jws" || typeof header.kid !== "string" || typeof header.alg !== "string" || !["EdDSA", "ES256"].includes(header.alg))
|
|
143
208
|
continue;
|
|
144
209
|
await this.options.directory.resolve(header.kid, origin);
|
|
145
|
-
const
|
|
146
|
-
if (!
|
|
147
|
-
|
|
210
|
+
const checked = this.options.directory.getVerifiedDirectory(origin);
|
|
211
|
+
if (!checked || new URL(checked.proof.request.url).origin !== origin || checked.expires <= this.now())
|
|
212
|
+
continue;
|
|
148
213
|
const jwk = checked.keys.find((key) => key.kid === header.kid);
|
|
149
214
|
if (!jwk) continue;
|
|
150
215
|
const result = await compactVerify(token, await importJWK(jwk, header.alg), {
|
|
@@ -161,7 +226,8 @@ class OriginRevocationResolver {
|
|
|
161
226
|
32
|
|
162
227
|
);
|
|
163
228
|
const updated = Date.parse(snapshot.updated) / 1e3, next = Date.parse(snapshot.next_update) / 1e3, now = this.now();
|
|
164
|
-
if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) ||
|
|
229
|
+
if (snapshot.issuer !== origin || !Number.isFinite(updated) || !Number.isFinite(next) || // Tolerate a publisher clock slightly ahead, as for request `created`.
|
|
230
|
+
updated > now + 60 || next <= now || next <= updated || !Array.isArray(snapshot.revoked_kids) || snapshot.revoked_kids.length > 4096 || snapshot.revoked_kids.some((k) => typeof k !== "string" || k.length > 256) || updated < (this.updated.get(origin) ?? 0))
|
|
165
231
|
throw new Error("Invalid revocation snapshot");
|
|
166
232
|
for (const [kid, detail] of Object.entries(snapshot.revocation_details ?? {}))
|
|
167
233
|
if (!snapshot.revoked_kids.includes(kid) || !detail || detail.reason && !REASONS.includes(detail.reason) || detail.invalidity_date && !Number.isFinite(Date.parse(detail.invalidity_date)))
|
|
@@ -179,11 +245,15 @@ class OriginRevocationResolver {
|
|
|
179
245
|
this.updated.delete(oldest);
|
|
180
246
|
this.graceUntil.delete(oldest);
|
|
181
247
|
}
|
|
182
|
-
this.graceUntil.set(
|
|
248
|
+
this.graceUntil.set(
|
|
249
|
+
origin,
|
|
250
|
+
Math.min(next, now + MAX_POLLING_SECONDS) + 4 * (this.options.pollingIntervalSeconds ?? 300)
|
|
251
|
+
);
|
|
183
252
|
this.updated.set(origin, updated);
|
|
184
253
|
this.freshness.set(origin, Math.min(next, now + (this.options.pollingIntervalSeconds ?? 300)));
|
|
185
254
|
this.failures.delete(origin);
|
|
186
255
|
this.persistenceFailures.delete(origin);
|
|
256
|
+
this.absentUntil.delete(origin);
|
|
187
257
|
} catch (cause) {
|
|
188
258
|
if (cause instanceof WebBotAuthError && cause.reason === "revocation_persistence_failed") throw cause;
|
|
189
259
|
const error = new WebBotAuthError(403, "revocation_unavailable");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OriginRevocationStatus } from './revocation.mjs';
|
|
1
2
|
import type { AuthPrincipal } from '../server/auth.mjs';
|
|
2
3
|
import type { HandlerContext, ProtocolHandlers } from '../server/create-adcp-server.mjs';
|
|
3
4
|
import { type VerifiedWebBotSigner } from '../signing/web-bot-auth.mjs';
|
|
@@ -10,6 +11,17 @@ export declare function createIdentity33PrincipalResolver(options: {
|
|
|
10
11
|
lifecycle: OriginIdentityLifecycle;
|
|
11
12
|
anonymousPrincipal?: string;
|
|
12
13
|
anonymousTools?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* Origin revocation, checked here (the identity layer), not during signature
|
|
16
|
+
* verification. A revoked key is refused without a challenge; unknown
|
|
17
|
+
* status leaves the identity unverified; an origin serving no list stays
|
|
18
|
+
* identity-established and cannot be bound or spend. Usually an
|
|
19
|
+
* `OriginRevocationResolver`. Without it, the server must supply the
|
|
20
|
+
* deprecated `webBotAuth.isRevoked`.
|
|
21
|
+
*/
|
|
22
|
+
revocation?: {
|
|
23
|
+
status(origin: string, keyid: string): Promise<OriginRevocationStatus>;
|
|
24
|
+
};
|
|
13
25
|
}): (signers: readonly VerifiedWebBotSigner[], operation: ResolvedOperation) => Promise<AuthPrincipal>;
|
|
14
26
|
/** Pass to createAdcpServer.identity33.authorize; framework runs it before its replay cache. */
|
|
15
27
|
export declare function createIdentity33Authorization<TAccount>(options: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { OriginRevocationStatus } from './revocation';
|
|
1
2
|
import type { AuthPrincipal } from '../server/auth';
|
|
2
3
|
import type { HandlerContext, ProtocolHandlers } from '../server/create-adcp-server';
|
|
3
4
|
import { type VerifiedWebBotSigner } from '../signing/web-bot-auth';
|
|
@@ -10,6 +11,17 @@ export declare function createIdentity33PrincipalResolver(options: {
|
|
|
10
11
|
lifecycle: OriginIdentityLifecycle;
|
|
11
12
|
anonymousPrincipal?: string;
|
|
12
13
|
anonymousTools?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* Origin revocation, checked here (the identity layer), not during signature
|
|
16
|
+
* verification. A revoked key is refused without a challenge; unknown
|
|
17
|
+
* status leaves the identity unverified; an origin serving no list stays
|
|
18
|
+
* identity-established and cannot be bound or spend. Usually an
|
|
19
|
+
* `OriginRevocationResolver`. Without it, the server must supply the
|
|
20
|
+
* deprecated `webBotAuth.isRevoked`.
|
|
21
|
+
*/
|
|
22
|
+
revocation?: {
|
|
23
|
+
status(origin: string, keyid: string): Promise<OriginRevocationStatus>;
|
|
24
|
+
};
|
|
13
25
|
}): (signers: readonly VerifiedWebBotSigner[], operation: ResolvedOperation) => Promise<AuthPrincipal>;
|
|
14
26
|
/** Pass to createAdcpServer.identity33.authorize; framework runs it before its replay cache. */
|
|
15
27
|
export declare function createIdentity33Authorization<TAccount>(options: {
|