@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
|
@@ -50,6 +50,9 @@ function claimFromContext(ctx) {
|
|
|
50
50
|
const claim = ctx.authInfo?.extra?.identity33;
|
|
51
51
|
return claim && (0, import_web_bot_auth3.isVerifiedWebBotSigner)(claim.signer) ? claim : void 0;
|
|
52
52
|
}
|
|
53
|
+
function anonymousPrincipalFor(origin, prefix = "anonymous") {
|
|
54
|
+
return `${prefix}:${(0, import_crypto.createHash)("sha256").update(origin).digest("hex")}`;
|
|
55
|
+
}
|
|
53
56
|
function isIdentity33UnboundContext(ctx) {
|
|
54
57
|
return claimFromContext(ctx)?.state === "identity-established";
|
|
55
58
|
}
|
|
@@ -67,17 +70,44 @@ function createIdentity33PrincipalResolver(options) {
|
|
|
67
70
|
if (!signers.length || signers.some((s) => !(0, import_web_bot_auth3.isVerifiedWebBotSigner)(s)))
|
|
68
71
|
throw new import_web_bot_auth3.WebBotAuthError(403, "unverified_identity");
|
|
69
72
|
const signer = signers[signers.length - 1];
|
|
73
|
+
let revocation;
|
|
74
|
+
if (options.revocation) {
|
|
75
|
+
revocation = "current";
|
|
76
|
+
for (const each of signers) {
|
|
77
|
+
let status;
|
|
78
|
+
try {
|
|
79
|
+
status = await options.revocation.status(each.origin, each.keyid);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new import_web_bot_auth.WebBotAuthUnavailableError("revocation_persistence_failed", error);
|
|
82
|
+
}
|
|
83
|
+
if (status === "revoked") throw new import_web_bot_auth3.WebBotAuthError(403, "key_revoked");
|
|
84
|
+
if (status === "unknown") throw new import_web_bot_auth3.WebBotAuthError(403, "identity_unverified");
|
|
85
|
+
if (status === "absent") revocation = "absent";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
const revoked = signers.find((s) => s.authorizationFailure);
|
|
71
|
-
|
|
89
|
+
let { binding } = revoked || revocation === "absent" ? { binding: void 0 } : await options.lifecycle.getPrincipal(signer.origin);
|
|
90
|
+
if (binding) {
|
|
91
|
+
try {
|
|
92
|
+
await options.lifecycle.authorizeBound(signer.origin, { readOnly: true });
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof import_trust.TrustMembershipError) binding = void 0;
|
|
95
|
+
else if (error instanceof import_trust.TrustRecordUnavailableError)
|
|
96
|
+
throw new import_web_bot_auth.WebBotAuthUnavailableError("trust_record_unavailable", error);
|
|
97
|
+
else throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
72
100
|
if (!binding && operation !== void 0 && typeof operation !== "string")
|
|
73
101
|
throw new import_web_bot_auth3.WebBotAuthError(400, "operation");
|
|
74
102
|
const authorizationFailure = revoked?.authorizationFailure ?? (!binding && typeof operation === "string" && !anonymousTools.has(operation) ? (0, import_web_bot_auth2.createWebBotAuthAuthorizationFailure)("identity_not_bound") : void 0);
|
|
75
103
|
return {
|
|
76
|
-
principal: binding?.principalId ??
|
|
104
|
+
principal: binding?.principalId ?? anonymousPrincipalFor(signer.origin, options.anonymousPrincipal),
|
|
77
105
|
extra: {
|
|
78
106
|
identity33: {
|
|
79
107
|
state: binding ? "bound" : "identity-established",
|
|
80
108
|
signer: revoked ?? signer,
|
|
109
|
+
...revocation && { revocation },
|
|
110
|
+
anonymousPrincipal: anonymousPrincipalFor(signer.origin, options.anonymousPrincipal),
|
|
81
111
|
...authorizationFailure && { authorizationFailure }
|
|
82
112
|
}
|
|
83
113
|
},
|
|
@@ -91,6 +121,7 @@ function createIdentity33PrincipalResolver(options) {
|
|
|
91
121
|
} : {}
|
|
92
122
|
};
|
|
93
123
|
};
|
|
124
|
+
if (options.revocation) (0, import_web_bot_auth.registerWebBotAuthRevocationCheck)(resolver);
|
|
94
125
|
(0, import_web_bot_auth.registerWebBotAuthLegacyOriginGate)(resolver, async (origin) => {
|
|
95
126
|
if ((await options.lifecycle.getPrincipal(origin)).binding)
|
|
96
127
|
throw new import_errors.RequestSignatureError(
|
|
@@ -102,6 +133,37 @@ function createIdentity33PrincipalResolver(options) {
|
|
|
102
133
|
return resolver;
|
|
103
134
|
}
|
|
104
135
|
function createIdentity33Authorization(options) {
|
|
136
|
+
const anonymousTools = options.anonymousTools ?? [
|
|
137
|
+
"get_principal",
|
|
138
|
+
"get_adcp_capabilities",
|
|
139
|
+
"list_creative_formats",
|
|
140
|
+
"get_products"
|
|
141
|
+
];
|
|
142
|
+
const authorizeUnbound = async (ctx, identity, tool, params) => {
|
|
143
|
+
if (tool === "sync_principal") {
|
|
144
|
+
if (identity.revocation === "absent")
|
|
145
|
+
throw new import_async_outcome.AdcpError("PERMISSION_DENIED", { message: "Agent identity cannot be onboarded" });
|
|
146
|
+
const dryRun = !!params && typeof params === "object" && params.dry_run === true;
|
|
147
|
+
const binding = await options.lifecycle.syncOpen(identity.signer.origin, { dryRun });
|
|
148
|
+
if (dryRun) return;
|
|
149
|
+
if (!ctx.authInfo) throw new import_trust.IdentityAuthorizationError();
|
|
150
|
+
ctx.authInfo.clientId = binding.principalId;
|
|
151
|
+
ctx.authInfo.credential = (0, import_buyer_agent.markVerifiedHttpSig)({
|
|
152
|
+
kind: "http_sig",
|
|
153
|
+
keyid: identity.signer.keyid,
|
|
154
|
+
agent_url: identity.signer.origin,
|
|
155
|
+
verified_at: identity.signer.verified_at
|
|
156
|
+
});
|
|
157
|
+
ctx.authInfo.extra = {
|
|
158
|
+
...ctx.authInfo.extra,
|
|
159
|
+
credential: ctx.authInfo.credential,
|
|
160
|
+
identity33: { ...identity, state: "bound" }
|
|
161
|
+
};
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (anonymousTools.includes(tool) && !needsSpendAuthorization(tool, params)) return;
|
|
165
|
+
throw new import_async_outcome.AdcpError("PERMISSION_DENIED", { message: "Agent identity is not onboarded for this operation" });
|
|
166
|
+
};
|
|
105
167
|
return async (ctx, tool, params) => {
|
|
106
168
|
const identity = claimFromContext(ctx);
|
|
107
169
|
const failure = (0, import_web_bot_auth2.getWebBotAuthAuthorizationFailure)(ctx.authInfo?.extra);
|
|
@@ -114,48 +176,43 @@ function createIdentity33Authorization(options) {
|
|
|
114
176
|
});
|
|
115
177
|
return;
|
|
116
178
|
}
|
|
117
|
-
if (identity.state === "identity-established")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
ctx.authInfo.clientId = binding.principalId;
|
|
124
|
-
ctx.authInfo.credential = (0, import_buyer_agent.markVerifiedHttpSig)({
|
|
125
|
-
kind: "http_sig",
|
|
126
|
-
keyid: identity.signer.keyid,
|
|
127
|
-
agent_url: identity.signer.origin,
|
|
128
|
-
verified_at: identity.signer.verified_at
|
|
129
|
-
});
|
|
130
|
-
ctx.authInfo.extra = {
|
|
131
|
-
...ctx.authInfo.extra,
|
|
132
|
-
credential: ctx.authInfo.credential,
|
|
133
|
-
identity33: { ...identity, state: "bound" }
|
|
134
|
-
};
|
|
179
|
+
if (identity.state === "identity-established") return authorizeUnbound(ctx, identity, tool, params);
|
|
180
|
+
const origin = identity.signer.origin;
|
|
181
|
+
const spend = !IDENTITY_ADMIN_TOOLS.has(tool) && needsSpendAuthorization(tool, params) && !(!KNOWN_TOOLS.has(tool) && options.readOnlyTools?.includes(tool));
|
|
182
|
+
try {
|
|
183
|
+
if (IDENTITY_ADMIN_TOOLS.has(tool)) {
|
|
184
|
+
await options.lifecycle.authorizeBound(origin, { identityConfiguration: true });
|
|
135
185
|
return;
|
|
136
186
|
}
|
|
137
|
-
if (
|
|
187
|
+
if (!spend) {
|
|
188
|
+
await options.lifecycle.authorizeBound(origin, { readOnly: true });
|
|
138
189
|
return;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
190
|
+
}
|
|
191
|
+
const claims = await options.resolveClaims(ctx, tool, params);
|
|
192
|
+
if (!claims.length) throw new import_trust.IdentityAuthorizationError();
|
|
193
|
+
for (const claim of claims) {
|
|
194
|
+
await options.lifecycle.authorizeBound(origin, {
|
|
195
|
+
sandbox: claim.sandbox,
|
|
196
|
+
billing: claim.billing,
|
|
197
|
+
brandDomain: claim.brand.domain
|
|
198
|
+
});
|
|
199
|
+
await (0, import_trust.authorizeDemandGrant)({ ...claim, signerOrigin: origin }, options.grants);
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof import_trust.TrustRecordUnavailableError)
|
|
203
|
+
throw new import_async_outcome.AdcpError("SERVICE_UNAVAILABLE", { message: error.message, recovery: "transient" });
|
|
204
|
+
if (error instanceof import_trust.TrustMembershipError && !spend) {
|
|
205
|
+
const demoted = { ...identity, state: "identity-established" };
|
|
206
|
+
if (ctx.authInfo) {
|
|
207
|
+
const { credential: _credential, ...extra } = ctx.authInfo.extra ?? {};
|
|
208
|
+
ctx.authInfo.clientId = identity.anonymousPrincipal ?? anonymousPrincipalFor(origin);
|
|
209
|
+
delete ctx.authInfo.credential;
|
|
210
|
+
ctx.authInfo.extra = { ...extra, identity33: demoted };
|
|
211
|
+
}
|
|
212
|
+
delete ctx.agent;
|
|
213
|
+
return authorizeUnbound(ctx, demoted, tool, params);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
159
216
|
}
|
|
160
217
|
};
|
|
161
218
|
}
|
|
@@ -176,7 +233,10 @@ function withIdentity33PrincipalHandlers(base, lifecycle) {
|
|
|
176
233
|
return base.getPrincipal(params, ctx);
|
|
177
234
|
}
|
|
178
235
|
const read = await lifecycle.getPrincipal(identity.signer.origin);
|
|
179
|
-
if (!read.binding
|
|
236
|
+
if (!read.binding || identity.state === "identity-established")
|
|
237
|
+
return {
|
|
238
|
+
result: { kind: "unconfigured", ...read.result.relationship && { relationship: read.result.relationship } }
|
|
239
|
+
};
|
|
180
240
|
if (!base.getPrincipal) return { result: read.result };
|
|
181
241
|
const response = await base.getPrincipal(params, ctx);
|
|
182
242
|
if (read.result.relationship && response && typeof response === "object") {
|
|
@@ -200,7 +260,7 @@ function createIdentity33PrincipalScopeResolver(options) {
|
|
|
200
260
|
return options.legacy(ctx);
|
|
201
261
|
}
|
|
202
262
|
const { binding } = await options.lifecycle.getPrincipal(identity.signer.origin);
|
|
203
|
-
if (!binding) throw new import_trust.IdentityAuthorizationError();
|
|
263
|
+
if (!binding || identity.state !== "bound") throw new import_trust.IdentityAuthorizationError();
|
|
204
264
|
return {
|
|
205
265
|
tenant_id: options.tenantId,
|
|
206
266
|
principal_id: binding.principalId,
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
registerWebBotAuthLegacyOriginGate,
|
|
4
|
+
registerWebBotAuthRevocationCheck,
|
|
5
|
+
WebBotAuthUnavailableError
|
|
6
|
+
} from "../signing/web-bot-auth.mjs";
|
|
3
7
|
import { RequestSignatureError } from "../signing/errors.mjs";
|
|
4
8
|
import { TaskTypeValues } from "../types/enums.generated.mjs";
|
|
5
9
|
import { AdcpError } from "../server/decisioning/async-outcome.mjs";
|
|
@@ -8,7 +12,9 @@ import { markVerifiedHttpSig } from "../server/decisioning/buyer-agent.mjs";
|
|
|
8
12
|
import { isVerifiedWebBotSigner, WebBotAuthError } from "../signing/web-bot-auth.mjs";
|
|
9
13
|
import {
|
|
10
14
|
authorizeDemandGrant,
|
|
11
|
-
IdentityAuthorizationError
|
|
15
|
+
IdentityAuthorizationError,
|
|
16
|
+
TrustMembershipError,
|
|
17
|
+
TrustRecordUnavailableError
|
|
12
18
|
} from "./trust.mjs";
|
|
13
19
|
import { MUTATING_TASKS } from "../utils/idempotency.mjs";
|
|
14
20
|
const IDENTITY_ADMIN_TOOLS = /* @__PURE__ */ new Set(["sync_principal", "sync_agent_notification_configs"]);
|
|
@@ -26,6 +32,9 @@ function claimFromContext(ctx) {
|
|
|
26
32
|
const claim = ctx.authInfo?.extra?.identity33;
|
|
27
33
|
return claim && isVerifiedWebBotSigner(claim.signer) ? claim : void 0;
|
|
28
34
|
}
|
|
35
|
+
function anonymousPrincipalFor(origin, prefix = "anonymous") {
|
|
36
|
+
return `${prefix}:${createHash("sha256").update(origin).digest("hex")}`;
|
|
37
|
+
}
|
|
29
38
|
function isIdentity33UnboundContext(ctx) {
|
|
30
39
|
return claimFromContext(ctx)?.state === "identity-established";
|
|
31
40
|
}
|
|
@@ -43,17 +52,44 @@ function createIdentity33PrincipalResolver(options) {
|
|
|
43
52
|
if (!signers.length || signers.some((s) => !isVerifiedWebBotSigner(s)))
|
|
44
53
|
throw new WebBotAuthError(403, "unverified_identity");
|
|
45
54
|
const signer = signers[signers.length - 1];
|
|
55
|
+
let revocation;
|
|
56
|
+
if (options.revocation) {
|
|
57
|
+
revocation = "current";
|
|
58
|
+
for (const each of signers) {
|
|
59
|
+
let status;
|
|
60
|
+
try {
|
|
61
|
+
status = await options.revocation.status(each.origin, each.keyid);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new WebBotAuthUnavailableError("revocation_persistence_failed", error);
|
|
64
|
+
}
|
|
65
|
+
if (status === "revoked") throw new WebBotAuthError(403, "key_revoked");
|
|
66
|
+
if (status === "unknown") throw new WebBotAuthError(403, "identity_unverified");
|
|
67
|
+
if (status === "absent") revocation = "absent";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
46
70
|
const revoked = signers.find((s) => s.authorizationFailure);
|
|
47
|
-
|
|
71
|
+
let { binding } = revoked || revocation === "absent" ? { binding: void 0 } : await options.lifecycle.getPrincipal(signer.origin);
|
|
72
|
+
if (binding) {
|
|
73
|
+
try {
|
|
74
|
+
await options.lifecycle.authorizeBound(signer.origin, { readOnly: true });
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (error instanceof TrustMembershipError) binding = void 0;
|
|
77
|
+
else if (error instanceof TrustRecordUnavailableError)
|
|
78
|
+
throw new WebBotAuthUnavailableError("trust_record_unavailable", error);
|
|
79
|
+
else throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
48
82
|
if (!binding && operation !== void 0 && typeof operation !== "string")
|
|
49
83
|
throw new WebBotAuthError(400, "operation");
|
|
50
84
|
const authorizationFailure = revoked?.authorizationFailure ?? (!binding && typeof operation === "string" && !anonymousTools.has(operation) ? createWebBotAuthAuthorizationFailure("identity_not_bound") : void 0);
|
|
51
85
|
return {
|
|
52
|
-
principal: binding?.principalId ??
|
|
86
|
+
principal: binding?.principalId ?? anonymousPrincipalFor(signer.origin, options.anonymousPrincipal),
|
|
53
87
|
extra: {
|
|
54
88
|
identity33: {
|
|
55
89
|
state: binding ? "bound" : "identity-established",
|
|
56
90
|
signer: revoked ?? signer,
|
|
91
|
+
...revocation && { revocation },
|
|
92
|
+
anonymousPrincipal: anonymousPrincipalFor(signer.origin, options.anonymousPrincipal),
|
|
57
93
|
...authorizationFailure && { authorizationFailure }
|
|
58
94
|
}
|
|
59
95
|
},
|
|
@@ -67,6 +103,7 @@ function createIdentity33PrincipalResolver(options) {
|
|
|
67
103
|
} : {}
|
|
68
104
|
};
|
|
69
105
|
};
|
|
106
|
+
if (options.revocation) registerWebBotAuthRevocationCheck(resolver);
|
|
70
107
|
registerWebBotAuthLegacyOriginGate(resolver, async (origin) => {
|
|
71
108
|
if ((await options.lifecycle.getPrincipal(origin)).binding)
|
|
72
109
|
throw new RequestSignatureError(
|
|
@@ -78,6 +115,37 @@ function createIdentity33PrincipalResolver(options) {
|
|
|
78
115
|
return resolver;
|
|
79
116
|
}
|
|
80
117
|
function createIdentity33Authorization(options) {
|
|
118
|
+
const anonymousTools = options.anonymousTools ?? [
|
|
119
|
+
"get_principal",
|
|
120
|
+
"get_adcp_capabilities",
|
|
121
|
+
"list_creative_formats",
|
|
122
|
+
"get_products"
|
|
123
|
+
];
|
|
124
|
+
const authorizeUnbound = async (ctx, identity, tool, params) => {
|
|
125
|
+
if (tool === "sync_principal") {
|
|
126
|
+
if (identity.revocation === "absent")
|
|
127
|
+
throw new AdcpError("PERMISSION_DENIED", { message: "Agent identity cannot be onboarded" });
|
|
128
|
+
const dryRun = !!params && typeof params === "object" && params.dry_run === true;
|
|
129
|
+
const binding = await options.lifecycle.syncOpen(identity.signer.origin, { dryRun });
|
|
130
|
+
if (dryRun) return;
|
|
131
|
+
if (!ctx.authInfo) throw new IdentityAuthorizationError();
|
|
132
|
+
ctx.authInfo.clientId = binding.principalId;
|
|
133
|
+
ctx.authInfo.credential = markVerifiedHttpSig({
|
|
134
|
+
kind: "http_sig",
|
|
135
|
+
keyid: identity.signer.keyid,
|
|
136
|
+
agent_url: identity.signer.origin,
|
|
137
|
+
verified_at: identity.signer.verified_at
|
|
138
|
+
});
|
|
139
|
+
ctx.authInfo.extra = {
|
|
140
|
+
...ctx.authInfo.extra,
|
|
141
|
+
credential: ctx.authInfo.credential,
|
|
142
|
+
identity33: { ...identity, state: "bound" }
|
|
143
|
+
};
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (anonymousTools.includes(tool) && !needsSpendAuthorization(tool, params)) return;
|
|
147
|
+
throw new AdcpError("PERMISSION_DENIED", { message: "Agent identity is not onboarded for this operation" });
|
|
148
|
+
};
|
|
81
149
|
return async (ctx, tool, params) => {
|
|
82
150
|
const identity = claimFromContext(ctx);
|
|
83
151
|
const failure = getWebBotAuthAuthorizationFailure(ctx.authInfo?.extra);
|
|
@@ -90,48 +158,43 @@ function createIdentity33Authorization(options) {
|
|
|
90
158
|
});
|
|
91
159
|
return;
|
|
92
160
|
}
|
|
93
|
-
if (identity.state === "identity-established")
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
ctx.authInfo.clientId = binding.principalId;
|
|
100
|
-
ctx.authInfo.credential = markVerifiedHttpSig({
|
|
101
|
-
kind: "http_sig",
|
|
102
|
-
keyid: identity.signer.keyid,
|
|
103
|
-
agent_url: identity.signer.origin,
|
|
104
|
-
verified_at: identity.signer.verified_at
|
|
105
|
-
});
|
|
106
|
-
ctx.authInfo.extra = {
|
|
107
|
-
...ctx.authInfo.extra,
|
|
108
|
-
credential: ctx.authInfo.credential,
|
|
109
|
-
identity33: { ...identity, state: "bound" }
|
|
110
|
-
};
|
|
161
|
+
if (identity.state === "identity-established") return authorizeUnbound(ctx, identity, tool, params);
|
|
162
|
+
const origin = identity.signer.origin;
|
|
163
|
+
const spend = !IDENTITY_ADMIN_TOOLS.has(tool) && needsSpendAuthorization(tool, params) && !(!KNOWN_TOOLS.has(tool) && options.readOnlyTools?.includes(tool));
|
|
164
|
+
try {
|
|
165
|
+
if (IDENTITY_ADMIN_TOOLS.has(tool)) {
|
|
166
|
+
await options.lifecycle.authorizeBound(origin, { identityConfiguration: true });
|
|
111
167
|
return;
|
|
112
168
|
}
|
|
113
|
-
if (
|
|
169
|
+
if (!spend) {
|
|
170
|
+
await options.lifecycle.authorizeBound(origin, { readOnly: true });
|
|
114
171
|
return;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
172
|
+
}
|
|
173
|
+
const claims = await options.resolveClaims(ctx, tool, params);
|
|
174
|
+
if (!claims.length) throw new IdentityAuthorizationError();
|
|
175
|
+
for (const claim of claims) {
|
|
176
|
+
await options.lifecycle.authorizeBound(origin, {
|
|
177
|
+
sandbox: claim.sandbox,
|
|
178
|
+
billing: claim.billing,
|
|
179
|
+
brandDomain: claim.brand.domain
|
|
180
|
+
});
|
|
181
|
+
await authorizeDemandGrant({ ...claim, signerOrigin: origin }, options.grants);
|
|
182
|
+
}
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof TrustRecordUnavailableError)
|
|
185
|
+
throw new AdcpError("SERVICE_UNAVAILABLE", { message: error.message, recovery: "transient" });
|
|
186
|
+
if (error instanceof TrustMembershipError && !spend) {
|
|
187
|
+
const demoted = { ...identity, state: "identity-established" };
|
|
188
|
+
if (ctx.authInfo) {
|
|
189
|
+
const { credential: _credential, ...extra } = ctx.authInfo.extra ?? {};
|
|
190
|
+
ctx.authInfo.clientId = identity.anonymousPrincipal ?? anonymousPrincipalFor(origin);
|
|
191
|
+
delete ctx.authInfo.credential;
|
|
192
|
+
ctx.authInfo.extra = { ...extra, identity33: demoted };
|
|
193
|
+
}
|
|
194
|
+
delete ctx.agent;
|
|
195
|
+
return authorizeUnbound(ctx, demoted, tool, params);
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
135
198
|
}
|
|
136
199
|
};
|
|
137
200
|
}
|
|
@@ -152,7 +215,10 @@ function withIdentity33PrincipalHandlers(base, lifecycle) {
|
|
|
152
215
|
return base.getPrincipal(params, ctx);
|
|
153
216
|
}
|
|
154
217
|
const read = await lifecycle.getPrincipal(identity.signer.origin);
|
|
155
|
-
if (!read.binding
|
|
218
|
+
if (!read.binding || identity.state === "identity-established")
|
|
219
|
+
return {
|
|
220
|
+
result: { kind: "unconfigured", ...read.result.relationship && { relationship: read.result.relationship } }
|
|
221
|
+
};
|
|
156
222
|
if (!base.getPrincipal) return { result: read.result };
|
|
157
223
|
const response = await base.getPrincipal(params, ctx);
|
|
158
224
|
if (read.result.relationship && response && typeof response === "object") {
|
|
@@ -176,7 +242,7 @@ function createIdentity33PrincipalScopeResolver(options) {
|
|
|
176
242
|
return options.legacy(ctx);
|
|
177
243
|
}
|
|
178
244
|
const { binding } = await options.lifecycle.getPrincipal(identity.signer.origin);
|
|
179
|
-
if (!binding) throw new IdentityAuthorizationError();
|
|
245
|
+
if (!binding || identity.state !== "bound") throw new IdentityAuthorizationError();
|
|
180
246
|
return {
|
|
181
247
|
tenant_id: options.tenantId,
|
|
182
248
|
principal_id: binding.principalId,
|
|
@@ -6,6 +6,32 @@ export declare class IdentityAuthorizationError extends Error {
|
|
|
6
6
|
readonly details: AgentGrantDeniedDetails | undefined;
|
|
7
7
|
constructor(reason?: 'authorization_failed' | 'grant_absent');
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The trust record could not be read (transport error, outage, capacity,
|
|
11
|
+
* invalid content). A fetch failure is never absence or delisting: spend
|
|
12
|
+
* fails closed with a retryable SERVICE_UNAVAILABLE, other calls continue
|
|
13
|
+
* under an existing binding.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TrustRecordUnavailableError extends IdentityAuthorizationError {
|
|
16
|
+
readonly transient: true;
|
|
17
|
+
/**
|
|
18
|
+
* Set by `match()`: the last record read for this operator listed the origin
|
|
19
|
+
* with the role and is still within the seller's outage bound
|
|
20
|
+
* (`maxOutageContinuationSeconds` past its freshness). Only then may non-spend
|
|
21
|
+
* calls continue under a binding.
|
|
22
|
+
*/
|
|
23
|
+
lastKnownListed: boolean;
|
|
24
|
+
constructor();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The operator's last record read does not list the origin with the required
|
|
28
|
+
* role (`unlisted`; operators retire agents by publishing an empty `agents[]`),
|
|
29
|
+
* or the operator has never published one (`absent`).
|
|
30
|
+
*/
|
|
31
|
+
export declare class TrustMembershipError extends IdentityAuthorizationError {
|
|
32
|
+
readonly membership: 'unlisted' | 'absent';
|
|
33
|
+
constructor(membership?: 'unlisted' | 'absent');
|
|
34
|
+
}
|
|
9
35
|
/** Schema plus semantic checks; publication never includes identity keys. */
|
|
10
36
|
export declare function validateTrustRecord(value: unknown): asserts value is TrustRecord;
|
|
11
37
|
export declare function createTrustRecord(record: TrustRecord): TrustRecord;
|
|
@@ -24,9 +50,30 @@ export type TrustFetchResult = {
|
|
|
24
50
|
export interface TrustEvaluatorOptions {
|
|
25
51
|
/** Matches configured revocation polling: one minute through thirty minutes. */
|
|
26
52
|
pollingIntervalSeconds?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Seller policy: how long past the last listing record's freshness a bound
|
|
55
|
+
* origin's non-spend calls may continue while its operator's record cannot
|
|
56
|
+
* be read. Spend never continues on an outage. Default four polling
|
|
57
|
+
* intervals.
|
|
58
|
+
*/
|
|
59
|
+
maxOutageContinuationSeconds?: number;
|
|
27
60
|
now?: () => number;
|
|
28
61
|
fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
|
|
29
62
|
maxEntries?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Process-local authority history only: operators remembered per hosting
|
|
65
|
+
* provider, i.e. per ICANN registrable domain of a PSL PRIVATE-section name
|
|
66
|
+
* (every `*.vercel.app`, `*.s3.<region>.amazonaws.com` or
|
|
67
|
+
* `*.<project>.platformsh.site` operator shares one budget). ICANN
|
|
68
|
+
* registrable domains each count once. Default 32.
|
|
69
|
+
*/
|
|
70
|
+
maxAuthoritativeOperatorsPerPrivateSuffix?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Process-local authority history only: total slots all PRIVATE-section
|
|
73
|
+
* operators may hold together, so registrants of ICANN domains always keep
|
|
74
|
+
* headroom. Default half of `maxEntries`.
|
|
75
|
+
*/
|
|
76
|
+
maxPrivateAuthoritativeOperators?: number;
|
|
30
77
|
maxConcurrentFetches?: number;
|
|
31
78
|
/** Share authoritative-history across restarts/replicas when legacy fallback is enabled. */
|
|
32
79
|
authorityHistory?: {
|
|
@@ -41,10 +88,14 @@ export declare class TrustEvaluator {
|
|
|
41
88
|
private readonly negative;
|
|
42
89
|
private readonly pending;
|
|
43
90
|
private readonly urlRequests;
|
|
91
|
+
/** Keyed by (operator, host): one operator's authoritative_location hops never spend another's budget. */
|
|
44
92
|
private readonly hostRates;
|
|
45
93
|
private readonly authoritativeOperators;
|
|
46
|
-
|
|
94
|
+
/** Remembered authoritative operators per budget group (see authorityGroup). */
|
|
95
|
+
private readonly authorityGroups;
|
|
96
|
+
private privateAuthorityOperators;
|
|
47
97
|
readonly pollingIntervalSeconds: number;
|
|
98
|
+
readonly maxOutageContinuationSeconds: number;
|
|
48
99
|
constructor(options?: TrustEvaluatorOptions);
|
|
49
100
|
get(domainOrOrigin: string, force?: boolean): Promise<TrustFetchResult>;
|
|
50
101
|
match(origin: string, role: string, countries?: readonly string[]): Promise<{
|
|
@@ -54,6 +105,16 @@ export declare class TrustEvaluator {
|
|
|
54
105
|
freshUntil: number;
|
|
55
106
|
}>;
|
|
56
107
|
invalidate(domain: string): void;
|
|
108
|
+
/**
|
|
109
|
+
* Record that `operator` served a trust record. With durable history the
|
|
110
|
+
* in-memory set is only a cache and may evict; without it, nothing is
|
|
111
|
+
* evicted and an operator over its group budget or the total is refused.
|
|
112
|
+
*/
|
|
113
|
+
private rememberAuthority;
|
|
114
|
+
/**
|
|
115
|
+
* Process-local view only. With durable `authorityHistory` this set is a
|
|
116
|
+
* cache and may have evicted an operator that durable history still records.
|
|
117
|
+
*/
|
|
57
118
|
hasAuthoritativeRecord(domain: string): boolean;
|
|
58
119
|
private now;
|
|
59
120
|
private request;
|
|
@@ -6,6 +6,32 @@ export declare class IdentityAuthorizationError extends Error {
|
|
|
6
6
|
readonly details: AgentGrantDeniedDetails | undefined;
|
|
7
7
|
constructor(reason?: 'authorization_failed' | 'grant_absent');
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* The trust record could not be read (transport error, outage, capacity,
|
|
11
|
+
* invalid content). A fetch failure is never absence or delisting: spend
|
|
12
|
+
* fails closed with a retryable SERVICE_UNAVAILABLE, other calls continue
|
|
13
|
+
* under an existing binding.
|
|
14
|
+
*/
|
|
15
|
+
export declare class TrustRecordUnavailableError extends IdentityAuthorizationError {
|
|
16
|
+
readonly transient: true;
|
|
17
|
+
/**
|
|
18
|
+
* Set by `match()`: the last record read for this operator listed the origin
|
|
19
|
+
* with the role and is still within the seller's outage bound
|
|
20
|
+
* (`maxOutageContinuationSeconds` past its freshness). Only then may non-spend
|
|
21
|
+
* calls continue under a binding.
|
|
22
|
+
*/
|
|
23
|
+
lastKnownListed: boolean;
|
|
24
|
+
constructor();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The operator's last record read does not list the origin with the required
|
|
28
|
+
* role (`unlisted`; operators retire agents by publishing an empty `agents[]`),
|
|
29
|
+
* or the operator has never published one (`absent`).
|
|
30
|
+
*/
|
|
31
|
+
export declare class TrustMembershipError extends IdentityAuthorizationError {
|
|
32
|
+
readonly membership: 'unlisted' | 'absent';
|
|
33
|
+
constructor(membership?: 'unlisted' | 'absent');
|
|
34
|
+
}
|
|
9
35
|
/** Schema plus semantic checks; publication never includes identity keys. */
|
|
10
36
|
export declare function validateTrustRecord(value: unknown): asserts value is TrustRecord;
|
|
11
37
|
export declare function createTrustRecord(record: TrustRecord): TrustRecord;
|
|
@@ -24,9 +50,30 @@ export type TrustFetchResult = {
|
|
|
24
50
|
export interface TrustEvaluatorOptions {
|
|
25
51
|
/** Matches configured revocation polling: one minute through thirty minutes. */
|
|
26
52
|
pollingIntervalSeconds?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Seller policy: how long past the last listing record's freshness a bound
|
|
55
|
+
* origin's non-spend calls may continue while its operator's record cannot
|
|
56
|
+
* be read. Spend never continues on an outage. Default four polling
|
|
57
|
+
* intervals.
|
|
58
|
+
*/
|
|
59
|
+
maxOutageContinuationSeconds?: number;
|
|
27
60
|
now?: () => number;
|
|
28
61
|
fetchOptions?: Pick<SsrfFetchOptions, 'lookup' | 'trustedFetchFn'>;
|
|
29
62
|
maxEntries?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Process-local authority history only: operators remembered per hosting
|
|
65
|
+
* provider, i.e. per ICANN registrable domain of a PSL PRIVATE-section name
|
|
66
|
+
* (every `*.vercel.app`, `*.s3.<region>.amazonaws.com` or
|
|
67
|
+
* `*.<project>.platformsh.site` operator shares one budget). ICANN
|
|
68
|
+
* registrable domains each count once. Default 32.
|
|
69
|
+
*/
|
|
70
|
+
maxAuthoritativeOperatorsPerPrivateSuffix?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Process-local authority history only: total slots all PRIVATE-section
|
|
73
|
+
* operators may hold together, so registrants of ICANN domains always keep
|
|
74
|
+
* headroom. Default half of `maxEntries`.
|
|
75
|
+
*/
|
|
76
|
+
maxPrivateAuthoritativeOperators?: number;
|
|
30
77
|
maxConcurrentFetches?: number;
|
|
31
78
|
/** Share authoritative-history across restarts/replicas when legacy fallback is enabled. */
|
|
32
79
|
authorityHistory?: {
|
|
@@ -41,10 +88,14 @@ export declare class TrustEvaluator {
|
|
|
41
88
|
private readonly negative;
|
|
42
89
|
private readonly pending;
|
|
43
90
|
private readonly urlRequests;
|
|
91
|
+
/** Keyed by (operator, host): one operator's authoritative_location hops never spend another's budget. */
|
|
44
92
|
private readonly hostRates;
|
|
45
93
|
private readonly authoritativeOperators;
|
|
46
|
-
|
|
94
|
+
/** Remembered authoritative operators per budget group (see authorityGroup). */
|
|
95
|
+
private readonly authorityGroups;
|
|
96
|
+
private privateAuthorityOperators;
|
|
47
97
|
readonly pollingIntervalSeconds: number;
|
|
98
|
+
readonly maxOutageContinuationSeconds: number;
|
|
48
99
|
constructor(options?: TrustEvaluatorOptions);
|
|
49
100
|
get(domainOrOrigin: string, force?: boolean): Promise<TrustFetchResult>;
|
|
50
101
|
match(origin: string, role: string, countries?: readonly string[]): Promise<{
|
|
@@ -54,6 +105,16 @@ export declare class TrustEvaluator {
|
|
|
54
105
|
freshUntil: number;
|
|
55
106
|
}>;
|
|
56
107
|
invalidate(domain: string): void;
|
|
108
|
+
/**
|
|
109
|
+
* Record that `operator` served a trust record. With durable history the
|
|
110
|
+
* in-memory set is only a cache and may evict; without it, nothing is
|
|
111
|
+
* evicted and an operator over its group budget or the total is refused.
|
|
112
|
+
*/
|
|
113
|
+
private rememberAuthority;
|
|
114
|
+
/**
|
|
115
|
+
* Process-local view only. With durable `authorityHistory` this set is a
|
|
116
|
+
* cache and may have evicted an operator that durable history still records.
|
|
117
|
+
*/
|
|
57
118
|
hasAuthoritativeRecord(domain: string): boolean;
|
|
58
119
|
private now;
|
|
59
120
|
private request;
|