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.
- package/README.md +52 -1
- package/dist/{cauthy-XkIbG5gE.d.mts → cauthy-BXgsZ-Um.d.mts} +19 -1
- package/dist/{cauthy-DGAy45G2.d.ts → cauthy-CHENs452.d.ts} +19 -1
- package/dist/cli.js +155 -54
- package/dist/client.d.mts +47 -7
- package/dist/client.d.ts +47 -7
- package/dist/client.js +23 -1
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +23 -1
- package/dist/client.mjs.map +1 -1
- package/dist/express.d.mts +2 -2
- package/dist/express.d.ts +2 -2
- package/dist/fastify.d.mts +2 -2
- package/dist/fastify.d.ts +2 -2
- package/dist/hono.d.mts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +352 -131
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +350 -131
- package/dist/index.mjs.map +1 -1
- package/dist/next.d.mts +2 -2
- package/dist/next.d.ts +2 -2
- package/dist/react.d.mts +214 -18
- package/dist/react.d.ts +214 -18
- package/dist/react.js +129 -32
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +129 -32
- package/dist/react.mjs.map +1 -1
- package/dist/svelte.d.mts +2 -2
- package/dist/svelte.d.ts +2 -2
- package/dist/svelte.js +37 -2
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +37 -2
- package/dist/svelte.mjs.map +1 -1
- package/dist/{system-BxMsSIu1.d.mts → system-0mHrdAgO.d.mts} +312 -112
- package/dist/{system-DTIeDXDP.d.ts → system-BIYbS7h2.d.ts} +312 -112
- package/dist/vue.d.mts +2 -2
- package/dist/vue.d.ts +2 -2
- package/dist/vue.js +37 -2
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +37 -2
- package/dist/vue.mjs.map +1 -1
- 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,
|
|
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,
|
|
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,
|
|
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) =>
|
|
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(
|
|
387
|
-
|
|
392
|
+
return `${pad} {
|
|
393
|
+
${pad} ${parts.join(`,
|
|
394
|
+
${pad} `)},
|
|
395
|
+
${pad} },`;
|
|
388
396
|
};
|
|
389
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
441
|
+
function reach(o, doing) {
|
|
421
442
|
const env2 = (n) => typeof process !== "undefined" ? process.env?.[n] : void 0;
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
|
436
|
-
throw new Error(`cauthy-auth: Cauthy
|
|
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
|
-
{ "
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1363
|
+
let given;
|
|
1364
|
+
let withCatalog = false;
|
|
1266
1365
|
try {
|
|
1267
1366
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
1268
|
-
|
|
1269
|
-
|
|
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 {
|
|
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 (`
|
|
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
|
-
/**
|
|
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
|
|
247
|
-
*
|
|
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 {
|
|
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 (`
|
|
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
|
-
/**
|
|
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
|
|
247
|
-
*
|
|
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 };
|