cauthy-auth 0.28.1 → 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 +51 -0
  2. package/dist/{cauthy-CTVg4kvo.d.mts → cauthy-BXgsZ-Um.d.mts} +19 -1
  3. package/dist/{cauthy-BAwAJChs.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 +325 -129
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +323 -129
  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-CVBW_AV8.d.mts → system-0mHrdAgO.d.mts} +307 -109
  38. package/dist/{system-Bn8uckvQ.d.ts → system-BIYbS7h2.d.ts} +307 -109
  39. package/dist/vue.d.mts +2 -2
  40. package/dist/vue.d.ts +2 -2
  41. package/dist/vue.js +37 -2
  42. package/dist/vue.js.map +1 -1
  43. package/dist/vue.mjs +37 -2
  44. package/dist/vue.mjs.map +1 -1
  45. package/package.json +1 -1
package/dist/next.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Cauthy } from './cauthy-CTVg4kvo.mjs';
2
- import { S as Session } from './system-CVBW_AV8.mjs';
1
+ import { C as Cauthy } from './cauthy-BXgsZ-Um.mjs';
2
+ import { S as Session } from './system-0mHrdAgO.mjs';
3
3
  import 'jose';
4
4
  import './theme.mjs';
5
5
 
package/dist/next.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Cauthy } from './cauthy-BAwAJChs.js';
2
- import { S as Session } from './system-Bn8uckvQ.js';
1
+ import { C as Cauthy } from './cauthy-CHENs452.js';
2
+ import { S as Session } from './system-BIYbS7h2.js';
3
3
  import 'jose';
4
4
  import './theme.js';
5
5
 
package/dist/react.d.mts CHANGED
@@ -85,6 +85,8 @@ interface Permitted {
85
85
  permissions?: string[];
86
86
  /** Entries the person is denied whatever else allows them. */
87
87
  denied?: string[];
88
+ /** Permissions with a condition: in neither list, so `can` never counts them. Ask with `auth.check`. */
89
+ conditional?: ConditionalPermission[];
88
90
  }
89
91
  /**
90
92
  * Whether the person may do an action on a resource in an organization:
@@ -93,6 +95,11 @@ interface Permitted {
93
95
  * An organization of a system that keeps no catalog in Cauthy carries no
94
96
  * permissions, and the answer is `false`: decide by its `role` instead.
95
97
  *
98
+ * A permission with a condition (`organization.conditional`) is not in the
99
+ * lists, so `can` answers `false` for what only it allows: the condition reads
100
+ * the record at hand, which only Cauthy can weigh. Ask with `auth.check`
101
+ * (`organizations.check` in the browser).
102
+ *
96
103
  * if (!can(organization, 'invoicing', 'write')) return forbidden()
97
104
  */
98
105
  declare function can(organization: Permitted | null | undefined, resource: string, action: string): boolean;
@@ -101,6 +108,21 @@ interface PolicyPermission {
101
108
  kind?: string;
102
109
  resource: string;
103
110
  action: string;
111
+ /**
112
+ * When it holds, in CEL: `resource.attrs.amount <= 1000000`. A permission
113
+ * with one is decided by Cauthy per question (`auth.check`), never by `can`.
114
+ */
115
+ condition?: string;
116
+ }
117
+ /** The type of an attribute a condition may read: `list` is any JSON array. */
118
+ type AttributeType = 'string' | 'number' | 'bool' | 'list';
119
+ /** A resource of a system's catalog: the actions done on it and the attributes its conditions read. */
120
+ interface CatalogResource {
121
+ resource: string;
122
+ description?: string;
123
+ actions: string[];
124
+ /** Name → type, as `resource.attrs.<name>` in a condition. */
125
+ attributes?: Record<string, AttributeType>;
104
126
  }
105
127
  interface PolicyRole {
106
128
  role: string;
@@ -110,17 +132,28 @@ interface PolicyRole {
110
132
  inherits?: string[];
111
133
  permissions?: PolicyPermission[];
112
134
  }
135
+ type PermissionShape = {
136
+ readonly kind?: string;
137
+ readonly resource: string;
138
+ readonly action: string;
139
+ readonly condition?: string;
140
+ };
113
141
  /** A role as `definePolicy` reads it: any `PolicyRole`, kept as written. */
114
142
  type RoleShape = {
115
143
  readonly role: string;
116
144
  readonly name?: string;
117
145
  readonly description?: string;
118
146
  readonly inherits?: readonly string[];
119
- readonly permissions?: readonly {
120
- readonly kind?: string;
121
- readonly resource: string;
122
- readonly action: string;
123
- }[];
147
+ readonly permissions?: readonly PermissionShape[];
148
+ };
149
+ /** A resource as `definePolicy` reads it: any `CatalogResource`, kept as written. */
150
+ type ResourceShape = {
151
+ readonly resource: string;
152
+ readonly description?: string;
153
+ readonly actions: readonly string[];
154
+ readonly attributes?: {
155
+ readonly [name: string]: AttributeType;
156
+ };
124
157
  };
125
158
  type Named<S extends string> = S extends '*' ? string : S;
126
159
  type PermissionsIn<R> = R extends {
@@ -129,13 +162,56 @@ type PermissionsIn<R> = R extends {
129
162
  readonly resource: infer Res extends string;
130
163
  readonly action: infer Act extends string;
131
164
  } ? `${Named<Res>}:${Named<Act>}` : never : never;
165
+ type ValueOf<T> = T extends 'string' ? string : T extends 'number' ? number : T extends 'bool' ? boolean : T extends 'list' ? unknown[] : never;
166
+ /** What a check may say about one declared resource. */
167
+ type QuestionAbout<E> = E extends {
168
+ readonly resource: infer R extends string;
169
+ readonly actions: readonly (infer A extends string)[];
170
+ } ? {
171
+ resource: R;
172
+ action: A;
173
+ attributes?: E extends {
174
+ readonly attributes: infer T;
175
+ } ? {
176
+ [K in keyof T]?: ValueOf<T[K]>;
177
+ } : Record<string, never>;
178
+ resourceId?: string;
179
+ ownerId?: string;
180
+ } : never;
181
+ /**
182
+ * What a role of a catalog that declares its resources may name: a declared
183
+ * resource with one of its actions, or "*", or one of Cauthy's own powers.
184
+ */
185
+ type PermissionFor<C extends readonly ResourceShape[]> = {
186
+ readonly kind?: string;
187
+ readonly condition?: string;
188
+ } & ((C[number] extends infer E ? E extends {
189
+ readonly resource: infer R extends string;
190
+ readonly actions: readonly (infer A extends string)[];
191
+ } ? {
192
+ readonly resource: R;
193
+ readonly action: A | '*';
194
+ } : never : never) | {
195
+ readonly resource: '*';
196
+ readonly action: string;
197
+ } | {
198
+ readonly resource: `cauthy:${string}`;
199
+ readonly action: string;
200
+ });
201
+ type RoleFor<C extends readonly ResourceShape[]> = Omit<RoleShape, 'permissions'> & {
202
+ readonly permissions?: readonly PermissionFor<C>[];
203
+ };
132
204
  /** The roles a catalog names: `RoleOf<typeof policy>`. */
133
- type RoleOf<P> = P extends Policy<infer R, string> ? R : never;
205
+ type RoleOf<P> = P extends Policy<infer R, any, any> ? R : never;
134
206
  /** The "resource:action" permissions a catalog names: `PermissionOf<typeof policy>`. */
135
- type PermissionOf<P> = P extends Policy<string, infer X> ? X : never;
136
- interface Policy<Role extends string = string, Permission extends string = string> {
207
+ type PermissionOf<P> = P extends Policy<any, infer X, any> ? X : never;
208
+ /** The fields of a question: whatever else the asker takes (the organization, the person) passes through. */
209
+ type QuestionKeys = keyof CheckQuestion;
210
+ interface Policy<Role extends string = string, Permission extends string = string, Question extends CheckQuestion = CheckQuestion> {
137
211
  /** The catalog as Cauthy takes it: `pushPolicy({ roles: policy.roles })`. */
138
212
  roles: PolicyRole[];
213
+ /** The resources the system declares, as Cauthy takes them; empty when it declares none. */
214
+ catalog: CatalogResource[];
139
215
  /** Every role's name. */
140
216
  roleNames: Role[];
141
217
  /** Every permission some role has, as "resource:action". */
@@ -143,7 +219,8 @@ interface Policy<Role extends string = string, Permission extends string = strin
143
219
  /**
144
220
  * Whether the person may do this in the organization: `can(org, 'ledger:write')`.
145
221
  * It reads what Cauthy sent with the session, so a catalog edited in Cauthy
146
- * since this file was written is still what decides.
222
+ * since this file was written is still what decides. A permission with a
223
+ * condition is not there: ask with `check`.
147
224
  */
148
225
  can(organization: Permitted | null | undefined, permission: Permission): boolean;
149
226
  /** Whether the person's role in the organization is one of these. */
@@ -155,9 +232,30 @@ interface Policy<Role extends string = string, Permission extends string = strin
155
232
  * role, never for deciding about a person (their exceptions are not here).
156
233
  */
157
234
  permissionsOf(role: Role): Permission[];
235
+ /**
236
+ * Asks Cauthy, with the resource, action and attributes typed by the
237
+ * catalog. Give it whoever asks: the server's `auth` with the request,
238
+ * the browser's `organizations`, or the application's `system`.
239
+ *
240
+ * await policy.check(auth, request, { organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
241
+ * await policy.check(auth.system, { organizationId, userId, resource: 'invoices', action: 'read' })
242
+ */
243
+ check<I extends CheckQuestion>(asker: {
244
+ check(input: I): Promise<CheckResult>;
245
+ }, input: Omit<I, QuestionKeys> & Question): Promise<CheckResult>;
246
+ check<R, I extends CheckQuestion>(asker: {
247
+ check(request: R, input: I): Promise<CheckResult>;
248
+ }, request: R, input: Omit<I, QuestionKeys> & Question): Promise<CheckResult>;
249
+ }
250
+ /** A catalog with its resources declared, for `definePolicy`. */
251
+ interface PolicyDefinition<C extends readonly ResourceShape[], T extends readonly RoleShape[]> {
252
+ catalog: C;
253
+ roles: T;
158
254
  }
159
255
  /** Gives a catalog of roles its types. See the top of this section. */
160
256
  declare function definePolicy<const T extends readonly RoleShape[]>(roles: T): Policy<T[number]['role'], PermissionsIn<T[number]>>;
257
+ /** Gives a catalog of resources and roles its types: a role names only what the catalog declares. */
258
+ declare function definePolicy<const C extends readonly ResourceShape[], const T extends readonly RoleFor<C>[]>(definition: PolicyDefinition<C, T>): Policy<T[number]['role'], PermissionsIn<T[number]>, QuestionAbout<C[number]>>;
161
259
 
162
260
  /** The person signed in. Every moment is UTC. */
163
261
  interface SessionUser {
@@ -200,6 +298,54 @@ interface SessionOrganization {
200
298
  permissions?: string[];
201
299
  /** What the person is denied here, whatever else allows it. */
202
300
  denied?: string[];
301
+ /**
302
+ * Permissions that hold only when their condition does (an amount under a
303
+ * limit, the person's own records…). They are in neither list above: `can`
304
+ * answers no for them, and `auth.check` asks Cauthy. Present, maybe empty,
305
+ * whenever `permissions` is.
306
+ */
307
+ conditional?: ConditionalPermission[];
308
+ }
309
+ /** A permission with a condition: Cauthy decides it per question (`auth.check`). */
310
+ interface ConditionalPermission {
311
+ resource: string;
312
+ action: string;
313
+ effect: 'allow' | 'deny';
314
+ }
315
+ /** What to ask Cauthy about: an action on a resource, with what the conditions read of it. */
316
+ interface CheckQuestion {
317
+ resource: string;
318
+ action: string;
319
+ /** What a condition reads as `resource.attrs`: `{ amount: 500 }`. */
320
+ attributes?: Record<string, unknown>;
321
+ /** The record's id, as a condition reads it (`resource.id`). */
322
+ resourceId?: string;
323
+ /** Whose the record is (`resource.owner_id`): `resource.owner_id == subject.id`. */
324
+ ownerId?: string;
325
+ }
326
+ /** An organization, by its id or as the session has it. */
327
+ type OrganizationRef = string | {
328
+ id: string;
329
+ };
330
+ /** The rule that decided a check. */
331
+ interface CheckRule {
332
+ /** A permission of the person's role, or an exception given to the person. */
333
+ source: 'role' | 'exception';
334
+ /** The role, when `source` is `role`. */
335
+ role?: string;
336
+ kind: string;
337
+ resource: string;
338
+ action: string;
339
+ effect: 'allow' | 'deny';
340
+ condition?: string;
341
+ }
342
+ /** Cauthy's answer to a check. */
343
+ interface CheckResult {
344
+ allowed: boolean;
345
+ /** Why, in words: "allowed by role accountant: invoices:approve". */
346
+ reason: string;
347
+ /** The rule that decided; absent when none did (nothing allows it). */
348
+ rule?: CheckRule;
203
349
  }
204
350
  /** What the browser is told of a session: who, and their organizations — never a token. */
205
351
  interface PublicSession {
@@ -385,19 +531,29 @@ interface ConnectedAgent {
385
531
  connectedAt: string;
386
532
  lastUsedAt: string;
387
533
  }
388
- /** An organization the person belongs to, with what their role lets them do with its people. */
534
+ /** An organization the person belongs to, with what their role lets them do with it and its people. */
389
535
  interface OrganizationSeat {
390
536
  id: string;
391
- /** The client system it is of (`pondari`). */
537
+ /** The client system it is of (`acme-crm`). */
392
538
  system: string;
393
539
  name: string;
394
540
  kind: string;
395
541
  /** The person's role in it. */
396
542
  role: string;
397
- /** Cauthy's member powers, from the system's catalog: cauthy:members read and manage. */
543
+ /**
544
+ * Cauthy's reserved powers, from the system's catalog. `manageMembers`
545
+ * (cauthy:members:manage) adds, invites and changes roles; removing another
546
+ * member is `removeMembers` (cauthy:members:remove), and anybody may leave.
547
+ * `update`, `archive` and `delete` are cauthy:organization's: rename it,
548
+ * archive it, delete it for good.
549
+ */
398
550
  can: {
399
551
  readMembers: boolean;
400
552
  manageMembers: boolean;
553
+ removeMembers: boolean;
554
+ update: boolean;
555
+ archive: boolean;
556
+ delete: boolean;
401
557
  };
402
558
  }
403
559
  /** A person of an organization, as a member who may see them reads it. */
@@ -427,8 +583,12 @@ interface OrganizationRoleChoice {
427
583
  }
428
584
  /**
429
585
  * The signed-in person's organizations, and their people. Seeing them needs
430
- * cauthy:members read in the person's role; changing them needs manage. Cauthy
431
- * decides, with the person's own session: the application authorizes nothing.
586
+ * cauthy:members read in the person's role; changing them needs manage, and
587
+ * removing someone needs remove. The organization itself is renamed, archived
588
+ * or deleted with cauthy:organization update, archive or delete. Only an
589
+ * `owner` gives the owner role or changes an owner, and the last owner stays
590
+ * (`last_owner`). Cauthy decides, with the person's own session: the
591
+ * application authorizes nothing.
432
592
  */
433
593
  interface OrganizationsClient {
434
594
  /** The organizations the person belongs to; one system's only with `system`. */
@@ -460,6 +620,31 @@ interface OrganizationsClient {
460
620
  }): Promise<AuthResult<'sent', {
461
621
  userId: string;
462
622
  }>>;
623
+ /** Gives the organization a new name (cauthy:organization update). */
624
+ rename(organizationId: string, name: string): Promise<AuthResult<'renamed', {
625
+ organization: OrganizationSeat;
626
+ }>>;
627
+ /** Archives the organization: its members no longer reach it, and only the workspace restores it. */
628
+ archive(organizationId: string): Promise<AuthResult<'archived', {
629
+ organizationId: string;
630
+ }>>;
631
+ /** Deletes the organization for good. */
632
+ delete(organizationId: string): Promise<AuthResult<'deleted', {
633
+ organizationId: string;
634
+ }>>;
635
+ /**
636
+ * Asks Cauthy, through the application's server, whether the person may do
637
+ * an action on a resource in the organization, with what its conditions
638
+ * read. A question that could not be asked is a no, with `error` saying why.
639
+ *
640
+ * const r = await organizations.check({ organization, resource: 'invoices', action: 'approve', attributes: { amount: 500 } })
641
+ * if (r.allowed) showApprove()
642
+ */
643
+ check(question: CheckQuestion & {
644
+ organization: OrganizationRef;
645
+ }): Promise<CheckResult & {
646
+ error?: AuthError;
647
+ }>;
463
648
  }
464
649
  interface AccountSession {
465
650
  /** "Mac · Chrome", or "" when the browser did not say. */
@@ -1027,6 +1212,15 @@ interface AuthTexts {
1027
1212
  memberLeaveConfirm: string;
1028
1213
  memberInviteAgain: string;
1029
1214
  memberInvitationSent: string;
1215
+ organization: string;
1216
+ organizationRename: string;
1217
+ organizationRenamed: string;
1218
+ organizationArchive: string;
1219
+ organizationArchiveConfirm: string;
1220
+ organizationArchived: string;
1221
+ organizationDelete: string;
1222
+ organizationDeleteConfirm: string;
1223
+ organizationDeleted: string;
1030
1224
  yes: string;
1031
1225
  cancel: string;
1032
1226
  /** By error code; one missing shows what the server said. */
@@ -1144,10 +1338,12 @@ interface OrganizationMembersProps extends Omit<AuthFormProps, 'links' | 'return
1144
1338
  * The people of one of the signed-in person's organizations, administered
1145
1339
  * from the application. What it offers follows the person's role in the
1146
1340
  * system's catalog: cauthy:members read lists the people; manage adds them by
1147
- * address (one with no account is invited), changes their role, sends an
1148
- * invitation again and removes them. Anybody may leave. Cauthy checks each
1149
- * change with the person's own session; the application authorizes nothing.
1150
- * Like the other forms, it carries no styles.
1341
+ * address (one with no account is invited), changes their role and sends an
1342
+ * invitation again; remove removes them. Anybody may leave. Only an owner
1343
+ * gives the owner role or changes an owner. cauthy:organization update,
1344
+ * archive and delete rename the organization, archive it, delete it. Cauthy
1345
+ * checks each change with the person's own session; the application
1346
+ * authorizes nothing. Like the other forms, it carries no styles.
1151
1347
  */
1152
1348
  declare function OrganizationMembers(props: OrganizationMembersProps): react.JSX.Element;
1153
1349