cauthy-auth 0.28.1 → 0.31.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 (46) hide show
  1. package/README.md +61 -3
  2. package/dist/{cauthy-CTVg4kvo.d.mts → cauthy-2jtP2LVQ.d.mts} +19 -1
  3. package/dist/{cauthy-BAwAJChs.d.ts → cauthy-CKfvvv_i.d.ts} +19 -1
  4. package/dist/cli.js +163 -59
  5. package/dist/client.d.mts +91 -10
  6. package/dist/client.d.ts +91 -10
  7. package/dist/client.js +39 -3
  8. package/dist/client.js.map +1 -1
  9. package/dist/client.mjs +39 -3
  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 +387 -148
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +385 -148
  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 +275 -23
  26. package/dist/react.d.ts +275 -23
  27. package/dist/react.js +312 -78
  28. package/dist/react.js.map +1 -1
  29. package/dist/react.mjs +311 -78
  30. package/dist/react.mjs.map +1 -1
  31. package/dist/styles.css +8 -0
  32. package/dist/svelte.d.mts +2 -2
  33. package/dist/svelte.d.ts +2 -2
  34. package/dist/svelte.js +53 -4
  35. package/dist/svelte.js.map +1 -1
  36. package/dist/svelte.mjs +53 -4
  37. package/dist/svelte.mjs.map +1 -1
  38. package/dist/{system-CVBW_AV8.d.mts → system-BrrJsrFW.d.mts} +321 -109
  39. package/dist/{system-Bn8uckvQ.d.ts → system-BvvoNs2y.d.ts} +321 -109
  40. package/dist/vue.d.mts +2 -2
  41. package/dist/vue.d.ts +2 -2
  42. package/dist/vue.js +53 -4
  43. package/dist/vue.js.map +1 -1
  44. package/dist/vue.mjs +53 -4
  45. package/dist/vue.mjs.map +1 -1
  46. 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,9 @@ 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") ?? "";
749
+ const acceptInviteUrl = given.acceptInviteUrl ?? env("CAUTHY_ACCEPT_INVITE_URL") ?? "";
750
+ const inviteHeaders = /^https?:\/\//.test(acceptInviteUrl) ? { "x-cauthy-client-id": clientId, "x-cauthy-link-to": acceptInviteUrl } : {};
537
751
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
538
752
  const tokenEndpoint = `${issuer}/v1/oauth/token`;
539
753
  function configured() {
@@ -576,10 +790,10 @@ function createSystem(given = {}) {
576
790
  asking = void 0;
577
791
  });
578
792
  }
579
- async function call(method, path, body) {
793
+ async function call(method, path, body, extra = {}) {
580
794
  const send = async () => doFetch(`${issuer}${path}`, {
581
795
  method,
582
- headers: { authorization: `Bearer ${await token()}`, ...body === void 0 ? {} : { "content-type": "application/json" } },
796
+ headers: { authorization: `Bearer ${await token()}`, ...extra, ...body === void 0 ? {} : { "content-type": "application/json" } },
583
797
  body: body === void 0 ? void 0 : JSON.stringify(body)
584
798
  });
585
799
  let res = await send();
@@ -590,7 +804,7 @@ function createSystem(given = {}) {
590
804
  if (!res.ok) await fail(res, `${method} ${path}`);
591
805
  return res.status === 204 ? void 0 : res.json();
592
806
  }
593
- const data = async (method, path, body) => (await call(method, path, body))?.data;
807
+ const data = async (method, path, body, extra) => (await call(method, path, body, extra))?.data;
594
808
  const id = encodeURIComponent;
595
809
  return {
596
810
  token,
@@ -627,7 +841,8 @@ function createSystem(given = {}) {
627
841
  return toMember(await data(
628
842
  "POST",
629
843
  `/v1/organizations/${id(orgId)}/members`,
630
- { role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId }
844
+ { role: m.role, user_id: m.userId, email: m.email, name: m.name, locale: m.locale, client_id: clientId },
845
+ inviteHeaders
631
846
  ));
632
847
  },
633
848
  async setRole(orgId, userId, role) {
@@ -638,7 +853,7 @@ function createSystem(given = {}) {
638
853
  }
639
854
  },
640
855
  async invite(person) {
641
- const u = await data("POST", "/v1/users", { ...person, client_id: clientId });
856
+ const u = await data("POST", "/v1/users", { ...person, client_id: clientId }, inviteHeaders);
642
857
  return { id: u.id, email: u.email };
643
858
  },
644
859
  theme: {
@@ -651,6 +866,19 @@ function createSystem(given = {}) {
651
866
  return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
652
867
  }
653
868
  },
869
+ catalog: {
870
+ async get(system) {
871
+ return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
872
+ },
873
+ async set(system, resources) {
874
+ return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
875
+ }
876
+ },
877
+ async check(q) {
878
+ const system = q.system ?? ownSystem;
879
+ if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
880
+ return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
881
+ },
654
882
  async publicJwks() {
655
883
  if (!key) return { keys: [] };
656
884
  const { kty, n, e } = asJwk(key) ?? await (0, import_jose2.exportJWK)(await signingKey());
@@ -798,6 +1026,12 @@ function checkOrganization(value, required = false) {
798
1026
  if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
799
1027
  return null;
800
1028
  }
1029
+ function checkOrganizationName(value) {
1030
+ const v = typeof value === "string" ? value.trim() : "";
1031
+ if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
1032
+ if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
1033
+ return null;
1034
+ }
801
1035
  function checkCode(value) {
802
1036
  const v = typeof value === "string" ? value.trim() : "";
803
1037
  if (v === "") return { field: "code", code: "required", message: "Enter the code." };
@@ -929,6 +1163,9 @@ function redirect(location, cookies = []) {
929
1163
  for (const c of cookies) h.append("set-cookie", c);
930
1164
  return new Response(null, { status: 302, headers: h });
931
1165
  }
1166
+ function conditionalOf(c) {
1167
+ return { resource: String(c?.resource ?? ""), action: String(c?.action ?? ""), effect: c?.effect === "deny" ? "deny" : "allow" };
1168
+ }
932
1169
  function createCauthy(given = {}) {
933
1170
  const issuer = withoutTrailingSlashes(given.issuer ?? env2("CAUTHY_ISSUER") ?? "");
934
1171
  const clientId = given.clientId ?? env2("CAUTHY_CLIENT_ID") ?? "";
@@ -951,13 +1188,16 @@ function createCauthy(given = {}) {
951
1188
  const againCookie = `${cookieName}_again`;
952
1189
  const stepCookie = `${cookieName}_step`;
953
1190
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
1191
+ const iconOf = iconFor(issuer);
954
1192
  const key = keyOf(secret);
955
1193
  const pages = {
956
1194
  signIn: given.pages?.signIn ? localPath(given.pages.signIn, "/") : void 0,
957
1195
  resetPassword: given.pages?.resetPassword ? localPath(given.pages.resetPassword, "/") : void 0,
958
- verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0
1196
+ verifyEmail: given.pages?.verifyEmail ? localPath(given.pages.verifyEmail, "/") : void 0,
1197
+ acceptInvite: given.pages?.acceptInvite ? localPath(given.pages.acceptInvite, "/") : void 0
959
1198
  };
960
- const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, fetch: given.fetch });
1199
+ const acceptInviteUrl = baseURL && pages.acceptInvite ? `${withoutTrailingSlashes(baseURL)}${pages.acceptInvite}` : void 0;
1200
+ const ownSystem = createSystem({ issuer, clientId, clientSecret, key: given.systemKey, system: system || void 0, acceptInviteUrl, fetch: given.fetch });
961
1201
  const originOf = (request) => {
962
1202
  if (baseURL) return withoutTrailingSlashes(baseURL);
963
1203
  const url = new URL(request.url);
@@ -1092,13 +1332,35 @@ function createCauthy(given = {}) {
1092
1332
  role: String(o.role ?? ""),
1093
1333
  metadata: o.metadata ?? {},
1094
1334
  ...o.external_id ? { externalId: String(o.external_id) } : {},
1095
- ...Array.isArray(o.permissions) ? { permissions: o.permissions.map(String), denied: Array.isArray(o.denied) ? o.denied.map(String) : [] } : {}
1335
+ ...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) : [] } : {}
1096
1336
  })
1097
1337
  );
1098
1338
  if (orgCache.size > 5e3) orgCache.clear();
1099
1339
  orgCache.set(session.accessToken, { at: Date.now(), value });
1100
1340
  return value;
1101
1341
  }
1342
+ async function decide(token2, question) {
1343
+ const org = organizationIdOf(question.organization);
1344
+ if (org === "") return { allowed: false, reason: "no organization named" };
1345
+ const res = await doFetch(`${issuer}/v1/oauth/organizations/${encodeURIComponent(org)}/check`, {
1346
+ method: "POST",
1347
+ headers: { authorization: `Bearer ${token2}`, "content-type": "application/json", accept: "application/json" },
1348
+ body: JSON.stringify(checkWire(question))
1349
+ }).catch(() => null);
1350
+ if (res === null) throw new CauthyApiError(503, "unavailable", "Cauthy could not be reached to check a permission");
1351
+ const body2 = await res.json().catch(() => null);
1352
+ if (res.ok) return checkResultOf(body2?.data);
1353
+ const first = body2?.errors?.[0];
1354
+ if (res.status === 401 || res.status === 403 || res.status === 404) {
1355
+ return { allowed: false, reason: first?.message ?? (res.status === 401 ? "not signed in" : res.status === 404 ? "not a member of the organization" : "refused") };
1356
+ }
1357
+ throw new CauthyApiError(res.status, first?.code ?? "error", `Cauthy refused to check a permission (${res.status}): ${first?.message ?? res.statusText}`);
1358
+ }
1359
+ async function check(who, question) {
1360
+ const session = "accessToken" in who ? who : await getSession(who);
1361
+ if (!session) return { allowed: false, reason: "not signed in" };
1362
+ return decide(session.accessToken, question);
1363
+ }
1102
1364
  const forgetting = (fn) => async (...args) => {
1103
1365
  const out2 = await fn(...args);
1104
1366
  orgCache.clear();
@@ -1231,7 +1493,15 @@ function createCauthy(given = {}) {
1231
1493
  name: String(d?.name ?? ""),
1232
1494
  kind: String(d?.kind ?? ""),
1233
1495
  role: String(d?.role ?? ""),
1234
- can: { readMembers: d?.can?.read_members === true, manageMembers: d?.can?.manage_members === true }
1496
+ // A Cauthy from before a power says nothing of it: then it is not given.
1497
+ can: {
1498
+ readMembers: d?.can?.read_members === true,
1499
+ manageMembers: d?.can?.manage_members === true,
1500
+ removeMembers: d?.can?.remove_members === true,
1501
+ update: d?.can?.update === true,
1502
+ archive: d?.can?.archive === true,
1503
+ delete: d?.can?.delete === true
1504
+ }
1235
1505
  });
1236
1506
  const orgMemberOf = (d) => ({
1237
1507
  userId: String(d?.user_id ?? ""),
@@ -1354,6 +1624,14 @@ function createCauthy(given = {}) {
1354
1624
  for (const c of [...await write(request, s, readCookies(request.headers.get("cookie"))), ...extra]) h.append("set-cookie", c);
1355
1625
  return json({ status: "signed_in", session: await toPublic(toSession(s)), ...more }, 200, h);
1356
1626
  }
1627
+ async function loggedIn(request, data) {
1628
+ if (data?.mfa_required === true) {
1629
+ const step = { kind: "mfa", token: String(data.mfa_token ?? ""), returnTo: afterSignIn };
1630
+ const how = data.mfa_method === "email" ? { method: "email" } : {};
1631
+ return json({ status: "mfa_required", ...how }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1632
+ }
1633
+ return signedIn(request, data);
1634
+ }
1357
1635
  let lookCache = null;
1358
1636
  let policyCache = null;
1359
1637
  const keepPolicy = (settings) => {
@@ -1424,7 +1702,12 @@ function createCauthy(given = {}) {
1424
1702
  "/me/organizations/members/add",
1425
1703
  "/me/organizations/members/role",
1426
1704
  "/me/organizations/members/remove",
1427
- "/me/organizations/members/invite"
1705
+ "/me/organizations/members/invite",
1706
+ "/me/organizations/rename",
1707
+ "/me/organizations/archive",
1708
+ "/me/organizations/delete",
1709
+ "/invitation",
1710
+ "/invitation/accept"
1428
1711
  ].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";
1429
1712
  if (!own) return null;
1430
1713
  if (tenant === "") {
@@ -1446,17 +1729,21 @@ function createCauthy(given = {}) {
1446
1729
  ]);
1447
1730
  const list = Array.isArray(providers.data) ? providers.data : [];
1448
1731
  const password = keepPolicy(settings);
1449
- const here = place.data?.here === true;
1732
+ const passkeys = settings.data?.allow_passkeys !== false;
1733
+ const here = passkeys && place.data?.here === true;
1450
1734
  return json({
1451
- providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? "") })).filter((p) => p.id !== ""),
1735
+ providers: list.map((p) => ({ id: String(p.provider ?? p.id ?? ""), name: String(p.label ?? p.name ?? p.provider ?? ""), ...iconOf(p) })).filter((p) => p.id !== ""),
1736
+ // Whether the workspace signs people in with a password at all. A
1737
+ // Cauthy from before the setting says nothing of it: then it does.
1738
+ passwordSignIn: settings.data?.allow_password !== false,
1452
1739
  signUp: settings.data?.allow_signup !== false,
1453
1740
  // The application's system makes an organization per sign-up: the form asks the company's name.
1454
1741
  ...settings.data?.signup?.organization?.ask === true ? { organization: { ask: true, required: settings.data.signup.organization.required === true } } : {},
1455
1742
  // A sign-in link needs a page of the application to open.
1456
1743
  magicLink: settings.data?.allow_magic_link === true && pages.signIn !== void 0,
1457
- passkey: place.error ? true : here || place.data?.on_cauthy === true,
1744
+ passkey: passkeys && (place.error ? true : here || place.data?.on_cauthy === true),
1458
1745
  passkeyHere: here,
1459
- passkeyOnCauthy: place.error ? true : place.data?.on_cauthy === true,
1746
+ passkeyOnCauthy: passkeys && (place.error ? true : place.data?.on_cauthy === true),
1460
1747
  // A company's sign-in, found by the person's work address.
1461
1748
  sso: Array.isArray(sso.data) && sso.data.some((c) => c?.discoverable === true),
1462
1749
  // The terms a sign-up accepts and the consents it answers, when the workspace asks any.
@@ -1466,6 +1753,14 @@ function createCauthy(given = {}) {
1466
1753
  ...password ? { password } : {}
1467
1754
  });
1468
1755
  }
1756
+ if (route === "/invitation" && !post) {
1757
+ const token2 = (url.searchParams.get("token") ?? "").trim();
1758
+ if (token2 === "" || token2.length > 512) return problem(422, "invalid_token", "This invitation is not valid, or it has expired.");
1759
+ const res = await ask(request, "GET", `/v1/auth/invitation?token=${encodeURIComponent(token2)}`);
1760
+ if (res.error) return problem(res.status, res.error.code, res.error.message);
1761
+ const organization = typeof res.data?.organization === "string" && res.data.organization !== "" ? { organization: res.data.organization } : {};
1762
+ return json({ status: "ok", invitation: { email: String(res.data?.email ?? ""), workspace: String(res.data?.workspace ?? ""), ...organization } });
1763
+ }
1469
1764
  if (route === "/sign-in/sso" && !post) {
1470
1765
  const returnTo = localPath(url.searchParams.get("return_to"), afterSignIn);
1471
1766
  const failed2 = (code) => redirect(`${returnTo}${returnTo.includes("?") ? "&" : "?"}auth_error=${encodeURIComponent(code)}`);
@@ -1695,12 +1990,7 @@ function createCauthy(given = {}) {
1695
1990
  if (bad) return invalid(bad);
1696
1991
  const res = await ask(request, "POST", "/v1/auth/login", { email: String(b.email).trim(), password: b.password });
1697
1992
  if (res.error) return problem(res.status, res.error.code, res.error.message);
1698
- if (res.data?.mfa_required === true) {
1699
- const step = { kind: "mfa", token: String(res.data.mfa_token ?? ""), returnTo: afterSignIn };
1700
- const how = res.data.mfa_method === "email" ? { method: "email" } : {};
1701
- return json({ status: "mfa_required", ...how }, 200, new Headers({ "set-cookie": writeCookie(stepCookie, await seal(step, await key), attrs(request, STEP_SECONDS)) }));
1702
- }
1703
- return signedIn(request, res.data);
1993
+ return loggedIn(request, res.data);
1704
1994
  }
1705
1995
  if (route === "/sign-in/mfa") {
1706
1996
  const bad = checkCode(b.code);
@@ -1895,6 +2185,23 @@ function createCauthy(given = {}) {
1895
2185
  if (res.error) return refused(res, res.error.code === "weak_password" ? "password" : "email");
1896
2186
  return signedIn(request, res.data);
1897
2187
  }
2188
+ if (route === "/me/organizations/rename" || route === "/me/organizations/archive" || route === "/me/organizations/delete") {
2189
+ const org = typeof b.organizationId === "string" ? b.organizationId : "";
2190
+ if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
2191
+ const path = `/v1/auth/me/organizations/${org}`;
2192
+ if (route === "/me/organizations/rename") {
2193
+ const bad = checkOrganizationName(b.name);
2194
+ if (bad) return invalid(bad);
2195
+ const res2 = await as(path, { name: String(b.name).trim() }, "PATCH");
2196
+ if (res2.error) return refused(res2, res2.error.code === "validation_error" ? "name" : void 0);
2197
+ orgCache.delete(mine.token);
2198
+ return json({ status: "renamed", organization: seatOf(res2.data) }, 200, mine.headers);
2199
+ }
2200
+ const res = route === "/me/organizations/archive" ? await as(`${path}/archive`) : await as(path, void 0, "DELETE");
2201
+ if (res.error) return refused(res);
2202
+ orgCache.delete(mine.token);
2203
+ return json({ status: route === "/me/organizations/archive" ? "archived" : "deleted", organizationId: org }, 200, mine.headers);
2204
+ }
1898
2205
  if (route.startsWith("/me/organizations/members/")) {
1899
2206
  const org = typeof b.organizationId === "string" ? b.organizationId : "";
1900
2207
  if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, mine.headers);
@@ -1904,13 +2211,13 @@ function createCauthy(given = {}) {
1904
2211
  if (bad) return invalid(bad);
1905
2212
  const role = typeof b.role === "string" ? b.role : "";
1906
2213
  if (role === "") return problem(400, "invalid_role", "Choose a role.", "role", mine.headers);
1907
- const res = await as(base, {
2214
+ const res = await ask(request, "POST", base, {
1908
2215
  email: String(b.email).trim(),
1909
2216
  role,
1910
2217
  name: typeof b.name === "string" ? b.name.trim() : "",
1911
2218
  client_id: clientId,
1912
2219
  ...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {}
1913
- });
2220
+ }, pages.acceptInvite, mine.token);
1914
2221
  if (res.error) return refused(res, res.error.code === "unknown_role" ? "role" : "email");
1915
2222
  return json({ status: "added", member: orgMemberOf(res.data) }, 200, mine.headers);
1916
2223
  }
@@ -1930,7 +2237,7 @@ function createCauthy(given = {}) {
1930
2237
  return json({ status: "removed", userId: user }, 200, mine.headers);
1931
2238
  }
1932
2239
  if (route === "/me/organizations/members/invite") {
1933
- const res = await as(`${base}/${user}/invite`, { client_id: clientId, ...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {} });
2240
+ const res = await ask(request, "POST", `${base}/${user}/invite`, { client_id: clientId, ...typeof b.locale === "string" && b.locale ? { locale: b.locale } : {} }, pages.acceptInvite, mine.token);
1934
2241
  if (res.error) return refused(res);
1935
2242
  return json({ status: "sent", userId: user }, 200, mine.headers);
1936
2243
  }
@@ -1969,6 +2276,14 @@ function createCauthy(given = {}) {
1969
2276
  if (res.data?.access_token) return signedIn(request, res.data);
1970
2277
  return json({ status: "verified" });
1971
2278
  }
2279
+ if (route === "/invitation/accept") {
2280
+ if (!isToken(b.token)) return problem(422, "invalid_token", "This invitation is not valid, or it has expired.");
2281
+ const bad = await newPasswordProblem(request, b.password);
2282
+ if (bad) return invalid(bad);
2283
+ const res = await ask(request, "POST", "/v1/auth/invitation/accept", { token: String(b.token), password: b.password });
2284
+ if (res.error) return problem(res.status, res.error.code, res.error.message, res.error.code === "weak_password" ? "password" : void 0, void 0, res.error.reasons);
2285
+ return loggedIn(request, res.data);
2286
+ }
1972
2287
  if (route === "/sign-in/passkey/begin") {
1973
2288
  const email = typeof b.email === "string" && checkEmail(b.email) === null ? b.email.trim() : "";
1974
2289
  const res = await ask(request, "POST", "/v1/auth/webauthn/login/begin", email !== "" ? { email } : {});
@@ -2040,6 +2355,7 @@ function createCauthy(given = {}) {
2040
2355
  const get = request.method === "GET" || request.method === "HEAD";
2041
2356
  const mine = await ownRoute(request, url, route);
2042
2357
  if (mine !== null) return mine;
2358
+ if (route === "/me/organizations/check") return checkRoute(request);
2043
2359
  if (route === "/login" && get) return login(request, url);
2044
2360
  if (route === "/callback" && get) return callback(request, url);
2045
2361
  if (route === "/logout" && (get || request.method === "POST")) return logout(request);
@@ -2066,6 +2382,36 @@ function createCauthy(given = {}) {
2066
2382
  }
2067
2383
  return new Response("Not found", { status: 404 });
2068
2384
  }
2385
+ async function checkRoute(request) {
2386
+ if (request.method !== "POST") return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
2387
+ if (!ownRequest(request)) return problem(403, "cross_site", "This request did not come from this application.");
2388
+ const b = await body(request);
2389
+ if (b === null) return problem(400, "bad_request", "Send the question as JSON.");
2390
+ const asker = await signedInAs(request);
2391
+ if ("response" in asker) return asker.response;
2392
+ const org = typeof b.organizationId === "string" ? b.organizationId : "";
2393
+ if (!UUID.test(org)) return problem(400, "invalid_organization", "Say which organization.", void 0, asker.headers);
2394
+ const named = (v) => typeof v === "string" && v !== "" && v.length <= 200;
2395
+ if (!named(b.resource) || !named(b.action)) return problem(400, "bad_request", "Say which resource and action.", void 0, asker.headers);
2396
+ const attributes2 = b.attributes;
2397
+ if (attributes2 !== void 0 && (typeof attributes2 !== "object" || attributes2 === null || Array.isArray(attributes2))) {
2398
+ return problem(400, "bad_request", "The attributes are an object.", void 0, asker.headers);
2399
+ }
2400
+ try {
2401
+ const r = await decide(asker.token, {
2402
+ organization: org,
2403
+ resource: b.resource,
2404
+ action: b.action,
2405
+ ...attributes2 !== void 0 ? { attributes: attributes2 } : {},
2406
+ ...typeof b.resourceId === "string" ? { resourceId: b.resourceId } : {},
2407
+ ...typeof b.ownerId === "string" ? { ownerId: b.ownerId } : {}
2408
+ });
2409
+ return json({ status: "decided", ...r }, 200, asker.headers);
2410
+ } catch (e) {
2411
+ const err = e instanceof CauthyApiError ? e : new CauthyApiError(503, "unavailable", "Cauthy could not be reached.");
2412
+ return problem(err.status, err.code, err.message.replace(/^cauthy-auth: /, ""), void 0, asker.headers);
2413
+ }
2414
+ }
2069
2415
  async function protect(request) {
2070
2416
  const { value } = await read(request);
2071
2417
  if (value !== null && fresh(value)) return { session: toSession(value) };
@@ -2089,11 +2435,24 @@ function createCauthy(given = {}) {
2089
2435
  resolve: resolve2,
2090
2436
  protect,
2091
2437
  organizations,
2438
+ check,
2092
2439
  toPublic,
2093
2440
  urls,
2094
2441
  options: { issuer, clientId, basePath, afterSignIn, afterSignOut, cookieName }
2095
2442
  };
2096
2443
  }
2444
+ function iconFor(issuer) {
2445
+ return (p) => {
2446
+ const raw = typeof p?.icon_url === "string" ? p.icon_url.trim() : "";
2447
+ if (raw === "") return {};
2448
+ try {
2449
+ const url = new URL(raw.startsWith("/") && !raw.startsWith("//") ? `${issuer}${raw}` : raw);
2450
+ return url.protocol === "https:" || url.protocol === "http:" ? { iconUrl: url.href } : {};
2451
+ } catch {
2452
+ return {};
2453
+ }
2454
+ };
2455
+ }
2097
2456
  function legalOf(raw) {
2098
2457
  const documents = Array.isArray(raw?.documents) ? raw.documents : [];
2099
2458
  const purposes = Array.isArray(raw?.consent_purposes) ? raw.consent_purposes : [];
@@ -2103,128 +2462,6 @@ function legalOf(raw) {
2103
2462
  };
2104
2463
  }
2105
2464
 
2106
- // src/permissions.ts
2107
- function matches(entry, resource, action) {
2108
- const i = entry.lastIndexOf(":");
2109
- if (i < 0) return false;
2110
- const r = entry.slice(0, i);
2111
- const a = entry.slice(i + 1);
2112
- return (r === "*" || r === resource) && (a === "*" || a === action);
2113
- }
2114
- function can(organization, resource, action) {
2115
- if (!organization?.permissions) return false;
2116
- if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
2117
- return organization.permissions.some((p) => matches(p, resource, action));
2118
- }
2119
- function definePolicy(roles) {
2120
- const entry = (p) => `${p.resource}:${p.action}`;
2121
- const byName = new Map(roles.map((r) => [r.role, r]));
2122
- const split = (permission) => {
2123
- const i = permission.lastIndexOf(":");
2124
- return i < 0 ? [permission, ""] : [permission.slice(0, i), permission.slice(i + 1)];
2125
- };
2126
- return {
2127
- roles: roles.map((r) => ({
2128
- role: r.role,
2129
- ...r.name !== void 0 ? { name: r.name } : {},
2130
- ...r.description !== void 0 ? { description: r.description } : {},
2131
- ...r.inherits ? { inherits: [...r.inherits] } : {},
2132
- ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
2133
- })),
2134
- roleNames: roles.map((r) => r.role),
2135
- permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
2136
- can(organization, permission) {
2137
- const [resource, action] = split(permission);
2138
- return can(organization, resource, action);
2139
- },
2140
- hasRole(organization, ...wanted) {
2141
- return organization != null && wanted.includes(organization.role);
2142
- },
2143
- permissionsOf(role) {
2144
- const seen = /* @__PURE__ */ new Set();
2145
- const out2 = /* @__PURE__ */ new Set();
2146
- const walk = (name) => {
2147
- if (seen.has(name)) return;
2148
- seen.add(name);
2149
- const r = byName.get(name);
2150
- for (const p of r?.permissions ?? []) out2.add(entry(p));
2151
- for (const parent of r?.inherits ?? []) walk(parent);
2152
- };
2153
- walk(role);
2154
- return [...out2];
2155
- }
2156
- };
2157
- }
2158
- var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2159
- function policySource(system, roles) {
2160
- const text = (v) => JSON.stringify(v);
2161
- const key = (k) => IDENT.test(k) ? k : text(k);
2162
- const one = (r) => {
2163
- const parts = [`role: ${text(r.role)}`];
2164
- if (r.name) parts.push(`name: ${text(r.name)}`);
2165
- if (r.description) parts.push(`description: ${text(r.description)}`);
2166
- if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
2167
- if (r.permissions?.length) {
2168
- const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
2169
- parts.push(`permissions: [
2170
- ${list.join("\n")}
2171
- ]`);
2172
- }
2173
- return ` {
2174
- ${parts.join(",\n ")},
2175
- },`;
2176
- };
2177
- return `// The catalog of roles of ${text(system)}, as Cauthy has it.
2178
- // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
2179
- // changes, and do not edit this file by hand.
2180
-
2181
- import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
2182
-
2183
- export const policy = definePolicy([
2184
- ${roles.map(one).join("\n")}
2185
- ])
2186
-
2187
- export const { can, hasRole } = policy
2188
- export type Role = RoleOf<typeof policy>
2189
- export type Permission = PermissionOf<typeof policy>
2190
- `;
2191
- }
2192
- async function pullPolicy(o = {}) {
2193
- const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
2194
- const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
2195
- const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
2196
- const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
2197
- const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
2198
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
2199
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
2200
- }
2201
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
2202
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
2203
- });
2204
- const body = await res.json().catch(() => null);
2205
- if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
2206
- return body?.data?.roles ?? [];
2207
- }
2208
- async function pushPolicy(o) {
2209
- const env4 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
2210
- const issuer = withoutTrailingSlashes(o.issuer ?? env4("CAUTHY_ISSUER") ?? "");
2211
- const system = o.system ?? env4("CAUTHY_SYSTEM") ?? "";
2212
- const tenant = o.tenant ?? env4("CAUTHY_TENANT") ?? "";
2213
- const token = o.token ?? env4("CAUTHY_TOKEN") ?? "";
2214
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
2215
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
2216
- }
2217
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
2218
- method: "PUT",
2219
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
2220
- body: JSON.stringify({ roles: o.roles })
2221
- });
2222
- if (!res.ok) {
2223
- const body = await res.json().catch(() => null);
2224
- throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
2225
- }
2226
- }
2227
-
2228
2465
  // src/connect.ts
2229
2466
  function sessionSecret() {
2230
2467
  const bytes = crypto.getRandomValues(new Uint8Array(32));
@@ -2350,7 +2587,9 @@ async function verifyToken(from, options = {}) {
2350
2587
  passwordPolicyOf,
2351
2588
  passwordProblems,
2352
2589
  policySource,
2590
+ pullCatalog,
2353
2591
  pullPolicy,
2592
+ pushCatalog,
2354
2593
  pushPolicy,
2355
2594
  pushTheme,
2356
2595
  themeFromStackmyth,