@stapel/workspaces-react 0.5.0 → 0.7.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 (100) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/api/generated/schema.d.ts +384 -4
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +20 -5
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/workspacesApi.d.ts +23 -1
  7. package/dist/api/workspacesApi.d.ts.map +1 -1
  8. package/dist/api/workspacesApi.js +4 -0
  9. package/dist/api/workspacesApi.js.map +1 -1
  10. package/dist/default/InviteAcceptPage.d.ts +28 -0
  11. package/dist/default/InviteAcceptPage.d.ts.map +1 -0
  12. package/dist/default/InviteAcceptPage.js +141 -0
  13. package/dist/default/InviteAcceptPage.js.map +1 -0
  14. package/dist/default/MembersManager.d.ts +4 -4
  15. package/dist/default/MembersManager.d.ts.map +1 -1
  16. package/dist/default/MembersManager.js +61 -56
  17. package/dist/default/MembersManager.js.map +1 -1
  18. package/dist/default/index.d.ts +2 -0
  19. package/dist/default/index.d.ts.map +1 -1
  20. package/dist/default/index.js +1 -0
  21. package/dist/default/index.js.map +1 -1
  22. package/dist/flows/inviteAcceptFlow.d.ts +155 -0
  23. package/dist/flows/inviteAcceptFlow.d.ts.map +1 -0
  24. package/dist/flows/inviteAcceptFlow.js +126 -0
  25. package/dist/flows/inviteAcceptFlow.js.map +1 -0
  26. package/dist/headless/Can.d.ts +40 -0
  27. package/dist/headless/Can.d.ts.map +1 -0
  28. package/dist/headless/Can.js +30 -0
  29. package/dist/headless/Can.js.map +1 -0
  30. package/dist/headless/InviteAcceptFlow.d.ts +44 -0
  31. package/dist/headless/InviteAcceptFlow.d.ts.map +1 -0
  32. package/dist/headless/InviteAcceptFlow.js +67 -0
  33. package/dist/headless/InviteAcceptFlow.js.map +1 -0
  34. package/dist/headless/RoleSelect.d.ts +41 -0
  35. package/dist/headless/RoleSelect.d.ts.map +1 -0
  36. package/dist/headless/RoleSelect.js +34 -0
  37. package/dist/headless/RoleSelect.js.map +1 -0
  38. package/dist/i18n/generated/errors.gen.d.ts +48 -0
  39. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  40. package/dist/i18n/generated/errors.gen.js +24 -0
  41. package/dist/i18n/generated/errors.gen.js.map +1 -1
  42. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  43. package/dist/i18n/generated/errors.ru.gen.js +8 -0
  44. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  45. package/dist/i18n/keys.d.ts +28 -0
  46. package/dist/i18n/keys.d.ts.map +1 -1
  47. package/dist/i18n/keys.js +64 -0
  48. package/dist/i18n/keys.js.map +1 -1
  49. package/dist/i18n/ru.d.ts.map +1 -1
  50. package/dist/i18n/ru.js +30 -0
  51. package/dist/i18n/ru.js.map +1 -1
  52. package/dist/index.d.ts +14 -3
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +9 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/model/capabilities.d.ts +21 -0
  57. package/dist/model/capabilities.d.ts.map +1 -0
  58. package/dist/model/capabilities.js +32 -0
  59. package/dist/model/capabilities.js.map +1 -0
  60. package/dist/model/emailMask.d.ts +22 -0
  61. package/dist/model/emailMask.d.ts.map +1 -0
  62. package/dist/model/emailMask.js +36 -0
  63. package/dist/model/emailMask.js.map +1 -0
  64. package/dist/model/mutations.d.ts +23 -3
  65. package/dist/model/mutations.d.ts.map +1 -1
  66. package/dist/model/mutations.js +39 -1
  67. package/dist/model/mutations.js.map +1 -1
  68. package/dist/model/queries.d.ts +56 -6
  69. package/dist/model/queries.d.ts.map +1 -1
  70. package/dist/model/queries.js +75 -7
  71. package/dist/model/queries.js.map +1 -1
  72. package/dist/model/queryKeys.d.ts +2 -0
  73. package/dist/model/queryKeys.d.ts.map +1 -1
  74. package/dist/model/queryKeys.js +6 -0
  75. package/dist/model/queryKeys.js.map +1 -1
  76. package/llms.txt +40 -15
  77. package/manifest.json +225 -3
  78. package/package.json +5 -5
  79. package/src/analytics/generated/events.json +1 -1
  80. package/src/api/generated/schema.ts +384 -4
  81. package/src/api/types.ts +20 -5
  82. package/src/api/workspacesApi.ts +50 -0
  83. package/src/default/InviteAcceptPage.tsx +373 -0
  84. package/src/default/MembersManager.tsx +155 -145
  85. package/src/default/index.ts +2 -0
  86. package/src/flows/inviteAcceptFlow.ts +289 -0
  87. package/src/headless/Can.tsx +49 -0
  88. package/src/headless/InviteAcceptFlow.tsx +105 -0
  89. package/src/headless/RoleSelect.tsx +58 -0
  90. package/src/i18n/generated/errors.gen.ts +24 -0
  91. package/src/i18n/generated/errors.json +62 -0
  92. package/src/i18n/generated/errors.ru.gen.ts +8 -0
  93. package/src/i18n/keys.ts +68 -0
  94. package/src/i18n/ru.ts +34 -0
  95. package/src/index.ts +26 -0
  96. package/src/model/capabilities.ts +37 -0
  97. package/src/model/emailMask.ts +37 -0
  98. package/src/model/mutations.ts +56 -4
  99. package/src/model/queries.ts +99 -7
  100. package/src/model/queryKeys.ts +10 -0
@@ -181,6 +181,49 @@ export interface paths {
181
181
  patch?: never;
182
182
  trace?: never;
183
183
  };
184
+ "/workspaces/api/v1/{workspace_id}/members/provision": {
185
+ parameters: {
186
+ query?: never;
187
+ header?: never;
188
+ path?: never;
189
+ cookie?: never;
190
+ };
191
+ get?: never;
192
+ put?: never;
193
+ /**
194
+ * @description Provision an org-created (synthetic) member (org-program spec §C1).
195
+ *
196
+ * The org mints its own login/password account: full username is
197
+ * ``{workspace_slug}/{username_local}`` (namespaced — the slug is
198
+ * globally unique, so orgs cannot collide), the account is created by
199
+ * ``auth.provision_user`` with the workspace's first-login policy
200
+ * (``settings.security.provisioned_user_policy``: forced password change
201
+ * or mandatory MFA enrollment) and joins immediately
202
+ * (``accepted_at=now``, ``provisioned=True``).
203
+ *
204
+ * Gate stack, in order: HIGH step-up (``@requires_verification``, scope
205
+ * ``sensitive`` — the same store as admin step-up) → capability
206
+ * ``members.provision`` (403) → entitlement ``workspaces.provision_user``
207
+ * (402; degrades to allow without billing) → optional per-user debit
208
+ * (``STAPEL_WORKSPACES["PROVISION_USER_CREDITS"]`` > 0).
209
+ *
210
+ * Credentials: a synthetic account normally has no email — when the
211
+ * request omits ``email``, the server-generated password comes back in
212
+ * the response (``generated_password``) exactly once and no letter is
213
+ * sent. With ``email``, the ``workspace.provisioned_account`` letter
214
+ * carries the credentials as well. Auth's structured failures pass
215
+ * through keyed (``error.409.username_taken`` / auth's 400s); auth not
216
+ * wired → honest 503, this seam never degrades to allow.
217
+ *
218
+ * **Permissions:** `IsAuthenticated`
219
+ */
220
+ post: operations["workspaces_api_v1_members_provision_create"];
221
+ delete?: never;
222
+ options?: never;
223
+ head?: never;
224
+ patch?: never;
225
+ trace?: never;
226
+ };
184
227
  "/workspaces/api/v1/internal/{workspace_id}/members/{user_id}": {
185
228
  parameters: {
186
229
  query?: never;
@@ -223,6 +266,90 @@ export interface paths {
223
266
  patch?: never;
224
267
  trace?: never;
225
268
  };
269
+ "/workspaces/api/v1/invitations/{token}": {
270
+ parameters: {
271
+ query?: never;
272
+ header?: never;
273
+ path?: never;
274
+ cookie?: never;
275
+ };
276
+ /**
277
+ * @description Public invitation preview — what the /invite/{token} page renders.
278
+ *
279
+ * AllowAny by design (org-program spec §B2): the invitee has no session
280
+ * yet; the token in the URL is the bearer secret. The response leaks
281
+ * nothing harvestable — the email is masked, and ``status`` /
282
+ * ``email_registered`` are exactly what the frontend flow machine needs
283
+ * to route (login vs claim vs terminal-state screen). Throttled as an
284
+ * enumeration backstop. The token is never logged.
285
+ *
286
+ * **Permissions:** `AllowAny`
287
+ */
288
+ get: operations["workspaces_api_v1_invitations_retrieve"];
289
+ put?: never;
290
+ post?: never;
291
+ delete?: never;
292
+ options?: never;
293
+ head?: never;
294
+ patch?: never;
295
+ trace?: never;
296
+ };
297
+ "/workspaces/api/v1/invitations/{token}/claim": {
298
+ parameters: {
299
+ query?: never;
300
+ header?: never;
301
+ path?: never;
302
+ cookie?: never;
303
+ };
304
+ get?: never;
305
+ put?: never;
306
+ /**
307
+ * @description Mint a login grant for a not-yet-registered invitee (spec §B2-B3).
308
+ *
309
+ * AllowAny — the whole point is that no account exists yet. Only valid
310
+ * for ``email_registered == false``: an existing account gets 409 and the
311
+ * frontend switches to login. The grant comes from auth's
312
+ * ``auth.issue_login_grant`` comm Function (``create_if_missing`` — the
313
+ * verified account materializes on exchange); if that Function is not
314
+ * wired, the answer is an honest 503 — an invite flow without auth is
315
+ * meaningless, so this seam never degrades to allow. The invitation is
316
+ * NOT consumed here: accept stays a separate, deliberate step after
317
+ * setup. Neither the invite token nor the grant token is ever logged.
318
+ *
319
+ * **Permissions:** `AllowAny`
320
+ */
321
+ post: operations["workspaces_api_v1_invitations_claim_create"];
322
+ delete?: never;
323
+ options?: never;
324
+ head?: never;
325
+ patch?: never;
326
+ trace?: never;
327
+ };
328
+ "/workspaces/api/v1/invitations/{token}/decline": {
329
+ parameters: {
330
+ query?: never;
331
+ header?: never;
332
+ path?: never;
333
+ cookie?: never;
334
+ };
335
+ get?: never;
336
+ put?: never;
337
+ /**
338
+ * @description Decline an invitation — the invitee's terminal "no" (spec §B2).
339
+ *
340
+ * Authenticated + email-match, exactly like accept: only the invited
341
+ * account may resolve the invitation, in either direction. Decline ≠
342
+ * revoke — both states stay distinguishable in the preview ``status``.
343
+ *
344
+ * **Permissions:** `IsAuthenticated`
345
+ */
346
+ post: operations["workspaces_api_v1_invitations_decline_create"];
347
+ delete?: never;
348
+ options?: never;
349
+ head?: never;
350
+ patch?: never;
351
+ trace?: never;
352
+ };
226
353
  "/workspaces/api/v1/invitations/accept": {
227
354
  parameters: {
228
355
  query?: never;
@@ -250,6 +377,31 @@ export interface paths {
250
377
  patch?: never;
251
378
  trace?: never;
252
379
  };
380
+ "/workspaces/api/v1/roles": {
381
+ parameters: {
382
+ query?: never;
383
+ header?: never;
384
+ path?: never;
385
+ cookie?: never;
386
+ };
387
+ /**
388
+ * @description The effective role registry — metadata for frontends (spec §A2).
389
+ *
390
+ * Lets a RoleSelect stop hardcoding the builtin four: builtin roles plus
391
+ * the deployment's ``STAPEL_WORKSPACES["ROLES"]`` overlay, capability
392
+ * strings verbatim (wildcards included), ordered by descending rank.
393
+ *
394
+ * **Permissions:** `IsAuthenticated`
395
+ */
396
+ get: operations["workspaces_api_v1_roles_retrieve"];
397
+ put?: never;
398
+ post?: never;
399
+ delete?: never;
400
+ options?: never;
401
+ head?: never;
402
+ patch?: never;
403
+ trace?: never;
404
+ };
253
405
  }
254
406
  export type webhooks = Record<string, never>;
255
407
  export interface components {
@@ -267,6 +419,41 @@ export interface components {
267
419
  /** @description Invite token from the email link */
268
420
  token: string;
269
421
  };
422
+ /** @description Login-grant mint for an unregistered invitee (claim step). */
423
+ InvitationClaimResponse: {
424
+ /** @description Single-use, short-lived login grant token — exchange it at auth's POST /grant/exchange/ for a session (creates the verified account). A credential: never log it */
425
+ grant_token: string;
426
+ };
427
+ /** @description Public (AllowAny) invitation preview — what the /invite/{token} page renders before any auth decision. */
428
+ InvitationPreviewResponse: {
429
+ /**
430
+ * @description Display name of the inviting workspace
431
+ * @example Acme Engineering
432
+ */
433
+ workspace_name: string;
434
+ /**
435
+ * @description Role granted on acceptance
436
+ * @example member
437
+ */
438
+ role: string;
439
+ /**
440
+ * @description Invited email, masked for the public page
441
+ * @example m***@e***.com
442
+ */
443
+ email_masked: string;
444
+ /**
445
+ * @description Invitation state. One of pending / expired / revoked / accepted / declined
446
+ * @example pending
447
+ */
448
+ status: string;
449
+ /** @description Whether an account already exists for the invited email — steers the frontend to login vs claim */
450
+ email_registered: boolean;
451
+ /**
452
+ * @description ISO 8601 expiry time
453
+ * @example 2026-07-31T10:00:00Z
454
+ */
455
+ expires_at: string;
456
+ };
270
457
  /** @description InvitationResponse(id: uuid.UUID, workspace_id: uuid.UUID, email: str, role: str, expires_at: str, accepted_at: Optional[str], revoked_at: Optional[str], created_at: str) */
271
458
  InvitationResponse: {
272
459
  /** Format: uuid */
@@ -330,6 +517,12 @@ export interface components {
330
517
  accepted_at: string | null;
331
518
  /** @description ISO 8601 last access; null if never accessed */
332
519
  last_accessed_at: string | null;
520
+ /** @description Whether this is an org-created (synthetic) member joined via members/provision */
521
+ provisioned?: boolean;
522
+ /** @description ISO 8601 suspension timestamp; null while active. Suspension is not removal — the role stays but access is closed */
523
+ suspended_at?: string | null;
524
+ /** @description Why the membership is suspended (canonical value no_mfa); null while active */
525
+ suspension_reason?: string | null;
333
526
  };
334
527
  PaginatedMemberResponseList: {
335
528
  items: components["schemas"]["MemberResponse"][];
@@ -356,6 +549,78 @@ export interface components {
356
549
  [key: string]: unknown;
357
550
  } | null;
358
551
  };
552
+ /** @description Provision an org-created (synthetic) member (org-program spec §C1). */
553
+ ProvisionMemberRequest: {
554
+ /**
555
+ * @description Local part of the login; the full username becomes "{workspace_slug}/{username_local}". Stock username alphabet, no slash
556
+ * @example jdoe
557
+ */
558
+ username_local: string;
559
+ /**
560
+ * @description Role to grant (effective registry; owner is not provisionable)
561
+ * @example member
562
+ */
563
+ role?: string;
564
+ /** @description Initial password chosen by the admin. Omitted: the server generates a crypto-strong one, returned once as generated_password */
565
+ password?: string | null;
566
+ /**
567
+ * @description Display-name hint forwarded to auth/profiles
568
+ * @example Jane Doe
569
+ */
570
+ display_name?: string | null;
571
+ /** @description Optional email anchor (stored UNVERIFIED by auth). When present, the provisioned-account email with the credentials is sent there. Normally omitted — synthetic accounts have no email */
572
+ email?: string | null;
573
+ };
574
+ /** @description Result of provisioning an org member. */
575
+ ProvisionMemberResponse: {
576
+ /**
577
+ * Format: uuid
578
+ * @description UUID of the created account
579
+ */
580
+ user_id: string;
581
+ /**
582
+ * @description Full namespaced login
583
+ * @example acme-eng/jdoe
584
+ */
585
+ username: string;
586
+ /**
587
+ * @description Granted role
588
+ * @example member
589
+ */
590
+ role: string;
591
+ /** @description Server-generated initial password — returned exactly ONCE, only when the request omitted password. Store it or hand it to the user now; it cannot be re-fetched. A credential: never log it */
592
+ generated_password?: string | null;
593
+ };
594
+ /** @description RoleListResponse(roles: List[stapel_workspaces.dto.RoleResponse] = <factory>) */
595
+ RoleListResponse: {
596
+ roles?: components["schemas"]["RoleResponse"][];
597
+ };
598
+ /** @description One role of the effective registry (builtin + STAPEL_WORKSPACES overlay). */
599
+ RoleResponse: {
600
+ /**
601
+ * @description Role key
602
+ * @example admin
603
+ */
604
+ role: string;
605
+ /**
606
+ * @description Ordering weight; higher = more powerful
607
+ * @example 300
608
+ */
609
+ rank: number;
610
+ /**
611
+ * @description Granted capability strings, verbatim (wildcards like * included)
612
+ * @example [
613
+ * "workspace.view",
614
+ * "members.invite"
615
+ * ]
616
+ */
617
+ capabilities: string[];
618
+ /**
619
+ * @description Whether the role key is one of the builtin four
620
+ * @example true
621
+ */
622
+ builtin: boolean;
623
+ };
359
624
  /** @description Structured error returned by all Stapel API endpoints. */
360
625
  StapelError: {
361
626
  /**
@@ -363,10 +628,7 @@ export interface components {
363
628
  * @example error.404.not_found
364
629
  */
365
630
  localizable_error: string;
366
- /**
367
- * @description Human-readable message in English
368
- * @example Requested resource not found
369
- */
631
+ /** @description Human-readable fallback/debug message — not reliably English, */
370
632
  error: string;
371
633
  /**
372
634
  * @description Context values for template placeholders
@@ -377,6 +639,8 @@ export interface components {
377
639
  params?: {
378
640
  [key: string]: unknown;
379
641
  };
642
+ /** @description Active Django locale `error` was rendered in (e.g */
643
+ error_language?: string;
380
644
  };
381
645
  /** @description Create-workspace payload. */
382
646
  WorkspaceCreateRequest: {
@@ -454,6 +718,14 @@ export interface components {
454
718
  * @example 2026-05-20T10:00:00Z
455
719
  */
456
720
  updated_at: string;
721
+ /**
722
+ * @description Granted capability strings of the requesting user's role, verbatim from the registry (wildcards like * included)
723
+ * @example [
724
+ * "workspace.view",
725
+ * "members.view"
726
+ * ]
727
+ */
728
+ my_capabilities?: string[];
457
729
  };
458
730
  };
459
731
  responses: never;
@@ -682,6 +954,33 @@ export interface operations {
682
954
  };
683
955
  };
684
956
  };
957
+ workspaces_api_v1_members_provision_create: {
958
+ parameters: {
959
+ query?: never;
960
+ header?: never;
961
+ path: {
962
+ workspace_id: string;
963
+ };
964
+ cookie?: never;
965
+ };
966
+ requestBody: {
967
+ content: {
968
+ "application/json": components["schemas"]["ProvisionMemberRequest"];
969
+ "application/x-www-form-urlencoded": components["schemas"]["ProvisionMemberRequest"];
970
+ "multipart/form-data": components["schemas"]["ProvisionMemberRequest"];
971
+ };
972
+ };
973
+ responses: {
974
+ 201: {
975
+ headers: {
976
+ [name: string]: unknown;
977
+ };
978
+ content: {
979
+ "application/json": components["schemas"]["ProvisionMemberResponse"];
980
+ };
981
+ };
982
+ };
983
+ };
685
984
  workspaces_api_v1_internal_members_retrieve: {
686
985
  parameters: {
687
986
  query?: never;
@@ -733,6 +1032,68 @@ export interface operations {
733
1032
  };
734
1033
  };
735
1034
  };
1035
+ workspaces_api_v1_invitations_retrieve: {
1036
+ parameters: {
1037
+ query?: never;
1038
+ header?: never;
1039
+ path: {
1040
+ token: string;
1041
+ };
1042
+ cookie?: never;
1043
+ };
1044
+ requestBody?: never;
1045
+ responses: {
1046
+ 200: {
1047
+ headers: {
1048
+ [name: string]: unknown;
1049
+ };
1050
+ content: {
1051
+ "application/json": components["schemas"]["InvitationPreviewResponse"];
1052
+ };
1053
+ };
1054
+ };
1055
+ };
1056
+ workspaces_api_v1_invitations_claim_create: {
1057
+ parameters: {
1058
+ query?: never;
1059
+ header?: never;
1060
+ path: {
1061
+ token: string;
1062
+ };
1063
+ cookie?: never;
1064
+ };
1065
+ requestBody?: never;
1066
+ responses: {
1067
+ 200: {
1068
+ headers: {
1069
+ [name: string]: unknown;
1070
+ };
1071
+ content: {
1072
+ "application/json": components["schemas"]["InvitationClaimResponse"];
1073
+ };
1074
+ };
1075
+ };
1076
+ };
1077
+ workspaces_api_v1_invitations_decline_create: {
1078
+ parameters: {
1079
+ query?: never;
1080
+ header?: never;
1081
+ path: {
1082
+ token: string;
1083
+ };
1084
+ cookie?: never;
1085
+ };
1086
+ requestBody?: never;
1087
+ responses: {
1088
+ /** @description No response body */
1089
+ 204: {
1090
+ headers: {
1091
+ [name: string]: unknown;
1092
+ };
1093
+ content?: never;
1094
+ };
1095
+ };
1096
+ };
736
1097
  workspaces_api_v1_invitations_accept_create: {
737
1098
  parameters: {
738
1099
  query?: never;
@@ -758,4 +1119,23 @@ export interface operations {
758
1119
  };
759
1120
  };
760
1121
  };
1122
+ workspaces_api_v1_roles_retrieve: {
1123
+ parameters: {
1124
+ query?: never;
1125
+ header?: never;
1126
+ path?: never;
1127
+ cookie?: never;
1128
+ };
1129
+ requestBody?: never;
1130
+ responses: {
1131
+ 200: {
1132
+ headers: {
1133
+ [name: string]: unknown;
1134
+ };
1135
+ content: {
1136
+ "application/json": components["schemas"]["RoleListResponse"];
1137
+ };
1138
+ };
1139
+ };
1140
+ };
761
1141
  }
package/src/api/types.ts CHANGED
@@ -38,15 +38,30 @@ export type MemberRoleUpdate = Schemas["PatchedMemberUpdateRequest"];
38
38
  export type Invitation = Schemas["InvitationResponse"];
39
39
  /** POST /invitations/accept request body — the token from the email link. */
40
40
  export type InvitationAccept = Schemas["InvitationAcceptRequest"];
41
+ /** GET /invitations/{token} 200 body — the public (AllowAny) preview the
42
+ * `/invite/{token}` page renders before any auth decision (org-program §B2). */
43
+ export type InvitationPreview = Schemas["InvitationPreviewResponse"];
44
+ /** POST /invitations/{token}/claim 200 body — the single-use login grant for a
45
+ * not-yet-registered invitee; exchange it at auth's POST /grant/exchange/. */
46
+ export type InvitationClaim = Schemas["InvitationClaimResponse"];
47
+ /** One role of the effective registry (builtin + `STAPEL_WORKSPACES["ROLES"]`
48
+ * overlay) — GET /roles (org-program §A2). Capability strings are verbatim,
49
+ * wildcards (`*`, `members.*`) included. */
50
+ export type RoleInfo = Schemas["RoleResponse"];
51
+ /** GET /roles 200 body — the effective role registry, rank-descending. */
52
+ export type RoleList = Schemas["RoleListResponse"];
41
53
 
42
54
  // ── documented corrections (drf-spectacular under-describes) ──────────────────
43
55
 
44
56
  /**
45
- * A workspace membership role, ordered least→most privileged. The generated
46
- * schema types `MemberResponse.role` / the invite `role` as a bare `string`,
47
- * but the backend (`models.Role`, a Django `TextChoices`) constrains it to
48
- * exactly these values. Narrowing here gives call sites a checked union — a
49
- * documented correction of the same kind auth-react/billing-react apply.
57
+ * A BUILTIN workspace membership role, ordered least→most privileged. Since
58
+ * stapel-workspaces 0.6.0 (org-program §A1) the effective registry is
59
+ * settings-extensible — a deployment can add roles (`secretary`, …) via
60
+ * `STAPEL_WORKSPACES["ROLES"]`, so `MemberResponse.role` is genuinely an open
61
+ * `string` on the wire and the registry endpoint (GET /roles, {@link RoleInfo})
62
+ * is the source of truth for what exists. This union names the four builtin
63
+ * keys that are ALWAYS present (with `owner` system-protected); use it for
64
+ * literals, not to validate server data.
50
65
  */
51
66
  export type WorkspaceRole = "owner" | "admin" | "member" | "viewer";
52
67
 
@@ -7,6 +7,9 @@ import type {
7
7
  MemberRoleUpdate,
8
8
  MembersParams,
9
9
  InvitationAccept,
10
+ InvitationClaim,
11
+ InvitationPreview,
12
+ RoleList,
10
13
  Workspace,
11
14
  WorkspaceCreate,
12
15
  WorkspaceList,
@@ -86,6 +89,34 @@ export interface WorkspacesApi {
86
89
 
87
90
  /** Accept an invitation by its token — returns the caller's new membership. */
88
91
  acceptInvitation(body: InvitationAccept): Promise<Member>;
92
+
93
+ // ── invite flow (org-program §B2, stapel-workspaces ≥0.7.0) ────────────────
94
+
95
+ /**
96
+ * Public (AllowAny) invitation preview — what the `/invite/{token}` page
97
+ * renders before any auth decision. The token in the URL is the bearer
98
+ * secret: a credential, never log it.
99
+ */
100
+ getInvitationPreview(token: string): Promise<InvitationPreview>;
101
+ /**
102
+ * Mint a login grant for a NOT-yet-registered invitee (AllowAny; only valid
103
+ * while `email_registered === false` — an existing account gets 409
104
+ * `email_already_registered` and the frontend switches to login). The
105
+ * invitation is NOT consumed: accept stays a separate, deliberate step.
106
+ * The returned `grant_token` is exchanged at AUTH's `POST /grant/exchange/`
107
+ * — deliberately not this pair's surface (pairs don't depend on each
108
+ * other); `InviteAcceptFlow` hands it outward via `onLoginGrant`.
109
+ */
110
+ claimInvitation(token: string): Promise<InvitationClaim>;
111
+ /** Decline an invitation (authenticated + email-match) — the invitee's
112
+ * terminal "no". Decline ≠ revoke; both stay distinguishable in `status`. */
113
+ declineInvitation(token: string): Promise<void>;
114
+
115
+ // ── role registry (org-program §A2, stapel-workspaces ≥0.6.0) ──────────────
116
+
117
+ /** The effective role registry (builtin + deployment overlay), for
118
+ * RoleSelect-class UI — capability strings verbatim, rank-descending. */
119
+ listRoles(): Promise<RoleList>;
89
120
  }
90
121
 
91
122
  export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
@@ -147,5 +178,24 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
147
178
  body satisfies InvitationAccept,
148
179
  mutating()
149
180
  ),
181
+
182
+ getInvitationPreview: (token) =>
183
+ client.get(`/invitations/${encodeURIComponent(token)}`),
184
+
185
+ claimInvitation: (token) =>
186
+ client.post(
187
+ `/invitations/${encodeURIComponent(token)}/claim`,
188
+ undefined,
189
+ mutating()
190
+ ),
191
+
192
+ declineInvitation: (token) =>
193
+ client.post(
194
+ `/invitations/${encodeURIComponent(token)}/decline`,
195
+ undefined,
196
+ mutating()
197
+ ),
198
+
199
+ listRoles: () => client.get("/roles"),
150
200
  };
151
201
  }