cauthy-auth 0.28.0 → 0.30.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.
Files changed (45) hide show
  1. package/README.md +52 -1
  2. package/dist/{cauthy-XkIbG5gE.d.mts → cauthy-BXgsZ-Um.d.mts} +19 -1
  3. package/dist/{cauthy-DGAy45G2.d.ts → cauthy-CHENs452.d.ts} +19 -1
  4. package/dist/cli.js +155 -54
  5. package/dist/client.d.mts +47 -7
  6. package/dist/client.d.ts +47 -7
  7. package/dist/client.js +23 -1
  8. package/dist/client.js.map +1 -1
  9. package/dist/client.mjs +23 -1
  10. package/dist/client.mjs.map +1 -1
  11. package/dist/express.d.mts +2 -2
  12. package/dist/express.d.ts +2 -2
  13. package/dist/fastify.d.mts +2 -2
  14. package/dist/fastify.d.ts +2 -2
  15. package/dist/hono.d.mts +2 -2
  16. package/dist/hono.d.ts +2 -2
  17. package/dist/index.d.mts +2 -2
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +352 -131
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +350 -131
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/next.d.mts +2 -2
  24. package/dist/next.d.ts +2 -2
  25. package/dist/react.d.mts +214 -18
  26. package/dist/react.d.ts +214 -18
  27. package/dist/react.js +129 -32
  28. package/dist/react.js.map +1 -1
  29. package/dist/react.mjs +129 -32
  30. package/dist/react.mjs.map +1 -1
  31. package/dist/svelte.d.mts +2 -2
  32. package/dist/svelte.d.ts +2 -2
  33. package/dist/svelte.js +37 -2
  34. package/dist/svelte.js.map +1 -1
  35. package/dist/svelte.mjs +37 -2
  36. package/dist/svelte.mjs.map +1 -1
  37. package/dist/{system-BxMsSIu1.d.mts → system-0mHrdAgO.d.mts} +312 -112
  38. package/dist/{system-DTIeDXDP.d.ts → system-BIYbS7h2.d.ts} +312 -112
  39. package/dist/vue.d.mts +2 -2
  40. package/dist/vue.d.ts +2 -2
  41. package/dist/vue.js +37 -2
  42. package/dist/vue.js.map +1 -1
  43. package/dist/vue.mjs +37 -2
  44. package/dist/vue.mjs.map +1 -1
  45. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -41,7 +41,9 @@ __export(src_exports, {
41
41
  passwordPolicyOf: () => passwordPolicyOf,
42
42
  passwordProblems: () => passwordProblems,
43
43
  policySource: () => policySource,
44
+ pullCatalog: () => pullCatalog,
44
45
  pullPolicy: () => pullPolicy,
46
+ pushCatalog: () => pushCatalog,
45
47
  pushPolicy: () => pushPolicy,
46
48
  pushTheme: () => pushTheme,
47
49
  themeFromStackmyth: () => themeFromStackmyth,
@@ -107,9 +109,6 @@ async function unseal(sealed, key) {
107
109
  }
108
110
  }
109
111
 
110
- // src/system.ts
111
- var import_jose2 = require("jose");
112
-
113
112
  // src/url.ts
114
113
  function withoutTrailingSlashes(s) {
115
114
  let end = s.length;
@@ -117,6 +116,218 @@ function withoutTrailingSlashes(s) {
117
116
  return s.slice(0, end);
118
117
  }
119
118
 
119
+ // src/permissions.ts
120
+ function matches(entry, resource, action) {
121
+ const i = entry.lastIndexOf(":");
122
+ if (i < 0) return false;
123
+ const r = entry.slice(0, i);
124
+ const a = entry.slice(i + 1);
125
+ return (r === "*" || r === resource) && (a === "*" || a === action);
126
+ }
127
+ function can(organization, resource, action) {
128
+ if (!organization?.permissions) return false;
129
+ if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
130
+ return organization.permissions.some((p) => matches(p, resource, action));
131
+ }
132
+ function definePolicy(given) {
133
+ const roles = Array.isArray(given) ? given : given.roles;
134
+ const resources = Array.isArray(given) ? [] : given.catalog ?? [];
135
+ const entry = (p) => `${p.resource}:${p.action}`;
136
+ const byName = new Map(roles.map((r) => [r.role, r]));
137
+ const split = (permission) => {
138
+ const i = permission.lastIndexOf(":");
139
+ return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
140
+ };
141
+ return {
142
+ roles: roles.map((r) => ({
143
+ role: r.role,
144
+ ...r.name !== void 0 ? { name: r.name } : {},
145
+ ...r.description !== void 0 ? { description: r.description } : {},
146
+ ...r.inherits ? { inherits: [...r.inherits] } : {},
147
+ ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
148
+ })),
149
+ catalog: resources.map((c) => ({
150
+ resource: c.resource,
151
+ ...c.description !== void 0 ? { description: c.description } : {},
152
+ actions: [...c.actions],
153
+ ...c.attributes ? { attributes: { ...c.attributes } } : {}
154
+ })),
155
+ roleNames: roles.map((r) => r.role),
156
+ permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
157
+ can(organization, permission) {
158
+ const [resource, action] = split(permission);
159
+ return can(organization, resource, action);
160
+ },
161
+ hasRole(organization, ...wanted) {
162
+ return organization != null && wanted.includes(organization.role);
163
+ },
164
+ permissionsOf(role) {
165
+ const seen = /* @__PURE__ */ new Set();
166
+ const out2 = /* @__PURE__ */ new Set();
167
+ const walk = (name) => {
168
+ if (seen.has(name)) return;
169
+ seen.add(name);
170
+ const r = byName.get(name);
171
+ for (const p of r?.permissions ?? []) out2.add(entry(p));
172
+ for (const parent of r?.inherits ?? []) walk(parent);
173
+ };
174
+ walk(role);
175
+ return [...out2];
176
+ },
177
+ // The types did the work; this only hands the question on, as a method of
178
+ // the asker so it keeps its `this`.
179
+ check(asker, ...args) {
180
+ return asker.check(...args);
181
+ }
182
+ };
183
+ }
184
+ var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
185
+ function policySource(system, read) {
186
+ const roles = Array.isArray(read) ? read : read.roles;
187
+ const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
188
+ const text = (v) => JSON.stringify(v);
189
+ const key = (k) => IDENT.test(k) ? k : text(k);
190
+ const one = (r, pad) => {
191
+ const parts = [`role: ${text(r.role)}`];
192
+ if (r.name) parts.push(`name: ${text(r.name)}`);
193
+ if (r.description) parts.push(`description: ${text(r.description)}`);
194
+ if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
195
+ if (r.permissions?.length) {
196
+ const list = r.permissions.map((p) => `${pad} { ${[
197
+ ...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [],
198
+ `resource: ${text(p.resource)}`,
199
+ `action: ${text(p.action)}`,
200
+ ...p.condition ? [`condition: ${text(p.condition)}`] : []
201
+ ].join(", ")} },`);
202
+ parts.push(`permissions: [
203
+ ${list.join("\n")}
204
+ ${pad} ]`);
205
+ }
206
+ return `${pad} {
207
+ ${pad} ${parts.join(`,
208
+ ${pad} `)},
209
+ ${pad} },`;
210
+ };
211
+ const resource = (c) => {
212
+ const parts = [`resource: ${text(c.resource)}`];
213
+ if (c.description) parts.push(`description: ${text(c.description)}`);
214
+ parts.push(`actions: [${c.actions.map(text).join(", ")}]`);
215
+ const attrs = Object.entries(c.attributes ?? {});
216
+ if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text(v)}`).join(", ")} }`);
217
+ return ` { ${parts.join(", ")} },`;
218
+ };
219
+ const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
220
+ // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
221
+ // changes, and do not edit this file by hand.
222
+ `;
223
+ if (catalog.length === 0) {
224
+ return `${head}
225
+ import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
226
+
227
+ export const policy = definePolicy([
228
+ ${roles.map((r) => one(r, "")).join("\n")}
229
+ ])
230
+
231
+ export const { can, hasRole } = policy
232
+ export type Role = RoleOf<typeof policy>
233
+ export type Permission = PermissionOf<typeof policy>
234
+ `;
235
+ }
236
+ return `${head}
237
+ import { definePolicy, type CheckOf, type PermissionOf, type RoleOf } from 'cauthy-auth'
238
+
239
+ export const policy = definePolicy({
240
+ catalog: [
241
+ ${catalog.map(resource).join("\n")}
242
+ ],
243
+ roles: [
244
+ ${roles.map((r) => one(r, " ")).join("\n")}
245
+ ],
246
+ })
247
+
248
+ export const { can, hasRole } = policy
249
+ export type Role = RoleOf<typeof policy>
250
+ export type Permission = PermissionOf<typeof policy>
251
+ /** What a check may ask: \`policy.check(auth, request, { organization, ...question })\`. */
252
+ export type Check = CheckOf<typeof policy>
253
+ `;
254
+ }
255
+ function reach(o, doing) {
256
+ const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
257
+ const r = {
258
+ issuer: withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? ""),
259
+ system: o.system ?? env4("CAUTHY_SYSTEM") ?? "",
260
+ tenant: o.tenant ?? env4("CAUTHY_TENANT") ?? "",
261
+ token: o.token ?? env4("CAUTHY_TOKEN") ?? ""
262
+ };
263
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: r.issuer, CAUTHY_SYSTEM: r.system, CAUTHY_TENANT: r.tenant, CAUTHY_TOKEN: r.token })) {
264
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to ${doing}`);
265
+ }
266
+ return { ...r, fetch: o.fetch ?? fetch };
267
+ }
268
+ async function systemCall(r, method, what, body) {
269
+ const res = await r.fetch(`${r.issuer}/v1/systems/${encodeURIComponent(r.system)}/${what}`, {
270
+ ...method === "PUT" ? { method } : {},
271
+ headers: {
272
+ authorization: `Bearer ${r.token}`,
273
+ "x-tenant-key": r.tenant,
274
+ ...body === void 0 ? { accept: "application/json" } : { "content-type": "application/json" }
275
+ },
276
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
277
+ });
278
+ const answer = await res.json().catch(() => null);
279
+ if (!res.ok) {
280
+ const verb = method === "GET" ? `refused to give the ${what}` : `refused the ${what}`;
281
+ throw new Error(`cauthy-auth: Cauthy ${verb} (${res.status}): ${answer?.errors?.[0]?.message ?? res.statusText}`);
282
+ }
283
+ return answer?.data;
284
+ }
285
+ async function pullPolicy(o = {}) {
286
+ const data = await systemCall(reach(o, "read a policy"), "GET", "policy");
287
+ return data?.roles ?? [];
288
+ }
289
+ async function pullCatalog(o = {}) {
290
+ const data = await systemCall(reach(o, "read a catalog"), "GET", "catalog");
291
+ return data?.resources ?? [];
292
+ }
293
+ async function pushPolicy(o) {
294
+ const r = reach(o, "push a policy");
295
+ if (o.catalog !== void 0) await systemCall(r, "PUT", "catalog", { resources: o.catalog });
296
+ await systemCall(r, "PUT", "policy", { roles: o.roles });
297
+ }
298
+ async function pushCatalog(o) {
299
+ const data = await systemCall(reach(o, "push a catalog"), "PUT", "catalog", { resources: o.resources });
300
+ return data?.resources ?? [];
301
+ }
302
+ function checkWire(q) {
303
+ return {
304
+ resource: q.resource,
305
+ action: q.action,
306
+ ...q.resourceId !== void 0 ? { resource_id: q.resourceId } : {},
307
+ ...q.ownerId !== void 0 ? { owner_id: q.ownerId } : {},
308
+ ...q.attributes !== void 0 ? { attributes: q.attributes } : {}
309
+ };
310
+ }
311
+ function checkResultOf(d) {
312
+ const r = d?.rule;
313
+ const rule = r && typeof r === "object" ? {
314
+ source: r.source === "exception" ? "exception" : "role",
315
+ ...r.role ? { role: String(r.role) } : {},
316
+ kind: String(r.kind ?? "*"),
317
+ resource: String(r.resource ?? ""),
318
+ action: String(r.action ?? ""),
319
+ effect: r.effect === "allow" ? "allow" : "deny",
320
+ ...r.condition ? { condition: String(r.condition) } : {}
321
+ } : void 0;
322
+ return { allowed: d?.allowed === true, reason: String(d?.reason ?? ""), ...rule ? { rule } : {} };
323
+ }
324
+ function organizationIdOf(o) {
325
+ return typeof o === "string" ? o : typeof o?.id === "string" ? o.id : "";
326
+ }
327
+
328
+ // src/system.ts
329
+ var import_jose2 = require("jose");
330
+
120
331
  // src/theme.ts
121
332
  var MAX_RADIUS = 40;
122
333
  function withoutComments(css) {
@@ -534,6 +745,7 @@ function createSystem(given = {}) {
534
745
  const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
535
746
  const envKey = env("CAUTHY_SYSTEM_KEY");
536
747
  const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
748
+ const ownSystem = given.system ?? env("CAUTHY_SYSTEM") ?? "";
537
749
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
538
750
  const tokenEndpoint = `${issuer}/v1/oauth/token`;
539
751
  function configured() {
@@ -651,6 +863,19 @@ function createSystem(given = {}) {
651
863
  return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
652
864
  }
653
865
  },
866
+ catalog: {
867
+ async get(system) {
868
+ return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
869
+ },
870
+ async set(system, resources) {
871
+ return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
872
+ }
873
+ },
874
+ async check(q) {
875
+ const system = q.system ?? ownSystem;
876
+ if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
877
+ return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
878
+ },
654
879
  async publicJwks() {
655
880
  if (!key) return { keys: [] };
656
881
  const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
@@ -798,6 +1023,12 @@ function checkOrganization(value, required = false) {
798
1023
  if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
799
1024
  return null;
800
1025
  }
1026
+ function checkOrganizationName(value) {
1027
+ const v = typeof value === "string" ? value.trim() : "";
1028
+ if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
1029
+ if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
1030
+ return null;
1031
+ }
801
1032
  function checkCode(value) {
802
1033
  const v = typeof value === "string" ? value.trim() : "";
803
1034
  if (v === "") return { field: "code", code: "required", message: "Enter the code." };
@@ -899,6 +1130,13 @@ function providerIn(segment) {
899
1130
  function env2(name) {
900
1131
  return typeof process !== "undefined" ? process.env?.[name] : void 0;
901
1132
  }
1133
+ function hostOf(origin) {
1134
+ try {
1135
+ return new URL(origin).hostname;
1136
+ } catch {
1137
+ return void 0;
1138
+ }
1139
+ }
902
1140
  function b64url(bytes) {
903
1141
  let s = "";
904
1142
  for (const b of bytes) s += String.fromCharCode(b);
@@ -922,6 +1160,9 @@ function redirect(location, cookies = []) {
922
1160
  for (const c of cookies) h.append("set-cookie", c);
923
1161
  return new Response(null, { status: 302, headers: h });
924
1162
  }
1163
+ function conditionalOf(c) {
1164
+ return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
1165
+ }
925
1166
  function createCauthy(given = {}) {
926
1167
  const issuer = withoutTrailingSlashes(given.issuer ?? env2("CAUTHY_ISSUER") ?? "");
927
1168
  const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
@@ -932,6 +1173,10 @@ function createCauthy(given = {}) {
932
1173
  if (!/^https?:\/\//.test(issuer)) throw new CauthyConfigError("issuer (CAUTHY_ISSUER) must be Cauthy's address, e.g. https://id.example.com");
933
1174
  if (clientId === "") throw new CauthyConfigError("clientId (CAUTHY_CLIENT_ID) is required");
934
1175
  if (secret.length < 32) throw new CauthyConfigError("secret (CAUTHY_SECRET) must be at least 32 characters: it encrypts the session cookie");
1176
+ const baseURL = given.baseURL ?? env2("CAUTHY_BASE_URL");
1177
+ if (baseURL !== void 0 && baseURL !== "" && !/^https?:\/\/[^/]+\/*$/.test(baseURL)) {
1178
+ throw new CauthyConfigError("baseURL (CAUTHY_BASE_URL) must be the application's public origin, e.g. https://app.example.com");
1179
+ }
935
1180
  const basePath = "/" + (given.basePath ?? "/api/auth").replace(/^\/+|\/+$/g, "");
936
1181
  const afterSignIn = localPath(given.afterSignIn, "/");
937
1182
  const afterSignOut = localPath(given.afterSignOut, "/");
@@ -946,8 +1191,13 @@ function createCauthy(given = {}) {
946
1191
  resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
947
1192
  verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
948
1193
  };
949
- const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
950
- const originOf = (request) => withoutTrailingSlashes(given.baseURL ?? new URL(request.url).origin);
1194
+ const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, system: system || void 0, fetch: given.fetch });
1195
+ const originOf = (request) => {
1196
+ if (baseURL) return withoutTrailingSlashes(baseURL);
1197
+ const url = new URL(request.url);
1198
+ const proto = request.headers.get("x-forwarded-proto")?.split(",")[0]?.trim().toLowerCase();
1199
+ return proto === "https" || proto === "http" ? `${proto}://${url.host}` : url.origin;
1200
+ };
951
1201
  const attrs = (request, maxAge) => ({ secure: originOf(request).startsWith("https://"), maxAge });
952
1202
  async function token(form) {
953
1203
  const body2 = new URLSearchParams({ client_id: clientId, ...form });
@@ -1076,13 +1326,35 @@ function createCauthy(given = {}) {
1076
1326
  role: String(o.role ?? ""),
1077
1327
  metadata: o.metadata ?? {},
1078
1328
  ...o.external_id ? { externalId: String(o.external_id) } : {},
1079
- ...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}
1329
+ ...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [], conditional: Array.isArray(o.conditional) ? o.conditional.map(conditionalOf) : [] } : {}
1080
1330
  })
1081
1331
  );
1082
1332
  if (orgCache.size > 5e3) orgCache.clear();
1083
1333
  orgCache.set(session.accessToken, { at: Date.now(), value });
1084
1334
  return value;
1085
1335
  }
1336
+ async function decide(token2, question) {
1337
+ const org = organizationIdOf(question.organization);
1338
+ if (org === "") return { allowed: false, reason: "no organization named" };
1339
+ const res = await doFetch(`${issuer}/v1/oauth/organizations/${encodeURIComponent(org)}/check`, {
1340
+ method: "POST",
1341
+ headers: { authorization: `Bearer ${token2}`, "content-type": "application/json", accept: "application/json" },
1342
+ body: JSON.stringify(checkWire(question))
1343
+ }).catch(() => null);
1344
+ if (res === null) throw new CauthyApiError(503, "unavailable", "Cauthy could not be reached to check a permission");
1345
+ const body2 = await res.json().catch(() => null);
1346
+ if (res.ok) return checkResultOf(body2?.data);
1347
+ const first = body2?.errors?.[0];
1348
+ if (res.status === 401 || res.status === 403 || res.status === 404) {
1349
+ return { allowed: false, reason: first?.message ?? (res.status === 401 ? "not signed in" : res.status === 404 ? "not a member of the organization" : "refused") };
1350
+ }
1351
+ throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused to check a permission (${res.status}): ${first?.message ?? res.statusText}`);
1352
+ }
1353
+ async function check(who, question) {
1354
+ const session = "accessToken" in who ? who : await getSession(who);
1355
+ if (!session) return { allowed: false, reason: "not signed in" };
1356
+ return decide(session.accessToken, question);
1357
+ }
1086
1358
  const forgetting = (fn) => async (...args) => {
1087
1359
  const out2 = await fn(...args);
1088
1360
  orgCache.clear();
@@ -1215,7 +1487,15 @@ function createCauthy(given = {}) {
1215
1487
  name: String(d?.name ?? ""),
1216
1488
  kind: String(d?.kind ?? ""),
1217
1489
  role: String(d?.role ?? ""),
1218
- can: { readMembers: d?.can?.read_members === true, manageMembers: d?.can?.manage_members === true }
1490
+ // A Cauthy from before a power says nothing of it: then it is not given.
1491
+ can: {
1492
+ readMembers: d?.can?.read_members === true,
1493
+ manageMembers: d?.can?.manage_members === true,
1494
+ removeMembers: d?.can?.remove_members === true,
1495
+ update: d?.can?.update === true,
1496
+ archive: d?.can?.archive === true,
1497
+ delete: d?.can?.delete === true
1498
+ }
1219
1499
  });
1220
1500
  const orgMemberOf = (d) => ({
1221
1501
  userId: String(d?.user_id ?? ""),
@@ -1245,9 +1525,18 @@ function createCauthy(given = {}) {
1245
1525
  });
1246
1526
  const problem = (status2, code, message, field, headers, reasons) => json({ error: { code, message, ...field ? { field } : {}, ...reasons && reasons.length > 0 ? { reasons } : {} } }, status2, headers);
1247
1527
  const invalid = (p) => problem(400, p.code, p.message, p.field);
1528
+ let explained = false;
1248
1529
  function ownRequest(request) {
1249
1530
  const origin = request.headers.get("origin");
1250
- if (origin !== null) return origin === originOf(request) || origin === new URL(request.url).origin;
1531
+ if (origin !== null) {
1532
+ const own = originOf(request);
1533
+ if (origin === own || origin === new URL(request.url).origin) return true;
1534
+ if (!explained && hostOf(origin) === new URL(own).hostname) {
1535
+ explained = true;
1536
+ console.warn(`cauthy-auth: refused a request from ${origin} as cross_site: this application sees itself as ${own}. Behind a proxy? Set CAUTHY_BASE_URL (or baseURL) to the public origin.`);
1537
+ }
1538
+ return false;
1539
+ }
1251
1540
  return (request.headers.get("sec-fetch-site") ?? "same-origin") === "same-origin";
1252
1541
  }
1253
1542
  async function body(request) {
@@ -1399,7 +1688,10 @@ function createCauthy(given = {}) {
1399
1688
  "/me/organizations/members/add",
1400
1689
  "/me/organizations/members/role",
1401
1690
  "/me/organizations/members/remove",
1402
- "/me/organizations/members/invite"
1691
+ "/me/organizations/members/invite",
1692
+ "/me/organizations/rename",
1693
+ "/me/organizations/archive",
1694
+ "/me/organizations/delete"
1403
1695
  ].includes(route) || route.startsWith("/sign-in/social/") || route.startsWith("/callback/") || route.startsWith("/me/identities/link/") || route.startsWith("/me/upgrade/social/") || route === "/sign-in/sso" || route === "/sign-in/sso/discover" || route === "/sso/callback";
1404
1696
  if (!own) return null;
1405
1697
  if (tenant === "") {
@@ -1870,6 +2162,23 @@ function createCauthy(given = {}) {
1870
2162
  if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
1871
2163
  return signedIn(request, res.data);
1872
2164
  }
2165
+ if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
2166
+ const org = typeof b.organizationId === "string" ? b.organizationId : "";
2167
+ if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
2168
+ const path = `/v1/auth/me/organizations/${org}`;
2169
+ if (route === "/me/organizations/rename") {
2170
+ const bad = checkOrganizationName(b.name);
2171
+ if (bad) return invalid(bad);
2172
+ const res2 = await as(path, { name: String(b.name).trim() }, "PATCH");
2173
+ if (res2.error) return refused(res2, res2.error.code === "validation_error" ? "name" : void 0);
2174
+ orgCache.delete(mine.token);
2175
+ return json({ status: "renamed", organization: seatOf(res2.data) }, 200, mine.headers);
2176
+ }
2177
+ const res = route === "/me/organizations/archive" ? await as(`${path}/archive`) : await as(path, void 0, "DELETE");
2178
+ if (res.error) return refused(res);
2179
+ orgCache.delete(mine.token);
2180
+ return json({ status: route === "/me/organizations/archive" ? "archived" : "deleted", organizationId: org }, 200, mine.headers);
2181
+ }
1873
2182
  if (route.startsWith("/me/organizations/members/")) {
1874
2183
  const org = typeof b.organizationId === "string" ? b.organizationId : "";
1875
2184
  if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
@@ -2015,6 +2324,7 @@ function createCauthy(given = {}) {
2015
2324
  const get = request.method === "GET" || request.method === "HEAD";
2016
2325
  const mine = await ownRoute(request, url, route);
2017
2326
  if (mine !== null) return mine;
2327
+ if (route === "/me/organizations/check") return checkRoute(request);
2018
2328
  if (route === "/login" && get) return login(request, url);
2019
2329
  if (route === "/callback" && get) return callback(request, url);
2020
2330
  if (route === "/logout" && (get || request.method === "POST")) return logout(request);
@@ -2041,6 +2351,36 @@ function createCauthy(given = {}) {
2041
2351
  }
2042
2352
  return new Response("Not found", { status: 404 });
2043
2353
  }
2354
+ async function checkRoute(request) {
2355
+ if (request.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
2356
+ if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
2357
+ const b = await body(request);
2358
+ if (b === null) return problem(400, "bad_request", "Send the question as JSON.");
2359
+ const asker = await signedInAs(request);
2360
+ if ("response" in asker) return asker.response;
2361
+ const org = typeof b.organizationId === "string" ? b.organizationId : "";
2362
+ if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
2363
+ const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
2364
+ if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
2365
+ const attributes2 = b.attributes;
2366
+ if (attributes2 !== void 0 && (typeof attributes2 !== "object" || attributes2 === null || Array.isArray(attributes2))) {
2367
+ return problem(400, "bad_request", "The attributes are an object.", void 0, asker.headers);
2368
+ }
2369
+ try {
2370
+ const r = await decide(asker.token, {
2371
+ organization: org,
2372
+ resource: b.resource,
2373
+ action: b.action,
2374
+ ...attributes2 !== void 0 ? { attributes: attributes2 } : {},
2375
+ ...typeof b.resourceId === "string" ? { resourceId: b.resourceId } : {},
2376
+ ...typeof b.ownerId === "string" ? { ownerId: b.ownerId } : {}
2377
+ });
2378
+ return json({ status: "decided", ...r }, 200, asker.headers);
2379
+ } catch (e) {
2380
+ const err = e instanceof CauthyApiError ? e : new CauthyApiError(503, "unavailable", "Cauthy could not be reached.");
2381
+ return problem(err.status, err.code, err.message.replace(/^cauthy-auth: /, ""), void 0, asker.headers);
2382
+ }
2383
+ }
2044
2384
  async function protect(request) {
2045
2385
  const { value } = await read(request);
2046
2386
  if (value !== null && fresh(value)) return { session: toSession(value) };
@@ -2064,6 +2404,7 @@ function createCauthy(given = {}) {
2064
2404
  resolve: resolve2,
2065
2405
  protect,
2066
2406
  organizations,
2407
+ check,
2067
2408
  toPublic,
2068
2409
  urls,
2069
2410
  options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
@@ -2078,128 +2419,6 @@ function legalOf(raw) {
2078
2419
  };
2079
2420
  }
2080
2421
 
2081
- // src/permissions.ts
2082
- function matches(entry, resource, action) {
2083
- const i = entry.lastIndexOf(":");
2084
- if (i < 0) return false;
2085
- const r = entry.slice(0, i);
2086
- const a = entry.slice(i + 1);
2087
- return (r === "*" || r === resource) && (a === "*" || a === action);
2088
- }
2089
- function can(organization, resource, action) {
2090
- if (!organization?.permissions) return false;
2091
- if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
2092
- return organization.permissions.some((p) => matches(p, resource, action));
2093
- }
2094
- function definePolicy(roles) {
2095
- const entry = (p) => `${p.resource}:${p.action}`;
2096
- const byName = new Map(roles.map((r) => [r.role, r]));
2097
- const split = (permission) => {
2098
- const i = permission.lastIndexOf(":");
2099
- return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
2100
- };
2101
- return {
2102
- roles: roles.map((r) => ({
2103
- role: r.role,
2104
- ...r.name !== void 0 ? { name: r.name } : {},
2105
- ...r.description !== void 0 ? { description: r.description } : {},
2106
- ...r.inherits ? { inherits: [...r.inherits] } : {},
2107
- ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
2108
- })),
2109
- roleNames: roles.map((r) => r.role),
2110
- permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
2111
- can(organization, permission) {
2112
- const [resource, action] = split(permission);
2113
- return can(organization, resource, action);
2114
- },
2115
- hasRole(organization, ...wanted) {
2116
- return organization != null && wanted.includes(organization.role);
2117
- },
2118
- permissionsOf(role) {
2119
- const seen = /* @__PURE__ */ new Set();
2120
- const out2 = /* @__PURE__ */ new Set();
2121
- const walk = (name) => {
2122
- if (seen.has(name)) return;
2123
- seen.add(name);
2124
- const r = byName.get(name);
2125
- for (const p of r?.permissions ?? []) out2.add(entry(p));
2126
- for (const parent of r?.inherits ?? []) walk(parent);
2127
- };
2128
- walk(role);
2129
- return [...out2];
2130
- }
2131
- };
2132
- }
2133
- var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2134
- function policySource(system, roles) {
2135
- const text = (v) => JSON.stringify(v);
2136
- const key = (k) => IDENT.test(k) ? k : text(k);
2137
- const one = (r) => {
2138
- const parts = [`role: ${text(r.role)}`];
2139
- if (r.name) parts.push(`name: ${text(r.name)}`);
2140
- if (r.description) parts.push(`description: ${text(r.description)}`);
2141
- if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
2142
- if (r.permissions?.length) {
2143
- const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
2144
- parts.push(`permissions: [
2145
- ${list.join("\n")}
2146
- ]`);
2147
- }
2148
- return ` {
2149
- ${parts.join(",\n ")},
2150
- },`;
2151
- };
2152
- return `// The catalog of roles of ${text(system)}, as Cauthy has it.
2153
- // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
2154
- // changes, and do not edit this file by hand.
2155
-
2156
- import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
2157
-
2158
- export const policy = definePolicy([
2159
- ${roles.map(one).join("\n")}
2160
- ])
2161
-
2162
- export const { can, hasRole } = policy
2163
- export type Role = RoleOf<typeof policy>
2164
- export type Permission = PermissionOf<typeof policy>
2165
- `;
2166
- }
2167
- async function pullPolicy(o = {}) {
2168
- const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
2169
- const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
2170
- const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
2171
- const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
2172
- const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
2173
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
2174
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
2175
- }
2176
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
2177
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
2178
- });
2179
- const body = await res.json().catch(() => null);
2180
- if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
2181
- return body?.data?.roles ?? [];
2182
- }
2183
- async function pushPolicy(o) {
2184
- const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
2185
- const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
2186
- const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
2187
- const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
2188
- const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
2189
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
2190
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
2191
- }
2192
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
2193
- method: "PUT",
2194
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
2195
- body: JSON.stringify({ roles: o.roles })
2196
- });
2197
- if (!res.ok) {
2198
- const body = await res.json().catch(() => null);
2199
- throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
2200
- }
2201
- }
2202
-
2203
2422
  // src/connect.ts
2204
2423
  function sessionSecret() {
2205
2424
  const bytes = crypto.getRandomValues(new Uint8Array(32));
@@ -2325,7 +2544,9 @@ async function verifyToken(from, options = {}) {
2325
2544
  passwordPolicyOf,
2326
2545
  passwordProblems,
2327
2546
  policySource,
2547
+ pullCatalog,
2328
2548
  pullPolicy,
2549
+ pushCatalog,
2329
2550
  pushPolicy,
2330
2551
  pushTheme,
2331
2552
  themeFromStackmyth,