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.js CHANGED
@@ -20,10 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ CauthyApiError: () => CauthyApiError,
23
24
  CauthyConfigError: () => CauthyConfigError,
24
25
  CauthyWebhookError: () => CauthyWebhookError,
25
26
  can: () => can,
27
+ connectSystem: () => connectSystem,
26
28
  createCauthy: () => createCauthy,
29
+ createSystem: () => createSystem,
27
30
  pushPolicy: () => pushPolicy,
28
31
  pushTheme: () => pushTheme,
29
32
  themeFromStackmyth: () => themeFromStackmyth,
@@ -87,8 +90,436 @@ async function unseal(sealed, key) {
87
90
  }
88
91
  }
89
92
 
90
- // src/webhook.ts
93
+ // src/system.ts
91
94
  var import_jose2 = require("jose");
95
+
96
+ // src/theme.ts
97
+ var MAX_RADIUS = 40;
98
+ function declarations(css) {
99
+ const text = css.replace(/\/\*[\s\S]*?\*\//g, "");
100
+ const out2 = [];
101
+ const within = [];
102
+ let chunk = "";
103
+ let quote = "";
104
+ let depth = 0;
105
+ const flush = () => {
106
+ const m = /^\s*(--[\w-]+)\s*:\s*([\s\S]+?)\s*$/.exec(chunk);
107
+ if (m) out2.push({ name: m[1], value: m[2].replace(/\s+/g, " ").replace(/\s*!important$/, ""), within: [...within] });
108
+ chunk = "";
109
+ };
110
+ for (const ch of text) {
111
+ if (quote) {
112
+ chunk += ch;
113
+ if (ch === quote) quote = "";
114
+ continue;
115
+ }
116
+ if (ch === '"' || ch === "'") {
117
+ quote = ch;
118
+ chunk += ch;
119
+ } else if (ch === "(") {
120
+ depth++;
121
+ chunk += ch;
122
+ } else if (ch === ")") {
123
+ depth = Math.max(0, depth - 1);
124
+ chunk += ch;
125
+ } else if (ch === "{" && depth === 0) {
126
+ within.push(chunk.trim().replace(/\s+/g, " "));
127
+ chunk = "";
128
+ } else if (ch === "}" && depth === 0) {
129
+ flush();
130
+ within.pop();
131
+ } else if (ch === ";" && depth === 0) {
132
+ flush();
133
+ } else {
134
+ chunk += ch;
135
+ }
136
+ }
137
+ return out2;
138
+ }
139
+ var DARK = /prefers-color-scheme:\s*dark|data-(mode|theme|color-scheme)=["']?dark|\.dark\b/i;
140
+ var LIGHT = /prefers-color-scheme:\s*light|data-(mode|theme|color-scheme)=["']?light|\.light\b/i;
141
+ var NAMED = /data-theme=["']?([\w-]+)/i;
142
+ function applies(d, o) {
143
+ for (const scope of d.within) {
144
+ if (o.mode === "light" && DARK.test(scope)) return false;
145
+ if (o.mode === "dark" && LIGHT.test(scope)) return false;
146
+ const named = NAMED.exec(scope);
147
+ if (named && !/^(dark|light)$/i.test(named[1]) && named[1] !== o.theme) return false;
148
+ if (!scope.startsWith("@") && !/:root|^html\b|^body\b|\[data-|^\*$|:host/.test(scope)) return false;
149
+ }
150
+ return true;
151
+ }
152
+ function resolve(value, vars, seen = 0) {
153
+ if (seen > 16) return null;
154
+ let unresolved = false;
155
+ let out2 = value;
156
+ for (let guard = 0; guard < 32 && /var\(/.test(out2); guard++) {
157
+ out2 = out2.replace(/var\(\s*(--[\w-]+)\s*(?:,\s*([^()]*(?:\([^()]*\)[^()]*)*))?\)/, (_m, name, fallback) => {
158
+ const known = vars.get(name);
159
+ const next = known !== void 0 ? resolve(known, vars, seen + 1) : fallback !== void 0 ? resolve(fallback.trim(), vars, seen + 1) : null;
160
+ if (next === null) unresolved = true;
161
+ return next ?? "";
162
+ });
163
+ if (unresolved) return null;
164
+ }
165
+ return /var\(/.test(out2) ? null : out2.trim();
166
+ }
167
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
168
+ var hex2 = (n) => Math.round(clamp01(n) * 255).toString(16).padStart(2, "0");
169
+ var gamma = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
170
+ function out(r, g, b, a = 1) {
171
+ if (a >= 1) return `#${hex2(r)}${hex2(g)}${hex2(b)}`;
172
+ return `rgba(${Math.round(clamp01(r) * 255)}, ${Math.round(clamp01(g) * 255)}, ${Math.round(clamp01(b) * 255)}, ${Math.round(clamp01(a) * 1e3) / 1e3})`;
173
+ }
174
+ var alpha = (s) => s === void 0 ? 1 : s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s);
175
+ var KEYWORDS = { white: "#ffffff", black: "#000000" };
176
+ function toColor(value) {
177
+ const v = value.trim().toLowerCase();
178
+ if (KEYWORDS[v]) return KEYWORDS[v];
179
+ if (/^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(v)) return v;
180
+ let m = /^rgba?\(\s*([\d.]+%?)[\s,]+([\d.]+%?)[\s,]+([\d.]+%?)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
181
+ if (m) {
182
+ const ch = (s) => s.endsWith("%") ? parseFloat(s) / 100 : parseFloat(s) / 255;
183
+ return out(ch(m[1]), ch(m[2]), ch(m[3]), alpha(m[4]));
184
+ }
185
+ m = /^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(v);
186
+ if (m) {
187
+ const h = (parseFloat(m[1]) % 360 + 360) % 360;
188
+ const s = parseFloat(m[2]) / 100;
189
+ const l = parseFloat(m[3]) / 100;
190
+ const f = (n) => {
191
+ const k = (n + h / 30) % 12;
192
+ return l - s * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1));
193
+ };
194
+ return out(f(0), f(8), f(4), alpha(m[4]));
195
+ }
196
+ m = /^oklch\(\s*([\d.]+%?)\s+([\d.]+%?)\s+([\d.]+)(?:deg)?(?:\s*\/\s*([\d.]+%?))?\s*\)$/.exec(v);
197
+ if (m) {
198
+ const L = m[1].endsWith("%") ? parseFloat(m[1]) / 100 : parseFloat(m[1]);
199
+ const C = m[2].endsWith("%") ? parseFloat(m[2]) / 100 * 0.4 : parseFloat(m[2]);
200
+ const H = parseFloat(m[3]) * Math.PI / 180;
201
+ const a = C * Math.cos(H);
202
+ const b = C * Math.sin(H);
203
+ const l = Math.pow(L + 0.3963377774 * a + 0.2158037573 * b, 3);
204
+ const mm = Math.pow(L - 0.1055613458 * a - 0.0638541728 * b, 3);
205
+ const s = Math.pow(L - 0.0894841775 * a - 1.291485548 * b, 3);
206
+ return out(
207
+ gamma(4.0767416621 * l - 3.3077115913 * mm + 0.2309699292 * s),
208
+ gamma(-1.2684380046 * l + 2.6097574011 * mm - 0.3413193965 * s),
209
+ gamma(-0.0041960863 * l - 0.7034186147 * mm + 1.707614701 * s),
210
+ alpha(m[4])
211
+ );
212
+ }
213
+ return null;
214
+ }
215
+ function toRadius(value) {
216
+ const m = /^([\d.]+)(px|rem|em)?$/.exec(value.trim());
217
+ if (!m) return null;
218
+ const px = parseFloat(m[1]) * (m[2] === "rem" || m[2] === "em" ? 16 : 1);
219
+ return Number.isFinite(px) ? Math.min(MAX_RADIUS, Math.max(0, Math.round(px))) : null;
220
+ }
221
+ function toFont(value) {
222
+ const v = value.trim();
223
+ 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;
224
+ }
225
+ var COLORS = [
226
+ ["background", ["--sm-bg-surface", "--sm-surface"]],
227
+ ["surface", ["--sm-bg-surface-raised", "--sm-surface-raised"]],
228
+ ["text", ["--sm-text-primary", "--sm-surface-text"]],
229
+ ["textMuted", ["--sm-text-secondary"]],
230
+ ["border", ["--sm-border-default", "--sm-surface-border"]],
231
+ ["primary", ["--sm-primary", "--sm-bg-interactive"]],
232
+ ["primaryText", ["--sm-primary-text", "--sm-text-inverse"]],
233
+ ["accent", ["--sm-text-brand", "--sm-border-focus", "--sm-accent"]],
234
+ ["error", ["--sm-error-soft-text", "--sm-error"]],
235
+ ["errorBackground", ["--sm-error-soft"]],
236
+ ["success", ["--sm-success-soft-text", "--sm-success"]],
237
+ ["successBackground", ["--sm-success-soft"]]
238
+ ];
239
+ function themeFromStackmyth(css, options = {}) {
240
+ const o = { mode: options.mode ?? "light", theme: options.theme };
241
+ const vars = /* @__PURE__ */ new Map();
242
+ for (const sheet of Array.isArray(css) ? css : [css]) {
243
+ for (const d of declarations(sheet)) if (applies(d, o)) vars.set(d.name, d.value);
244
+ }
245
+ const theme = {};
246
+ const warnings = [];
247
+ const read = (tokens, to, what) => {
248
+ for (const token of tokens) {
249
+ const raw = vars.get(token);
250
+ if (raw === void 0) continue;
251
+ const value = resolve(raw, vars);
252
+ const got = value === null ? null : to(value);
253
+ if (got !== null) return got;
254
+ warnings.push(`${token}: "${raw}" is not ${what} Cauthy can take`);
255
+ }
256
+ return void 0;
257
+ };
258
+ for (const [field, tokens] of COLORS) {
259
+ const c = read(tokens, toColor, "a colour");
260
+ if (c !== void 0) theme[field] = c;
261
+ }
262
+ const radius = read(["--sm-radius-container", "--sm-radius-md"], toRadius, "a length");
263
+ if (radius !== void 0) theme.radius = radius;
264
+ const button = read(["--sm-radius-interactive", "--sm-radius-sm"], toRadius, "a length");
265
+ if (button !== void 0) theme.buttonRadius = button;
266
+ const font = read(["--sm-font-family"], toFont, "a font list");
267
+ if (font !== void 0) theme.fontFamily = font;
268
+ if (options.logoUrl) theme.logoUrl = options.logoUrl;
269
+ if (vars.size === 0) warnings.push("no custom properties were found: is this the stylesheet that declares the --sm-* tokens?");
270
+ else if (![...vars.keys()].some((k) => k.startsWith("--sm-"))) warnings.push("no --sm-* token was found in these stylesheets");
271
+ return { theme, warnings };
272
+ }
273
+ function toWire(theme) {
274
+ const names = {
275
+ background: "background",
276
+ surface: "surface",
277
+ text: "text",
278
+ textMuted: "text_muted",
279
+ border: "border",
280
+ primary: "primary",
281
+ primaryText: "primary_text",
282
+ accent: "accent",
283
+ error: "error",
284
+ errorBackground: "error_background",
285
+ success: "success",
286
+ successBackground: "success_background",
287
+ radius: "radius",
288
+ buttonRadius: "button_radius",
289
+ fontFamily: "font_family",
290
+ logoUrl: "logo_url",
291
+ fonts: "fonts"
292
+ };
293
+ const wire = {};
294
+ for (const [k, v] of Object.entries(theme)) {
295
+ if (v === void 0 || v === "" || Array.isArray(v) && v.length === 0) continue;
296
+ wire[names[k]] = v;
297
+ }
298
+ return wire;
299
+ }
300
+ function fromWire(wire) {
301
+ const names = {
302
+ background: "background",
303
+ surface: "surface",
304
+ text: "text",
305
+ text_muted: "textMuted",
306
+ border: "border",
307
+ primary: "primary",
308
+ primary_text: "primaryText",
309
+ accent: "accent",
310
+ error: "error",
311
+ error_background: "errorBackground",
312
+ success: "success",
313
+ success_background: "successBackground",
314
+ radius: "radius",
315
+ button_radius: "buttonRadius",
316
+ font_family: "fontFamily",
317
+ logo_url: "logoUrl",
318
+ fonts: "fonts"
319
+ };
320
+ const theme = {};
321
+ for (const [k, v] of Object.entries(wire ?? {})) if (names[k] && v !== void 0 && v !== null && v !== "") theme[names[k]] = v;
322
+ return theme;
323
+ }
324
+ async function pushTheme(o) {
325
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
326
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
327
+ const clientId = o.clientId ?? env3("CAUTHY_CLIENT_ID") ?? "";
328
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
329
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
330
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_CLIENT_ID: clientId, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
331
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a theme`);
332
+ }
333
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/oauth/clients/${encodeURIComponent(clientId)}/theme`, {
334
+ method: "PUT",
335
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
336
+ body: JSON.stringify(toWire(o.theme))
337
+ });
338
+ if (!res.ok) {
339
+ const body = await res.json().catch(() => null);
340
+ throw new Error(`cauthy-auth: Cauthy refused the theme (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
341
+ }
342
+ }
343
+
344
+ // src/types.ts
345
+ var CauthyConfigError = class extends Error {
346
+ constructor(message) {
347
+ super(`cauthy-auth: ${message}`);
348
+ this.name = "CauthyConfigError";
349
+ }
350
+ };
351
+
352
+ // src/system.ts
353
+ var ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
354
+ var SKEW_MS = 3e4;
355
+ var CauthyApiError = class extends Error {
356
+ constructor(status, code, message) {
357
+ super(`cauthy-auth: ${message}`);
358
+ this.status = status;
359
+ this.code = code;
360
+ this.name = "CauthyApiError";
361
+ }
362
+ status;
363
+ code;
364
+ };
365
+ function env(name) {
366
+ return typeof process !== "undefined" ? process.env?.[name] : void 0;
367
+ }
368
+ function toOrganization(o) {
369
+ return {
370
+ id: o.id,
371
+ system: o.system,
372
+ name: o.name,
373
+ kind: o.kind,
374
+ externalId: o.external_id,
375
+ status: o.status,
376
+ metadata: o.metadata ?? {},
377
+ members: o.members,
378
+ createdAt: o.created_at,
379
+ updatedAt: o.updated_at
380
+ };
381
+ }
382
+ function toMember(m) {
383
+ return { userId: m.user_id, email: m.email, name: m.name, status: m.status, role: m.role, ...m.invited === void 0 ? {} : { invited: m.invited } };
384
+ }
385
+ function asJwk(k) {
386
+ if (typeof k.privateKey !== "string") return k.privateKey;
387
+ return k.privateKey.includes("BEGIN") ? void 0 : JSON.parse(k.privateKey);
388
+ }
389
+ async function loadKey(k) {
390
+ const jwk = asJwk(k);
391
+ return jwk ? (0, import_jose2.importJWK)(jwk, "RS256") : (0, import_jose2.importPKCS8)(k.privateKey, "RS256", { extractable: true });
392
+ }
393
+ function createSystem(given = {}) {
394
+ const issuer = (given.issuer ?? env("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
395
+ const clientId = given.clientId ?? env("CAUTHY_CLIENT_ID") ?? "";
396
+ const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
397
+ const envKey = env("CAUTHY_SYSTEM_KEY");
398
+ const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
399
+ const doFetch = (...args) => (given.fetch ?? fetch)(...args);
400
+ const tokenEndpoint = `${issuer}/v1/oauth/token`;
401
+ function configured() {
402
+ if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address");
403
+ if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
404
+ if (key ? key.keyId === "" : clientSecret === "") {
405
+ 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)");
406
+ }
407
+ }
408
+ let imported;
409
+ const signingKey = () => imported ??= loadKey(key);
410
+ async function fail(res, what) {
411
+ const body = await res.json().catch(() => null);
412
+ const first = body?.errors?.[0];
413
+ throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused ${what} (${res.status}): ${first?.message ?? res.statusText}`);
414
+ }
415
+ let held;
416
+ let asking;
417
+ async function ask() {
418
+ configured();
419
+ const form = new URLSearchParams({ grant_type: "client_credentials", client_id: clientId });
420
+ if (key) {
421
+ const assertion = await new import_jose2.SignJWT({}).setProtectedHeader({ alg: "RS256", kid: key.keyId }).setIssuer(clientId).setSubject(clientId).setAudience(tokenEndpoint).setJti(crypto.randomUUID()).setIssuedAt().setExpirationTime("2m").sign(await signingKey());
422
+ form.set("client_assertion_type", ASSERTION_TYPE);
423
+ form.set("client_assertion", assertion);
424
+ } else {
425
+ form.set("client_secret", clientSecret);
426
+ }
427
+ const res = await doFetch(tokenEndpoint, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form });
428
+ if (!res.ok) await fail(res, "the application's own token");
429
+ const t = await res.json();
430
+ held = { value: t.access_token, until: Date.now() + t.expires_in * 1e3 - SKEW_MS };
431
+ return t.access_token;
432
+ }
433
+ function token() {
434
+ if (held && Date.now() < held.until) return Promise.resolve(held.value);
435
+ return asking ??= ask().finally(() => {
436
+ asking = void 0;
437
+ });
438
+ }
439
+ async function call(method, path, body) {
440
+ const send = async () => doFetch(`${issuer}${path}`, {
441
+ method,
442
+ headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
443
+ body: body === void 0 ? void 0 : JSON.stringify(body)
444
+ });
445
+ let res = await send();
446
+ if (res.status === 401 && held) {
447
+ held = void 0;
448
+ res = await send();
449
+ }
450
+ if (!res.ok) await fail(res, `${method} ${path}`);
451
+ return res.status === 204 ? void 0 : res.json();
452
+ }
453
+ const data = async (method, path, body) => (await call(method, path, body))?.data;
454
+ const id = encodeURIComponent;
455
+ return {
456
+ token,
457
+ request: data,
458
+ organizations: {
459
+ async list(query = {}) {
460
+ const q = new URLSearchParams();
461
+ for (const [k, v] of Object.entries(query)) if (v !== void 0) q.set(k, String(v));
462
+ const out2 = await call("GET", `/v1/organizations${q.size ? `?${q}` : ""}`);
463
+ return { items: out2.data.map(toOrganization), total: out2.total_count };
464
+ },
465
+ async create(input) {
466
+ return toOrganization(await data(
467
+ "POST",
468
+ "/v1/organizations",
469
+ { name: input.name, kind: input.kind, external_id: input.externalId, metadata: input.metadata }
470
+ ));
471
+ },
472
+ async get(orgId) {
473
+ return toOrganization(await data("GET", `/v1/organizations/${id(orgId)}`));
474
+ },
475
+ async update(orgId, patch) {
476
+ return toOrganization(await data("PATCH", `/v1/organizations/${id(orgId)}`, patch));
477
+ },
478
+ async remove(orgId) {
479
+ await call("DELETE", `/v1/organizations/${id(orgId)}`);
480
+ }
481
+ },
482
+ members: {
483
+ async list(orgId) {
484
+ return (await data("GET", `/v1/organizations/${id(orgId)}/members`)).map(toMember);
485
+ },
486
+ async add(orgId, m) {
487
+ return toMember(await data(
488
+ "POST",
489
+ `/v1/organizations/${id(orgId)}/members`,
490
+ { role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
491
+ ));
492
+ },
493
+ async setRole(orgId, userId, role) {
494
+ return toMember(await data("PUT", `/v1/organizations/${id(orgId)}/members/${id(userId)}`, { role }));
495
+ },
496
+ async remove(orgId, userId) {
497
+ await call("DELETE", `/v1/organizations/${id(orgId)}/members/${id(userId)}`);
498
+ }
499
+ },
500
+ async invite(person) {
501
+ const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
502
+ return { id: u.id, email: u.email };
503
+ },
504
+ theme: {
505
+ async get(of = clientId) {
506
+ return fromWire(await data("GET", `/v1/oauth/clients/${id(of)}/theme`));
507
+ },
508
+ async set(theme, of = clientId) {
509
+ if (theme === null) await call("DELETE", `/v1/oauth/clients/${id(of)}/theme`);
510
+ else await call("PUT", `/v1/oauth/clients/${id(of)}/theme`, toWire(theme));
511
+ }
512
+ },
513
+ async publicJwks() {
514
+ if (!key) return { keys: [] };
515
+ const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
516
+ return { keys: [{ kty, n, e, kid: key.keyId, alg: "RS256", use: "sig" }] };
517
+ }
518
+ };
519
+ }
520
+
521
+ // src/webhook.ts
522
+ var import_jose3 = require("jose");
92
523
  var CauthyWebhookError = class extends Error {
93
524
  constructor(reason) {
94
525
  super(`cauthy-auth: webhook refused: ${reason}`);
@@ -101,15 +532,15 @@ var keys = /* @__PURE__ */ new Map();
101
532
  function keysOf(issuer) {
102
533
  let set = keys.get(issuer);
103
534
  if (set === void 0) {
104
- set = (0, import_jose2.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
535
+ set = (0, import_jose3.createRemoteJWKSet)(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 3e4 });
105
536
  keys.set(issuer, set);
106
537
  }
107
538
  return set;
108
539
  }
109
540
  var hex = (s) => typeof s === "string" && s !== "" ? s : void 0;
110
541
  async function verifyWebhook(request, options = {}) {
111
- const env2 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
112
- const issuer = (options.issuer ?? env2 ?? "").replace(/\/+$/, "");
542
+ const env3 = typeof process !== "undefined" ? process.env?.CAUTHY_ISSUER : void 0;
543
+ const issuer = (options.issuer ?? env3 ?? "").replace(/\/+$/, "");
113
544
  if (issuer === "") throw new CauthyWebhookError("issuer (CAUTHY_ISSUER) is not set");
114
545
  const signature = request.headers.get("x-cauthy-signature-jwt");
115
546
  if (!signature) throw new CauthyWebhookError("no signature");
@@ -117,7 +548,7 @@ async function verifyWebhook(request, options = {}) {
117
548
  const audience = options.url ?? request.url.split("?")[0];
118
549
  let claims;
119
550
  try {
120
- const verified = await (0, import_jose2.jwtVerify)(signature, options.jwks ?? keysOf(issuer), {
551
+ const verified = await (0, import_jose3.jwtVerify)(signature, options.jwks ?? keysOf(issuer), {
121
552
  algorithms: ["RS256"],
122
553
  issuer,
123
554
  audience,
@@ -158,20 +589,12 @@ async function verifyWebhook(request, options = {}) {
158
589
  };
159
590
  }
160
591
 
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
592
  // src/cauthy.ts
170
593
  var SESSION_DAYS = 30;
171
594
  var FLOW_SECONDS = 600;
172
595
  var SKEW_SECONDS = 30;
173
596
  var ORGANIZATIONS_TTL_MS = 6e4;
174
- function env(name) {
597
+ function env2(name) {
175
598
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
176
599
  }
177
600
  function b64url(bytes) {
@@ -198,11 +621,11 @@ function redirect(location, cookies = []) {
198
621
  return new Response(null, { status: 302, headers: h });
199
622
  }
200
623
  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") ?? "";
624
+ const issuer = (given.issuer ?? env2("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
625
+ const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
626
+ const clientSecret = given.clientSecret ?? env2("CAUTHY_CLIENT_SECRET");
627
+ const secret = given.secret ?? env2("CAUTHY_SECRET") ?? "";
628
+ const system = given.system ?? env2("CAUTHY_SYSTEM") ?? "";
206
629
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
207
630
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
208
631
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
@@ -471,6 +894,7 @@ function createCauthy(given = {}) {
471
894
  return {
472
895
  handler,
473
896
  webhook,
897
+ system: createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch }),
474
898
  getSession,
475
899
  status,
476
900
  resolve: resolve2,
@@ -482,226 +906,6 @@ function createCauthy(given = {}) {
482
906
  };
483
907
  }
484
908
 
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
909
  // src/permissions.ts
706
910
  function matches(entry, resource, action) {
707
911
  const i = entry.lastIndexOf(":");
@@ -716,11 +920,11 @@ function can(organization, resource, action) {
716
920
  return organization.permissions.some((p) => matches(p, resource, action));
717
921
  }
718
922
  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") ?? "";
923
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
924
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
925
+ const system = o.system ?? env3("CAUTHY_SYSTEM") ?? "";
926
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
927
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
724
928
  for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
725
929
  if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
726
930
  }
@@ -734,12 +938,59 @@ async function pushPolicy(o) {
734
938
  throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
735
939
  }
736
940
  }
941
+
942
+ // src/connect.ts
943
+ function sessionSecret() {
944
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
945
+ let s = "";
946
+ for (const b of bytes) s += String.fromCharCode(b);
947
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
948
+ }
949
+ async function connectSystem(o) {
950
+ const env3 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
951
+ const issuer = (o.issuer ?? env3("CAUTHY_ISSUER") ?? "").replace(/\/+$/, "");
952
+ const tenant = o.tenant ?? env3("CAUTHY_TENANT") ?? "";
953
+ const token = o.token ?? env3("CAUTHY_TOKEN") ?? "";
954
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
955
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to connect a system`);
956
+ }
957
+ const res = await (o.fetch ?? fetch)(`${issuer}/v1/connect`, {
958
+ method: "POST",
959
+ headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
960
+ body: JSON.stringify({
961
+ system: o.system,
962
+ app_url: o.appUrl,
963
+ name: o.name,
964
+ callback_path: o.callbackPath,
965
+ events_url: o.eventsUrl,
966
+ events: o.events,
967
+ jwks_uri: o.jwksUri
968
+ })
969
+ });
970
+ const body = await res.json().catch(() => null);
971
+ if (!res.ok || !body?.data) {
972
+ const why = (body?.errors ?? []).map((e) => e.message).filter(Boolean).join("; ") || res.statusText;
973
+ throw new Error(`cauthy-auth: Cauthy did not connect the system (${res.status}): ${why}`);
974
+ }
975
+ const d = body.data;
976
+ return {
977
+ system: d.system,
978
+ clientId: d.client.client_id,
979
+ clientSecret: d.client_secret,
980
+ redirectUris: d.client.redirect_uris,
981
+ webhook: d.webhook,
982
+ env: { ...d.env, CAUTHY_SECRET: sessionSecret() }
983
+ };
984
+ }
737
985
  // Annotate the CommonJS export names for ESM import in node:
738
986
  0 && (module.exports = {
987
+ CauthyApiError,
739
988
  CauthyConfigError,
740
989
  CauthyWebhookError,
741
990
  can,
991
+ connectSystem,
742
992
  createCauthy,
993
+ createSystem,
743
994
  pushPolicy,
744
995
  pushTheme,
745
996
  themeFromStackmyth,