cauthy-auth 0.2.0 → 0.4.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.mjs CHANGED
@@ -54,6 +54,434 @@ async function unseal(sealed, key) {
54
54
  }
55
55
  }
56
56
 
57
+ // src/system.ts
58
+ import { exportJWK, importJWK, importPKCS8, SignJWT } from "jose";
59
+
60
+ // src/theme.ts
61
+ var MAX_RADIUS = 40;
62
+ function declarations(css) {
63
+ const text = css.replace(/\/\*[\s\S]*?\*\//g, "");
64
+ const out2 = [];
65
+ const within = [];
66
+ let chunk = "";
67
+ let quote = "";
68
+ let depth = 0;
69
+ const flush = () => {
70
+ const m = /^\s*(--[\w-]+)\s*:\s*([\s\S]+?)\s*$/.exec(chunk);
71
+ if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
72
+ chunk = "";
73
+ };
74
+ for (const ch of text) {
75
+ if (quote) {
76
+ chunk += ch;
77
+ if (ch === quote) quote = "";
78
+ continue;
79
+ }
80
+ if (ch === '"' || ch === "'") {
81
+ quote = ch;
82
+ chunk += ch;
83
+ } else if (ch === "(") {
84
+ depth++;
85
+ chunk += ch;
86
+ } else if (ch === ")") {
87
+ depth = Math.max(0, depth - 1);
88
+ chunk += ch;
89
+ } else if (ch === "{" && depth === 0) {
90
+ within.push(chunk.trim().replace(/\s+/g, " "));
91
+ chunk = "";
92
+ } else if (ch === "}" && depth === 0) {
93
+ flush();
94
+ within.pop();
95
+ } else if (ch === ";" && depth === 0) {
96
+ flush();
97
+ } else {
98
+ chunk += ch;
99
+ }
100
+ }
101
+ return out2;
102
+ }
103
+ var DARK = /prefers-color-scheme:\s*dark|data-(mode|theme|color-scheme)=["']?dark|\.dark\b/i;
104
+ var LIGHT = /prefers-color-scheme:\s*light|data-(mode|theme|color-scheme)=["']?light|\.light\b/i;
105
+ var NAMED = /data-theme=["']?([\w-]+)/i;
106
+ function applies(d, o) {
107
+ for (const scope of d.within) {
108
+ if (o.mode === "light" && DARK.test(scope)) return false;
109
+ if (o.mode === "dark" && LIGHT.test(scope)) return false;
110
+ const named = NAMED.exec(scope);
111
+ if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
112
+ if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
113
+ }
114
+ return true;
115
+ }
116
+ function resolve(value, vars, seen = 0) {
117
+ if (seen > 16) return null;
118
+ let unresolved = false;
119
+ let out2 = value;
120
+ for (let guard = 0; guard < 32 && /var\(/.test(out2); guard++) {
121
+ out2 = out2.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
122
+ const known = vars.get(name);
123
+ const next = known !== void 0 ? resolve(known, vars, seen + 1) : fallback !== void 0 ? resolve(fallback.trim(), vars, seen + 1) : null;
124
+ if (next === null) unresolved = true;
125
+ return next ?? "";
126
+ });
127
+ if (unresolved) return null;
128
+ }
129
+ return /var\(/.test(out2) ? null : out2.trim();
130
+ }
131
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
132
+ var hex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
133
+ var gamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
134
+ function out(r, g, b, a = 1) {
135
+ if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
136
+ return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1e3) / 1e3})`;
137
+ }
138
+ var alpha = (s) => s === void 0 ? 1 : s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
139
+ var KEYWORDS = { white: "#ffffff", black: "#000000" };
140
+ function toColor(value) {
141
+ const v = value.trim().toLowerCase();
142
+ if (KEYWORDS[v]) return KEYWORDS[v];
143
+ if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v;
144
+ let m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
145
+ if (m) {
146
+ const ch = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s) / 255;
147
+ return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]));
148
+ }
149
+ m = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
150
+ if (m) {
151
+ const h = (parseFloat(m[1]) % 360 + 360) % 360;
152
+ const s = parseFloat(m[2]) / 100;
153
+ const l = parseFloat(m[3]) / 100;
154
+ const f = (n) => {
155
+ const k = (n + h / 30) % 12;
156
+ return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
157
+ };
158
+ return out(f(0), f(8), f(4), alpha(m[4]));
159
+ }
160
+ m = /^oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(v);
161
+ if (m) {
162
+ const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
163
+ const C = m[2].endsWith("%") ? parseFloat(m[2]) / 100 * 0.4 : parseFloat(m[2]);
164
+ const H = parseFloat(m[3]) * Math.PI / 180;
165
+ const a = C * Math.cos(H);
166
+ const b = C * Math.sin(H);
167
+ const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
168
+ const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
169
+ const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);
170
+ return out(
171
+ gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),
172
+ gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),
173
+ gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),
174
+ alpha(m[4])
175
+ );
176
+ }
177
+ return null;
178
+ }
179
+ function toRadius(value) {
180
+ const m = /^([\d.]+)(px|rem|em)?$/.exec(value.trim());
181
+ if (!m) return null;
182
+ const px = parseFloat(m[1]) * (m[2] === "rem" || m[2] === "em" ? 16 : 1);
183
+ return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null;
184
+ }
185
+ function toFont(value) {
186
+ const v = value.trim();
187
+ 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;
188
+ }
189
+ var COLORS = [
190
+ ["background", ["--sm-bg-surface", "--sm-surface"]],
191
+ ["surface", ["--sm-bg-surface-raised", "--sm-surface-raised"]],
192
+ ["text", ["--sm-text-primary", "--sm-surface-text"]],
193
+ ["textMuted", ["--sm-text-secondary"]],
194
+ ["border", ["--sm-border-default", "--sm-surface-border"]],
195
+ ["primary", ["--sm-primary", "--sm-bg-interactive"]],
196
+ ["primaryText", ["--sm-primary-text", "--sm-text-inverse"]],
197
+ ["accent", ["--sm-text-brand", "--sm-border-focus", "--sm-accent"]],
198
+ ["error", ["--sm-error-soft-text", "--sm-error"]],
199
+ ["errorBackground", ["--sm-error-soft"]],
200
+ ["success", ["--sm-success-soft-text", "--sm-success"]],
201
+ ["successBackground", ["--sm-success-soft"]]
202
+ ];
203
+ function themeFromStackmyth(css, options = {}) {
204
+ const o = { mode: options.mode ?? "light", theme: options.theme };
205
+ const vars = /* @__PURE__ */ new Map();
206
+ for (const sheet of Array.isArray(css) ? css : [css]) {
207
+ for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value);
208
+ }
209
+ const theme = {};
210
+ const warnings = [];
211
+ const read = (tokens, to, what) => {
212
+ for (const token of tokens) {
213
+ const raw = vars.get(token);
214
+ if (raw === void 0) continue;
215
+ const value = resolve(raw, vars);
216
+ const got = value === null ? null : to(value);
217
+ if (got !== null) return got;
218
+ warnings.push(`${token}: "${raw}" is not ${what} Cauthy can take`);
219
+ }
220
+ return void 0;
221
+ };
222
+ for (const [field, tokens] of COLORS) {
223
+ const c = read(tokens, toColor, "a colour");
224
+ if (c !== void 0) theme[field] = c;
225
+ }
226
+ const radius = read(["--sm-radius-container", "--sm-radius-md"], toRadius, "a length");
227
+ if (radius !== void 0) theme.radius = radius;
228
+ const button = read(["--sm-radius-interactive", "--sm-radius-sm"], toRadius, "a length");
229
+ if (button !== void 0) theme.buttonRadius = button;
230
+ const font = read(["--sm-font-family"], toFont, "a font list");
231
+ if (font !== void 0) theme.fontFamily = font;
232
+ if (options.logoUrl) theme.logoUrl = options.logoUrl;
233
+ if (vars.size === 0) warnings.push("no custom properties were found: is this the stylesheet that declares the --sm-* tokens?");
234
+ else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
235
+ return { theme, warnings };
236
+ }
237
+ function toWire(theme) {
238
+ const names = {
239
+ background: "background",
240
+ surface: "surface",
241
+ text: "text",
242
+ textMuted: "text_muted",
243
+ border: "border",
244
+ primary: "primary",
245
+ primaryText: "primary_text",
246
+ accent: "accent",
247
+ error: "error",
248
+ errorBackground: "error_background",
249
+ success: "success",
250
+ successBackground: "success_background",
251
+ radius: "radius",
252
+ buttonRadius: "button_radius",
253
+ fontFamily: "font_family",
254
+ logoUrl: "logo_url",
255
+ fonts: "fonts"
256
+ };
257
+ const wire = {};
258
+ for (const [k, v] of Object.entries(theme)) {
259
+ if (v === void 0 || v === "" || Array.isArray(v) && v.length === 0) continue;
260
+ wire[names[k]] = v;
261
+ }
262
+ return wire;
263
+ }
264
+ function fromWire(wire) {
265
+ const names = {
266
+ background: "background",
267
+ surface: "surface",
268
+ text: "text",
269
+ text_muted: "textMuted",
270
+ border: "border",
271
+ primary: "primary",
272
+ primary_text: "primaryText",
273
+ accent: "accent",
274
+ error: "error",
275
+ error_background: "errorBackground",
276
+ success: "success",
277
+ success_background: "successBackground",
278
+ radius: "radius",
279
+ button_radius: "buttonRadius",
280
+ font_family: "fontFamily",
281
+ logo_url: "logoUrl",
282
+ fonts: "fonts"
283
+ };
284
+ const theme = {};
285
+ for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
286
+ return theme;
287
+ }
288
+ async function pushTheme(o) {
289
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
290
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
291
+ const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
292
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
293
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
294
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
295
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
296
+ }
297
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {
298
+ method: "PUT",
299
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
300
+ body: JSON.stringify(toWire(o.theme))
301
+ });
302
+ if (!res.ok) {
303
+ const body = await res.json().catch(() => null);
304
+ throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
305
+ }
306
+ }
307
+
308
+ // src/types.ts
309
+ var CauthyConfigError = class extends Error {
310
+ constructor(message) {
311
+ super(`cauthy-auth: ${message}`);
312
+ this.name = "CauthyConfigError";
313
+ }
314
+ };
315
+
316
+ // src/system.ts
317
+ var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
318
+ var SKEW_MS = 3e4;
319
+ var CauthyApiError = class extends Error {
320
+ constructor(status, code, message) {
321
+ super(`cauthy-auth: ${message}`);
322
+ this.status = status;
323
+ this.code = code;
324
+ this.name = "CauthyApiError";
325
+ }
326
+ status;
327
+ code;
328
+ };
329
+ function env(name) {
330
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
331
+ }
332
+ function toOrganization(o) {
333
+ return {
334
+ id: o.id,
335
+ system: o.system,
336
+ name: o.name,
337
+ kind: o.kind,
338
+ externalId: o.external_id,
339
+ status: o.status,
340
+ metadata: o.metadata ?? {},
341
+ members: o.members,
342
+ createdAt: o.created_at,
343
+ updatedAt: o.updated_at
344
+ };
345
+ }
346
+ function toMember(m) {
347
+ return { userId: m.user_id, email: m.email, name: m.name, status: m.status, role: m.role, ...m.invited === void 0 ? {} : { invited: m.invited } };
348
+ }
349
+ function asJwk(k) {
350
+ if (typeof k.privateKey !== "string") return k.privateKey;
351
+ return k.privateKey.includes("BEGIN") ? void 0 : JSON.parse(k.privateKey);
352
+ }
353
+ async function loadKey(k) {
354
+ const jwk = asJwk(k);
355
+ return jwk ? importJWK(jwk, "RS256") : importPKCS8(k.privateKey, "RS256", { extractable: true });
356
+ }
357
+ function createSystem(given = {}) {
358
+ const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
359
+ const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
360
+ const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
361
+ const envKey = env("CAUTHY_SYSTEM_KEY");
362
+ const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
363
+ const doFetch = (...args) => (given.fetch ?? fetch)(...args);
364
+ const tokenEndpoint = `${issuer}/v1/oauth/token`;
365
+ function configured() {
366
+ if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
367
+ if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
368
+ if (key ? key.keyId === "" : clientSecret === "") {
369
+ throw new CauthyConfigError(key ? "the key needs its id (CAUTHY_SYSTEM_KEY_ID)" : "to act on its own the application needs its secret (CAUTHY_CLIENT_SECRET) or a key (CAUTHY_SYSTEM_KEY)");
370
+ }
371
+ }
372
+ let imported;
373
+ const signingKey = () => imported ??= loadKey(key);
374
+ async function fail(res, what) {
375
+ const body = await res.json().catch(() => null);
376
+ const first = body?.errors?.[0];
377
+ throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused ${what} (${res.status}): ${first?.message ?? res.statusText}`);
378
+ }
379
+ let held;
380
+ let asking;
381
+ async function ask() {
382
+ configured();
383
+ const form = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId });
384
+ if (key) {
385
+ const assertion = await new SignJWT({}).setProtectedHeader({ alg: "RS256", kid: key.keyId }).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(await signingKey());
386
+ form.set("client_assertion_type", ASSERTION_TYPE);
387
+ form.set("client_assertion", assertion);
388
+ } else {
389
+ form.set("client_secret", clientSecret);
390
+ }
391
+ const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
392
+ if (!res.ok) await fail(res, "the application's own token");
393
+ const t = await res.json();
394
+ held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
395
+ return t.access_token;
396
+ }
397
+ function token() {
398
+ if (held && Date.now() < held.until) return Promise.resolve(held.value);
399
+ return asking ??= ask().finally(() => {
400
+ asking = void 0;
401
+ });
402
+ }
403
+ async function call(method, path, body) {
404
+ const send = async () => doFetch(`${issuer}${path}`, {
405
+ method,
406
+ headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
407
+ body: body === void 0 ? void 0 : JSON.stringify(body)
408
+ });
409
+ let res = await send();
410
+ if (res.status === 401 && held) {
411
+ held = void 0;
412
+ res = await send();
413
+ }
414
+ if (!res.ok) await fail(res, `${method} ${path}`);
415
+ return res.status === 204 ? void 0 : res.json();
416
+ }
417
+ const data = async (method, path, body) => (await call(method, path, body))?.data;
418
+ const id = encodeURIComponent;
419
+ return {
420
+ token,
421
+ request: data,
422
+ organizations: {
423
+ async list(query = {}) {
424
+ const q = new URLSearchParams();
425
+ for (const [k, v] of Object.entries(query)) if (v !== void 0) q.set(k, String(v));
426
+ const out2 = await call("GET", `/v1/organizations${q.size ? `?${q}` : ""}`);
427
+ return { items: out2.data.map(toOrganization), total: out2.total_count };
428
+ },
429
+ async create(input) {
430
+ return toOrganization(await data(
431
+ "POST",
432
+ "/v1/organizations",
433
+ { name: input.name, kind: input.kind, external_id: input.externalId, metadata: input.metadata }
434
+ ));
435
+ },
436
+ async get(orgId) {
437
+ return toOrganization(await data("GET", `/v1/organizations/${id(orgId)}`));
438
+ },
439
+ async update(orgId, patch) {
440
+ return toOrganization(await data("PATCH", `/v1/organizations/${id(orgId)}`, patch));
441
+ },
442
+ async remove(orgId) {
443
+ await call("DELETE", `/v1/organizations/${id(orgId)}`);
444
+ }
445
+ },
446
+ members: {
447
+ async list(orgId) {
448
+ return (await data("GET", `/v1/organizations/${id(orgId)}/members`)).map(toMember);
449
+ },
450
+ async add(orgId, m) {
451
+ return toMember(await data(
452
+ "POST",
453
+ `/v1/organizations/${id(orgId)}/members`,
454
+ { role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
455
+ ));
456
+ },
457
+ async setRole(orgId, userId, role) {
458
+ return toMember(await data("PUT", `/v1/organizations/${id(orgId)}/members/${id(userId)}`, { role }));
459
+ },
460
+ async remove(orgId, userId) {
461
+ await call("DELETE", `/v1/organizations/${id(orgId)}/members/${id(userId)}`);
462
+ }
463
+ },
464
+ async invite(person) {
465
+ const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
466
+ return { id: u.id, email: u.email };
467
+ },
468
+ theme: {
469
+ async get(of = clientId) {
470
+ return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
471
+ },
472
+ async set(theme, of = clientId) {
473
+ if (theme === null) await call("DELETE", `/v1/oauth/clients/${id(of)}/theme`);
474
+ else await call("PUT", `/v1/oauth/clients/${id(of)}/theme`, toWire(theme));
475
+ }
476
+ },
477
+ async publicJwks() {
478
+ if (!key) return { keys: [] };
479
+ const { kty, n, e } = asJwk(key) ?? await exportJWK(await signingKey());
480
+ return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
481
+ }
482
+ };
483
+ }
484
+
57
485
  // src/webhook.ts
58
486
  import { createRemoteJWKSet, jwtVerify } from "jose";
59
487
  var CauthyWebhookError = class extends Error {
@@ -75,8 +503,8 @@ function keysOf(issuer) {
75
503
  }
76
504
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
77
505
  async function verifyWebhook(request, options = {}) {
78
- const env2 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
79
- const issuer = (options.issuer ?? env2 ?? "").replace(/\/+$/, "");
506
+ const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
507
+ const issuer = (options.issuer ?? env3 ?? "").replace(/\/+$/, "");
80
508
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
81
509
  const signature = request.headers.get("x-cauthy-signature-jwt");
82
510
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -125,20 +553,12 @@ async function verifyWebhook(request, options = {}) {
125
553
  };
126
554
  }
127
555
 
128
- // src/types.ts
129
- var CauthyConfigError = class extends Error {
130
- constructor(message) {
131
- super(`cauthy-auth: ${message}`);
132
- this.name = "CauthyConfigError";
133
- }
134
- };
135
-
136
556
  // src/cauthy.ts
137
557
  var SESSION_DAYS = 30;
138
558
  var FLOW_SECONDS = 600;
139
559
  var SKEW_SECONDS = 30;
140
560
  var ORGANIZATIONS_TTL_MS = 6e4;
141
- function env(name) {
561
+ function env2(name) {
142
562
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
143
563
  }
144
564
  function b64url(bytes) {
@@ -165,11 +585,11 @@ function redirect(location, cookies = []) {
165
585
  return new Response(null, { status: 302, headers: h });
166
586
  }
167
587
  function createCauthy(given = {}) {
168
- const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
169
- const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
170
- const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET");
171
- const secret = given.secret ?? env("CAUTHY_SECRET") ?? "";
172
- const system = given.system ?? env("CAUTHY_SYSTEM") ?? "";
588
+ const issuer = (given.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
589
+ const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
590
+ const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
591
+ const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
592
+ const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
173
593
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
174
594
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
175
595
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -438,6 +858,7 @@ function createCauthy(given = {}) {
438
858
  return {
439
859
  handler,
440
860
  webhook,
861
+ system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
441
862
  getSession,
442
863
  status,
443
864
  resolve: resolve2,
@@ -449,226 +870,6 @@ function createCauthy(given = {}) {
449
870
  };
450
871
  }
451
872
 
452
- // src/theme.ts
453
- var MAX_RADIUS = 40;
454
- function declarations(css) {
455
- const text = css.replace(/\/\*[\s\S]*?\*\//g, "");
456
- const out2 = [];
457
- const within = [];
458
- let chunk = "";
459
- let quote = "";
460
- let depth = 0;
461
- const flush = () => {
462
- const m = /^\s*(--[\w-]+)\s*:\s*([\s\S]+?)\s*$/.exec(chunk);
463
- if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
464
- chunk = "";
465
- };
466
- for (const ch of text) {
467
- if (quote) {
468
- chunk += ch;
469
- if (ch === quote) quote = "";
470
- continue;
471
- }
472
- if (ch === '"' || ch === "'") {
473
- quote = ch;
474
- chunk += ch;
475
- } else if (ch === "(") {
476
- depth++;
477
- chunk += ch;
478
- } else if (ch === ")") {
479
- depth = Math.max(0, depth - 1);
480
- chunk += ch;
481
- } else if (ch === "{" && depth === 0) {
482
- within.push(chunk.trim().replace(/\s+/g, " "));
483
- chunk = "";
484
- } else if (ch === "}" && depth === 0) {
485
- flush();
486
- within.pop();
487
- } else if (ch === ";" && depth === 0) {
488
- flush();
489
- } else {
490
- chunk += ch;
491
- }
492
- }
493
- return out2;
494
- }
495
- var DARK = /prefers-color-scheme:\s*dark|data-(mode|theme|color-scheme)=["']?dark|\.dark\b/i;
496
- var LIGHT = /prefers-color-scheme:\s*light|data-(mode|theme|color-scheme)=["']?light|\.light\b/i;
497
- var NAMED = /data-theme=["']?([\w-]+)/i;
498
- function applies(d, o) {
499
- for (const scope of d.within) {
500
- if (o.mode === "light" && DARK.test(scope)) return false;
501
- if (o.mode === "dark" && LIGHT.test(scope)) return false;
502
- const named = NAMED.exec(scope);
503
- if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
504
- if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
505
- }
506
- return true;
507
- }
508
- function resolve(value, vars, seen = 0) {
509
- if (seen > 16) return null;
510
- let unresolved = false;
511
- let out2 = value;
512
- for (let guard = 0; guard < 32 && /var\(/.test(out2); guard++) {
513
- out2 = out2.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
514
- const known = vars.get(name);
515
- const next = known !== void 0 ? resolve(known, vars, seen + 1) : fallback !== void 0 ? resolve(fallback.trim(), vars, seen + 1) : null;
516
- if (next === null) unresolved = true;
517
- return next ?? "";
518
- });
519
- if (unresolved) return null;
520
- }
521
- return /var\(/.test(out2) ? null : out2.trim();
522
- }
523
- var clamp01 = (n) => Math.min(1, Math.max(0, n));
524
- var hex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
525
- var gamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
526
- function out(r, g, b, a = 1) {
527
- if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
528
- return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1e3) / 1e3})`;
529
- }
530
- var alpha = (s) => s === void 0 ? 1 : s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
531
- var KEYWORDS = { white: "#ffffff", black: "#000000" };
532
- function toColor(value) {
533
- const v = value.trim().toLowerCase();
534
- if (KEYWORDS[v]) return KEYWORDS[v];
535
- if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v;
536
- let m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
537
- if (m) {
538
- const ch = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s) / 255;
539
- return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]));
540
- }
541
- m = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
542
- if (m) {
543
- const h = (parseFloat(m[1]) % 360 + 360) % 360;
544
- const s = parseFloat(m[2]) / 100;
545
- const l = parseFloat(m[3]) / 100;
546
- const f = (n) => {
547
- const k = (n + h / 30) % 12;
548
- return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
549
- };
550
- return out(f(0), f(8), f(4), alpha(m[4]));
551
- }
552
- m = /^oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(v);
553
- if (m) {
554
- const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
555
- const C = m[2].endsWith("%") ? parseFloat(m[2]) / 100 * 0.4 : parseFloat(m[2]);
556
- const H = parseFloat(m[3]) * Math.PI / 180;
557
- const a = C * Math.cos(H);
558
- const b = C * Math.sin(H);
559
- const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
560
- const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
561
- const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);
562
- return out(
563
- gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),
564
- gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),
565
- gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),
566
- alpha(m[4])
567
- );
568
- }
569
- return null;
570
- }
571
- function toRadius(value) {
572
- const m = /^([\d.]+)(px|rem|em)?$/.exec(value.trim());
573
- if (!m) return null;
574
- const px = parseFloat(m[1]) * (m[2] === "rem" || m[2] === "em" ? 16 : 1);
575
- return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null;
576
- }
577
- function toFont(value) {
578
- const v = value.trim();
579
- 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;
580
- }
581
- var COLORS = [
582
- ["background", ["--sm-bg-surface", "--sm-surface"]],
583
- ["surface", ["--sm-bg-surface-raised", "--sm-surface-raised"]],
584
- ["text", ["--sm-text-primary", "--sm-surface-text"]],
585
- ["textMuted", ["--sm-text-secondary"]],
586
- ["border", ["--sm-border-default", "--sm-surface-border"]],
587
- ["primary", ["--sm-primary", "--sm-bg-interactive"]],
588
- ["primaryText", ["--sm-primary-text", "--sm-text-inverse"]],
589
- ["accent", ["--sm-text-brand", "--sm-border-focus", "--sm-accent"]],
590
- ["error", ["--sm-error-soft-text", "--sm-error"]],
591
- ["errorBackground", ["--sm-error-soft"]],
592
- ["success", ["--sm-success-soft-text", "--sm-success"]],
593
- ["successBackground", ["--sm-success-soft"]]
594
- ];
595
- function themeFromStackmyth(css, options = {}) {
596
- const o = { mode: options.mode ?? "light", theme: options.theme };
597
- const vars = /* @__PURE__ */ new Map();
598
- for (const sheet of Array.isArray(css) ? css : [css]) {
599
- for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value);
600
- }
601
- const theme = {};
602
- const warnings = [];
603
- const read = (tokens, to, what) => {
604
- for (const token of tokens) {
605
- const raw = vars.get(token);
606
- if (raw === void 0) continue;
607
- const value = resolve(raw, vars);
608
- const got = value === null ? null : to(value);
609
- if (got !== null) return got;
610
- warnings.push(`${token}: "${raw}" is not ${what} Cauthy can take`);
611
- }
612
- return void 0;
613
- };
614
- for (const [field, tokens] of COLORS) {
615
- const c = read(tokens, toColor, "a colour");
616
- if (c !== void 0) theme[field] = c;
617
- }
618
- const radius = read(["--sm-radius-container", "--sm-radius-md"], toRadius, "a length");
619
- if (radius !== void 0) theme.radius = radius;
620
- const button = read(["--sm-radius-interactive", "--sm-radius-sm"], toRadius, "a length");
621
- if (button !== void 0) theme.buttonRadius = button;
622
- const font = read(["--sm-font-family"], toFont, "a font list");
623
- if (font !== void 0) theme.fontFamily = font;
624
- if (options.logoUrl) theme.logoUrl = options.logoUrl;
625
- if (vars.size === 0) warnings.push("no custom properties were found: is this the stylesheet that declares the --sm-* tokens?");
626
- else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
627
- return { theme, warnings };
628
- }
629
- function toWire(theme) {
630
- const names = {
631
- background: "background",
632
- surface: "surface",
633
- text: "text",
634
- textMuted: "text_muted",
635
- border: "border",
636
- primary: "primary",
637
- primaryText: "primary_text",
638
- accent: "accent",
639
- error: "error",
640
- errorBackground: "error_background",
641
- success: "success",
642
- successBackground: "success_background",
643
- radius: "radius",
644
- buttonRadius: "button_radius",
645
- fontFamily: "font_family",
646
- logoUrl: "logo_url"
647
- };
648
- const wire = {};
649
- for (const [k, v] of Object.entries(theme)) if (v !== void 0 && v !== "") wire[names[k]] = v;
650
- return wire;
651
- }
652
- async function pushTheme(o) {
653
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
654
- const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
655
- const clientId = o.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
656
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
657
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
658
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
659
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
660
- }
661
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {
662
- method: "PUT",
663
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
664
- body: JSON.stringify(toWire(o.theme))
665
- });
666
- if (!res.ok) {
667
- const body = await res.json().catch(() => null);
668
- throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
669
- }
670
- }
671
-
672
873
  // src/permissions.ts
673
874
  function matches(entry, resource, action) {
674
875
  const i = entry.lastIndexOf(":");
@@ -683,11 +884,11 @@ function can(organization, resource, action) {
683
884
  return organization.permissions.some((p) => matches(p, resource, action));
684
885
  }
685
886
  async function pushPolicy(o) {
686
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
687
- const issuer = (o.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
688
- const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
689
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
690
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
887
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
888
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
889
+ const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
890
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
891
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
691
892
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
692
893
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
693
894
  }
@@ -701,11 +902,58 @@ async function pushPolicy(o) {
701
902
  throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
702
903
  }
703
904
  }
905
+
906
+ // src/connect.ts
907
+ function sessionSecret() {
908
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
909
+ let s = "";
910
+ for (const b of bytes) s += String.fromCharCode(b);
911
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
912
+ }
913
+ async function connectSystem(o) {
914
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
915
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
916
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
917
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
918
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
919
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
920
+ }
921
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/connect`, {
922
+ method: "POST",
923
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
924
+ body: JSON.stringify({
925
+ system: o.system,
926
+ app_url: o.appUrl,
927
+ name: o.name,
928
+ callback_path: o.callbackPath,
929
+ events_url: o.eventsUrl,
930
+ events: o.events,
931
+ jwks_uri: o.jwksUri
932
+ })
933
+ });
934
+ const body = await res.json().catch(() => null);
935
+ if (!res.ok || !body?.data) {
936
+ const why = (body?.errors ?? []).map((e) => e.message).filter(Boolean).join("; ") || res.statusText;
937
+ throw new Error(`cauthy-auth: Cauthy did not connect the system (${res.status}): ${why}`);
938
+ }
939
+ const d = body.data;
940
+ return {
941
+ system: d.system,
942
+ clientId: d.client.client_id,
943
+ clientSecret: d.client_secret,
944
+ redirectUris: d.client.redirect_uris,
945
+ webhook: d.webhook,
946
+ env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
947
+ };
948
+ }
704
949
  export {
950
+ CauthyApiError,
705
951
  CauthyConfigError,
706
952
  CauthyWebhookError,
707
953
  can,
954
+ connectSystem,
708
955
  createCauthy,
956
+ createSystem,
709
957
  pushPolicy,
710
958
  pushTheme,
711
959
  themeFromStackmyth,