@broberg/sso 0.13.0 → 0.14.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/README.md +18 -0
- package/dist/chunk-JGIO7XPR.cjs +833 -0
- package/dist/chunk-JGIO7XPR.cjs.map +1 -0
- package/dist/chunk-ZPRWYUFG.js +805 -0
- package/dist/chunk-ZPRWYUFG.js.map +1 -0
- package/dist/hono.cjs +37 -815
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +2 -386
- package/dist/hono.d.ts +2 -386
- package/dist/hono.js +1 -779
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +225 -794
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -569
- package/dist/index.d.ts +39 -569
- package/dist/index.js +112 -755
- package/dist/index.js.map +1 -1
- package/dist/session-DsjOyCJb.d.cts +580 -0
- package/dist/session-DsjOyCJb.d.ts +580 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,671 +1,146 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var chunkJGIO7XPR_cjs = require('./chunk-JGIO7XPR.cjs');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var http = require('http');
|
|
3
6
|
var jose = require('jose');
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
var
|
|
8
|
+
var DEFAULT_ISSUER = "https://id.broberg.ai";
|
|
9
|
+
var FLY_SOCKET = "/.fly/api";
|
|
10
|
+
var NoWorkloadIdentityError = class extends Error {
|
|
7
11
|
constructor(message) {
|
|
8
12
|
super(message);
|
|
9
|
-
this.name = "
|
|
13
|
+
this.name = "NoWorkloadIdentityError";
|
|
10
14
|
}
|
|
11
15
|
};
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (typeof raw !== "string" || raw.trim() === "") {
|
|
15
|
-
throw new SsoConfigError(`${name} is not set (or is blank). ${hint}`);
|
|
16
|
-
}
|
|
17
|
-
return raw.trim();
|
|
18
|
-
}
|
|
19
|
-
var DEFAULT_SESSION_MAX_AGE = 60 * 60 * 24 * 7;
|
|
20
|
-
function loadSsoConfig(env = process.env) {
|
|
21
|
-
const rawIssuer = required(
|
|
22
|
-
env,
|
|
23
|
-
"BID_ISSUER",
|
|
24
|
-
"It is Broberg ID's bare origin, e.g. https://id.broberg.ai"
|
|
25
|
-
);
|
|
26
|
-
let issuer;
|
|
27
|
-
try {
|
|
28
|
-
const url = new URL(rawIssuer);
|
|
29
|
-
if (url.protocol !== "https:" && url.hostname !== "localhost") {
|
|
30
|
-
throw new SsoConfigError(
|
|
31
|
-
`BID_ISSUER must be https (got ${url.protocol}//). Only localhost may be http.`
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
issuer = url.origin;
|
|
35
|
-
} catch (err) {
|
|
36
|
-
if (err instanceof SsoConfigError) throw err;
|
|
37
|
-
throw new SsoConfigError(`BID_ISSUER is not a valid URL: ${rawIssuer}`);
|
|
38
|
-
}
|
|
39
|
-
const cookieSecret = required(
|
|
40
|
-
env,
|
|
41
|
-
"SSO_COOKIE_SECRET",
|
|
42
|
-
"Generate one with `openssl rand -hex 32`. It signs this app's session cookie."
|
|
43
|
-
);
|
|
44
|
-
if (cookieSecret.length < 32) {
|
|
45
|
-
throw new SsoConfigError(
|
|
46
|
-
`SSO_COOKIE_SECRET is ${cookieSecret.length} characters; it must be at least 32. A short secret is a forgeable session, and a forged session is any user you like.`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
const rawMaxAge = env.SSO_SESSION_MAX_AGE?.trim();
|
|
50
|
-
let sessionMaxAge = DEFAULT_SESSION_MAX_AGE;
|
|
51
|
-
if (rawMaxAge) {
|
|
52
|
-
const n = Number(rawMaxAge);
|
|
53
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
54
|
-
throw new SsoConfigError(
|
|
55
|
-
`SSO_SESSION_MAX_AGE must be a positive number of seconds (got ${JSON.stringify(rawMaxAge)}).`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
sessionMaxAge = Math.floor(n);
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
issuer,
|
|
62
|
-
clientId: required(env, "SSO_CLIENT_ID", "The id this app is registered under in Broberg ID."),
|
|
63
|
-
redirectUri: required(
|
|
64
|
-
env,
|
|
65
|
-
"SSO_REDIRECT_URI",
|
|
66
|
-
"Must match the registered redirect EXACTLY \u2014 one trailing slash is a different address."
|
|
67
|
-
),
|
|
68
|
-
scopes: (env.SSO_SCOPES?.trim() || "openid profile email").split(/\s+/),
|
|
69
|
-
cookieSecret,
|
|
70
|
-
cookieName: env.SSO_COOKIE_NAME?.trim() || "bid_session",
|
|
71
|
-
sessionMaxAge,
|
|
72
|
-
postLogoutRedirectUri: env.SSO_POST_LOGOUT_REDIRECT_URI?.trim() || void 0,
|
|
73
|
-
clientSecret: env.SSO_CLIENT_SECRET?.trim() || void 0
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
var JwksError = class extends Error {
|
|
77
|
-
constructor(message) {
|
|
16
|
+
var TicketExchangeError = class extends Error {
|
|
17
|
+
constructor(message, code, status) {
|
|
78
18
|
super(message);
|
|
79
|
-
this.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
var JwksUnavailableError = class extends JwksError {
|
|
83
|
-
constructor(message) {
|
|
84
|
-
super(message);
|
|
85
|
-
this.name = "JwksUnavailableError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.name = "TicketExchangeError";
|
|
86
22
|
}
|
|
23
|
+
code;
|
|
24
|
+
status;
|
|
87
25
|
};
|
|
88
|
-
var
|
|
26
|
+
var TicketUnavailableError = class extends Error {
|
|
89
27
|
constructor(message) {
|
|
90
28
|
super(message);
|
|
91
|
-
this.name = "
|
|
29
|
+
this.name = "TicketUnavailableError";
|
|
92
30
|
}
|
|
93
31
|
};
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (inFlight) return inFlight;
|
|
108
|
-
inFlight = (async () => {
|
|
109
|
-
let res;
|
|
110
|
-
try {
|
|
111
|
-
res = await fetchImpl(jwksUri);
|
|
112
|
-
} catch (cause) {
|
|
113
|
-
throw new JwksUnavailableError(
|
|
114
|
-
`${jwksUri} could not be reached: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (!res.ok) {
|
|
118
|
-
throw new JwksUnavailableError(
|
|
119
|
-
`${jwksUri} answered ${res.status} \u2014 cannot verify any token right now`
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
let body;
|
|
123
|
-
try {
|
|
124
|
-
body = await res.json();
|
|
125
|
-
} catch {
|
|
126
|
-
throw new JwksUnavailableError(`${jwksUri} answered 200 with a body that is not JSON`);
|
|
127
|
-
}
|
|
128
|
-
if (!Array.isArray(body?.keys)) {
|
|
129
|
-
throw new JwksUnavailableError(`${jwksUri} returned no "keys" array`);
|
|
32
|
+
function flyOidcOverSocket(aud) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const body = JSON.stringify({ aud });
|
|
35
|
+
const req = http.request(
|
|
36
|
+
{ socketPath: FLY_SOCKET, path: "/v1/tokens/oidc", method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } },
|
|
37
|
+
(res) => {
|
|
38
|
+
let data = "";
|
|
39
|
+
res.setEncoding("utf8");
|
|
40
|
+
res.on("data", (c) => data += c);
|
|
41
|
+
res.on("end", () => {
|
|
42
|
+
if ((res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 300) resolve(data.trim());
|
|
43
|
+
else reject(new TicketUnavailableError(`Fly OIDC answered ${res.statusCode}`));
|
|
44
|
+
});
|
|
130
45
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
inFlight = null;
|
|
136
|
-
});
|
|
137
|
-
return inFlight;
|
|
138
|
-
}
|
|
139
|
-
if (warmUp) {
|
|
140
|
-
void refresh().catch(() => {
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
return {
|
|
144
|
-
get fetchCount() {
|
|
145
|
-
return fetchCount;
|
|
146
|
-
},
|
|
147
|
-
async getKey(kid, alg) {
|
|
148
|
-
let jwk = keys.find((k) => k.kid === kid);
|
|
149
|
-
if (!jwk) {
|
|
150
|
-
const sinceLast = now() - lastFetchAt;
|
|
151
|
-
if (sinceLast < minRefetchIntervalMs) {
|
|
152
|
-
throw new JwksUnavailableError(
|
|
153
|
-
`kid ${kid} is not in the key set we hold, fetched ${sinceLast}ms ago (we do not ask the issuer again within ${minRefetchIntervalMs}ms). A key set this fresh that lacks the kid means the token was not signed by this issuer \u2014 reject it. Only a key rotation within the last ${minRefetchIntervalMs}ms could make that wrong.`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
await refresh();
|
|
157
|
-
jwk = keys.find((k) => k.kid === kid);
|
|
158
|
-
}
|
|
159
|
-
if (!jwk) {
|
|
160
|
-
throw new JwksUnknownKeyError(
|
|
161
|
-
`Broberg ID does not publish a signing key with kid ${kid}. The token was not signed by this issuer.`
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
return jose.importJWK(jwk, alg);
|
|
165
|
-
}
|
|
166
|
-
};
|
|
46
|
+
);
|
|
47
|
+
req.on("error", (e) => reject(new TicketUnavailableError(`Fly OIDC socket: ${e.message}`)));
|
|
48
|
+
req.end(body);
|
|
49
|
+
});
|
|
167
50
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
var SsoReauthError = class extends SsoError {
|
|
177
|
-
constructor(message, reason) {
|
|
178
|
-
super(message);
|
|
179
|
-
this.reason = reason;
|
|
180
|
-
this.name = "SsoReauthError";
|
|
181
|
-
}
|
|
182
|
-
reason;
|
|
183
|
-
};
|
|
184
|
-
var SsoAppApiError = class extends SsoError {
|
|
185
|
-
constructor(message, status, code) {
|
|
186
|
-
super(message);
|
|
187
|
-
this.status = status;
|
|
188
|
-
this.code = code;
|
|
189
|
-
this.name = "SsoAppApiError";
|
|
190
|
-
}
|
|
191
|
-
status;
|
|
192
|
-
code;
|
|
193
|
-
};
|
|
194
|
-
var SsoInsufficientScopeError = class extends SsoAppApiError {
|
|
195
|
-
constructor(message, scope) {
|
|
196
|
-
super(message, 403, "insufficient_scope");
|
|
197
|
-
this.scope = scope;
|
|
198
|
-
this.name = "SsoInsufficientScopeError";
|
|
199
|
-
}
|
|
200
|
-
scope;
|
|
201
|
-
};
|
|
202
|
-
var SsoAvatarRejectedError = class extends SsoError {
|
|
203
|
-
constructor(message, reason) {
|
|
204
|
-
super(message);
|
|
205
|
-
this.reason = reason;
|
|
206
|
-
this.name = "SsoAvatarRejectedError";
|
|
51
|
+
function jwtExp(token) {
|
|
52
|
+
try {
|
|
53
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
54
|
+
return typeof payload.exp === "number" ? payload.exp : null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
207
57
|
}
|
|
208
|
-
reason;
|
|
209
|
-
};
|
|
210
|
-
var MAX_AVATAR_BYTES = 2 * 1024 * 1024;
|
|
211
|
-
var AVATAR_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
212
|
-
function mediaType(contentType) {
|
|
213
|
-
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
214
|
-
}
|
|
215
|
-
var b64url = (bytes) => {
|
|
216
|
-
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
217
|
-
let s = "";
|
|
218
|
-
for (const b of view) s += String.fromCharCode(b);
|
|
219
|
-
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
220
|
-
};
|
|
221
|
-
var randomToken = (bytes = 32) => b64url(crypto.getRandomValues(new Uint8Array(bytes)));
|
|
222
|
-
async function challengeFor(verifier) {
|
|
223
|
-
return b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
|
|
224
|
-
}
|
|
225
|
-
var ADDRESS_OWNERSHIP = /* @__PURE__ */ new Set(["verified", "unverified", "not_on_account"]);
|
|
226
|
-
var INVITATION_OUTCOMES = /* @__PURE__ */ new Set([
|
|
227
|
-
"invited",
|
|
228
|
-
"already_invited",
|
|
229
|
-
"existing",
|
|
230
|
-
"invalid",
|
|
231
|
-
"too_soon",
|
|
232
|
-
"too_many_today",
|
|
233
|
-
"send_failed"
|
|
234
|
-
]);
|
|
235
|
-
var MIGRATION_STATES = /* @__PURE__ */ new Set(["ready", "invited", "expired", "not_invited"]);
|
|
236
|
-
var BACKCHANNEL_EVENT = "http://schemas.openid.net/event/backchannel-logout";
|
|
237
|
-
var MAX_INVITATIONS = 500;
|
|
238
|
-
function excerptForLog(raw, clientSecret) {
|
|
239
|
-
let text = raw.replace(/\s+/g, " ").trim();
|
|
240
|
-
if (clientSecret) text = text.split(clientSecret).join("[redacted client_secret]");
|
|
241
|
-
if (text === "") return "(empty body)";
|
|
242
|
-
return text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
|
|
243
|
-
}
|
|
244
|
-
function invalidClientHint(error, weSentASecret) {
|
|
245
|
-
if (error !== "invalid_client") return "";
|
|
246
|
-
return weSentASecret ? " \u2014 we DID send a client_secret, so either it is wrong or this client is registered as PUBLIC (in which case unset SSO_CLIENT_SECRET)." : " \u2014 we sent NO client_secret, so this client is likely registered as CONFIDENTIAL (set SSO_CLIENT_SECRET to the value it was registered with).";
|
|
247
58
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// discovery id_token_signing_alg_values_supported: ["EdDSA"]
|
|
253
|
-
//
|
|
254
|
-
// 0.2.1 SHIPPED THIS LIST WITHOUT IT and rejected every real token for an
|
|
255
|
-
// hour. The list was written from what an OIDC client usually allows rather
|
|
256
|
-
// than from what our issuer uses — and the whole suite missed it because the
|
|
257
|
-
// fake issuer in the tests signs RS256. A fake agrees with whoever wrote it.
|
|
258
|
-
// There is now a test that mints EdDSA, the algorithm production uses.
|
|
259
|
-
"EdDSA",
|
|
260
|
-
"RS256",
|
|
261
|
-
"RS384",
|
|
262
|
-
"RS512",
|
|
263
|
-
"PS256",
|
|
264
|
-
"PS384",
|
|
265
|
-
"PS512",
|
|
266
|
-
"ES256",
|
|
267
|
-
"ES384",
|
|
268
|
-
"ES512"
|
|
269
|
-
];
|
|
270
|
-
function createSsoClient(config, options = {}) {
|
|
59
|
+
function createTicketClient(options = {}) {
|
|
60
|
+
const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/+$/, "");
|
|
61
|
+
const renewBeforeSec = options.renewBeforeSec ?? 30;
|
|
62
|
+
const env = options.env ?? process.env;
|
|
271
63
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const res = await Promise.race([
|
|
285
|
-
fetchImpl(url, {
|
|
286
|
-
method: "POST",
|
|
287
|
-
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
288
|
-
body: JSON.stringify(payload),
|
|
289
|
-
signal: ctrl.signal
|
|
290
|
-
}),
|
|
291
|
-
timedOut
|
|
292
|
-
]);
|
|
293
|
-
if (!res.ok) {
|
|
294
|
-
const detail = await res.text().catch(() => "");
|
|
295
|
-
const hint = detail.includes("invalid_app_key") ? " (invalid_app_key: this app's bidk_ key was refused)" : "";
|
|
296
|
-
throw new SsoError(`${url} answered ${res.status}${hint} \u2014 ${what} is unknown.`);
|
|
297
|
-
}
|
|
298
|
-
return await Promise.race([res.json().catch(() => null), timedOut]);
|
|
299
|
-
} catch (e) {
|
|
300
|
-
if (e instanceof SsoError) throw e;
|
|
301
|
-
throw new SsoError(`${url} could not be reached: ${e instanceof Error ? e.message : String(e)} \u2014 ${what} is unknown.`);
|
|
302
|
-
} finally {
|
|
303
|
-
clearTimeout(timer);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
async function appRequest(path, bearer, init, what) {
|
|
307
|
-
const url = new URL(path, config.issuer).toString();
|
|
308
|
-
const ctrl = new AbortController();
|
|
309
|
-
let timer;
|
|
310
|
-
const timedOut = new Promise((_, reject) => {
|
|
311
|
-
timer = setTimeout(() => {
|
|
312
|
-
reject(new SsoAppApiError(`${url} did not answer within ${timeoutMs} ms \u2014 ${what} is unknown.`, null, void 0));
|
|
313
|
-
ctrl.abort();
|
|
314
|
-
}, timeoutMs);
|
|
315
|
-
});
|
|
316
|
-
const headers = { authorization: `Bearer ${bearer}` };
|
|
317
|
-
let body;
|
|
318
|
-
if (init.json !== void 0) {
|
|
319
|
-
headers["content-type"] = "application/json";
|
|
320
|
-
body = JSON.stringify(init.json);
|
|
321
|
-
} else if (init.bytes !== void 0) {
|
|
322
|
-
headers["content-type"] = init.contentType ?? "application/octet-stream";
|
|
323
|
-
body = init.bytes;
|
|
324
|
-
}
|
|
325
|
-
try {
|
|
326
|
-
const res = await Promise.race([
|
|
327
|
-
fetchImpl(url, { method: init.method, headers, ...body !== void 0 ? { body } : {}, signal: ctrl.signal }),
|
|
328
|
-
timedOut
|
|
329
|
-
]);
|
|
330
|
-
const raw = await Promise.race([res.text(), timedOut]);
|
|
331
|
-
let parsed;
|
|
64
|
+
const now = options.now ?? (() => Date.now());
|
|
65
|
+
const flyOidc = options.flyOidc ?? flyOidcOverSocket;
|
|
66
|
+
const onFly = options.onFly ?? (() => !!env.FLY_APP_NAME && fs.existsSync(FLY_SOCKET));
|
|
67
|
+
const cache = /* @__PURE__ */ new Map();
|
|
68
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
69
|
+
async function platformToken() {
|
|
70
|
+
if (onFly()) return flyOidc(issuer);
|
|
71
|
+
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
72
|
+
const bearer = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
73
|
+
if (url && bearer) {
|
|
74
|
+
let res;
|
|
332
75
|
try {
|
|
333
|
-
|
|
334
|
-
} catch {
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
const fields = parsed && typeof parsed === "object" ? parsed : {};
|
|
338
|
-
const code = typeof fields.error === "string" ? fields.error : void 0;
|
|
339
|
-
if (!res.ok) {
|
|
340
|
-
if (res.status === 403 && code === "insufficient_scope") {
|
|
341
|
-
const scope = typeof fields.scope === "string" && fields.scope ? fields.scope : init.method === "GET" ? "profile" : "profile:write";
|
|
342
|
-
throw new SsoInsufficientScopeError(
|
|
343
|
-
`${url} answered 403 insufficient_scope \u2014 the user's token lacks scope "${scope}"; log in again with it requested.`,
|
|
344
|
-
scope
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
throw new SsoAppApiError(`${url} answered ${res.status}${code ? ` (${code})` : ""} \u2014 ${what} is unknown.`, res.status, code);
|
|
348
|
-
}
|
|
349
|
-
if (parsed === void 0) {
|
|
350
|
-
throw new SsoAppApiError(`${url} answered ${res.status} with a body that is not JSON \u2014 ${what} is unknown.`, res.status, void 0);
|
|
76
|
+
res = await fetchImpl(`${url}&audience=${encodeURIComponent(issuer)}`, { headers: { authorization: `Bearer ${bearer}` } });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw new TicketUnavailableError(`GitHub OIDC endpoint unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
351
79
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
} finally {
|
|
357
|
-
clearTimeout(timer);
|
|
80
|
+
if (!res.ok) throw new TicketUnavailableError(`GitHub OIDC endpoint answered ${res.status}`);
|
|
81
|
+
const { value } = await res.json();
|
|
82
|
+
if (!value) throw new TicketUnavailableError("GitHub OIDC endpoint returned no token");
|
|
83
|
+
return value;
|
|
358
84
|
}
|
|
85
|
+
throw new NoWorkloadIdentityError(
|
|
86
|
+
"No workload identity to exchange for a ticket: not on a Fly machine (FLY_APP_NAME + /.fly/api) and not in a GitHub Actions job (ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN \u2014 add `permissions: id-token: write`). A Mac session keeps using its session key."
|
|
87
|
+
);
|
|
359
88
|
}
|
|
360
|
-
function
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
let discoveryPromise = null;
|
|
369
|
-
let jwksCache = null;
|
|
370
|
-
async function discovery() {
|
|
371
|
-
discoveryPromise ??= (async () => {
|
|
372
|
-
const url = `${config.issuer}/.well-known/openid-configuration`;
|
|
373
|
-
const res = await fetchImpl(url);
|
|
374
|
-
if (!res.ok) throw new SsoError(`${url} answered ${res.status}`);
|
|
375
|
-
const doc = await res.json();
|
|
376
|
-
const advertised = doc.id_token_signing_alg_values_supported;
|
|
377
|
-
if (Array.isArray(advertised) && advertised.length > 0) {
|
|
378
|
-
const usable = advertised.filter((a) => ALLOWED_ALGS.includes(a));
|
|
379
|
-
if (usable.length === 0) {
|
|
380
|
-
throw new SsoError(
|
|
381
|
-
`${config.issuer} signs ID tokens with ${advertised.join(", ")}, and this client accepts none of those (it allows ${ALLOWED_ALGS.join(", ")}). Every token would be rejected. This is a mismatch between the package and the issuer \u2014 report it rather than working around it.`
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (doc.issuer !== config.issuer) {
|
|
386
|
-
throw new SsoError(
|
|
387
|
-
`BID_ISSUER is ${config.issuer} but ${url} says its issuer is ${doc.issuer}. These must match exactly \u2014 tokens are validated against the issuer string.`
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
return doc;
|
|
391
|
-
})().catch((err) => {
|
|
392
|
-
discoveryPromise = null;
|
|
393
|
-
throw err;
|
|
89
|
+
async function exchange(audience, scope) {
|
|
90
|
+
const subject = await platformToken();
|
|
91
|
+
const form = new URLSearchParams({
|
|
92
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
93
|
+
subject_token: subject,
|
|
94
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
|
|
95
|
+
audience
|
|
394
96
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
fetchImpl,
|
|
403
|
-
...options.minRefetchIntervalMs !== void 0 ? { minRefetchIntervalMs: options.minRefetchIntervalMs } : {}
|
|
97
|
+
if (scope) form.set("scope", scope);
|
|
98
|
+
let res;
|
|
99
|
+
try {
|
|
100
|
+
res = await fetchImpl(`${issuer}/oauth2/token`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
103
|
+
body: form
|
|
404
104
|
});
|
|
105
|
+
} catch (e) {
|
|
106
|
+
throw new TicketUnavailableError(`${issuer} unreachable: ${e instanceof Error ? e.message : String(e)}`);
|
|
405
107
|
}
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
async function verifyLogoutToken(token) {
|
|
409
|
-
const cache = await keys();
|
|
410
|
-
let header;
|
|
108
|
+
let body = {};
|
|
411
109
|
try {
|
|
412
|
-
|
|
110
|
+
body = await res.json();
|
|
413
111
|
} catch {
|
|
414
|
-
throw new SsoError("logout_token is not a JWS");
|
|
415
|
-
}
|
|
416
|
-
if (!header.kid) throw new SsoError("logout_token has no kid \u2014 cannot pick a signing key");
|
|
417
|
-
let payload;
|
|
418
|
-
try {
|
|
419
|
-
({ payload } = await jose.jwtVerify(token, async () => cache.getKey(header.kid, header.alg ?? "RS256"), {
|
|
420
|
-
issuer: config.issuer,
|
|
421
|
-
audience: config.clientId,
|
|
422
|
-
algorithms: ALLOWED_ALGS,
|
|
423
|
-
typ: "logout+jwt",
|
|
424
|
-
requiredClaims: ["iat", "exp", "jti"]
|
|
425
|
-
}));
|
|
426
|
-
} catch (e) {
|
|
427
|
-
throw new SsoError(`logout_token rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
428
|
-
}
|
|
429
|
-
const events = payload.events;
|
|
430
|
-
if (!events || typeof events !== "object" || !(BACKCHANNEL_EVENT in events)) {
|
|
431
|
-
throw new SsoError("logout_token has no back-channel logout event");
|
|
432
|
-
}
|
|
433
|
-
if ("nonce" in payload) throw new SsoError("logout_token carries a nonce \u2014 that is an ID token, not a logout token");
|
|
434
|
-
if ("sid" in payload) {
|
|
435
|
-
throw new SsoError("logout_token carries sid \u2014 only sub-wide \xABLog ud overalt\xBB is accepted (D-376ffa)");
|
|
436
|
-
}
|
|
437
|
-
if (typeof payload.sub !== "string" || payload.sub === "") throw new SsoError("logout_token has no sub");
|
|
438
|
-
return { sub: payload.sub, iat: payload.iat, jti: String(payload.jti), exp: payload.exp };
|
|
439
|
-
}
|
|
440
|
-
async function verifyIdToken(idToken, opts = {}) {
|
|
441
|
-
const cache = await keys();
|
|
442
|
-
const header = jose.decodeProtectedHeader(idToken);
|
|
443
|
-
if (!header.kid) throw new SsoError("ID token has no kid \u2014 cannot pick a signing key");
|
|
444
|
-
const { payload } = await jose.jwtVerify(
|
|
445
|
-
idToken,
|
|
446
|
-
async () => cache.getKey(header.kid, header.alg ?? "RS256"),
|
|
447
|
-
{ issuer: config.issuer, audience: config.clientId, algorithms: ALLOWED_ALGS }
|
|
448
|
-
);
|
|
449
|
-
if (opts.nonce !== void 0 && payload.nonce !== opts.nonce) {
|
|
450
|
-
throw new SsoError(
|
|
451
|
-
"ID token nonce does not match this login request \u2014 refusing a token minted for another sign-in."
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
if (typeof payload.sub !== "string" || payload.sub === "") {
|
|
455
|
-
throw new SsoError("ID token has no sub \u2014 there is no user to be");
|
|
456
112
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const res = await fetchImpl(doc.userinfo_endpoint, {
|
|
463
|
-
headers: { authorization: `Bearer ${accessToken}` }
|
|
464
|
-
});
|
|
465
|
-
if (!res.ok) return { claims, unverifiedClaims: [] };
|
|
466
|
-
const info = await res.json();
|
|
467
|
-
if (info.sub !== claims.sub) {
|
|
468
|
-
throw new SsoError(
|
|
469
|
-
`userinfo describes ${String(info.sub)} but the ID token is for ${claims.sub} \u2014 refusing to merge another user's profile onto this session.`
|
|
470
|
-
);
|
|
113
|
+
if (res.status >= 500) throw new TicketUnavailableError(`${issuer} answered ${res.status}`);
|
|
114
|
+
if (!res.ok || !body.access_token) {
|
|
115
|
+
const desc = body.error_description ?? "";
|
|
116
|
+
const code = desc.includes(":") ? desc.slice(0, desc.indexOf(":")).trim() : body.error ?? "exchange_refused";
|
|
117
|
+
throw new TicketExchangeError(`Broberg ID refused the ticket for ${audience}: ${desc || body.error || res.status}`, code, res.status);
|
|
471
118
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
119
|
+
const nowSec = Math.floor(now() / 1e3);
|
|
120
|
+
const exp = jwtExp(body.access_token) ?? nowSec + (body.expires_in ?? 300);
|
|
121
|
+
return { token: body.access_token, exp };
|
|
474
122
|
}
|
|
475
123
|
return {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
response_type: "code",
|
|
489
|
-
client_id: config.clientId,
|
|
490
|
-
redirect_uri: config.redirectUri,
|
|
491
|
-
scope: [.../* @__PURE__ */ new Set([...config.scopes, ...opts.scopes ?? []])].join(" "),
|
|
492
|
-
state,
|
|
493
|
-
nonce,
|
|
494
|
-
code_challenge: await challengeFor(codeVerifier),
|
|
495
|
-
code_challenge_method: "S256"
|
|
496
|
-
});
|
|
497
|
-
if (opts.prompt) params.set("prompt", opts.prompt);
|
|
498
|
-
return { url: `${authorization_endpoint}?${params}`, state, codeVerifier, nonce };
|
|
499
|
-
},
|
|
500
|
-
async completeLogin({ params, state, codeVerifier, nonce }) {
|
|
501
|
-
const error = params.get("error");
|
|
502
|
-
if (error) {
|
|
503
|
-
throw new SsoError(
|
|
504
|
-
`Broberg ID refused this login: ${error}` + (params.get("error_description") ? ` \u2014 ${params.get("error_description")}` : "")
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
const returned = params.get("state");
|
|
508
|
-
if (!returned || returned !== state) {
|
|
509
|
-
throw new SsoError(
|
|
510
|
-
"state does not match the login this browser started \u2014 refusing the callback."
|
|
511
|
-
);
|
|
512
|
-
}
|
|
513
|
-
const code = params.get("code");
|
|
514
|
-
if (!code) throw new SsoError("callback carried neither an error nor a code");
|
|
515
|
-
const { token_endpoint } = await discovery();
|
|
516
|
-
const res = await fetchImpl(token_endpoint, {
|
|
517
|
-
method: "POST",
|
|
518
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
519
|
-
body: new URLSearchParams({
|
|
520
|
-
grant_type: "authorization_code",
|
|
521
|
-
code,
|
|
522
|
-
redirect_uri: config.redirectUri,
|
|
523
|
-
client_id: config.clientId,
|
|
524
|
-
// ALWAYS sent, secret or not. See SsoConfig.clientSecret: the two
|
|
525
|
-
// prove different things, so a confidential client keeps PKCE.
|
|
526
|
-
code_verifier: codeVerifier,
|
|
527
|
-
...config.clientSecret ? { client_secret: config.clientSecret } : {}
|
|
528
|
-
})
|
|
529
|
-
});
|
|
530
|
-
const raw = await res.text();
|
|
531
|
-
let body;
|
|
532
|
-
try {
|
|
533
|
-
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
534
|
-
} catch {
|
|
535
|
-
throw new SsoError(
|
|
536
|
-
`token exchange failed (${res.status}): the response body is not JSON${res.headers.get("content-type") ? ` (content-type: ${res.headers.get("content-type")})` : ""} \u2014 ${excerptForLog(raw, config.clientSecret)}`
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
if (!res.ok || !body.id_token) {
|
|
540
|
-
throw new SsoError(
|
|
541
|
-
`token exchange failed (${res.status}): ${body.error ?? "no id_token in response"}` + (body.error_description ? ` \u2014 ${body.error_description}` : "") + invalidClientHint(body.error, config.clientSecret !== void 0)
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
const claims = await verifyIdToken(body.id_token, { nonce });
|
|
545
|
-
const merged = body.access_token ? await withUserInfo(claims, body.access_token) : { claims, unverifiedClaims: [] };
|
|
546
|
-
return {
|
|
547
|
-
claims: merged.claims,
|
|
548
|
-
unverifiedClaims: merged.unverifiedClaims,
|
|
549
|
-
idToken: body.id_token,
|
|
550
|
-
...body.access_token ? { accessToken: body.access_token } : {},
|
|
551
|
-
...body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
552
|
-
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
553
|
-
};
|
|
554
|
-
},
|
|
555
|
-
async refreshTokens(refreshToken) {
|
|
556
|
-
const { token_endpoint } = await discovery();
|
|
557
|
-
const res = await fetchImpl(token_endpoint, {
|
|
558
|
-
method: "POST",
|
|
559
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
560
|
-
body: new URLSearchParams({
|
|
561
|
-
grant_type: "refresh_token",
|
|
562
|
-
refresh_token: refreshToken,
|
|
563
|
-
client_id: config.clientId,
|
|
564
|
-
...config.clientSecret ? { client_secret: config.clientSecret } : {}
|
|
565
|
-
})
|
|
566
|
-
});
|
|
567
|
-
const raw = await res.text();
|
|
568
|
-
let body;
|
|
569
|
-
try {
|
|
570
|
-
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
571
|
-
} catch {
|
|
572
|
-
throw new SsoError(`token refresh failed (${res.status}): the response body is not JSON`);
|
|
573
|
-
}
|
|
574
|
-
if (!res.ok || typeof body.access_token !== "string" || body.access_token === "") {
|
|
575
|
-
throw new SsoError(`token refresh failed (${res.status}): ${body.error ?? "no access_token in response"}`);
|
|
576
|
-
}
|
|
577
|
-
return {
|
|
578
|
-
accessToken: body.access_token,
|
|
579
|
-
...typeof body.refresh_token === "string" && body.refresh_token ? { refreshToken: body.refresh_token } : {},
|
|
580
|
-
...typeof body.expires_in === "number" ? { expiresIn: body.expires_in } : {}
|
|
581
|
-
};
|
|
582
|
-
},
|
|
583
|
-
async logoutUrl(opts = {}) {
|
|
584
|
-
const doc = await discovery();
|
|
585
|
-
if (!doc.end_session_endpoint) {
|
|
586
|
-
throw new SsoError(
|
|
587
|
-
`${config.issuer} does not advertise end_session_endpoint \u2014 central logout is unavailable.`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
const params = new URLSearchParams();
|
|
591
|
-
if (opts.idTokenHint) params.set("id_token_hint", opts.idTokenHint);
|
|
592
|
-
const post = opts.postLogoutRedirectUri ?? config.postLogoutRedirectUri;
|
|
593
|
-
if (post) params.set("post_logout_redirect_uri", post);
|
|
594
|
-
params.set("client_id", config.clientId);
|
|
595
|
-
return `${doc.end_session_endpoint}?${params}`;
|
|
596
|
-
},
|
|
597
|
-
async addressOwnership(accessToken, address) {
|
|
598
|
-
const body = await appPost(
|
|
599
|
-
"/api/app/address-ownership",
|
|
600
|
-
accessToken,
|
|
601
|
-
{ address },
|
|
602
|
-
'address ownership (not "unverified")'
|
|
603
|
-
);
|
|
604
|
-
const status = body?.status;
|
|
605
|
-
if (typeof status !== "string" || !ADDRESS_OWNERSHIP.has(status)) {
|
|
606
|
-
throw new SsoError(`address-ownership returned status ${JSON.stringify(status)} \u2014 expected verified, unverified or not_on_account.`);
|
|
607
|
-
}
|
|
608
|
-
return status;
|
|
609
|
-
},
|
|
610
|
-
verifyLogoutToken,
|
|
611
|
-
async getProfile(accessToken) {
|
|
612
|
-
const path = "/api/app/profile";
|
|
613
|
-
return asProfile(await appRequest(path, accessToken, { method: "GET" }, "the profile"), path);
|
|
614
|
-
},
|
|
615
|
-
async updateProfile(accessToken, name) {
|
|
616
|
-
const path = "/api/app/profile";
|
|
617
|
-
return asProfile(await appRequest(path, accessToken, { method: "POST", json: { name } }, "the new name"), path);
|
|
618
|
-
},
|
|
619
|
-
async uploadAvatar(accessToken, bytes, contentType) {
|
|
620
|
-
const type = mediaType(contentType);
|
|
621
|
-
if (!AVATAR_TYPES.includes(type)) {
|
|
622
|
-
throw new SsoAvatarRejectedError(
|
|
623
|
-
`uploadAvatar: "${type}" is not accepted \u2014 BID takes ${AVATAR_TYPES.join(", ")}.`,
|
|
624
|
-
"unsupported_type"
|
|
625
|
-
);
|
|
626
|
-
}
|
|
627
|
-
if (bytes.byteLength > MAX_AVATAR_BYTES) {
|
|
628
|
-
throw new SsoAvatarRejectedError(
|
|
629
|
-
`uploadAvatar: ${bytes.byteLength} bytes \u2014 BID takes at most ${MAX_AVATAR_BYTES}.`,
|
|
630
|
-
"too_large"
|
|
631
|
-
);
|
|
632
|
-
}
|
|
633
|
-
const path = "/api/app/profile/avatar";
|
|
634
|
-
return asProfile(await appRequest(path, accessToken, { method: "POST", bytes, contentType: type }, "the new picture"), path);
|
|
635
|
-
},
|
|
636
|
-
async removeAvatar(accessToken) {
|
|
637
|
-
const path = "/api/app/profile/avatar/remove";
|
|
638
|
-
return asProfile(await appRequest(path, accessToken, { method: "POST" }, "the removed picture"), path);
|
|
639
|
-
},
|
|
640
|
-
async inviteUsers(appKey, input) {
|
|
641
|
-
if (input.users.length > MAX_INVITATIONS) {
|
|
642
|
-
throw new SsoError(`inviteUsers: ${input.users.length} users, BID accepts at most ${MAX_INVITATIONS} per call.`);
|
|
643
|
-
}
|
|
644
|
-
const body = await appPost("/api/app/invitations", appKey, input, "the invitation outcome");
|
|
645
|
-
if (!Array.isArray(body?.results)) {
|
|
646
|
-
throw new SsoError(`invitations returned no "results" array \u2014 the invitation outcome is unknown.`);
|
|
647
|
-
}
|
|
648
|
-
for (const r of body.results) {
|
|
649
|
-
if (typeof r?.outcome !== "string" || !INVITATION_OUTCOMES.has(r.outcome)) {
|
|
650
|
-
throw new SsoError(`invitations returned outcome ${JSON.stringify(r?.outcome)} for ${String(r?.email)} \u2014 not one this client knows.`);
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
return body.results;
|
|
654
|
-
},
|
|
655
|
-
async migrationStatus(appKey, emails) {
|
|
656
|
-
const body = await appPost("/api/app/migration-status", appKey, { emails }, "the migration state");
|
|
657
|
-
if (!Array.isArray(body?.users) || typeof body.complete !== "boolean") {
|
|
658
|
-
throw new SsoError(`migration-status returned no "users" array or no "complete" flag \u2014 the migration state is unknown.`);
|
|
659
|
-
}
|
|
660
|
-
for (const u of body.users) {
|
|
661
|
-
if (typeof u?.state !== "string" || !MIGRATION_STATES.has(u.state)) {
|
|
662
|
-
throw new SsoError(`migration-status returned state ${JSON.stringify(u?.state)} for ${String(u?.email)} \u2014 never read as "ready".`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
return body;
|
|
124
|
+
async get({ audience, scope }) {
|
|
125
|
+
const key = `${audience} ${scope ?? ""}`;
|
|
126
|
+
const hit = cache.get(key);
|
|
127
|
+
if (hit && hit.exp - renewBeforeSec > now() / 1e3) return hit.token;
|
|
128
|
+
const pending = inFlight.get(key);
|
|
129
|
+
if (pending) return pending;
|
|
130
|
+
const p = exchange(audience, scope).then((t) => {
|
|
131
|
+
cache.set(key, t);
|
|
132
|
+
return t.token;
|
|
133
|
+
}).finally(() => inFlight.delete(key));
|
|
134
|
+
inFlight.set(key, p);
|
|
135
|
+
return p;
|
|
666
136
|
}
|
|
667
137
|
};
|
|
668
138
|
}
|
|
139
|
+
var shared = null;
|
|
140
|
+
function fetchTicket(opts) {
|
|
141
|
+
shared ??= createTicketClient();
|
|
142
|
+
return shared.get(opts);
|
|
143
|
+
}
|
|
669
144
|
var PRINCIPAL_TYPES = ["human", "service", "agent"];
|
|
670
145
|
function createTicketVerifier(options) {
|
|
671
146
|
const {
|
|
@@ -675,10 +150,10 @@ function createTicketVerifier(options) {
|
|
|
675
150
|
clockToleranceSec = 30,
|
|
676
151
|
fetchImpl = fetch
|
|
677
152
|
} = options;
|
|
678
|
-
if (!issuer) throw new SsoError("createTicketVerifier needs an issuer");
|
|
679
|
-
if (!audience) throw new SsoError("createTicketVerifier needs this receiver's audience");
|
|
153
|
+
if (!issuer) throw new chunkJGIO7XPR_cjs.SsoError("createTicketVerifier needs an issuer");
|
|
154
|
+
if (!audience) throw new chunkJGIO7XPR_cjs.SsoError("createTicketVerifier needs this receiver's audience");
|
|
680
155
|
let cachePromise = null;
|
|
681
|
-
const cacheFor = (jwksUri) => createJwksCache({
|
|
156
|
+
const cacheFor = (jwksUri) => chunkJGIO7XPR_cjs.createJwksCache({
|
|
682
157
|
jwksUri,
|
|
683
158
|
fetchImpl,
|
|
684
159
|
...options.warmUp ? { warmUp: true } : {},
|
|
@@ -694,12 +169,12 @@ function createTicketVerifier(options) {
|
|
|
694
169
|
if (!res.ok) throw new Error(`answered ${res.status}`);
|
|
695
170
|
doc = await res.json();
|
|
696
171
|
} catch (cause) {
|
|
697
|
-
throw new JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
172
|
+
throw new chunkJGIO7XPR_cjs.JwksUnavailableError(`${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
698
173
|
}
|
|
699
174
|
if (doc.issuer !== issuer) {
|
|
700
|
-
throw new SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} \u2014 they must match exactly`);
|
|
175
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`issuer is ${issuer} but ${url} says ${doc.issuer} \u2014 they must match exactly`);
|
|
701
176
|
}
|
|
702
|
-
if (!doc.jwks_uri) throw new SsoError(`${url} names no jwks_uri`);
|
|
177
|
+
if (!doc.jwks_uri) throw new chunkJGIO7XPR_cjs.SsoError(`${url} names no jwks_uri`);
|
|
703
178
|
return cacheFor(doc.jwks_uri);
|
|
704
179
|
})().catch((err) => {
|
|
705
180
|
cachePromise = null;
|
|
@@ -715,39 +190,39 @@ function createTicketVerifier(options) {
|
|
|
715
190
|
try {
|
|
716
191
|
header = jose.decodeProtectedHeader(token);
|
|
717
192
|
} catch {
|
|
718
|
-
throw new SsoError("ticket is not a JWS");
|
|
193
|
+
throw new chunkJGIO7XPR_cjs.SsoError("ticket is not a JWS");
|
|
719
194
|
}
|
|
720
|
-
if (!header.kid) throw new SsoError("ticket has no kid \u2014 cannot pick a signing key");
|
|
721
|
-
if (header.typ === "logout+jwt") throw new SsoError("a logout token is not a ticket");
|
|
195
|
+
if (!header.kid) throw new chunkJGIO7XPR_cjs.SsoError("ticket has no kid \u2014 cannot pick a signing key");
|
|
196
|
+
if (header.typ === "logout+jwt") throw new chunkJGIO7XPR_cjs.SsoError("a logout token is not a ticket");
|
|
722
197
|
const cache = await keys();
|
|
723
198
|
let payload;
|
|
724
199
|
try {
|
|
725
200
|
({ payload } = await jose.jwtVerify(token, async () => cache.getKey(header.kid, header.alg ?? "EdDSA"), {
|
|
726
201
|
issuer,
|
|
727
202
|
audience,
|
|
728
|
-
algorithms: ALLOWED_ALGS,
|
|
203
|
+
algorithms: chunkJGIO7XPR_cjs.ALLOWED_ALGS,
|
|
729
204
|
clockTolerance: clockToleranceSec,
|
|
730
205
|
requiredClaims: ["exp", "iat", "jti", "sub"]
|
|
731
206
|
}));
|
|
732
207
|
} catch (e) {
|
|
733
|
-
if (e instanceof JwksUnavailableError) throw e;
|
|
734
|
-
throw new SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
208
|
+
if (e instanceof chunkJGIO7XPR_cjs.JwksUnavailableError) throw e;
|
|
209
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket rejected: ${e instanceof Error ? e.message : String(e)}`);
|
|
735
210
|
}
|
|
736
|
-
if ("events" in payload) throw new SsoError("a logout token is not a ticket");
|
|
737
|
-
if ("nonce" in payload) throw new SsoError("an ID token is not a ticket");
|
|
211
|
+
if ("events" in payload) throw new chunkJGIO7XPR_cjs.SsoError("a logout token is not a ticket");
|
|
212
|
+
if ("nonce" in payload) throw new chunkJGIO7XPR_cjs.SsoError("an ID token is not a ticket");
|
|
738
213
|
const type = payload.principal_type;
|
|
739
214
|
if (typeof type !== "string" || !PRINCIPAL_TYPES.includes(type)) {
|
|
740
|
-
throw new SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);
|
|
215
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket has no valid principal_type (got ${JSON.stringify(type)})`);
|
|
741
216
|
}
|
|
742
217
|
const lifetime = payload.exp - payload.iat;
|
|
743
218
|
if (lifetime > maxLifetimeSec) {
|
|
744
|
-
throw new SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);
|
|
219
|
+
throw new chunkJGIO7XPR_cjs.SsoError(`ticket claims a ${lifetime}s lifetime; at most ${maxLifetimeSec}s is accepted`);
|
|
745
220
|
}
|
|
746
|
-
if (typeof payload.sub !== "string" || payload.sub === "") throw new SsoError("ticket has no sub");
|
|
221
|
+
if (typeof payload.sub !== "string" || payload.sub === "") throw new chunkJGIO7XPR_cjs.SsoError("ticket has no sub");
|
|
747
222
|
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ").filter(Boolean) : [];
|
|
748
|
-
const
|
|
749
|
-
const missing =
|
|
750
|
-
if (missing.length) throw new SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
223
|
+
const required = opts.scope === void 0 ? [] : Array.isArray(opts.scope) ? opts.scope : [opts.scope];
|
|
224
|
+
const missing = required.filter((s) => !scopes.includes(s));
|
|
225
|
+
if (missing.length) throw new chunkJGIO7XPR_cjs.SsoError(`ticket lacks scope ${missing.join(", ")}`);
|
|
751
226
|
const actRaw = payload.act;
|
|
752
227
|
const act = actRaw && typeof actRaw.sub === "string" && actRaw.sub ? { sub: actRaw.sub } : null;
|
|
753
228
|
return {
|
|
@@ -764,148 +239,104 @@ function createTicketVerifier(options) {
|
|
|
764
239
|
};
|
|
765
240
|
}
|
|
766
241
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
return
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
const rows = /* @__PURE__ */ new Map();
|
|
867
|
-
return {
|
|
868
|
-
get size() {
|
|
869
|
-
return rows.size;
|
|
870
|
-
},
|
|
871
|
-
async get(sid) {
|
|
872
|
-
return rows.get(sid);
|
|
873
|
-
},
|
|
874
|
-
async set(sid, tokens) {
|
|
875
|
-
rows.set(sid, { ...tokens });
|
|
876
|
-
},
|
|
877
|
-
async delete(sid) {
|
|
878
|
-
rows.delete(sid);
|
|
879
|
-
},
|
|
880
|
-
async deleteSub(sub) {
|
|
881
|
-
for (const [sid, t] of rows) if (t.sub === sub) rows.delete(sid);
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
exports.AVATAR_TYPES = AVATAR_TYPES;
|
|
887
|
-
exports.DEFAULT_SESSION_MAX_AGE = DEFAULT_SESSION_MAX_AGE;
|
|
888
|
-
exports.JwksError = JwksError;
|
|
889
|
-
exports.JwksUnavailableError = JwksUnavailableError;
|
|
890
|
-
exports.JwksUnknownKeyError = JwksUnknownKeyError;
|
|
891
|
-
exports.MAX_AVATAR_BYTES = MAX_AVATAR_BYTES;
|
|
892
|
-
exports.MAX_INVITATIONS = MAX_INVITATIONS;
|
|
893
|
-
exports.SsoAppApiError = SsoAppApiError;
|
|
894
|
-
exports.SsoAvatarRejectedError = SsoAvatarRejectedError;
|
|
895
|
-
exports.SsoConfigError = SsoConfigError;
|
|
896
|
-
exports.SsoError = SsoError;
|
|
897
|
-
exports.SsoInsufficientScopeError = SsoInsufficientScopeError;
|
|
898
|
-
exports.SsoReauthError = SsoReauthError;
|
|
899
|
-
exports.cookieHeader = cookieHeader;
|
|
900
|
-
exports.createJwksCache = createJwksCache;
|
|
901
|
-
exports.createSsoClient = createSsoClient;
|
|
242
|
+
Object.defineProperty(exports, "AVATAR_TYPES", {
|
|
243
|
+
enumerable: true,
|
|
244
|
+
get: function () { return chunkJGIO7XPR_cjs.AVATAR_TYPES; }
|
|
245
|
+
});
|
|
246
|
+
Object.defineProperty(exports, "DEFAULT_SESSION_MAX_AGE", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
get: function () { return chunkJGIO7XPR_cjs.DEFAULT_SESSION_MAX_AGE; }
|
|
249
|
+
});
|
|
250
|
+
Object.defineProperty(exports, "JwksError", {
|
|
251
|
+
enumerable: true,
|
|
252
|
+
get: function () { return chunkJGIO7XPR_cjs.JwksError; }
|
|
253
|
+
});
|
|
254
|
+
Object.defineProperty(exports, "JwksUnavailableError", {
|
|
255
|
+
enumerable: true,
|
|
256
|
+
get: function () { return chunkJGIO7XPR_cjs.JwksUnavailableError; }
|
|
257
|
+
});
|
|
258
|
+
Object.defineProperty(exports, "JwksUnknownKeyError", {
|
|
259
|
+
enumerable: true,
|
|
260
|
+
get: function () { return chunkJGIO7XPR_cjs.JwksUnknownKeyError; }
|
|
261
|
+
});
|
|
262
|
+
Object.defineProperty(exports, "MAX_AVATAR_BYTES", {
|
|
263
|
+
enumerable: true,
|
|
264
|
+
get: function () { return chunkJGIO7XPR_cjs.MAX_AVATAR_BYTES; }
|
|
265
|
+
});
|
|
266
|
+
Object.defineProperty(exports, "MAX_INVITATIONS", {
|
|
267
|
+
enumerable: true,
|
|
268
|
+
get: function () { return chunkJGIO7XPR_cjs.MAX_INVITATIONS; }
|
|
269
|
+
});
|
|
270
|
+
Object.defineProperty(exports, "SsoAppApiError", {
|
|
271
|
+
enumerable: true,
|
|
272
|
+
get: function () { return chunkJGIO7XPR_cjs.SsoAppApiError; }
|
|
273
|
+
});
|
|
274
|
+
Object.defineProperty(exports, "SsoAvatarRejectedError", {
|
|
275
|
+
enumerable: true,
|
|
276
|
+
get: function () { return chunkJGIO7XPR_cjs.SsoAvatarRejectedError; }
|
|
277
|
+
});
|
|
278
|
+
Object.defineProperty(exports, "SsoConfigError", {
|
|
279
|
+
enumerable: true,
|
|
280
|
+
get: function () { return chunkJGIO7XPR_cjs.SsoConfigError; }
|
|
281
|
+
});
|
|
282
|
+
Object.defineProperty(exports, "SsoError", {
|
|
283
|
+
enumerable: true,
|
|
284
|
+
get: function () { return chunkJGIO7XPR_cjs.SsoError; }
|
|
285
|
+
});
|
|
286
|
+
Object.defineProperty(exports, "SsoInsufficientScopeError", {
|
|
287
|
+
enumerable: true,
|
|
288
|
+
get: function () { return chunkJGIO7XPR_cjs.SsoInsufficientScopeError; }
|
|
289
|
+
});
|
|
290
|
+
Object.defineProperty(exports, "SsoReauthError", {
|
|
291
|
+
enumerable: true,
|
|
292
|
+
get: function () { return chunkJGIO7XPR_cjs.SsoReauthError; }
|
|
293
|
+
});
|
|
294
|
+
Object.defineProperty(exports, "cookieHeader", {
|
|
295
|
+
enumerable: true,
|
|
296
|
+
get: function () { return chunkJGIO7XPR_cjs.cookieHeader; }
|
|
297
|
+
});
|
|
298
|
+
Object.defineProperty(exports, "createJwksCache", {
|
|
299
|
+
enumerable: true,
|
|
300
|
+
get: function () { return chunkJGIO7XPR_cjs.createJwksCache; }
|
|
301
|
+
});
|
|
302
|
+
Object.defineProperty(exports, "createSsoClient", {
|
|
303
|
+
enumerable: true,
|
|
304
|
+
get: function () { return chunkJGIO7XPR_cjs.createSsoClient; }
|
|
305
|
+
});
|
|
306
|
+
Object.defineProperty(exports, "loadSsoConfig", {
|
|
307
|
+
enumerable: true,
|
|
308
|
+
get: function () { return chunkJGIO7XPR_cjs.loadSsoConfig; }
|
|
309
|
+
});
|
|
310
|
+
Object.defineProperty(exports, "memoryTokenStore", {
|
|
311
|
+
enumerable: true,
|
|
312
|
+
get: function () { return chunkJGIO7XPR_cjs.memoryTokenStore; }
|
|
313
|
+
});
|
|
314
|
+
Object.defineProperty(exports, "readCookie", {
|
|
315
|
+
enumerable: true,
|
|
316
|
+
get: function () { return chunkJGIO7XPR_cjs.readCookie; }
|
|
317
|
+
});
|
|
318
|
+
Object.defineProperty(exports, "signSession", {
|
|
319
|
+
enumerable: true,
|
|
320
|
+
get: function () { return chunkJGIO7XPR_cjs.signSession; }
|
|
321
|
+
});
|
|
322
|
+
Object.defineProperty(exports, "signValue", {
|
|
323
|
+
enumerable: true,
|
|
324
|
+
get: function () { return chunkJGIO7XPR_cjs.signValue; }
|
|
325
|
+
});
|
|
326
|
+
Object.defineProperty(exports, "verifySession", {
|
|
327
|
+
enumerable: true,
|
|
328
|
+
get: function () { return chunkJGIO7XPR_cjs.verifySession; }
|
|
329
|
+
});
|
|
330
|
+
Object.defineProperty(exports, "verifyValue", {
|
|
331
|
+
enumerable: true,
|
|
332
|
+
get: function () { return chunkJGIO7XPR_cjs.verifyValue; }
|
|
333
|
+
});
|
|
334
|
+
exports.DEFAULT_ISSUER = DEFAULT_ISSUER;
|
|
335
|
+
exports.NoWorkloadIdentityError = NoWorkloadIdentityError;
|
|
336
|
+
exports.TicketExchangeError = TicketExchangeError;
|
|
337
|
+
exports.TicketUnavailableError = TicketUnavailableError;
|
|
338
|
+
exports.createTicketClient = createTicketClient;
|
|
902
339
|
exports.createTicketVerifier = createTicketVerifier;
|
|
903
|
-
exports.
|
|
904
|
-
exports.memoryTokenStore = memoryTokenStore;
|
|
905
|
-
exports.readCookie = readCookie;
|
|
906
|
-
exports.signSession = signSession;
|
|
907
|
-
exports.signValue = signValue;
|
|
908
|
-
exports.verifySession = verifySession;
|
|
909
|
-
exports.verifyValue = verifyValue;
|
|
340
|
+
exports.fetchTicket = fetchTicket;
|
|
910
341
|
//# sourceMappingURL=index.cjs.map
|
|
911
342
|
//# sourceMappingURL=index.cjs.map
|