agenthooksprotocol 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +28 -2
- package/dist/src/client/auth.d.ts +147 -0
- package/dist/src/client/auth.d.ts.map +1 -0
- package/dist/src/client/auth.js +629 -0
- package/dist/src/client/auth.js.map +1 -0
- package/dist/src/client/composition.d.ts +42 -0
- package/dist/src/client/composition.d.ts.map +1 -0
- package/dist/src/client/composition.js +523 -0
- package/dist/src/client/composition.js.map +1 -0
- package/dist/src/client/content.d.ts +73 -0
- package/dist/src/client/content.d.ts.map +1 -0
- package/dist/src/client/content.js +404 -0
- package/dist/src/client/content.js.map +1 -0
- package/dist/src/client/hooks.d.ts +62 -0
- package/dist/src/client/hooks.d.ts.map +1 -0
- package/dist/src/client/hooks.js +997 -0
- package/dist/src/client/hooks.js.map +1 -0
- package/dist/src/client/index.d.ts +11 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +9 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/client/transport.d.ts +12 -0
- package/dist/src/client/transport.d.ts.map +1 -0
- package/dist/src/client/transport.js +347 -0
- package/dist/src/client/transport.js.map +1 -0
- package/dist/src/client/types.d.ts +117 -0
- package/dist/src/client/types.d.ts.map +1 -0
- package/dist/src/client/types.js +10 -0
- package/dist/src/client/types.js.map +1 -0
- package/dist/src/client/validation.d.ts +3 -0
- package/dist/src/client/validation.d.ts.map +1 -0
- package/dist/src/client/validation.js +25 -0
- package/dist/src/client/validation.js.map +1 -0
- package/dist/src/compaction.d.ts +27 -0
- package/dist/src/compaction.d.ts.map +1 -0
- package/dist/src/compaction.js +155 -0
- package/dist/src/compaction.js.map +1 -0
- package/dist/src/content-upload.d.ts +60 -0
- package/dist/src/content-upload.d.ts.map +1 -0
- package/dist/src/content-upload.js +129 -0
- package/dist/src/content-upload.js.map +1 -0
- package/dist/src/draft/generated.d.ts +3085 -0
- package/dist/src/draft/generated.d.ts.map +1 -0
- package/dist/src/draft/generated.js +15372 -0
- package/dist/src/draft/generated.js.map +1 -0
- package/dist/src/draft/index.d.ts +31 -0
- package/dist/src/draft/index.d.ts.map +1 -0
- package/dist/src/draft/index.js +129 -0
- package/dist/src/draft/index.js.map +1 -0
- package/dist/src/draft/schemas.d.ts +8848 -0
- package/dist/src/draft/schemas.d.ts.map +1 -0
- package/dist/src/draft/schemas.js +9219 -0
- package/dist/src/draft/schemas.js.map +1 -0
- package/dist/src/draft-atomic.d.ts +18 -0
- package/dist/src/draft-atomic.d.ts.map +1 -0
- package/dist/src/draft-atomic.js +102 -0
- package/dist/src/draft-atomic.js.map +1 -0
- package/dist/src/draft-runtime.d.ts +89 -0
- package/dist/src/draft-runtime.d.ts.map +1 -0
- package/dist/src/draft-runtime.js +242 -0
- package/dist/src/draft-runtime.js.map +1 -0
- package/dist/src/elicitation.d.ts +18 -0
- package/dist/src/elicitation.d.ts.map +1 -0
- package/dist/src/elicitation.js +191 -0
- package/dist/src/elicitation.js.map +1 -0
- package/dist/src/errors.d.ts +7 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +15 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/framing.d.ts +7 -0
- package/dist/src/framing.d.ts.map +1 -0
- package/dist/src/framing.js +39 -0
- package/dist/src/framing.js.map +1 -0
- package/dist/src/generated.d.ts +304 -0
- package/dist/src/generated.d.ts.map +1 -0
- package/dist/src/generated.js +1885 -0
- package/dist/src/generated.js.map +1 -0
- package/dist/src/hook-runner.d.ts +11 -0
- package/dist/src/hook-runner.d.ts.map +1 -0
- package/dist/src/hook-runner.js +223 -0
- package/dist/src/hook-runner.js.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/observation.d.ts +26 -0
- package/dist/src/observation.d.ts.map +1 -0
- package/dist/src/observation.js +28 -0
- package/dist/src/observation.js.map +1 -0
- package/dist/src/server/attachments.d.ts +29 -0
- package/dist/src/server/attachments.d.ts.map +1 -0
- package/dist/src/server/attachments.js +112 -0
- package/dist/src/server/attachments.js.map +1 -0
- package/dist/src/server/hooks.d.ts +20 -0
- package/dist/src/server/hooks.d.ts.map +1 -0
- package/dist/src/server/hooks.js +129 -0
- package/dist/src/server/hooks.js.map +1 -0
- package/dist/src/server/index.d.ts +7 -0
- package/dist/src/server/index.d.ts.map +1 -0
- package/dist/src/server/index.js +4 -0
- package/dist/src/server/index.js.map +1 -0
- package/dist/src/server/stdio.d.ts +15 -0
- package/dist/src/server/stdio.d.ts.map +1 -0
- package/dist/src/server/stdio.js +69 -0
- package/dist/src/server/stdio.js.map +1 -0
- package/dist/src/transport.d.ts +7 -0
- package/dist/src/transport.d.ts.map +1 -0
- package/dist/src/transport.js +195 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/types.d.ts +116 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +14 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/validation.d.ts +7 -0
- package/dist/src/validation.d.ts.map +1 -0
- package/dist/src/validation.js +209 -0
- package/dist/src/validation.js.map +1 -0
- package/package.json +62 -4
- package/src/client/auth.ts +1076 -0
- package/src/client/composition.ts +708 -0
- package/src/client/content.ts +510 -0
- package/src/client/hooks.ts +1368 -0
- package/src/client/index.ts +28 -0
- package/src/client/transport.ts +408 -0
- package/src/client/types.ts +173 -0
- package/src/client/validation.ts +24 -0
- package/src/compaction.ts +196 -0
- package/src/content-upload.ts +205 -0
- package/src/draft/README.md +16 -0
- package/src/draft/ahp-codegen.lock.json +123 -0
- package/src/draft/generated.ts +19143 -0
- package/src/draft/index.ts +246 -0
- package/src/draft/schemas.ts +9413 -0
- package/src/draft-atomic.ts +119 -0
- package/src/draft-runtime.ts +365 -0
- package/src/elicitation.ts +254 -0
- package/src/errors.ts +30 -0
- package/src/framing.ts +56 -0
- package/src/generated.ts +2408 -0
- package/src/hook-runner.ts +310 -0
- package/src/index.ts +8 -0
- package/src/observation.ts +65 -0
- package/src/server/attachments.ts +128 -0
- package/src/server/hooks.ts +205 -0
- package/src/server/index.ts +18 -0
- package/src/server/node-crypto.d.ts +7 -0
- package/src/server/stdio.ts +81 -0
- package/src/transport.ts +273 -0
- package/src/types.ts +124 -0
- package/src/validation.ts +372 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
function error(message) {
|
|
2
|
+
// Never include credential values, callback URLs, or token endpoint bodies.
|
|
3
|
+
throw new Error(`Authentication failed: ${message}`);
|
|
4
|
+
}
|
|
5
|
+
function record(value) {
|
|
6
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
7
|
+
error("invalid metadata");
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function string(value, name) {
|
|
11
|
+
if (typeof value !== "string" || !value)
|
|
12
|
+
error(`missing ${name}`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function strings(value) {
|
|
16
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
|
|
17
|
+
error("invalid metadata list");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function loopback(host) {
|
|
21
|
+
return (host === "localhost" ||
|
|
22
|
+
host.endsWith(".localhost") ||
|
|
23
|
+
host === "[::1]" ||
|
|
24
|
+
host === "::1" ||
|
|
25
|
+
/^127\./.test(host));
|
|
26
|
+
}
|
|
27
|
+
function publicAddress(address) {
|
|
28
|
+
const host = address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
29
|
+
if (host.includes(":"))
|
|
30
|
+
return (/^[23][\da-f]{3}:/.test(host) && !/^(?:2001:(?:db8|0):|2002:)/.test(host));
|
|
31
|
+
const octets = host.split(".").map(Number);
|
|
32
|
+
if (octets.length !== 4 ||
|
|
33
|
+
octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255))
|
|
34
|
+
return false;
|
|
35
|
+
const [a = 0, b = 0, c = 0] = octets;
|
|
36
|
+
return !(a === 0 ||
|
|
37
|
+
a === 10 ||
|
|
38
|
+
a === 127 ||
|
|
39
|
+
a >= 224 ||
|
|
40
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
41
|
+
(a === 169 && b === 254) ||
|
|
42
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
43
|
+
(a === 192 && (b === 168 || b === 0 || (b === 88 && c === 99))) ||
|
|
44
|
+
(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100))) ||
|
|
45
|
+
(a === 203 && b === 0 && c === 113));
|
|
46
|
+
}
|
|
47
|
+
function secureUrl(value, options = {}) {
|
|
48
|
+
const url = new URL(value);
|
|
49
|
+
const local = url.protocol === "http:" &&
|
|
50
|
+
loopback(url.hostname) &&
|
|
51
|
+
options.allowedLoopbackOrigins?.includes(url.origin);
|
|
52
|
+
if ((url.protocol !== "https:" && !local) ||
|
|
53
|
+
url.username ||
|
|
54
|
+
url.password ||
|
|
55
|
+
url.hash)
|
|
56
|
+
error("HTTPS URL required");
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
async function destination(value, purpose, context, options) {
|
|
60
|
+
const url = secureUrl(value, options);
|
|
61
|
+
const host = url.hostname.toLowerCase();
|
|
62
|
+
const local = loopback(host) && options.allowedLoopbackOrigins?.includes(url.origin);
|
|
63
|
+
if (!local) {
|
|
64
|
+
if (loopback(host) ||
|
|
65
|
+
host.endsWith(".local") ||
|
|
66
|
+
(!host.includes(".") && !host.includes(":")))
|
|
67
|
+
error("forbidden destination");
|
|
68
|
+
if ((/^[\d.]+$/.test(host) || host.includes(":")) && !publicAddress(host))
|
|
69
|
+
error("forbidden destination");
|
|
70
|
+
}
|
|
71
|
+
let addresses = host.includes(":")
|
|
72
|
+
? ["::1"]
|
|
73
|
+
: ["127.0.0.1"];
|
|
74
|
+
if (!local) {
|
|
75
|
+
addresses = await withinBudget((async () => {
|
|
76
|
+
if (options.resolveHostname)
|
|
77
|
+
return options.resolveHostname(host);
|
|
78
|
+
// Keep the Node-only resolver lazy: browser hosts must provide a resolver
|
|
79
|
+
// and a fetch adapter that enforces the checked destination addresses.
|
|
80
|
+
const moduleName = "node:dns/promises";
|
|
81
|
+
const dns = (await import(moduleName));
|
|
82
|
+
return (await dns.lookup(host.replace(/^\[|\]$/g, ""), { all: true })).map((entry) => entry.address);
|
|
83
|
+
})(), context.signal);
|
|
84
|
+
if (!addresses.length ||
|
|
85
|
+
addresses.some((address) => !publicAddress(address)))
|
|
86
|
+
error("forbidden resolved destination");
|
|
87
|
+
}
|
|
88
|
+
if (options.validateDestination)
|
|
89
|
+
await withinBudget(Promise.resolve(options.validateDestination(url, purpose, context)), context.signal);
|
|
90
|
+
return Object.assign(url, { resolvedAddresses: addresses });
|
|
91
|
+
}
|
|
92
|
+
/** Node's default transport pins the checked addresses, preventing DNS rebinding.
|
|
93
|
+
* A custom fetch is a trusted host adapter and receives resolvedAddresses on URL.
|
|
94
|
+
* Redirects are never followed, and token/metadata response bodies are bounded.
|
|
95
|
+
*/
|
|
96
|
+
async function fetchChecked(url, init, options) {
|
|
97
|
+
if (options.fetch)
|
|
98
|
+
return options.fetch(url, init);
|
|
99
|
+
const moduleName = url.protocol === "http:" ? "node:http" : "node:https";
|
|
100
|
+
const http = (await import(moduleName));
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const request = http.request(url, {
|
|
103
|
+
method: init.method ?? "GET",
|
|
104
|
+
headers: Object.fromEntries(new Headers(init.headers)),
|
|
105
|
+
signal: init.signal,
|
|
106
|
+
lookup(_hostname, lookupOptions, callback) {
|
|
107
|
+
const addresses = url.resolvedAddresses.map((address) => ({
|
|
108
|
+
address,
|
|
109
|
+
family: address.includes(":") ? 6 : 4,
|
|
110
|
+
}));
|
|
111
|
+
if (lookupOptions.all)
|
|
112
|
+
callback(null, addresses);
|
|
113
|
+
else
|
|
114
|
+
callback(null, addresses[0]?.address, addresses[0]?.family);
|
|
115
|
+
},
|
|
116
|
+
}, (incoming) => {
|
|
117
|
+
void (async () => {
|
|
118
|
+
const headers = new Headers();
|
|
119
|
+
for (const [key, value] of Object.entries(incoming.headers)) {
|
|
120
|
+
if (Array.isArray(value))
|
|
121
|
+
for (const item of value)
|
|
122
|
+
headers.append(key, item);
|
|
123
|
+
else if (value !== undefined)
|
|
124
|
+
headers.set(key, value);
|
|
125
|
+
}
|
|
126
|
+
const chunks = [];
|
|
127
|
+
let size = 0;
|
|
128
|
+
for await (const chunk of incoming) {
|
|
129
|
+
size += chunk.length;
|
|
130
|
+
if (size > 1_048_576) {
|
|
131
|
+
request.destroy();
|
|
132
|
+
error("metadata or token response too large");
|
|
133
|
+
}
|
|
134
|
+
chunks.push(chunk);
|
|
135
|
+
}
|
|
136
|
+
const body = new Uint8Array(size);
|
|
137
|
+
let offset = 0;
|
|
138
|
+
for (const chunk of chunks) {
|
|
139
|
+
body.set(chunk, offset);
|
|
140
|
+
offset += chunk.length;
|
|
141
|
+
}
|
|
142
|
+
const status = incoming.statusCode ?? 500;
|
|
143
|
+
resolve(new Response([204, 205, 304].includes(status) ? null : body, {
|
|
144
|
+
status,
|
|
145
|
+
headers,
|
|
146
|
+
}));
|
|
147
|
+
})().catch(reject);
|
|
148
|
+
});
|
|
149
|
+
request.on("error", () => reject(new Error("Authentication failed: network request failed")));
|
|
150
|
+
request.end(init.body?.toString());
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function withinBudget(promise, signal) {
|
|
154
|
+
if (!signal)
|
|
155
|
+
return promise;
|
|
156
|
+
if (signal.aborted) {
|
|
157
|
+
// The host callback may have aborted synchronously after returning work.
|
|
158
|
+
void promise.catch(() => { });
|
|
159
|
+
signal.throwIfAborted();
|
|
160
|
+
}
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const abort = () => reject(signal.reason ?? new Error("Authentication cancelled"));
|
|
163
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
164
|
+
promise
|
|
165
|
+
.then(resolve, reject)
|
|
166
|
+
.finally(() => signal.removeEventListener("abort", abort));
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function validateCredential(credential, options) {
|
|
170
|
+
if (!credential.token || /[\s\x00-\x1f\x7f]/.test(credential.token))
|
|
171
|
+
error("invalid bearer token");
|
|
172
|
+
if (credential.expiresAt !== undefined &&
|
|
173
|
+
(!Number.isFinite(credential.expiresAt) ||
|
|
174
|
+
credential.expiresAt <= (options.now ?? Date.now)()))
|
|
175
|
+
error("expired token");
|
|
176
|
+
return credential;
|
|
177
|
+
}
|
|
178
|
+
function cacheKey(context, client) {
|
|
179
|
+
return JSON.stringify([
|
|
180
|
+
context.url,
|
|
181
|
+
client.issuer,
|
|
182
|
+
client.resource,
|
|
183
|
+
client.clientId,
|
|
184
|
+
client.flow,
|
|
185
|
+
client.clientSecretRef,
|
|
186
|
+
[...(client.scopes ?? [])].sort(),
|
|
187
|
+
]);
|
|
188
|
+
}
|
|
189
|
+
function base64url(bytes) {
|
|
190
|
+
return btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(""))
|
|
191
|
+
.replace(/\+/g, "-")
|
|
192
|
+
.replace(/\//g, "_")
|
|
193
|
+
.replace(/=+$/, "");
|
|
194
|
+
}
|
|
195
|
+
function challengeParameter(challenge, name) {
|
|
196
|
+
if (!challenge)
|
|
197
|
+
return undefined;
|
|
198
|
+
const bearer = /(?:^|,)\s*Bearer\s+/i.exec(challenge);
|
|
199
|
+
if (!bearer)
|
|
200
|
+
return undefined;
|
|
201
|
+
const tail = challenge.slice(bearer.index + bearer[0].length);
|
|
202
|
+
// Tokenize quoted parameters (commas inside quoted strings are not separators).
|
|
203
|
+
const parameters = tail.match(/(?:[^,"]|"(?:\\.|[^"\\])*")+/g) ?? [];
|
|
204
|
+
for (const parameter of parameters) {
|
|
205
|
+
const match = /^\s*([\w-]+)\s*=\s*(?:"((?:\\.|[^"\\])*)"|([^,\s]+))\s*$/.exec(parameter);
|
|
206
|
+
if (!match)
|
|
207
|
+
break; // Another authentication scheme begins here.
|
|
208
|
+
if (match[1]?.toLowerCase() === name.toLowerCase())
|
|
209
|
+
return (match[2] ?? match[3] ?? "").replace(/\\(.)/g, "$1");
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
async function metadataAt(urls, context, options) {
|
|
214
|
+
for (const value of [...new Set(urls)]) {
|
|
215
|
+
const url = await destination(value, "metadata", context, options);
|
|
216
|
+
const response = await withinBudget(fetchChecked(url, {
|
|
217
|
+
redirect: "error",
|
|
218
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
219
|
+
}, options), context.signal);
|
|
220
|
+
if (response.status === 404)
|
|
221
|
+
continue;
|
|
222
|
+
if (!response.ok)
|
|
223
|
+
error("metadata request rejected");
|
|
224
|
+
return record(await withinBudget(response.json(), context.signal));
|
|
225
|
+
}
|
|
226
|
+
return error("metadata unavailable");
|
|
227
|
+
}
|
|
228
|
+
function validateResource(value, endpoint, options) {
|
|
229
|
+
const resource = secureUrl(value, options), target = secureUrl(endpoint, options);
|
|
230
|
+
if (resource.search ||
|
|
231
|
+
resource.origin !== target.origin ||
|
|
232
|
+
!(target.pathname === resource.pathname ||
|
|
233
|
+
target.pathname.startsWith(resource.pathname.replace(/\/$/, "") + "/")))
|
|
234
|
+
error("resource does not cover endpoint");
|
|
235
|
+
}
|
|
236
|
+
const defaults = {
|
|
237
|
+
getToken(key, context) {
|
|
238
|
+
return context.cache?.get(key);
|
|
239
|
+
},
|
|
240
|
+
setToken(key, credential, context) {
|
|
241
|
+
(context.cache ?? (context.cache = new Map())).set(key, credential);
|
|
242
|
+
},
|
|
243
|
+
deleteToken(key, context) {
|
|
244
|
+
context.cache?.delete(key);
|
|
245
|
+
},
|
|
246
|
+
resolveEnvironmentVariable(name) {
|
|
247
|
+
return globalThis.process?.env?.[name];
|
|
248
|
+
},
|
|
249
|
+
resolveCredentialReference() {
|
|
250
|
+
return undefined;
|
|
251
|
+
},
|
|
252
|
+
async discover(context, options = {}) {
|
|
253
|
+
const binding = context.authentication;
|
|
254
|
+
const endpoint = secureUrl(context.url, options);
|
|
255
|
+
let client;
|
|
256
|
+
let resource;
|
|
257
|
+
if (binding?.type === "oauth") {
|
|
258
|
+
const flow = binding.flow;
|
|
259
|
+
if (flow !== "authorization_code_pkce" && flow !== "client_credentials")
|
|
260
|
+
error("unsupported OAuth flow");
|
|
261
|
+
client = {
|
|
262
|
+
issuer: string(binding.issuer, "issuer"),
|
|
263
|
+
resource: string(binding.resource, "resource"),
|
|
264
|
+
clientId: string(binding.clientId, "clientId"),
|
|
265
|
+
flow,
|
|
266
|
+
...(binding.scopes !== undefined
|
|
267
|
+
? { scopes: strings(binding.scopes) }
|
|
268
|
+
: {}),
|
|
269
|
+
...(binding.clientSecretRef !== undefined
|
|
270
|
+
? {
|
|
271
|
+
clientSecretRef: string(binding.clientSecretRef, "clientSecretRef"),
|
|
272
|
+
}
|
|
273
|
+
: {}),
|
|
274
|
+
};
|
|
275
|
+
resource = client.resource;
|
|
276
|
+
const advertised = challengeParameter(context.challenge, "resource_metadata");
|
|
277
|
+
const metadata = await metadataAt(advertised
|
|
278
|
+
? [advertised]
|
|
279
|
+
: [
|
|
280
|
+
`${endpoint.origin}/.well-known/oauth-protected-resource${endpoint.pathname === "/" ? "" : endpoint.pathname}`,
|
|
281
|
+
`${endpoint.origin}/.well-known/oauth-protected-resource`,
|
|
282
|
+
], context, options);
|
|
283
|
+
if (metadata.resource !== resource ||
|
|
284
|
+
!strings(metadata.authorization_servers).includes(client.issuer))
|
|
285
|
+
error("preset constraints violated");
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
const advertised = challengeParameter(context.challenge, "resource_metadata");
|
|
289
|
+
const metadata = await metadataAt(advertised
|
|
290
|
+
? [advertised]
|
|
291
|
+
: [
|
|
292
|
+
`${endpoint.origin}/.well-known/oauth-protected-resource${endpoint.pathname === "/" ? "" : endpoint.pathname}`,
|
|
293
|
+
`${endpoint.origin}/.well-known/oauth-protected-resource`,
|
|
294
|
+
], context, options);
|
|
295
|
+
resource = string(metadata.resource, "resource");
|
|
296
|
+
validateResource(resource, context.url, options);
|
|
297
|
+
const issuers = strings(metadata.authorization_servers);
|
|
298
|
+
if (issuers.length === 0)
|
|
299
|
+
error("authorization server missing");
|
|
300
|
+
for (const issuer of issuers)
|
|
301
|
+
secureUrl(issuer, options);
|
|
302
|
+
if (!options.selectClient)
|
|
303
|
+
error("OAuth client selection required");
|
|
304
|
+
client = await withinBudget(Promise.resolve(options.selectClient(issuers, resource, context)), context.signal);
|
|
305
|
+
if (!issuers.includes(client.issuer) || client.resource !== resource)
|
|
306
|
+
error("untrusted issuer or resource");
|
|
307
|
+
}
|
|
308
|
+
validateResource(resource, context.url, options);
|
|
309
|
+
const issuer = secureUrl(client.issuer, options);
|
|
310
|
+
if (issuer.search)
|
|
311
|
+
error("invalid issuer");
|
|
312
|
+
if (!client.clientId ||
|
|
313
|
+
!["authorization_code_pkce", "client_credentials"].includes(client.flow))
|
|
314
|
+
error("invalid client configuration");
|
|
315
|
+
const requiredScopes = challengeParameter(context.challenge, "scope")
|
|
316
|
+
?.split(/\s+/)
|
|
317
|
+
.filter(Boolean) ?? [];
|
|
318
|
+
if (requiredScopes.some((scope) => !(client.scopes ?? []).includes(scope)))
|
|
319
|
+
error("challenge exceeds permitted scopes");
|
|
320
|
+
const path = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/$/, "");
|
|
321
|
+
const discovered = await metadataAt([
|
|
322
|
+
`${issuer.origin}/.well-known/oauth-authorization-server${path}`,
|
|
323
|
+
`${issuer.origin}/.well-known/openid-configuration${path}`,
|
|
324
|
+
`${issuer.origin}${path}/.well-known/openid-configuration`,
|
|
325
|
+
], context, options);
|
|
326
|
+
if (discovered.issuer !== client.issuer)
|
|
327
|
+
error("issuer mismatch");
|
|
328
|
+
const metadata = {
|
|
329
|
+
issuer: client.issuer,
|
|
330
|
+
token_endpoint: string(discovered.token_endpoint, "token endpoint"),
|
|
331
|
+
...(discovered.authorization_endpoint !== undefined
|
|
332
|
+
? {
|
|
333
|
+
authorization_endpoint: string(discovered.authorization_endpoint, "authorization endpoint"),
|
|
334
|
+
}
|
|
335
|
+
: {}),
|
|
336
|
+
...(discovered.grant_types_supported !== undefined
|
|
337
|
+
? { grant_types_supported: strings(discovered.grant_types_supported) }
|
|
338
|
+
: {}),
|
|
339
|
+
...(discovered.code_challenge_methods_supported !== undefined
|
|
340
|
+
? {
|
|
341
|
+
code_challenge_methods_supported: strings(discovered.code_challenge_methods_supported),
|
|
342
|
+
}
|
|
343
|
+
: {}),
|
|
344
|
+
...(discovered.token_endpoint_auth_methods_supported !== undefined
|
|
345
|
+
? {
|
|
346
|
+
token_endpoint_auth_methods_supported: strings(discovered.token_endpoint_auth_methods_supported),
|
|
347
|
+
}
|
|
348
|
+
: {}),
|
|
349
|
+
};
|
|
350
|
+
await destination(metadata.token_endpoint, "token", context, options);
|
|
351
|
+
return { client, metadata };
|
|
352
|
+
},
|
|
353
|
+
async requestToken(context, request, options = {}) {
|
|
354
|
+
const { client, metadata } = request;
|
|
355
|
+
const parameters = new URLSearchParams(request.parameters);
|
|
356
|
+
parameters.set("client_id", client.clientId);
|
|
357
|
+
parameters.set("resource", client.resource);
|
|
358
|
+
if (client.scopes?.length)
|
|
359
|
+
parameters.set("scope", client.scopes.join(" "));
|
|
360
|
+
const url = await destination(metadata.token_endpoint, "token", context, options);
|
|
361
|
+
const headers = {
|
|
362
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
363
|
+
accept: "application/json",
|
|
364
|
+
};
|
|
365
|
+
if (client.clientSecretRef) {
|
|
366
|
+
const secret = await withinBudget(Promise.resolve(this.resolveCredentialReference(client.clientSecretRef, context)), context.signal);
|
|
367
|
+
if (!secret)
|
|
368
|
+
error("client secret unavailable");
|
|
369
|
+
const methods = metadata.token_endpoint_auth_methods_supported ?? [
|
|
370
|
+
"client_secret_basic",
|
|
371
|
+
];
|
|
372
|
+
if (methods.includes("client_secret_basic")) {
|
|
373
|
+
const encode = (value) => new URLSearchParams({ v: value }).toString().slice(2);
|
|
374
|
+
headers.authorization = `Basic ${btoa(`${encode(client.clientId)}:${encode(secret)}`)}`;
|
|
375
|
+
}
|
|
376
|
+
else if (methods.includes("client_secret_post"))
|
|
377
|
+
parameters.set("client_secret", secret);
|
|
378
|
+
else
|
|
379
|
+
error("unsupported client authentication");
|
|
380
|
+
}
|
|
381
|
+
else if (metadata.token_endpoint_auth_methods_supported &&
|
|
382
|
+
!metadata.token_endpoint_auth_methods_supported.includes("none"))
|
|
383
|
+
error("client secret required");
|
|
384
|
+
const response = await withinBudget(fetchChecked(url, {
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers,
|
|
387
|
+
body: parameters,
|
|
388
|
+
redirect: "error",
|
|
389
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
390
|
+
}, options), context.signal);
|
|
391
|
+
if (!response.ok)
|
|
392
|
+
error("token request rejected");
|
|
393
|
+
const body = record(await withinBudget(response.json(), context.signal));
|
|
394
|
+
if (typeof body.token_type !== "string" ||
|
|
395
|
+
body.token_type.toLowerCase() !== "bearer")
|
|
396
|
+
error("unsupported token type");
|
|
397
|
+
if (body.resource !== undefined && body.resource !== client.resource)
|
|
398
|
+
error("token resource mismatch");
|
|
399
|
+
if (body.expires_in !== undefined &&
|
|
400
|
+
(typeof body.expires_in !== "number" ||
|
|
401
|
+
!Number.isFinite(body.expires_in) ||
|
|
402
|
+
body.expires_in <= 0))
|
|
403
|
+
error("invalid token expiry");
|
|
404
|
+
const scopes = body.scope === undefined
|
|
405
|
+
? client.scopes
|
|
406
|
+
: string(body.scope, "scope").split(/\s+/);
|
|
407
|
+
if (scopes?.some((scope) => !(client.scopes ?? []).includes(scope)) ||
|
|
408
|
+
client.scopes?.some((scope) => !scopes?.includes(scope)))
|
|
409
|
+
error("token scope mismatch");
|
|
410
|
+
return validateCredential({
|
|
411
|
+
token: string(body.access_token, "access token"),
|
|
412
|
+
...(typeof body.expires_in === "number"
|
|
413
|
+
? { expiresAt: (options.now ?? Date.now)() + body.expires_in * 1000 }
|
|
414
|
+
: {}),
|
|
415
|
+
...(body.refresh_token !== undefined
|
|
416
|
+
? { refreshToken: string(body.refresh_token, "refresh token") }
|
|
417
|
+
: {}),
|
|
418
|
+
...(scopes ? { scopes } : {}),
|
|
419
|
+
}, options);
|
|
420
|
+
},
|
|
421
|
+
async exchangeAuthorizationCode(request, context, options) {
|
|
422
|
+
const parameters = new URLSearchParams(request.parameters);
|
|
423
|
+
parameters.set("grant_type", "authorization_code");
|
|
424
|
+
parameters.set("code", request.code);
|
|
425
|
+
parameters.set("code_verifier", request.verifier);
|
|
426
|
+
parameters.set("redirect_uri", request.redirectUri);
|
|
427
|
+
return this.requestToken(context, { ...request, parameters }, options);
|
|
428
|
+
},
|
|
429
|
+
async refresh(context, request, refreshToken, options) {
|
|
430
|
+
const parameters = new URLSearchParams(request.parameters);
|
|
431
|
+
parameters.set("grant_type", "refresh_token");
|
|
432
|
+
parameters.set("refresh_token", refreshToken);
|
|
433
|
+
const credential = await this.requestToken(context, { ...request, parameters }, options);
|
|
434
|
+
return {
|
|
435
|
+
...credential,
|
|
436
|
+
refreshToken: credential.refreshToken ?? refreshToken,
|
|
437
|
+
};
|
|
438
|
+
},
|
|
439
|
+
async clientCredentials(context, request, options) {
|
|
440
|
+
if (!request.metadata.grant_types_supported?.includes("client_credentials"))
|
|
441
|
+
error("client credentials not advertised");
|
|
442
|
+
const parameters = new URLSearchParams(request.parameters);
|
|
443
|
+
parameters.set("grant_type", "client_credentials");
|
|
444
|
+
return this.requestToken(context, { ...request, parameters }, options);
|
|
445
|
+
},
|
|
446
|
+
async authenticate(context, options = {}) {
|
|
447
|
+
context.signal?.throwIfAborted();
|
|
448
|
+
const binding = context.authentication;
|
|
449
|
+
if (binding?.type === "bearer") {
|
|
450
|
+
const env = binding.tokenEnv, ref = binding.tokenRef;
|
|
451
|
+
if ((env !== undefined) === (ref !== undefined))
|
|
452
|
+
error("bearer requires exactly one credential source");
|
|
453
|
+
const token = await withinBudget(Promise.resolve(env !== undefined
|
|
454
|
+
? this.resolveEnvironmentVariable(string(env, "tokenEnv"), context)
|
|
455
|
+
: this.resolveCredentialReference(string(ref, "tokenRef"), context)), context.signal);
|
|
456
|
+
if (!token)
|
|
457
|
+
error("bearer credential unavailable");
|
|
458
|
+
return validateCredential({ token }, options);
|
|
459
|
+
}
|
|
460
|
+
if (binding && binding.type !== "oauth")
|
|
461
|
+
error("unsupported authentication binding");
|
|
462
|
+
const discoveryCache = context.discoveryCache ?? (context.discoveryCache = new Map());
|
|
463
|
+
const discoveryKey = JSON.stringify([
|
|
464
|
+
context.url,
|
|
465
|
+
context.authentication ?? null,
|
|
466
|
+
]);
|
|
467
|
+
const known = discoveryCache.get(discoveryKey);
|
|
468
|
+
const challenged = context.challenged || context.challenge !== undefined;
|
|
469
|
+
if (!binding && !challenged && !known)
|
|
470
|
+
return undefined;
|
|
471
|
+
const discovered = known && !challenged ? known : await this.discover(context, options);
|
|
472
|
+
const { client, metadata } = discovered;
|
|
473
|
+
if (known && cacheKey(context, known.client) !== cacheKey(context, client))
|
|
474
|
+
error("OAuth identity changed");
|
|
475
|
+
const key = cacheKey(context, client);
|
|
476
|
+
const cached = await withinBudget(Promise.resolve(this.getToken(key, context)), context.signal);
|
|
477
|
+
const now = (options.now ?? Date.now)();
|
|
478
|
+
if (cached &&
|
|
479
|
+
!context.challenged &&
|
|
480
|
+
context.challenge === undefined &&
|
|
481
|
+
(cached.expiresAt === undefined ||
|
|
482
|
+
cached.expiresAt > now + (options.expirySkewMs ?? 30_000)))
|
|
483
|
+
return validateCredential(cached, options);
|
|
484
|
+
const request = {
|
|
485
|
+
client,
|
|
486
|
+
metadata,
|
|
487
|
+
parameters: new URLSearchParams(),
|
|
488
|
+
};
|
|
489
|
+
let credential;
|
|
490
|
+
if (cached?.refreshToken) {
|
|
491
|
+
await withinBudget(Promise.resolve(this.deleteToken(key, context)), context.signal);
|
|
492
|
+
credential = await this.refresh(context, request, cached.refreshToken, options);
|
|
493
|
+
}
|
|
494
|
+
else if (client.flow === "client_credentials") {
|
|
495
|
+
credential = await this.clientCredentials(context, request, options);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
if (!options.authorize)
|
|
499
|
+
error("interactive authorization unavailable");
|
|
500
|
+
if (!metadata.code_challenge_methods_supported?.includes("S256"))
|
|
501
|
+
error("PKCE S256 unavailable");
|
|
502
|
+
if (metadata.grant_types_supported &&
|
|
503
|
+
!metadata.grant_types_supported.includes("authorization_code"))
|
|
504
|
+
error("authorization code not supported");
|
|
505
|
+
const redirectUri = client.redirectUri ?? options.redirectUri;
|
|
506
|
+
if (!redirectUri)
|
|
507
|
+
error("redirect URI required");
|
|
508
|
+
const redirect = new URL(redirectUri);
|
|
509
|
+
if (redirect.username ||
|
|
510
|
+
redirect.password ||
|
|
511
|
+
redirect.hash ||
|
|
512
|
+
!(redirect.protocol === "https:" ||
|
|
513
|
+
(redirect.protocol === "http:" &&
|
|
514
|
+
["127.0.0.1", "[::1]"].includes(redirect.hostname))))
|
|
515
|
+
error("invalid redirect URI");
|
|
516
|
+
if (redirect.searchParams.has("code") ||
|
|
517
|
+
redirect.searchParams.has("state") ||
|
|
518
|
+
redirect.searchParams.has("error"))
|
|
519
|
+
error("reserved redirect parameter");
|
|
520
|
+
const authorization = await destination(string(metadata.authorization_endpoint, "authorization endpoint"), "authorization", context, options);
|
|
521
|
+
const verifier = base64url(globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
|
522
|
+
const state = base64url(globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
|
523
|
+
const challenge = base64url(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
|
|
524
|
+
for (const [name, value] of Object.entries({
|
|
525
|
+
response_type: "code",
|
|
526
|
+
client_id: client.clientId,
|
|
527
|
+
redirect_uri: redirectUri,
|
|
528
|
+
resource: client.resource,
|
|
529
|
+
state,
|
|
530
|
+
code_challenge: challenge,
|
|
531
|
+
code_challenge_method: "S256",
|
|
532
|
+
}))
|
|
533
|
+
authorization.searchParams.set(name, value);
|
|
534
|
+
if (client.scopes?.length)
|
|
535
|
+
authorization.searchParams.set("scope", client.scopes.join(" "));
|
|
536
|
+
const callback = new URL(await withinBudget(Promise.resolve(options.authorize({
|
|
537
|
+
url: authorization.href,
|
|
538
|
+
redirectUri,
|
|
539
|
+
state,
|
|
540
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
541
|
+
}, context)), context.signal));
|
|
542
|
+
if (callback.origin !== redirect.origin ||
|
|
543
|
+
callback.pathname !== redirect.pathname ||
|
|
544
|
+
callback.hash ||
|
|
545
|
+
callback.username ||
|
|
546
|
+
callback.password)
|
|
547
|
+
error("redirect mismatch");
|
|
548
|
+
for (const [name, value] of redirect.searchParams)
|
|
549
|
+
if (callback.searchParams.get(name) !== value)
|
|
550
|
+
error("redirect query mismatch");
|
|
551
|
+
if (callback.searchParams.getAll("state").length !== 1 ||
|
|
552
|
+
callback.searchParams.get("state") !== state)
|
|
553
|
+
error("state mismatch");
|
|
554
|
+
if (callback.searchParams.has("error"))
|
|
555
|
+
error("authorization denied");
|
|
556
|
+
if (callback.searchParams.has("iss") &&
|
|
557
|
+
callback.searchParams.get("iss") !== client.issuer)
|
|
558
|
+
error("authorization issuer mismatch");
|
|
559
|
+
if (callback.searchParams.getAll("code").length !== 1)
|
|
560
|
+
error("authorization code missing");
|
|
561
|
+
credential = await this.exchangeAuthorizationCode({
|
|
562
|
+
...request,
|
|
563
|
+
code: string(callback.searchParams.get("code"), "authorization code"),
|
|
564
|
+
verifier,
|
|
565
|
+
redirectUri,
|
|
566
|
+
}, context, options);
|
|
567
|
+
}
|
|
568
|
+
validateCredential(credential, options);
|
|
569
|
+
await withinBudget(Promise.resolve(this.setToken(key, credential, context)), context.signal);
|
|
570
|
+
discoveryCache.set(discoveryKey, discovered);
|
|
571
|
+
return credential;
|
|
572
|
+
},
|
|
573
|
+
};
|
|
574
|
+
/** Compose host-specific policy with SDK defaults; defaults remain callable for delegation. */
|
|
575
|
+
export const auth = Object.assign(function auth(overrides = {}) {
|
|
576
|
+
const provider = { ...defaults, ...overrides };
|
|
577
|
+
// Each composed provider owns its fallback cache. Callers can supply endpoint-
|
|
578
|
+
// scoped caches when sharing a provider; keys also include the full endpoint.
|
|
579
|
+
const cache = new Map();
|
|
580
|
+
// Hooks can spread each context per request while retaining its token Map.
|
|
581
|
+
// A WeakMap keyed by that Map isolates discovery across Hooks instances even
|
|
582
|
+
// when they deliberately share this composed provider.
|
|
583
|
+
const discoveryCaches = new WeakMap();
|
|
584
|
+
const authenticate = provider.authenticate;
|
|
585
|
+
provider.authenticate = (context, options) => {
|
|
586
|
+
const configured = { ...overrides, ...options };
|
|
587
|
+
const tokens = context.cache ?? cache;
|
|
588
|
+
let discoveryCache = context.discoveryCache ?? discoveryCaches.get(tokens);
|
|
589
|
+
if (!discoveryCache) {
|
|
590
|
+
discoveryCache = new Map();
|
|
591
|
+
discoveryCaches.set(tokens, discoveryCache);
|
|
592
|
+
}
|
|
593
|
+
return authenticate.call(provider, {
|
|
594
|
+
...context,
|
|
595
|
+
cache: tokens,
|
|
596
|
+
discoveryCache,
|
|
597
|
+
provider,
|
|
598
|
+
authOptions: configured,
|
|
599
|
+
}, configured);
|
|
600
|
+
};
|
|
601
|
+
return provider;
|
|
602
|
+
}, {
|
|
603
|
+
...defaults,
|
|
604
|
+
authenticate(context, options) {
|
|
605
|
+
return defaults.authenticate.call(context.provider ?? defaults, context, {
|
|
606
|
+
...context.authOptions,
|
|
607
|
+
...options,
|
|
608
|
+
});
|
|
609
|
+
},
|
|
610
|
+
exchangeAuthorizationCode(request, context, options) {
|
|
611
|
+
return defaults.exchangeAuthorizationCode.call(context.provider ?? defaults, request, context, { ...context.authOptions, ...options });
|
|
612
|
+
},
|
|
613
|
+
discover(context, options) {
|
|
614
|
+
return defaults.discover.call(context.provider ?? defaults, context, {
|
|
615
|
+
...context.authOptions,
|
|
616
|
+
...options,
|
|
617
|
+
});
|
|
618
|
+
},
|
|
619
|
+
requestToken(context, request, options) {
|
|
620
|
+
return defaults.requestToken.call(context.provider ?? defaults, context, request, { ...context.authOptions, ...options });
|
|
621
|
+
},
|
|
622
|
+
refresh(context, request, refreshToken, options) {
|
|
623
|
+
return defaults.refresh.call(context.provider ?? defaults, context, request, refreshToken, { ...context.authOptions, ...options });
|
|
624
|
+
},
|
|
625
|
+
clientCredentials(context, request, options) {
|
|
626
|
+
return defaults.clientCredentials.call(context.provider ?? defaults, context, request, { ...context.authOptions, ...options });
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
//# sourceMappingURL=auth.js.map
|