cauthy-auth 0.0.0-stage → 0.2.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/dist/index.js ADDED
@@ -0,0 +1,748 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ CauthyConfigError: () => CauthyConfigError,
24
+ CauthyWebhookError: () => CauthyWebhookError,
25
+ can: () => can,
26
+ createCauthy: () => createCauthy,
27
+ pushPolicy: () => pushPolicy,
28
+ pushTheme: () => pushTheme,
29
+ themeFromStackmyth: () => themeFromStackmyth,
30
+ verifyWebhook: () => verifyWebhook
31
+ });
32
+ module.exports = __toCommonJS(src_exports);
33
+
34
+ // src/cookies.ts
35
+ var PART = 3500;
36
+ function readCookies(header) {
37
+ const out2 = /* @__PURE__ */ new Map();
38
+ for (const pair of (header ?? "").split(";")) {
39
+ const i = pair.indexOf("=");
40
+ if (i > 0) out2.set(pair.slice(0, i).trim(), pair.slice(i + 1).trim());
41
+ }
42
+ return out2;
43
+ }
44
+ function readParts(cookies, name) {
45
+ let value = "";
46
+ for (let i = 0; cookies.has(`${name}.${i}`); i++) value += cookies.get(`${name}.${i}`);
47
+ return value === "" ? null : value;
48
+ }
49
+ function attributes(a) {
50
+ return `Path=${a.path ?? "/"}; HttpOnly; SameSite=Lax; Max-Age=${a.maxAge}${a.secure ? "; Secure" : ""}`;
51
+ }
52
+ function writeCookie(name, value, a) {
53
+ return `${name}=${value}; ${attributes(a)}`;
54
+ }
55
+ function writeParts(name, value, a, had) {
56
+ const out2 = [];
57
+ let n = 0;
58
+ for (let i = 0; i < value.length; i += PART) out2.push(writeCookie(`${name}.${n++}`, value.slice(i, i + PART), a));
59
+ for (let i = n; had.has(`${name}.${i}`); i++) out2.push(writeCookie(`${name}.${i}`, "", { ...a, maxAge: 0 }));
60
+ return out2;
61
+ }
62
+ function clearParts(name, a, had) {
63
+ const out2 = [];
64
+ for (let i = 0; had.has(`${name}.${i}`) || i === 0; i++) out2.push(writeCookie(`${name}.${i}`, "", { ...a, maxAge: 0 }));
65
+ return out2;
66
+ }
67
+
68
+ // src/seal.ts
69
+ var import_jose = require("jose");
70
+ var encoder = new TextEncoder();
71
+ var decoder = new TextDecoder();
72
+ async function keyOf(secret) {
73
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(secret)));
74
+ }
75
+ async function seal(value, key) {
76
+ return new import_jose.CompactEncrypt(encoder.encode(JSON.stringify(value))).setProtectedHeader({ alg: "dir", enc: "A256GCM" }).encrypt(key);
77
+ }
78
+ async function unseal(sealed, key) {
79
+ try {
80
+ const { plaintext } = await (0, import_jose.compactDecrypt)(sealed, key, {
81
+ keyManagementAlgorithms: ["dir"],
82
+ contentEncryptionAlgorithms: ["A256GCM"]
83
+ });
84
+ return JSON.parse(decoder.decode(plaintext));
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ // src/webhook.ts
91
+ var import_jose2 = require("jose");
92
+ var CauthyWebhookError = class extends Error {
93
+ constructor(reason) {
94
+ super(`cauthy-auth: webhook refused: ${reason}`);
95
+ this.reason = reason;
96
+ this.name = "CauthyWebhookError";
97
+ }
98
+ reason;
99
+ };
100
+ var keys = /* @__PURE__ */ new Map();
101
+ function keysOf(issuer) {
102
+ let set = keys.get(issuer);
103
+ if (set === void 0) {
104
+ set = (0, import_jose2.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
105
+ keys.set(issuer, set);
106
+ }
107
+ return set;
108
+ }
109
+ var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
110
+ async function verifyWebhook(request, options = {}) {
111
+ const env2 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
112
+ const issuer = (options.issuer ?? env2 ?? "").replace(/\/+$/, "");
113
+ if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
114
+ const signature = request.headers.get("x-cauthy-signature-jwt");
115
+ if (!signature) throw new CauthyWebhookError("no signature");
116
+ const raw = new Uint8Array(await request.arrayBuffer());
117
+ const audience = options.url ?? request.url.split("?")[0];
118
+ let claims;
119
+ try {
120
+ const verified = await (0, import_jose2.jwtVerify)(signature, options.jwks ?? keysOf(issuer), {
121
+ algorithms: ["RS256"],
122
+ issuer,
123
+ audience,
124
+ typ: "cauthy-webhook+jwt",
125
+ requiredClaims: ["exp", "iat", "jti", "body_sha256"],
126
+ currentDate: options.now,
127
+ clockTolerance: 30
128
+ });
129
+ claims = verified.payload;
130
+ } catch (err) {
131
+ throw new CauthyWebhookError(err instanceof Error ? err.message : "bad signature");
132
+ }
133
+ const now = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
134
+ if (typeof claims.iat !== "number" || claims.iat > now + 30) throw new CauthyWebhookError("signed in the future");
135
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", raw));
136
+ let b64 = "";
137
+ for (const b of digest) b64 += String.fromCharCode(b);
138
+ const hash = btoa(b64).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
139
+ if (hash !== claims.body_sha256) throw new CauthyWebhookError("the body is not the one that was signed");
140
+ let body;
141
+ try {
142
+ body = JSON.parse(new TextDecoder().decode(raw));
143
+ } catch {
144
+ throw new CauthyWebhookError("the body is not JSON");
145
+ }
146
+ if (body.id !== claims.jti || body.event !== claims.evt || body.tenant_id !== claims.tid) {
147
+ throw new CauthyWebhookError("the body and its signature disagree");
148
+ }
149
+ const inner = body.data ?? {};
150
+ return {
151
+ id: String(body.id),
152
+ type: String(body.event),
153
+ tenantId: String(body.tenant_id),
154
+ createdAt: String(inner.created_at ?? body.created_at ?? ""),
155
+ ...hex(inner.user_id) ? { userId: inner.user_id } : {},
156
+ ...hex(inner.actor_id) ? { actorId: inner.actor_id } : {},
157
+ data: inner.data ?? {}
158
+ };
159
+ }
160
+
161
+ // src/types.ts
162
+ var CauthyConfigError = class extends Error {
163
+ constructor(message) {
164
+ super(`cauthy-auth: ${message}`);
165
+ this.name = "CauthyConfigError";
166
+ }
167
+ };
168
+
169
+ // src/cauthy.ts
170
+ var SESSION_DAYS = 30;
171
+ var FLOW_SECONDS = 600;
172
+ var SKEW_SECONDS = 30;
173
+ var ORGANIZATIONS_TTL_MS = 6e4;
174
+ function env(name) {
175
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
176
+ }
177
+ function b64url(bytes) {
178
+ let s = "";
179
+ for (const b of bytes) s += String.fromCharCode(b);
180
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
181
+ }
182
+ function random(bytes = 32) {
183
+ return b64url(crypto.getRandomValues(new Uint8Array(bytes)));
184
+ }
185
+ function localPath(candidate, fallback) {
186
+ if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//") || candidate.includes("\\")) return fallback;
187
+ return candidate;
188
+ }
189
+ function json(body, status = 200, headers) {
190
+ const h = headers ?? new Headers();
191
+ h.set("content-type", "application/json; charset=utf-8");
192
+ h.set("cache-control", "no-store");
193
+ return new Response(JSON.stringify(body), { status, headers: h });
194
+ }
195
+ function redirect(location, cookies = []) {
196
+ const h = new Headers({ location, "cache-control": "no-store" });
197
+ for (const c of cookies) h.append("set-cookie", c);
198
+ return new Response(null, { status: 302, headers: h });
199
+ }
200
+ function createCauthy(given = {}) {
201
+ const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
202
+ const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
203
+ const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET");
204
+ const secret = given.secret ?? env("CAUTHY_SECRET") ?? "";
205
+ const system = given.system ?? env("CAUTHY_SYSTEM") ?? "";
206
+ if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
207
+ if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
208
+ if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
209
+ const basePath = "/" + (given.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "");
210
+ const afterSignIn = localPath(given.afterSignIn, "/");
211
+ const afterSignOut = localPath(given.afterSignOut, "/");
212
+ const cookieName = given.cookieName ?? "cauthy_session";
213
+ const flowCookie = `${cookieName}_flow`;
214
+ const doFetch = (...args) => (given.fetch ?? fetch)(...args);
215
+ const key = keyOf(secret);
216
+ const originOf = (request) => (given.baseURL ?? new URL(request.url).origin).replace(/\/+$/, "");
217
+ const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
218
+ async function token(form) {
219
+ const body = new URLSearchParams({ client_id: clientId, ...form });
220
+ if (clientSecret) body.set("client_secret", clientSecret);
221
+ try {
222
+ const res = await doFetch(`${issuer}/v1/oauth/token`, {
223
+ method: "POST",
224
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
225
+ body
226
+ });
227
+ if (res.ok) return { ok: true, body: await res.json() };
228
+ return { ok: false, ended: res.status >= 400 && res.status < 500 };
229
+ } catch {
230
+ return { ok: false, ended: false };
231
+ }
232
+ }
233
+ async function userinfo(accessToken) {
234
+ try {
235
+ const res = await doFetch(`${issuer}/v1/oauth/userinfo`, { headers: { authorization: `Bearer ${accessToken}` } });
236
+ if (!res.ok) return null;
237
+ const b = await res.json();
238
+ return {
239
+ tid: String(b.tid ?? ""),
240
+ roles: Array.isArray(b.roles) ? b.roles.filter((r) => typeof r === "string") : [],
241
+ u: {
242
+ id: String(b.sub ?? ""),
243
+ email: String(b.email ?? ""),
244
+ name: String(b.name ?? ""),
245
+ emailVerified: b.email_verified === true,
246
+ restricted: b.restricted === true,
247
+ ...typeof b.email_verify_by === "string" ? { emailVerifyBy: b.email_verify_by } : {},
248
+ ...typeof b.locale === "string" && b.locale !== "" ? { locale: b.locale } : {}
249
+ }
250
+ };
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+ async function stored(body) {
256
+ if (typeof body?.access_token !== "string" || typeof body?.refresh_token !== "string") return null;
257
+ const who = await userinfo(body.access_token);
258
+ if (who === null || who.u.id === "") return null;
259
+ const lifetime = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in : 300;
260
+ return { at: body.access_token, rt: body.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime, ...who };
261
+ }
262
+ function toSession(s) {
263
+ return { user: s.u, tenantId: s.tid, roles: s.roles, accessToken: s.at, expiresAt: new Date(s.exp * 1e3).toISOString() };
264
+ }
265
+ async function read(request) {
266
+ const had = readCookies(request.headers.get("cookie"));
267
+ const raw = readParts(had, cookieName);
268
+ if (raw === null) return { had, value: null, present: false };
269
+ const value = await unseal(raw, await key);
270
+ const sound = value !== null && typeof value.at === "string" && typeof value.rt === "string" && typeof value.exp === "number";
271
+ return { had, value: sound ? value : null, present: true };
272
+ }
273
+ async function write(request, s, had) {
274
+ return writeParts(cookieName, await seal(s, await key), attrs(request, SESSION_DAYS * 86400), had);
275
+ }
276
+ const fresh = (s) => s.exp - Date.now() / 1e3 > SKEW_SECONDS;
277
+ const renewing = /* @__PURE__ */ new Map();
278
+ function renew(was) {
279
+ let running = renewing.get(was.rt);
280
+ if (running === void 0) {
281
+ const rt = was.rt;
282
+ running = (async () => {
283
+ const res = await token({ grant_type: "refresh_token", refresh_token: rt });
284
+ if (!res.ok) return res.ended ? "ended" : "unreachable";
285
+ const next = await stored(res.body);
286
+ if (next !== null) return next;
287
+ if (typeof res.body?.access_token !== "string" || typeof res.body?.refresh_token !== "string") return "unreachable";
288
+ const lifetime = typeof res.body.expires_in === "number" && res.body.expires_in > 0 ? res.body.expires_in : 300;
289
+ return { ...was, at: res.body.access_token, rt: res.body.refresh_token, exp: Math.floor(Date.now() / 1e3) + lifetime };
290
+ })();
291
+ renewing.set(rt, running);
292
+ void running.then((result) => {
293
+ if (result === "unreachable") return renewing.delete(rt);
294
+ const timer = setTimeout(() => renewing.delete(rt), 5e3);
295
+ timer.unref?.();
296
+ });
297
+ }
298
+ return running;
299
+ }
300
+ async function resolve2(request) {
301
+ const { had, value, present } = await read(request);
302
+ if (value === null) {
303
+ return { session: null, status: "unauthenticated", cookies: present ? clearParts(cookieName, attrs(request, 0), had) : [] };
304
+ }
305
+ if (fresh(value)) return { session: toSession(value), status: "authenticated", cookies: [] };
306
+ const renewed = await renew(value);
307
+ if (renewed === "ended") {
308
+ return { session: null, status: "unauthenticated", cookies: clearParts(cookieName, attrs(request, 0), had) };
309
+ }
310
+ if (renewed === "unreachable") return { session: null, status: "expired", cookies: [] };
311
+ return { session: toSession(renewed), status: "authenticated", cookies: await write(request, renewed, had) };
312
+ }
313
+ async function getSession(request, responseHeaders) {
314
+ if (responseHeaders === void 0) {
315
+ const { value } = await read(request);
316
+ return value !== null && fresh(value) ? toSession(value) : null;
317
+ }
318
+ const r = await resolve2(request);
319
+ for (const c of r.cookies) responseHeaders.append("set-cookie", c);
320
+ return r.session;
321
+ }
322
+ async function status(request) {
323
+ const { value } = await read(request);
324
+ if (value === null) return "unauthenticated";
325
+ return fresh(value) ? "authenticated" : "expired";
326
+ }
327
+ const orgCache = /* @__PURE__ */ new Map();
328
+ async function organizations(session) {
329
+ const hit = orgCache.get(session.accessToken);
330
+ if (hit && Date.now() - hit.at < ORGANIZATIONS_TTL_MS) return hit.value;
331
+ const url = `${issuer}/v1/oauth/organizations${system ? `?system=${encodeURIComponent(system)}` : ""}`;
332
+ const res = await doFetch(url, { headers: { authorization: `Bearer ${session.accessToken}` } });
333
+ if (!res.ok) throw new Error(`cauthy-auth: reading organizations: ${res.status}`);
334
+ const body = await res.json();
335
+ const value = (body.data ?? []).map(
336
+ (o) => ({
337
+ id: String(o.id),
338
+ system: String(o.system ?? ""),
339
+ name: String(o.name ?? ""),
340
+ kind: String(o.kind ?? ""),
341
+ role: String(o.role ?? ""),
342
+ metadata: o.metadata ?? {},
343
+ ...o.external_id ? { externalId: String(o.external_id) } : {},
344
+ ...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}
345
+ })
346
+ );
347
+ if (orgCache.size > 5e3) orgCache.clear();
348
+ orgCache.set(session.accessToken, { at: Date.now(), value });
349
+ return value;
350
+ }
351
+ async function toPublic(session) {
352
+ let orgs = [];
353
+ try {
354
+ orgs = await organizations(session);
355
+ } catch {
356
+ }
357
+ return { user: session.user, tenantId: session.tenantId, roles: session.roles, organizations: orgs, expiresAt: session.expiresAt };
358
+ }
359
+ const urls = {
360
+ signIn: (returnTo) => `${basePath}/login${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ""}`,
361
+ signOut: () => `${basePath}/logout`,
362
+ account: (returnTo) => `${basePath}/account${returnTo ? `?return_to=${encodeURIComponent(returnTo)}` : ""}`,
363
+ session: `${basePath}/session`
364
+ };
365
+ async function login(request, url) {
366
+ const flow = { state: random(), verifier: random(48), returnTo: localPath(url.searchParams.get("return_to"), afterSignIn) };
367
+ const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(flow.verifier))));
368
+ const q = new URLSearchParams({
369
+ client_id: clientId,
370
+ redirect_uri: `${originOf(request)}${basePath}/callback`,
371
+ response_type: "code",
372
+ state: flow.state,
373
+ code_challenge: challenge,
374
+ code_challenge_method: "S256"
375
+ });
376
+ const prompt = url.searchParams.get("prompt");
377
+ if (prompt === "login" || prompt === "none") q.set("prompt", prompt);
378
+ const locale = url.searchParams.get("locale") ?? url.searchParams.get("ui_locales");
379
+ if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
380
+ return redirect(`${issuer}/v1/oauth/authorize?${q}`, [
381
+ writeCookie(flowCookie, await seal(flow, await key), attrs(request, FLOW_SECONDS))
382
+ ]);
383
+ }
384
+ function failed(request, reason) {
385
+ const h = new Headers({ "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
386
+ h.append("set-cookie", writeCookie(flowCookie, "", attrs(request, 0)));
387
+ return new Response(`Sign-in could not be completed (${reason}). Go back and try again.`, { status: 400, headers: h });
388
+ }
389
+ async function callback(request, url) {
390
+ const had = readCookies(request.headers.get("cookie"));
391
+ const sealed = had.get(flowCookie);
392
+ const flow = sealed ? await unseal(sealed, await key) : null;
393
+ if (flow === null || url.searchParams.get("state") !== flow.state) return failed(request, "state_mismatch");
394
+ const error = url.searchParams.get("error");
395
+ if (error !== null) {
396
+ if (error === "login_required") return redirect(flow.returnTo, [writeCookie(flowCookie, "", attrs(request, 0))]);
397
+ return failed(request, error.slice(0, 60));
398
+ }
399
+ const code = url.searchParams.get("code");
400
+ if (!code) return failed(request, "no_code");
401
+ const res = await token({
402
+ grant_type: "authorization_code",
403
+ code,
404
+ redirect_uri: `${originOf(request)}${basePath}/callback`,
405
+ code_verifier: flow.verifier
406
+ });
407
+ const s = res.ok ? await stored(res.body) : null;
408
+ if (s === null) return failed(request, "exchange_failed");
409
+ return redirect(flow.returnTo, [...await write(request, s, had), writeCookie(flowCookie, "", attrs(request, 0))]);
410
+ }
411
+ async function logout(request) {
412
+ const { had, value } = await read(request);
413
+ if (value !== null) {
414
+ const body = new URLSearchParams({ client_id: clientId, token: value.rt });
415
+ if (clientSecret) body.set("client_secret", clientSecret);
416
+ await doFetch(`${issuer}/v1/oauth/revoke`, {
417
+ method: "POST",
418
+ headers: { "content-type": "application/x-www-form-urlencoded" },
419
+ body
420
+ }).catch(() => void 0);
421
+ orgCache.delete(value.at);
422
+ }
423
+ const q = new URLSearchParams({ client_id: clientId, post_logout_redirect_uri: `${originOf(request)}${afterSignOut}` });
424
+ return redirect(`${issuer}/v1/oauth/logout?${q}`, clearParts(cookieName, attrs(request, 0), had));
425
+ }
426
+ async function handler(request) {
427
+ const url = new URL(request.url);
428
+ if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) return new Response("Not found", { status: 404 });
429
+ const route = url.pathname.slice(basePath.length).replace(/\/+$/, "") || "/";
430
+ const get = request.method === "GET" || request.method === "HEAD";
431
+ if (route === "/login" && get) return login(request, url);
432
+ if (route === "/callback" && get) return callback(request, url);
433
+ if (route === "/logout" && (get || request.method === "POST")) return logout(request);
434
+ if (route === "/session" && get) {
435
+ const r = await resolve2(request);
436
+ const h = new Headers();
437
+ for (const c of r.cookies) h.append("set-cookie", c);
438
+ return json({ session: r.session ? await toPublic(r.session) : null, status: r.status }, 200, h);
439
+ }
440
+ if (route === "/refresh" && get) {
441
+ const r = await resolve2(request);
442
+ const back = localPath(url.searchParams.get("return_to"), afterSignIn);
443
+ if (r.status === "unauthenticated") return redirect(urls.signIn(back), r.cookies);
444
+ if (r.status === "expired") return new Response("Sign-in is unavailable right now. Try again in a moment.", { status: 503 });
445
+ return redirect(back, r.cookies);
446
+ }
447
+ if (route === "/account" && get) {
448
+ const q = new URLSearchParams({ client_id: clientId });
449
+ const back = localPath(url.searchParams.get("return_to"), "");
450
+ if (back !== "") q.set("return_to", `${originOf(request)}${back}`);
451
+ const locale = url.searchParams.get("locale");
452
+ if (locale && /^[a-zA-Z-]{2,10}$/.test(locale)) q.set("ui_locales", locale);
453
+ return redirect(`${issuer}/v1/oauth/account?${q}`);
454
+ }
455
+ return new Response("Not found", { status: 404 });
456
+ }
457
+ async function protect(request) {
458
+ const { value } = await read(request);
459
+ if (value !== null && fresh(value)) return { session: toSession(value) };
460
+ const url = new URL(request.url);
461
+ const state = value === null ? "none" : "expired";
462
+ const page = request.method === "GET" && (request.headers.get("sec-fetch-mode") ?? "navigate") === "navigate" && (request.headers.get("accept") ?? "text/html").includes("text/html");
463
+ if (!page) {
464
+ return { response: json({ error: "unauthenticated", session: state }, 401, new Headers({ "x-cauthy-session": state })) };
465
+ }
466
+ const back = url.pathname + url.search;
467
+ const to = value === null ? urls.signIn(back) : `${basePath}/refresh?return_to=${encodeURIComponent(back)}`;
468
+ return { response: redirect(to) };
469
+ }
470
+ const webhook = (request, o = {}) => verifyWebhook(request, { issuer, url: o.url ?? `${originOf(request)}${new URL(request.url).pathname}`, jwks: o.jwks, now: o.now });
471
+ return {
472
+ handler,
473
+ webhook,
474
+ getSession,
475
+ status,
476
+ resolve: resolve2,
477
+ protect,
478
+ organizations,
479
+ toPublic,
480
+ urls,
481
+ options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
482
+ };
483
+ }
484
+
485
+ // src/theme.ts
486
+ var MAX_RADIUS = 40;
487
+ function declarations(css) {
488
+ const text = css.replace(/\/\*[\s\S]*?\*\//g, "");
489
+ const out2 = [];
490
+ const within = [];
491
+ let chunk = "";
492
+ let quote = "";
493
+ let depth = 0;
494
+ const flush = () => {
495
+ const m = /^\s*(--[\w-]+)\s*:\s*([\s\S]+?)\s*$/.exec(chunk);
496
+ if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
497
+ chunk = "";
498
+ };
499
+ for (const ch of text) {
500
+ if (quote) {
501
+ chunk += ch;
502
+ if (ch === quote) quote = "";
503
+ continue;
504
+ }
505
+ if (ch === '"' || ch === "'") {
506
+ quote = ch;
507
+ chunk += ch;
508
+ } else if (ch === "(") {
509
+ depth++;
510
+ chunk += ch;
511
+ } else if (ch === ")") {
512
+ depth = Math.max(0, depth - 1);
513
+ chunk += ch;
514
+ } else if (ch === "{" && depth === 0) {
515
+ within.push(chunk.trim().replace(/\s+/g, " "));
516
+ chunk = "";
517
+ } else if (ch === "}" && depth === 0) {
518
+ flush();
519
+ within.pop();
520
+ } else if (ch === ";" && depth === 0) {
521
+ flush();
522
+ } else {
523
+ chunk += ch;
524
+ }
525
+ }
526
+ return out2;
527
+ }
528
+ var DARK = /prefers-color-scheme:\s*dark|data-(mode|theme|color-scheme)=["']?dark|\.dark\b/i;
529
+ var LIGHT = /prefers-color-scheme:\s*light|data-(mode|theme|color-scheme)=["']?light|\.light\b/i;
530
+ var NAMED = /data-theme=["']?([\w-]+)/i;
531
+ function applies(d, o) {
532
+ for (const scope of d.within) {
533
+ if (o.mode === "light" && DARK.test(scope)) return false;
534
+ if (o.mode === "dark" && LIGHT.test(scope)) return false;
535
+ const named = NAMED.exec(scope);
536
+ if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
537
+ if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
538
+ }
539
+ return true;
540
+ }
541
+ function resolve(value, vars, seen = 0) {
542
+ if (seen > 16) return null;
543
+ let unresolved = false;
544
+ let out2 = value;
545
+ for (let guard = 0; guard < 32 && /var\(/.test(out2); guard++) {
546
+ out2 = out2.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
547
+ const known = vars.get(name);
548
+ const next = known !== void 0 ? resolve(known, vars, seen + 1) : fallback !== void 0 ? resolve(fallback.trim(), vars, seen + 1) : null;
549
+ if (next === null) unresolved = true;
550
+ return next ?? "";
551
+ });
552
+ if (unresolved) return null;
553
+ }
554
+ return /var\(/.test(out2) ? null : out2.trim();
555
+ }
556
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
557
+ var hex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
558
+ var gamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
559
+ function out(r, g, b, a = 1) {
560
+ if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
561
+ return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1e3) / 1e3})`;
562
+ }
563
+ var alpha = (s) => s === void 0 ? 1 : s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
564
+ var KEYWORDS = { white: "#ffffff", black: "#000000" };
565
+ function toColor(value) {
566
+ const v = value.trim().toLowerCase();
567
+ if (KEYWORDS[v]) return KEYWORDS[v];
568
+ if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v;
569
+ let m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
570
+ if (m) {
571
+ const ch = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s) / 255;
572
+ return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]));
573
+ }
574
+ m = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
575
+ if (m) {
576
+ const h = (parseFloat(m[1]) % 360 + 360) % 360;
577
+ const s = parseFloat(m[2]) / 100;
578
+ const l = parseFloat(m[3]) / 100;
579
+ const f = (n) => {
580
+ const k = (n + h / 30) % 12;
581
+ return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
582
+ };
583
+ return out(f(0), f(8), f(4), alpha(m[4]));
584
+ }
585
+ m = /^oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(v);
586
+ if (m) {
587
+ const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
588
+ const C = m[2].endsWith("%") ? parseFloat(m[2]) / 100 * 0.4 : parseFloat(m[2]);
589
+ const H = parseFloat(m[3]) * Math.PI / 180;
590
+ const a = C * Math.cos(H);
591
+ const b = C * Math.sin(H);
592
+ const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
593
+ const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
594
+ const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);
595
+ return out(
596
+ gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),
597
+ gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),
598
+ gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),
599
+ alpha(m[4])
600
+ );
601
+ }
602
+ return null;
603
+ }
604
+ function toRadius(value) {
605
+ const m = /^([\d.]+)(px|rem|em)?$/.exec(value.trim());
606
+ if (!m) return null;
607
+ const px = parseFloat(m[1]) * (m[2] === "rem" || m[2] === "em" ? 16 : 1);
608
+ return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null;
609
+ }
610
+ function toFont(value) {
611
+ const v = value.trim();
612
+ return /^[A-Za-z0-9 ,'"_-]{1,200}$/.test(v) && (v.match(/"/g)?.length ?? 0) % 2 === 0 && (v.match(/'/g)?.length ?? 0) % 2 === 0 ? v : null;
613
+ }
614
+ var COLORS = [
615
+ ["background", ["--sm-bg-surface", "--sm-surface"]],
616
+ ["surface", ["--sm-bg-surface-raised", "--sm-surface-raised"]],
617
+ ["text", ["--sm-text-primary", "--sm-surface-text"]],
618
+ ["textMuted", ["--sm-text-secondary"]],
619
+ ["border", ["--sm-border-default", "--sm-surface-border"]],
620
+ ["primary", ["--sm-primary", "--sm-bg-interactive"]],
621
+ ["primaryText", ["--sm-primary-text", "--sm-text-inverse"]],
622
+ ["accent", ["--sm-text-brand", "--sm-border-focus", "--sm-accent"]],
623
+ ["error", ["--sm-error-soft-text", "--sm-error"]],
624
+ ["errorBackground", ["--sm-error-soft"]],
625
+ ["success", ["--sm-success-soft-text", "--sm-success"]],
626
+ ["successBackground", ["--sm-success-soft"]]
627
+ ];
628
+ function themeFromStackmyth(css, options = {}) {
629
+ const o = { mode: options.mode ?? "light", theme: options.theme };
630
+ const vars = /* @__PURE__ */ new Map();
631
+ for (const sheet of Array.isArray(css) ? css : [css]) {
632
+ for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value);
633
+ }
634
+ const theme = {};
635
+ const warnings = [];
636
+ const read = (tokens, to, what) => {
637
+ for (const token of tokens) {
638
+ const raw = vars.get(token);
639
+ if (raw === void 0) continue;
640
+ const value = resolve(raw, vars);
641
+ const got = value === null ? null : to(value);
642
+ if (got !== null) return got;
643
+ warnings.push(`${token}: "${raw}" is not ${what} Cauthy can take`);
644
+ }
645
+ return void 0;
646
+ };
647
+ for (const [field, tokens] of COLORS) {
648
+ const c = read(tokens, toColor, "a colour");
649
+ if (c !== void 0) theme[field] = c;
650
+ }
651
+ const radius = read(["--sm-radius-container", "--sm-radius-md"], toRadius, "a length");
652
+ if (radius !== void 0) theme.radius = radius;
653
+ const button = read(["--sm-radius-interactive", "--sm-radius-sm"], toRadius, "a length");
654
+ if (button !== void 0) theme.buttonRadius = button;
655
+ const font = read(["--sm-font-family"], toFont, "a font list");
656
+ if (font !== void 0) theme.fontFamily = font;
657
+ if (options.logoUrl) theme.logoUrl = options.logoUrl;
658
+ if (vars.size === 0) warnings.push("no custom properties were found: is this the stylesheet that declares the --sm-* tokens?");
659
+ else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
660
+ return { theme, warnings };
661
+ }
662
+ function toWire(theme) {
663
+ const names = {
664
+ background: "background",
665
+ surface: "surface",
666
+ text: "text",
667
+ textMuted: "text_muted",
668
+ border: "border",
669
+ primary: "primary",
670
+ primaryText: "primary_text",
671
+ accent: "accent",
672
+ error: "error",
673
+ errorBackground: "error_background",
674
+ success: "success",
675
+ successBackground: "success_background",
676
+ radius: "radius",
677
+ buttonRadius: "button_radius",
678
+ fontFamily: "font_family",
679
+ logoUrl: "logo_url"
680
+ };
681
+ const wire = {};
682
+ for (const [k, v] of Object.entries(theme)) if (v !== void 0 && v !== "") wire[names[k]] = v;
683
+ return wire;
684
+ }
685
+ async function pushTheme(o) {
686
+ const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
687
+ const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
688
+ const clientId = o.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
689
+ const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
690
+ const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
691
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
692
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
693
+ }
694
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {
695
+ method: "PUT",
696
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
697
+ body: JSON.stringify(toWire(o.theme))
698
+ });
699
+ if (!res.ok) {
700
+ const body = await res.json().catch(() => null);
701
+ throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
702
+ }
703
+ }
704
+
705
+ // src/permissions.ts
706
+ function matches(entry, resource, action) {
707
+ const i = entry.lastIndexOf(":");
708
+ if (i < 0) return false;
709
+ const r = entry.slice(0, i);
710
+ const a = entry.slice(i + 1);
711
+ return (r === "*" || r === resource) && (a === "*" || a === action);
712
+ }
713
+ function can(organization, resource, action) {
714
+ if (!organization?.permissions) return false;
715
+ if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
716
+ return organization.permissions.some((p) => matches(p, resource, action));
717
+ }
718
+ async function pushPolicy(o) {
719
+ const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
720
+ const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
721
+ const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
722
+ const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
723
+ const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
724
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
725
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
726
+ }
727
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
728
+ method: "PUT",
729
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
730
+ body: JSON.stringify({ roles: o.roles })
731
+ });
732
+ if (!res.ok) {
733
+ const body = await res.json().catch(() => null);
734
+ throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
735
+ }
736
+ }
737
+ // Annotate the CommonJS export names for ESM import in node:
738
+ 0 && (module.exports = {
739
+ CauthyConfigError,
740
+ CauthyWebhookError,
741
+ can,
742
+ createCauthy,
743
+ pushPolicy,
744
+ pushTheme,
745
+ themeFromStackmyth,
746
+ verifyWebhook
747
+ });
748
+ //# sourceMappingURL=index.js.map