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/README.md CHANGED
@@ -12,7 +12,7 @@ npm install cauthy-auth
12
12
  ```
13
13
 
14
14
  ```ts
15
- // auth.ts — reads CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_SECRET (and CAUTHY_SYSTEM)
15
+ // auth.ts — reads CAUTHY_ISSUER, CAUTHY_CLIENT_ID, CAUTHY_SECRET (and CAUTHY_SYSTEM, CAUTHY_BASE_URL)
16
16
  import { createCauthy } from 'cauthy-auth'
17
17
  export const auth = createCauthy()
18
18
  ```
@@ -64,6 +64,33 @@ import { Account } from 'cauthy-auth/react'
64
64
  <Account /> // or: const { account } = useAuth()
65
65
  ```
66
66
 
67
+ And the person's organizations, run by their own members: the people of one, and the organization itself. Cauthy decides each change by the person's role in the system's catalog, with their own session:
68
+
69
+ ```tsx
70
+ import { OrganizationMembers } from 'cauthy-auth/react'
71
+ <OrganizationMembers organizationId={org.id} /> // shows only what the person's role allows
72
+
73
+ const { organizations } = useAuth()
74
+ const [org] = await organizations.list() // org.can: { readMembers, manageMembers, removeMembers, update, archive, delete }
75
+ await organizations.add(org.id, { email: 'ana@example.com', role: 'reader' })
76
+ await organizations.setRole(org.id, userId, 'admin')
77
+ await organizations.remove(org.id, userId) // anybody may remove themselves: leave
78
+ await organizations.rename(org.id, 'Acme Books') // r.organization
79
+ await organizations.archive(org.id) // only the workspace restores it
80
+ await organizations.delete(org.id) // for good
81
+ ```
82
+
83
+ | Reserved power | Lets a member |
84
+ | --- | --- |
85
+ | `cauthy:members:read` | see the people (`can.readMembers`) |
86
+ | `cauthy:members:manage` | add and invite people, change their role (`can.manageMembers`) |
87
+ | `cauthy:members:remove` | remove another member (`can.removeMembers`) |
88
+ | `cauthy:organization:update` | rename the organization (`can.update`) |
89
+ | `cauthy:organization:archive` | archive it (`can.archive`) |
90
+ | `cauthy:organization:delete` | delete it for good (`can.delete`) |
91
+
92
+ Every catalog has a reserved role, `owner`, with all of them. Only an owner gives the owner role, or changes or removes another owner, and an organization always keeps one: its last owner cannot be removed, demoted or leave (`last_owner`). On the server they read like any other permission: `can(organization, 'cauthy:organization', 'delete')`.
93
+
67
94
  Roles and permissions with their names, from the catalog Cauthy has:
68
95
 
69
96
  ```bash
@@ -75,6 +102,30 @@ import { can, hasRole } from './policy'
75
102
  can(organization, 'ledger:write') // only what the catalog names compiles
76
103
  ```
77
104
 
105
+ A system may also declare its resources: their actions, and the attributes a condition reads. A permission may then carry a condition, in CEL (`resource.attrs.amount <= 1000000`, `resource.owner_id == subject.id`). Such a permission is not in `permissions`: it comes in `organization.conditional`, `can` never counts it, and Cauthy decides it per question:
106
+
107
+ ```ts
108
+ const r = await auth.check(request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
109
+ r.allowed, r.reason, r.rule // rule: which role or exception decided
110
+
111
+ await organizations.check({ organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } }) // in the browser
112
+ await auth.system.check({ organizationId, userId, resource: 'invoices', action: 'read' }) // about any member
113
+ ```
114
+
115
+ `--pull` writes the declared resources too, and a check is typed by them:
116
+
117
+ ```ts
118
+ export const policy = definePolicy({
119
+ catalog: [{ resource: 'invoices', actions: ['read', 'approve'], attributes: { amount: 'number', branch: 'string' } }],
120
+ roles: [{ role: 'accountant', permissions: [{ resource: 'invoices', action: 'approve', condition: 'resource.attrs.amount <= 1000000' }] }],
121
+ })
122
+
123
+ await policy.check(auth, request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
124
+ // 'payroll', action 'delete' or amount: '500' do not compile
125
+ ```
126
+
127
+ `npx cauthy-auth policy --file policy.json --push` loads a first catalog: a list of roles, or `{ "catalog": [...], "roles": [...] }`, whose resources go first so the roles are checked against them. Pushing takes a personal access token with `systems:policy`.
128
+
78
129
  Each framework has its own few lines:
79
130
 
80
131
  | | |
@@ -1,4 +1,4 @@
1
- import { J as JsonValue, S as Session, l as SessionStatus, k as SessionOrganization, c as CauthySystem, i as PublicSession, b as CauthyOptions } from './system-BxMsSIu1.mjs';
1
+ import { J as JsonValue, S as Session, t as SessionStatus, s as SessionOrganization, f as CheckQuestion, k as OrganizationRef, g as CheckResult, d as CauthySystem, q as PublicSession, c as CauthyOptions } from './system-0mHrdAgO.mjs';
2
2
  import { JWTVerifyGetKey } from 'jose';
3
3
 
4
4
  /** Something that happened in Cauthy, as a subscriber is told. Moments are UTC. */
@@ -80,6 +80,24 @@ interface Cauthy {
80
80
  }>;
81
81
  /** The organizations the person belongs to (of `system`, when set), with their role in each. */
82
82
  organizations(session: Session): Promise<SessionOrganization[]>;
83
+ /**
84
+ * Asks Cauthy whether the signed-in person may do an action on a resource
85
+ * in an organization, with what its conditions read (`attributes`, whose
86
+ * the record is). It is how a permission with a condition is decided —
87
+ * `can` never counts those — and it decides any other the same way.
88
+ *
89
+ * const r = await auth.check(request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
90
+ * if (!r.allowed) return forbidden(r.reason)
91
+ *
92
+ * Nobody signed in, an organization the person is not in, or a disabled
93
+ * account answer `allowed: false`. A request whose session must be renewed
94
+ * first reads as nobody signed in: give it the `Session` from
95
+ * `getSession(request, responseHeaders)` instead. Throws `CauthyApiError`
96
+ * when Cauthy refuses the question itself (a malformed one) or cannot be reached.
97
+ */
98
+ check(who: Request | Session, question: CheckQuestion & {
99
+ organization: OrganizationRef;
100
+ }): Promise<CheckResult>;
83
101
  /**
84
102
  * Verifies a webhook delivery from Cauthy and answers the event it carries;
85
103
  * throws `CauthyWebhookError` for anything else. No secret is involved: the
@@ -1,4 +1,4 @@
1
- import { J as JsonValue, S as Session, l as SessionStatus, k as SessionOrganization, c as CauthySystem, i as PublicSession, b as CauthyOptions } from './system-DTIeDXDP.js';
1
+ import { J as JsonValue, S as Session, t as SessionStatus, s as SessionOrganization, f as CheckQuestion, k as OrganizationRef, g as CheckResult, d as CauthySystem, q as PublicSession, c as CauthyOptions } from './system-BIYbS7h2.js';
2
2
  import { JWTVerifyGetKey } from 'jose';
3
3
 
4
4
  /** Something that happened in Cauthy, as a subscriber is told. Moments are UTC. */
@@ -80,6 +80,24 @@ interface Cauthy {
80
80
  }>;
81
81
  /** The organizations the person belongs to (of `system`, when set), with their role in each. */
82
82
  organizations(session: Session): Promise<SessionOrganization[]>;
83
+ /**
84
+ * Asks Cauthy whether the signed-in person may do an action on a resource
85
+ * in an organization, with what its conditions read (`attributes`, whose
86
+ * the record is). It is how a permission with a condition is decided —
87
+ * `can` never counts those — and it decides any other the same way.
88
+ *
89
+ * const r = await auth.check(request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
90
+ * if (!r.allowed) return forbidden(r.reason)
91
+ *
92
+ * Nobody signed in, an organization the person is not in, or a disabled
93
+ * account answer `allowed: false`. A request whose session must be renewed
94
+ * first reads as nobody signed in: give it the `Session` from
95
+ * `getSession(request, responseHeaders)` instead. Throws `CauthyApiError`
96
+ * when Cauthy refuses the question itself (a malformed one) or cannot be reached.
97
+ */
98
+ check(who: Request | Session, question: CheckQuestion & {
99
+ organization: OrganizationRef;
100
+ }): Promise<CheckResult>;
83
101
  /**
84
102
  * Verifies a webhook delivery from Cauthy and answers the event it carries;
85
103
  * throws `CauthyWebhookError` for anything else. No secret is involved: the
package/dist/cli.js CHANGED
@@ -368,73 +368,147 @@ ${declaration}() {
368
368
 
369
369
  // src/permissions.ts
370
370
  var IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
371
- function policySource(system, roles) {
371
+ function policySource(system, read) {
372
+ const roles = Array.isArray(read) ? read : read.roles;
373
+ const catalog = Array.isArray(read) ? [] : read.catalog ?? [];
372
374
  const text = (v) => JSON.stringify(v);
373
375
  const key = (k) => IDENT.test(k) ? k : text(k);
374
- const one = (r) => {
376
+ const one = (r, pad) => {
375
377
  const parts = [`role: ${text(r.role)}`];
376
378
  if (r.name) parts.push(`name: ${text(r.name)}`);
377
379
  if (r.description) parts.push(`description: ${text(r.description)}`);
378
380
  if (r.inherits?.length) parts.push(`inherits: [${r.inherits.map(text).join(", ")}]`);
379
381
  if (r.permissions?.length) {
380
- const list = r.permissions.map((p) => ` { ${[...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [], `resource: ${text(p.resource)}`, `action: ${text(p.action)}`].join(", ")} },`);
382
+ const list = r.permissions.map((p) => `${pad} { ${[
383
+ ...p.kind && p.kind !== "*" ? [`${key("kind")}: ${text(p.kind)}`] : [],
384
+ `resource: ${text(p.resource)}`,
385
+ `action: ${text(p.action)}`,
386
+ ...p.condition ? [`condition: ${text(p.condition)}`] : []
387
+ ].join(", ")} },`);
381
388
  parts.push(`permissions: [
382
389
  ${list.join("\n")}
383
- ]`);
390
+ ${pad} ]`);
384
391
  }
385
- return ` {
386
- ${parts.join(",\n ")},
387
- },`;
392
+ return `${pad} {
393
+ ${pad} ${parts.join(`,
394
+ ${pad} `)},
395
+ ${pad} },`;
388
396
  };
389
- return `// The catalog of roles of ${text(system)}, as Cauthy has it.
397
+ const resource = (c) => {
398
+ const parts = [`resource: ${text(c.resource)}`];
399
+ if (c.description) parts.push(`description: ${text(c.description)}`);
400
+ parts.push(`actions: [${c.actions.map(text).join(", ")}]`);
401
+ const attrs = Object.entries(c.attributes ?? {});
402
+ if (attrs.length) parts.push(`attributes: { ${attrs.map(([k, v]) => `${key(k)}: ${text(v)}`).join(", ")} }`);
403
+ return ` { ${parts.join(", ")} },`;
404
+ };
405
+ const head = `// The catalog of roles of ${text(system)}, as Cauthy has it.
390
406
  // Written by \`cauthy-auth policy --pull\`: run it again after the catalog
391
407
  // changes, and do not edit this file by hand.
392
-
408
+ `;
409
+ if (catalog.length === 0) {
410
+ return `${head}
393
411
  import { definePolicy, type PermissionOf, type RoleOf } from 'cauthy-auth'
394
412
 
395
413
  export const policy = definePolicy([
396
- ${roles.map(one).join("\n")}
414
+ ${roles.map((r) => one(r, "")).join("\n")}
397
415
  ])
398
416
 
399
417
  export const { can, hasRole } = policy
400
418
  export type Role = RoleOf<typeof policy>
401
419
  export type Permission = PermissionOf<typeof policy>
402
420
  `;
403
- }
404
- async function pullPolicy(o = {}) {
405
- const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
406
- const issuer = withoutTrailingSlashes(o.issuer ?? env2("CAUTHY_ISSUER") ?? "");
407
- const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
408
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
409
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
410
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
411
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to read a policy`);
412
421
  }
413
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
414
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, accept: "application/json" }
415
- });
416
- const body = await res.json().catch(() => null);
417
- if (!res.ok) throw new Error(`cauthy-auth: Cauthy refused to give the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
418
- return body?.data?.roles ?? [];
422
+ return `${head}
423
+ import { definePolicy, type CheckOf, type PermissionOf, type RoleOf } from 'cauthy-auth'
424
+
425
+ export const policy = definePolicy({
426
+ catalog: [
427
+ ${catalog.map(resource).join("\n")}
428
+ ],
429
+ roles: [
430
+ ${roles.map((r) => one(r, " ")).join("\n")}
431
+ ],
432
+ })
433
+
434
+ export const { can, hasRole } = policy
435
+ export type Role = RoleOf<typeof policy>
436
+ export type Permission = PermissionOf<typeof policy>
437
+ /** What a check may ask: \`policy.check(auth, request, { organization, ...question })\`. */
438
+ export type Check = CheckOf<typeof policy>
439
+ `;
419
440
  }
420
- async function pushPolicy(o) {
441
+ function reach(o, doing) {
421
442
  const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
422
- const issuer = withoutTrailingSlashes(o.issuer ?? env2("CAUTHY_ISSUER") ?? "");
423
- const system = o.system ?? env2("CAUTHY_SYSTEM") ?? "";
424
- const tenant = o.tenant ?? env2("CAUTHY_TENANT") ?? "";
425
- const token = o.token ?? env2("CAUTHY_TOKEN") ?? "";
426
- for (const [name, v] of Object.entries({ CAUTHY_ISSUER: issuer, CAUTHY_SYSTEM: system, CAUTHY_TENANT: tenant, CAUTHY_TOKEN: token })) {
427
- if (v === "") throw new Error(`cauthy-auth: ${name} is required to push a policy`);
443
+ const r = {
444
+ issuer: withoutTrailingSlashes(o.issuer ?? env2("CAUTHY_ISSUER") ?? ""),
445
+ system: o.system ?? env2("CAUTHY_SYSTEM") ?? "",
446
+ tenant: o.tenant ?? env2("CAUTHY_TENANT") ?? "",
447
+ token: o.token ?? env2("CAUTHY_TOKEN") ?? ""
448
+ };
449
+ for (const [name, v] of Object.entries({ CAUTHY_ISSUER: r.issuer, CAUTHY_SYSTEM: r.system, CAUTHY_TENANT: r.tenant, CAUTHY_TOKEN: r.token })) {
450
+ if (v === "") throw new Error(`cauthy-auth: ${name} is required to ${doing}`);
428
451
  }
429
- const res = await (o.fetch ?? fetch)(`${issuer}/v1/systems/${encodeURIComponent(system)}/policy`, {
430
- method: "PUT",
431
- headers: { authorization: `Bearer ${token}`, "x-tenant-key": tenant, "content-type": "application/json" },
432
- body: JSON.stringify({ roles: o.roles })
452
+ return { ...r, fetch: o.fetch ?? fetch };
453
+ }
454
+ async function systemCall(r, method, what, body) {
455
+ const res = await r.fetch(`${r.issuer}/v1/systems/${encodeURIComponent(r.system)}/${what}`, {
456
+ ...method === "PUT" ? { method } : {},
457
+ headers: {
458
+ authorization: `Bearer ${r.token}`,
459
+ "x-tenant-key": r.tenant,
460
+ ...body === void 0 ? { accept: "application/json" } : { "content-type": "application/json" }
461
+ },
462
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
433
463
  });
464
+ const answer = await res.json().catch(() => null);
434
465
  if (!res.ok) {
435
- const body = await res.json().catch(() => null);
436
- throw new Error(`cauthy-auth: Cauthy refused the policy (${res.status}): ${body?.errors?.[0]?.message ?? res.statusText}`);
466
+ const verb = method === "GET" ? `refused to give the ${what}` : `refused the ${what}`;
467
+ throw new Error(`cauthy-auth: Cauthy ${verb} (${res.status}): ${answer?.errors?.[0]?.message ?? res.statusText}`);
437
468
  }
469
+ return answer?.data;
470
+ }
471
+ async function pullPolicy(o = {}) {
472
+ const data = await systemCall(reach(o, "read a policy"), "GET", "policy");
473
+ return data?.roles ?? [];
474
+ }
475
+ async function pullCatalog(o = {}) {
476
+ const data = await systemCall(reach(o, "read a catalog"), "GET", "catalog");
477
+ return data?.resources ?? [];
478
+ }
479
+ async function pushPolicy(o) {
480
+ const r = reach(o, "push a policy");
481
+ if (o.catalog !== void 0) await systemCall(r, "PUT", "catalog", { resources: o.catalog });
482
+ await systemCall(r, "PUT", "policy", { roles: o.roles });
483
+ }
484
+ function readPolicyFile(parsed) {
485
+ if (Array.isArray(parsed)) return { catalog: [], roles: parsed };
486
+ const o = parsed ?? {};
487
+ if (!Array.isArray(o.roles)) throw new Error('no "roles" list');
488
+ if (o.catalog !== void 0 && !Array.isArray(o.catalog)) throw new Error('"catalog" is not a list');
489
+ return { catalog: o.catalog ?? [], roles: o.roles };
490
+ }
491
+ function checkWire(q) {
492
+ return {
493
+ resource: q.resource,
494
+ action: q.action,
495
+ ...q.resourceId !== void 0 ? { resource_id: q.resourceId } : {},
496
+ ...q.ownerId !== void 0 ? { owner_id: q.ownerId } : {},
497
+ ...q.attributes !== void 0 ? { attributes: q.attributes } : {}
498
+ };
499
+ }
500
+ function checkResultOf(d) {
501
+ const r = d?.rule;
502
+ const rule = r && typeof r === "object" ? {
503
+ source: r.source === "exception" ? "exception" : "role",
504
+ ...r.role ? { role: String(r.role) } : {},
505
+ kind: String(r.kind ?? "*"),
506
+ resource: String(r.resource ?? ""),
507
+ action: String(r.action ?? ""),
508
+ effect: r.effect === "allow" ? "allow" : "deny",
509
+ ...r.condition ? { condition: String(r.condition) } : {}
510
+ } : void 0;
511
+ return { allowed: d?.allowed === true, reason: String(d?.reason ?? ""), ...rule ? { rule } : {} };
438
512
  }
439
513
 
440
514
  // src/system.ts
@@ -914,6 +988,7 @@ function createSystem(given = {}) {
914
988
  const clientSecret = given.clientSecret ?? env("CAUTHY_CLIENT_SECRET") ?? "";
915
989
  const envKey = env("CAUTHY_SYSTEM_KEY");
916
990
  const key = given.key ?? (envKey ? { privateKey: envKey, keyId: env("CAUTHY_SYSTEM_KEY_ID") ?? "" } : void 0);
991
+ const ownSystem = given.system ?? env("CAUTHY_SYSTEM") ?? "";
917
992
  const doFetch = (...args) => (given.fetch ?? fetch)(...args);
918
993
  const tokenEndpoint = `${issuer}/v1/oauth/token`;
919
994
  function configured() {
@@ -1031,6 +1106,19 @@ function createSystem(given = {}) {
1031
1106
  return (await data("GET", `/v1/systems/${id(system)}/policy`)).roles ?? [];
1032
1107
  }
1033
1108
  },
1109
+ catalog: {
1110
+ async get(system) {
1111
+ return (await data("GET", `/v1/systems/${id(system)}/catalog`)).resources ?? [];
1112
+ },
1113
+ async set(system, resources) {
1114
+ return (await data("PUT", `/v1/systems/${id(system)}/catalog`, { resources })).resources ?? [];
1115
+ }
1116
+ },
1117
+ async check(q) {
1118
+ const system = q.system ?? ownSystem;
1119
+ if (system === "") throw new CauthyConfigError("name the system to check in (system, or CAUTHY_SYSTEM)");
1120
+ return checkResultOf(await data("POST", `/v1/systems/${id(system)}/check`, { organization_id: q.organizationId, user_id: q.userId, ...checkWire(q) }));
1121
+ },
1034
1122
  async publicJwks() {
1035
1123
  if (!key) return { keys: [] };
1036
1124
  const { kty, n, e } = asJwk(key) ?? await (0, import_jose.exportJWK)(await signingKey());
@@ -1194,10 +1282,12 @@ var POLICY_HELP = `cauthy-auth policy --pull [--out <file.ts>] [--system <name>]
1194
1282
  cauthy-auth policy --file <roles.json> [--system <name>] [--push]
1195
1283
 
1196
1284
  --pull writes the catalog Cauthy has as a TypeScript module, so the
1197
- application's roles and permissions are typed from it:
1285
+ application's roles and permissions are typed from it, and, when the system
1286
+ declares its resources, what a check may ask (resources, actions, attributes):
1198
1287
 
1199
- import { can, hasRole } from './policy'
1288
+ import { can, hasRole, policy } from './policy'
1200
1289
  can(organization, 'ledger:write')
1290
+ await policy.check(auth, request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
1201
1291
 
1202
1292
  --pull Read the catalog from Cauthy.
1203
1293
  --out <file> Write the module there. Without it, it is printed.
@@ -1208,13 +1298,19 @@ CAUTHY_TOKEN (a personal access token with organizations:manage).
1208
1298
 
1209
1299
  --file does the opposite, once:
1210
1300
 
1211
- Loads a system's catalog of roles into Cauthy from a file:
1301
+ Loads a system's catalog of roles into Cauthy from a file: a list of roles,
1302
+ or the resources the system declares with them.
1212
1303
 
1213
- { "roles": [ { "role": "accountant", "name": "Accountant", "inherits": ["reader"],
1214
- "permissions": [ { "resource": "ledger", "action": "write" },
1215
- { "kind": "business", "resource": "books.close", "action": "write" } ] } ] }
1304
+ { "catalog": [ { "resource": "invoices", "actions": ["read", "approve"],
1305
+ "attributes": { "amount": "number", "branch": "string" } } ],
1306
+ "roles": [ { "role": "accountant", "name": "Accountant", "inherits": ["reader"],
1307
+ "permissions": [ { "resource": "invoices", "action": "read" },
1308
+ { "resource": "invoices", "action": "approve",
1309
+ "condition": "resource.attrs.amount <= 1000000" } ] } ] }
1216
1310
 
1217
- --file <file> The catalog, as JSON.
1311
+ --file <file> The catalog, as JSON. Without "catalog", the resources
1312
+ Cauthy has stay as they are; with it, they are sent first
1313
+ and the roles are checked against them.
1218
1314
  --system <name> The system it is of. Default: CAUTHY_SYSTEM.
1219
1315
  --push Send it to Cauthy, REPLACING the catalog it has. Without it
1220
1316
  the catalog is only checked and shown.
@@ -1223,7 +1319,7 @@ Use it to load a first catalog. Afterwards the catalog is Cauthy's, edited in
1223
1319
  its dashboard: pushing again overwrites what was edited there.
1224
1320
 
1225
1321
  To push: CAUTHY_ISSUER, CAUTHY_TENANT (the workspace's slug) and CAUTHY_TOKEN
1226
- (a personal access token with organizations:manage).
1322
+ (a personal access token with systems:policy).
1227
1323
  `;
1228
1324
  async function policy(argv) {
1229
1325
  if (argv.includes("--help") || argv.includes("-h")) {
@@ -1249,12 +1345,14 @@ ${POLICY_HELP}`);
1249
1345
  }
1250
1346
  if (pull) {
1251
1347
  const of = system ?? process.env.CAUTHY_SYSTEM ?? fail("name the system with --system or CAUTHY_SYSTEM");
1252
- const read = process.env.CAUTHY_TOKEN ? await pullPolicy({ system: of }) : await createSystem().policy.get(of);
1348
+ const own = process.env.CAUTHY_TOKEN ? void 0 : createSystem();
1349
+ const [read, catalog2] = await Promise.all(own ? [own.policy.get(of), own.catalog.get(of).catch(() => [])] : [pullPolicy({ system: of }), pullCatalog({ system: of }).catch(() => [])]);
1253
1350
  if (read.length === 0) fail(`Cauthy has no catalog of roles for "${of}"`);
1254
- const source = policySource(of, read);
1351
+ const source = policySource(of, { catalog: catalog2, roles: read });
1255
1352
  if (out2) {
1256
1353
  (0, import_node_fs.writeFileSync)(out2, source);
1257
- process.stdout.write(`${read.length} roles written to ${out2}: ${read.map((r) => r.role).join(", ")}
1354
+ const resources = catalog2.length ? ` and ${catalog2.length} resources (${catalog2.map((c) => c.resource).join(", ")})` : "";
1355
+ process.stdout.write(`${read.length} roles${resources} written to ${out2}: ${read.map((r) => r.role).join(", ")}
1258
1356
  `);
1259
1357
  } else {
1260
1358
  process.stdout.write(source);
@@ -1262,20 +1360,23 @@ ${POLICY_HELP}`);
1262
1360
  return;
1263
1361
  }
1264
1362
  if (!file) fail("give the catalog with --file\n\n" + POLICY_HELP);
1265
- let roles;
1363
+ let given;
1364
+ let withCatalog = false;
1266
1365
  try {
1267
1366
  const parsed = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
1268
- const list = Array.isArray(parsed) ? parsed : parsed.roles;
1269
- if (!Array.isArray(list)) throw new Error('no "roles" list');
1270
- roles = list;
1367
+ given = readPolicyFile(parsed);
1368
+ withCatalog = !Array.isArray(parsed) && parsed.catalog !== void 0;
1271
1369
  } catch (err) {
1272
1370
  return fail(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
1273
1371
  }
1372
+ const { roles, catalog } = given;
1274
1373
  const permissions = roles.reduce((n, r) => n + (r.permissions?.length ?? 0), 0);
1374
+ if (withCatalog) process.stdout.write(`${catalog.length} resources: ${catalog.map((c) => c.resource).join(", ")}
1375
+ `);
1275
1376
  process.stdout.write(`${roles.length} roles, ${permissions} permissions: ${roles.map((r) => r.role).join(", ")}
1276
1377
  `);
1277
1378
  if (push) {
1278
- await pushPolicy({ system, roles });
1379
+ await pushPolicy({ system, roles, ...withCatalog ? { catalog } : {} });
1279
1380
  process.stdout.write("Sent to Cauthy: this is now the catalog of the system.\n");
1280
1381
  } else {
1281
1382
  process.stdout.write("Not sent. Add --push to give it to Cauthy.\n");
package/dist/client.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CauthyTheme } from './theme.mjs';
2
- import { i as PublicSession } from './system-BxMsSIu1.mjs';
2
+ import { q as PublicSession, f as CheckQuestion, k as OrganizationRef, g as CheckResult } from './system-0mHrdAgO.mjs';
3
+ export { h as CheckRule } from './system-0mHrdAgO.mjs';
3
4
  import { A as AuthField, b as PasswordPolicy } from './validate-sEXG82po.mjs';
4
5
  export { D as DEFAULT_PASSWORD_POLICY, F as FieldProblem, M as MIN_PASSWORD, P as PasswordClass, a as PasswordLocale, c as PasswordReason, d as checkCode, e as checkEmail, f as checkName, g as checkNewPassword, h as checkPassword, i as describePasswordPolicy, j as explainPasswordProblems, p as passwordPolicyOf, k as passwordProblems } from './validate-sEXG82po.mjs';
5
6
  import 'jose';
@@ -201,19 +202,29 @@ interface ConnectedAgent {
201
202
  connectedAt: string;
202
203
  lastUsedAt: string;
203
204
  }
204
- /** An organization the person belongs to, with what their role lets them do with its people. */
205
+ /** An organization the person belongs to, with what their role lets them do with it and its people. */
205
206
  interface OrganizationSeat {
206
207
  id: string;
207
- /** The client system it is of (`pondari`). */
208
+ /** The client system it is of (`acme-crm`). */
208
209
  system: string;
209
210
  name: string;
210
211
  kind: string;
211
212
  /** The person's role in it. */
212
213
  role: string;
213
- /** Cauthy's member powers, from the system's catalog: cauthy:members read and manage. */
214
+ /**
215
+ * Cauthy's reserved powers, from the system's catalog. `manageMembers`
216
+ * (cauthy:members:manage) adds, invites and changes roles; removing another
217
+ * member is `removeMembers` (cauthy:members:remove), and anybody may leave.
218
+ * `update`, `archive` and `delete` are cauthy:organization's: rename it,
219
+ * archive it, delete it for good.
220
+ */
214
221
  can: {
215
222
  readMembers: boolean;
216
223
  manageMembers: boolean;
224
+ removeMembers: boolean;
225
+ update: boolean;
226
+ archive: boolean;
227
+ delete: boolean;
217
228
  };
218
229
  }
219
230
  /** A person of an organization, as a member who may see them reads it. */
@@ -243,8 +254,12 @@ interface OrganizationRoleChoice {
243
254
  }
244
255
  /**
245
256
  * The signed-in person's organizations, and their people. Seeing them needs
246
- * cauthy:members read in the person's role; changing them needs manage. Cauthy
247
- * decides, with the person's own session: the application authorizes nothing.
257
+ * cauthy:members read in the person's role; changing them needs manage, and
258
+ * removing someone needs remove. The organization itself is renamed, archived
259
+ * or deleted with cauthy:organization update, archive or delete. Only an
260
+ * `owner` gives the owner role or changes an owner, and the last owner stays
261
+ * (`last_owner`). Cauthy decides, with the person's own session: the
262
+ * application authorizes nothing.
248
263
  */
249
264
  interface OrganizationsClient {
250
265
  /** The organizations the person belongs to; one system's only with `system`. */
@@ -276,6 +291,31 @@ interface OrganizationsClient {
276
291
  }): Promise<AuthResult<'sent', {
277
292
  userId: string;
278
293
  }>>;
294
+ /** Gives the organization a new name (cauthy:organization update). */
295
+ rename(organizationId: string, name: string): Promise<AuthResult<'renamed', {
296
+ organization: OrganizationSeat;
297
+ }>>;
298
+ /** Archives the organization: its members no longer reach it, and only the workspace restores it. */
299
+ archive(organizationId: string): Promise<AuthResult<'archived', {
300
+ organizationId: string;
301
+ }>>;
302
+ /** Deletes the organization for good. */
303
+ delete(organizationId: string): Promise<AuthResult<'deleted', {
304
+ organizationId: string;
305
+ }>>;
306
+ /**
307
+ * Asks Cauthy, through the application's server, whether the person may do
308
+ * an action on a resource in the organization, with what its conditions
309
+ * read. A question that could not be asked is a no, with `error` saying why.
310
+ *
311
+ * const r = await organizations.check({ organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
312
+ * if (r.allowed) showApprove()
313
+ */
314
+ check(question: CheckQuestion & {
315
+ organization: OrganizationRef;
316
+ }): Promise<CheckResult & {
317
+ error?: AuthError;
318
+ }>;
279
319
  }
280
320
  interface AccountSession {
281
321
  /** "Mac · Chrome", or "" when the browser did not say. */
@@ -725,4 +765,4 @@ interface AuthClientOptions {
725
765
  }
726
766
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
727
767
 
728
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type ConnectedAgent, type LinkedIdentity, type OrganizationPerson, type OrganizationRoleChoice, type OrganizationSeat, type OrganizationsClient, type Passkey, type PasskeyKind, PasswordPolicy, type PendingLegal, type SSOConnection, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };
768
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, CheckQuestion, CheckResult, type ConnectedAgent, type LinkedIdentity, type OrganizationPerson, OrganizationRef, type OrganizationRoleChoice, type OrganizationSeat, type OrganizationsClient, type Passkey, type PasskeyKind, PasswordPolicy, type PendingLegal, type SSOConnection, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { CauthyTheme } from './theme.js';
2
- import { i as PublicSession } from './system-DTIeDXDP.js';
2
+ import { q as PublicSession, f as CheckQuestion, k as OrganizationRef, g as CheckResult } from './system-BIYbS7h2.js';
3
+ export { h as CheckRule } from './system-BIYbS7h2.js';
3
4
  import { A as AuthField, b as PasswordPolicy } from './validate-sEXG82po.js';
4
5
  export { D as DEFAULT_PASSWORD_POLICY, F as FieldProblem, M as MIN_PASSWORD, P as PasswordClass, a as PasswordLocale, c as PasswordReason, d as checkCode, e as checkEmail, f as checkName, g as checkNewPassword, h as checkPassword, i as describePasswordPolicy, j as explainPasswordProblems, p as passwordPolicyOf, k as passwordProblems } from './validate-sEXG82po.js';
5
6
  import 'jose';
@@ -201,19 +202,29 @@ interface ConnectedAgent {
201
202
  connectedAt: string;
202
203
  lastUsedAt: string;
203
204
  }
204
- /** An organization the person belongs to, with what their role lets them do with its people. */
205
+ /** An organization the person belongs to, with what their role lets them do with it and its people. */
205
206
  interface OrganizationSeat {
206
207
  id: string;
207
- /** The client system it is of (`pondari`). */
208
+ /** The client system it is of (`acme-crm`). */
208
209
  system: string;
209
210
  name: string;
210
211
  kind: string;
211
212
  /** The person's role in it. */
212
213
  role: string;
213
- /** Cauthy's member powers, from the system's catalog: cauthy:members read and manage. */
214
+ /**
215
+ * Cauthy's reserved powers, from the system's catalog. `manageMembers`
216
+ * (cauthy:members:manage) adds, invites and changes roles; removing another
217
+ * member is `removeMembers` (cauthy:members:remove), and anybody may leave.
218
+ * `update`, `archive` and `delete` are cauthy:organization's: rename it,
219
+ * archive it, delete it for good.
220
+ */
214
221
  can: {
215
222
  readMembers: boolean;
216
223
  manageMembers: boolean;
224
+ removeMembers: boolean;
225
+ update: boolean;
226
+ archive: boolean;
227
+ delete: boolean;
217
228
  };
218
229
  }
219
230
  /** A person of an organization, as a member who may see them reads it. */
@@ -243,8 +254,12 @@ interface OrganizationRoleChoice {
243
254
  }
244
255
  /**
245
256
  * The signed-in person's organizations, and their people. Seeing them needs
246
- * cauthy:members read in the person's role; changing them needs manage. Cauthy
247
- * decides, with the person's own session: the application authorizes nothing.
257
+ * cauthy:members read in the person's role; changing them needs manage, and
258
+ * removing someone needs remove. The organization itself is renamed, archived
259
+ * or deleted with cauthy:organization update, archive or delete. Only an
260
+ * `owner` gives the owner role or changes an owner, and the last owner stays
261
+ * (`last_owner`). Cauthy decides, with the person's own session: the
262
+ * application authorizes nothing.
248
263
  */
249
264
  interface OrganizationsClient {
250
265
  /** The organizations the person belongs to; one system's only with `system`. */
@@ -276,6 +291,31 @@ interface OrganizationsClient {
276
291
  }): Promise<AuthResult<'sent', {
277
292
  userId: string;
278
293
  }>>;
294
+ /** Gives the organization a new name (cauthy:organization update). */
295
+ rename(organizationId: string, name: string): Promise<AuthResult<'renamed', {
296
+ organization: OrganizationSeat;
297
+ }>>;
298
+ /** Archives the organization: its members no longer reach it, and only the workspace restores it. */
299
+ archive(organizationId: string): Promise<AuthResult<'archived', {
300
+ organizationId: string;
301
+ }>>;
302
+ /** Deletes the organization for good. */
303
+ delete(organizationId: string): Promise<AuthResult<'deleted', {
304
+ organizationId: string;
305
+ }>>;
306
+ /**
307
+ * Asks Cauthy, through the application's server, whether the person may do
308
+ * an action on a resource in the organization, with what its conditions
309
+ * read. A question that could not be asked is a no, with `error` saying why.
310
+ *
311
+ * const r = await organizations.check({ organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
312
+ * if (r.allowed) showApprove()
313
+ */
314
+ check(question: CheckQuestion & {
315
+ organization: OrganizationRef;
316
+ }): Promise<CheckResult & {
317
+ error?: AuthError;
318
+ }>;
279
319
  }
280
320
  interface AccountSession {
281
321
  /** "Mac · Chrome", or "" when the browser did not say. */
@@ -725,4 +765,4 @@ interface AuthClientOptions {
725
765
  }
726
766
  declare function createAuthClient(options?: AuthClientOptions): AuthClient;
727
767
 
728
- export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, type ConnectedAgent, type LinkedIdentity, type OrganizationPerson, type OrganizationRoleChoice, type OrganizationSeat, type OrganizationsClient, type Passkey, type PasskeyKind, PasswordPolicy, type PendingLegal, type SSOConnection, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };
768
+ export { type Account, type AccountClient, type AccountSession, type AuthClient, type AuthClientOptions, type AuthError, AuthField, type AuthMethods, type AuthResult, CheckQuestion, CheckResult, type ConnectedAgent, type LinkedIdentity, type OrganizationPerson, OrganizationRef, type OrganizationRoleChoice, type OrganizationSeat, type OrganizationsClient, type Passkey, type PasskeyKind, PasswordPolicy, type PendingLegal, type SSOConnection, type SecondStep, type SessionState, type SessionStore, type SessionStoreOptions, canAutofillPasskeys, canUsePasskeys, createAuthClient, createSessionStore };