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/react.d.ts 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
 
package/dist/react.js CHANGED
@@ -321,6 +321,12 @@ function checkOrganization(value, required = false) {
321
321
  if (v.length > MAX_ORGANIZATION) return { field: "organization", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
322
322
  return null;
323
323
  }
324
+ function checkOrganizationName(value) {
325
+ const v = typeof value === "string" ? value.trim() : "";
326
+ if (v === "") return { field: "name", code: "required", message: "Enter the organization's name." };
327
+ if (v.length > MAX_ORGANIZATION) return { field: "name", code: "too_long", message: `Use at most ${MAX_ORGANIZATION} characters.` };
328
+ return null;
329
+ }
324
330
  function checkCode(value) {
325
331
  const v = typeof value === "string" ? value.trim() : "";
326
332
  if (v === "") return { field: "code", code: "required", message: "Enter the code." };
@@ -573,7 +579,23 @@ function createAuthClient(options = {}) {
573
579
  add: (id, f) => post("/me/organizations/members/add", { organizationId: id, email: f.email, role: f.role, name: f.name ?? "", locale: f.locale ?? "" }, checkEmail(f.email)),
574
580
  setRole: (id, userId, role) => post("/me/organizations/members/role", { organizationId: id, userId, role }, null),
575
581
  remove: (id, userId) => post("/me/organizations/members/remove", { organizationId: id, userId }, null),
576
- inviteAgain: (id, userId, f = {}) => post("/me/organizations/members/invite", { organizationId: id, userId, locale: f.locale ?? "" }, null)
582
+ inviteAgain: (id, userId, f = {}) => post("/me/organizations/members/invite", { organizationId: id, userId, locale: f.locale ?? "" }, null),
583
+ rename: (id, name) => post("/me/organizations/rename", { organizationId: id, name }, checkOrganizationName(name)),
584
+ archive: (id) => post("/me/organizations/archive", { organizationId: id }, null),
585
+ delete: (id) => post("/me/organizations/delete", { organizationId: id }, null),
586
+ async check(q) {
587
+ const organizationId = typeof q.organization === "string" ? q.organization : q.organization?.id ?? "";
588
+ const r = await post("/me/organizations/check", {
589
+ organizationId,
590
+ resource: q.resource,
591
+ action: q.action,
592
+ ...q.attributes !== void 0 ? { attributes: q.attributes } : {},
593
+ ...q.resourceId !== void 0 ? { resourceId: q.resourceId } : {},
594
+ ...q.ownerId !== void 0 ? { ownerId: q.ownerId } : {}
595
+ }, null);
596
+ if (!r.ok) return { allowed: false, reason: r.error.message, error: r.error };
597
+ return { allowed: r.allowed === true, reason: String(r.reason ?? ""), ...r.rule ? { rule: r.rule } : {} };
598
+ }
577
599
  },
578
600
  account: {
579
601
  async get() {
@@ -669,7 +691,9 @@ function can(organization, resource, action) {
669
691
  if ((organization.denied ?? []).some((d) => matches(d, resource, action))) return false;
670
692
  return organization.permissions.some((p) => matches(p, resource, action));
671
693
  }
672
- function definePolicy(roles) {
694
+ function definePolicy(given) {
695
+ const roles = Array.isArray(given) ? given : given.roles;
696
+ const resources = Array.isArray(given) ? [] : given.catalog ?? [];
673
697
  const entry = (p) => `${p.resource}:${p.action}`;
674
698
  const byName = new Map(roles.map((r) => [r.role, r]));
675
699
  const split = (permission) => {
@@ -684,6 +708,12 @@ function definePolicy(roles) {
684
708
  ...r.inherits ? { inherits: [...r.inherits] } : {},
685
709
  ...r.permissions ? { permissions: r.permissions.map((p) => ({ ...p })) } : {}
686
710
  })),
711
+ catalog: resources.map((c) => ({
712
+ resource: c.resource,
713
+ ...c.description !== void 0 ? { description: c.description } : {},
714
+ actions: [...c.actions],
715
+ ...c.attributes ? { attributes: { ...c.attributes } } : {}
716
+ })),
687
717
  roleNames: roles.map((r) => r.role),
688
718
  permissions: [...new Set(roles.flatMap((r) => (r.permissions ?? []).map(entry)))],
689
719
  can(organization, permission) {
@@ -705,6 +735,11 @@ function definePolicy(roles) {
705
735
  };
706
736
  walk(role);
707
737
  return [...out];
738
+ },
739
+ // The types did the work; this only hands the question on, as a method of
740
+ // the asker so it keeps its `this`.
741
+ check(asker, ...args) {
742
+ return asker.check(...args);
708
743
  }
709
744
  };
710
745
  }
@@ -823,11 +858,22 @@ var TEXTS = {
823
858
  memberLeaveConfirm: "Leave {org}? You will need an invitation to come back.",
824
859
  memberInviteAgain: "Send the invitation again",
825
860
  memberInvitationSent: "The invitation is on its way.",
861
+ organization: "Organization",
862
+ organizationRename: "Rename",
863
+ organizationRenamed: "The organization is now called {name}.",
864
+ organizationArchive: "Archive",
865
+ organizationArchiveConfirm: "Archive {org}? Its members will no longer reach it, and only the workspace can restore it.",
866
+ organizationArchived: "{org} was archived.",
867
+ organizationDelete: "Delete organization",
868
+ organizationDeleteConfirm: "Delete {org} for good? Its members lose it and this cannot be undone.",
869
+ organizationDeleted: "{org} was deleted.",
826
870
  yes: "Yes",
827
871
  cancel: "Cancel",
828
872
  errors: {
829
873
  not_allowed: "Your role in this organization does not allow that.",
830
874
  last_manager: "Somebody else must manage the members first: give that role to another member.",
875
+ last_owner: "An organization keeps at least one owner: make another member owner first.",
876
+ organization_not_found: "That organization no longer exists.",
831
877
  already_member: "That person already belongs to the organization. Change their role instead.",
832
878
  unknown_role: "Choose one of the roles offered.",
833
879
  user_not_active: "That account is disabled.",
@@ -979,11 +1025,22 @@ var TEXTS = {
979
1025
  memberLeaveConfirm: "\xBFSalir de {org}? Necesitar\xE1s una invitaci\xF3n para volver.",
980
1026
  memberInviteAgain: "Enviar la invitaci\xF3n de nuevo",
981
1027
  memberInvitationSent: "La invitaci\xF3n va en camino.",
1028
+ organization: "Organizaci\xF3n",
1029
+ organizationRename: "Cambiar nombre",
1030
+ organizationRenamed: "La organizaci\xF3n ahora se llama {name}.",
1031
+ organizationArchive: "Archivar",
1032
+ organizationArchiveConfirm: "\xBFArchivar {org}? Sus miembros ya no podr\xE1n entrar y solo el espacio de trabajo puede restaurarla.",
1033
+ organizationArchived: "{org} qued\xF3 archivada.",
1034
+ organizationDelete: "Eliminar organizaci\xF3n",
1035
+ organizationDeleteConfirm: "\xBFEliminar {org} para siempre? Sus miembros la pierden y no se puede deshacer.",
1036
+ organizationDeleted: "{org} qued\xF3 eliminada.",
982
1037
  yes: "S\xED",
983
1038
  cancel: "Cancelar",
984
1039
  errors: {
985
1040
  not_allowed: "Tu rol en esta organizaci\xF3n no permite eso.",
986
1041
  last_manager: "Primero alguien m\xE1s debe administrar los miembros: dale ese rol a otro miembro.",
1042
+ last_owner: "Una organizaci\xF3n conserva al menos un propietario: primero haz propietario a otro miembro.",
1043
+ organization_not_found: "Esa organizaci\xF3n ya no existe.",
987
1044
  already_member: "Esa persona ya pertenece a la organizaci\xF3n. Cambia su rol.",
988
1045
  unknown_role: "Escoge uno de los roles ofrecidos.",
989
1046
  user_not_active: "Esa cuenta est\xE1 desactivada.",
@@ -1660,32 +1717,52 @@ function Account(props) {
1660
1717
  ] })
1661
1718
  ] });
1662
1719
  }
1720
+ var OWNER = "owner";
1721
+ function memberActions(seat, m) {
1722
+ const reachable = m.role !== OWNER || seat.role === OWNER;
1723
+ return {
1724
+ changeRole: seat.can.manageMembers && reachable,
1725
+ // Leaving is anybody's; removing somebody else is cauthy:members:remove.
1726
+ remove: m.you || seat.can.removeMembers && reachable,
1727
+ inviteAgain: seat.can.manageMembers && m.invited
1728
+ };
1729
+ }
1730
+ function rolesToGive(seat, roles) {
1731
+ return seat.role === OWNER ? roles : roles.filter((c) => c.role !== OWNER);
1732
+ }
1663
1733
  function OrganizationMembers(props) {
1664
1734
  const auth = useAuth();
1665
1735
  const { root } = useForm(props);
1666
1736
  const { f, done, setDone, run } = useSection(props);
1667
- const problem = f.error && f.error.field !== "email" ? f.error : null;
1737
+ const problem = f.error && f.error.field !== "email" && f.error.field !== "name" ? f.error : null;
1668
1738
  const [seat, setSeat] = (0, import_react.useState)(void 0);
1669
1739
  const [members, setMembers] = (0, import_react.useState)(null);
1670
1740
  const [roles, setRoles] = (0, import_react.useState)([]);
1671
1741
  const [email, setEmail] = (0, import_react.useState)("");
1672
1742
  const [role, setRole] = (0, import_react.useState)("");
1743
+ const [name, setName] = (0, import_react.useState)("");
1673
1744
  const [asking, setAsking] = (0, import_react.useState)(null);
1745
+ const [gone, setGone] = (0, import_react.useState)(null);
1674
1746
  const id = props.organizationId;
1675
1747
  const when = props.formatDate ?? ((iso) => new Date(iso).toLocaleDateString());
1676
1748
  const load = async () => {
1677
1749
  const [list, people, choices] = await Promise.all([auth.organizations.list(), auth.organizations.members(id), auth.organizations.roles(id)]);
1678
- setSeat(list.find((o) => o.id === id) ?? null);
1750
+ const mine = list.find((o) => o.id === id) ?? null;
1751
+ setSeat(mine);
1752
+ if (mine) setName(mine.name);
1679
1753
  setMembers(people);
1680
1754
  setRoles(choices ?? []);
1681
- if (choices && choices.length > 0 && !choices.some((c) => c.role === role)) setRole(choices[0].role);
1755
+ const offered = mine ? rolesToGive(mine, choices ?? []) : [];
1756
+ if (offered.length > 0 && !offered.some((c) => c.role === role)) setRole(offered[0].role);
1682
1757
  };
1683
1758
  (0, import_react.useEffect)(() => {
1684
1759
  void load();
1685
1760
  }, [id]);
1761
+ if (gone !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: f.cx("account"), ...root, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: gone }) });
1686
1762
  if (seat === void 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: f.cx("account"), ...root, "aria-busy": "true" });
1687
1763
  if (seat === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: f.cx("account"), ...root, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.organizationMissing }) });
1688
1764
  const manage = seat.can.manageMembers;
1765
+ const giveable = rolesToGive(seat, roles);
1689
1766
  const label = (r) => roles.find((c) => c.role === r)?.name || r;
1690
1767
  const add = async (e) => {
1691
1768
  e.preventDefault();
@@ -1710,6 +1787,25 @@ function OrganizationMembers(props) {
1710
1787
  const r = await run(() => auth.organizations.inviteAgain(id, m.userId, { locale: props.locale }));
1711
1788
  if (r.ok) setDone(f.t.memberInvitationSent);
1712
1789
  };
1790
+ const rename = async (e) => {
1791
+ e.preventDefault();
1792
+ const r = await run(() => auth.organizations.rename(id, name));
1793
+ if (!r.ok) return;
1794
+ setSeat(r.organization);
1795
+ setName(r.organization.name);
1796
+ setDone(f.t.organizationRenamed.replace("{name}", r.organization.name));
1797
+ };
1798
+ const close = async (how) => {
1799
+ setAsking(null);
1800
+ const r = await run(() => how === "archive" ? auth.organizations.archive(id) : auth.organizations.delete(id));
1801
+ if (r.ok) setGone((how === "archive" ? f.t.organizationArchived : f.t.organizationDeleted).replace("{org}", seat.name));
1802
+ };
1803
+ const confirming = (text2, label2, yes) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { role: "group", "aria-label": label2, children: [
1804
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: text2 }),
1805
+ " ",
1806
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: yes, children: f.t.yes }),
1807
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(null), children: f.t.cancel })
1808
+ ] });
1713
1809
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: f.cx("account"), ...root, children: [
1714
1810
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
1715
1811
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h2", { className: f.cx("heading"), children: [
@@ -1720,23 +1816,14 @@ function OrganizationMembers(props) {
1720
1816
  problem && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(problem) }),
1721
1817
  members === null ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1722
1818
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.membersHidden }),
1723
- auth.session && (asking === auth.session.user.id ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { role: "group", "aria-label": f.t.memberLeave, children: [
1724
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: f.t.memberLeaveConfirm.replace("{org}", seat.name) }),
1725
- " ",
1726
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1727
- f.Button,
1728
- {
1729
- type: "button",
1730
- disabled: f.busy,
1731
- className: f.cx("providerButton"),
1732
- onClick: () => void remove({ userId: auth.session.user.id, you: true }),
1733
- children: f.t.yes
1734
- }
1735
- ),
1736
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(null), children: f.t.cancel })
1737
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(auth.session.user.id), children: f.t.memberLeave }))
1819
+ auth.session && (asking === auth.session.user.id ? confirming(
1820
+ f.t.memberLeaveConfirm.replace("{org}", seat.name),
1821
+ f.t.memberLeave,
1822
+ () => void remove({ userId: auth.session.user.id, you: true })
1823
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(auth.session.user.id), children: f.t.memberLeave }))
1738
1824
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: f.cx("sessions"), children: members.map((m) => {
1739
1825
  const who = m.name || m.email;
1826
+ const may = memberActions(seat, m);
1740
1827
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: f.cx("session"), children: [
1741
1828
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
1742
1829
  who,
@@ -1748,7 +1835,7 @@ function OrganizationMembers(props) {
1748
1835
  when(m.since)
1749
1836
  ] }),
1750
1837
  " ",
1751
- manage && roles.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1838
+ may.changeRole && giveable.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1752
1839
  "select",
1753
1840
  {
1754
1841
  "aria-label": `${f.t.memberRole}: ${who}`,
@@ -1756,31 +1843,41 @@ function OrganizationMembers(props) {
1756
1843
  value: m.role,
1757
1844
  disabled: f.busy,
1758
1845
  onChange: (e) => void change(m, e.target.value),
1759
- children: roles.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: c.role, children: c.name || c.role }, c.role))
1846
+ children: (giveable.some((c) => c.role === m.role) ? giveable : [...roles.filter((c) => c.role === m.role), ...giveable]).map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: c.role, children: c.name || c.role }, c.role))
1760
1847
  }
1761
1848
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label(m.role) }),
1762
1849
  " ",
1763
- asking === m.userId ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { role: "group", "aria-label": m.you ? f.t.memberLeave : f.t.memberRemove, children: [
1764
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: m.you ? f.t.memberLeaveConfirm.replace("{org}", seat.name) : f.t.memberRemoveConfirm.replace("{who}", who) }),
1765
- " ",
1766
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void remove(m), children: f.t.yes }),
1767
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(null), children: f.t.cancel })
1768
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1769
- manage && m.invited && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void inviteAgain(m), children: f.t.memberInviteAgain }),
1770
- (manage || m.you) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(m.userId), children: m.you ? f.t.memberLeave : f.t.memberRemove })
1850
+ asking === m.userId ? confirming(
1851
+ m.you ? f.t.memberLeaveConfirm.replace("{org}", seat.name) : f.t.memberRemoveConfirm.replace("{who}", who),
1852
+ m.you ? f.t.memberLeave : f.t.memberRemove,
1853
+ () => void remove(m)
1854
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1855
+ may.inviteAgain && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void inviteAgain(m), children: f.t.memberInviteAgain }),
1856
+ may.remove && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking(m.userId), children: m.you ? f.t.memberLeave : f.t.memberRemove })
1771
1857
  ] })
1772
1858
  ] }, m.userId);
1773
1859
  }) }),
1774
1860
  done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: done })
1775
1861
  ] }),
1776
- manage && roles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
1862
+ manage && giveable.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
1777
1863
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.memberAdd }),
1778
1864
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "off", required: true, onChange: setEmail }),
1779
1865
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: f.cx("field"), children: [
1780
1866
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: f.cx("label"), children: f.t.memberRole }),
1781
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { name: "role", className: f.cx("input"), value: role, onChange: (e) => setRole(e.target.value), children: roles.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: c.role, children: c.name || c.role }, c.role)) })
1867
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", { name: "role", className: f.cx("input"), value: role, onChange: (e) => setRole(e.target.value), children: giveable.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: c.role, children: c.name || c.role }, c.role)) })
1782
1868
  ] }),
1783
1869
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.memberAdd })
1870
+ ] }),
1871
+ (seat.can.update || seat.can.archive || seat.can.delete) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
1872
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.organization }),
1873
+ seat.can.update && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: rename, noValidate: true, children: [
1874
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.name, value: name, autoComplete: "organization", required: true, onChange: setName }),
1875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.organizationRename })
1876
+ ] }),
1877
+ asking === "archive" ? confirming(f.t.organizationArchiveConfirm.replace("{org}", seat.name), f.t.organizationArchive, () => void close("archive")) : asking === "delete" ? confirming(f.t.organizationDeleteConfirm.replace("{org}", seat.name), f.t.organizationDelete, () => void close("delete")) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
1878
+ seat.can.archive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking("archive"), children: f.t.organizationArchive }),
1879
+ seat.can.delete && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => setAsking("delete"), children: f.t.organizationDelete })
1880
+ ] })
1784
1881
  ] })
1785
1882
  ] });
1786
1883
  }