@stapel/workspaces-react 0.8.0 → 0.10.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 (48) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/api/generated/schema.d.ts +263 -2
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +21 -0
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/workspacesApi.d.ts +31 -1
  7. package/dist/api/workspacesApi.d.ts.map +1 -1
  8. package/dist/api/workspacesApi.js +3 -0
  9. package/dist/api/workspacesApi.js.map +1 -1
  10. package/dist/default/MembersManager.js +2 -2
  11. package/dist/i18n/generated/errors.gen.d.ts +36 -0
  12. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  13. package/dist/i18n/generated/errors.gen.js +18 -0
  14. package/dist/i18n/generated/errors.gen.js.map +1 -1
  15. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  16. package/dist/i18n/generated/errors.ru.gen.js +6 -0
  17. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  18. package/dist/index.d.ts +4 -4
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/model/mutations.d.ts +84 -2
  23. package/dist/model/mutations.d.ts.map +1 -1
  24. package/dist/model/mutations.js +94 -1
  25. package/dist/model/mutations.js.map +1 -1
  26. package/dist/model/queries.d.ts +19 -1
  27. package/dist/model/queries.d.ts.map +1 -1
  28. package/dist/model/queries.js +25 -0
  29. package/dist/model/queries.js.map +1 -1
  30. package/dist/model/queryKeys.d.ts +2 -0
  31. package/dist/model/queryKeys.d.ts.map +1 -1
  32. package/dist/model/queryKeys.js +9 -1
  33. package/dist/model/queryKeys.js.map +1 -1
  34. package/llms.txt +10 -3
  35. package/manifest.json +97 -2
  36. package/package.json +3 -3
  37. package/src/analytics/generated/events.json +1 -1
  38. package/src/api/generated/schema.ts +263 -2
  39. package/src/api/types.ts +22 -0
  40. package/src/api/workspacesApi.ts +59 -0
  41. package/src/default/MembersManager.tsx +2 -2
  42. package/src/i18n/generated/errors.gen.ts +18 -0
  43. package/src/i18n/generated/errors.json +44 -0
  44. package/src/i18n/generated/errors.ru.gen.ts +6 -0
  45. package/src/index.ts +7 -0
  46. package/src/model/mutations.ts +128 -1
  47. package/src/model/queries.ts +30 -0
  48. package/src/model/queryKeys.ts +11 -1
@@ -126,6 +126,46 @@ export interface paths {
126
126
  patch?: never;
127
127
  trace?: never;
128
128
  };
129
+ "/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/name": {
130
+ parameters: {
131
+ query?: never;
132
+ header?: never;
133
+ path?: never;
134
+ cookie?: never;
135
+ };
136
+ get?: never;
137
+ put?: never;
138
+ post?: never;
139
+ delete?: never;
140
+ options?: never;
141
+ head?: never;
142
+ /**
143
+ * @description ``PATCH <ws>/invitations/<id>/name`` — fix a pending invite's name hint.
144
+ *
145
+ * The same correction as :class:`MemberNameView`, one step earlier: the
146
+ * invitee has not accepted, so there is no profile of theirs to write and
147
+ * the name lives on the invitation as ``display_name_hint`` — the invite
148
+ * modal's "Name" field, which ``accept_invitation`` copies onto the
149
+ * membership at acceptance. Editing it after the fact is why this endpoint
150
+ * exists: before #109's invitation surface the only fix for a typo in an
151
+ * invitee's name was to revoke and re-invite, which re-mails the person.
152
+ *
153
+ * Only a **pending** invitation is editable, with the same keyed refusals
154
+ * revoke gives for each terminal state (``invitation_revoked`` /
155
+ * ``already_used`` / ``declined`` / ``expired``): an accepted invitation's
156
+ * name is the member's name now — use the member endpoint — and a dead
157
+ * invitation is not a thing to relabel. Unknown and cross-workspace ids
158
+ * collapse into one identical 404 (inherited resolution).
159
+ *
160
+ * The value is still held to stapel-profiles' name canon even though the
161
+ * column being written is local: this hint becomes a displayed name, and
162
+ * the two endpoints must not disagree about what a name may contain.
163
+ *
164
+ * **Permissions:** `IsAuthenticated`
165
+ */
166
+ patch: operations["workspaces_api_v1_invitations_name_partial_update"];
167
+ trace?: never;
168
+ };
129
169
  "/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/resend": {
130
170
  parameters: {
131
171
  query?: never;
@@ -261,6 +301,50 @@ export interface paths {
261
301
  patch: operations["workspaces_api_v1_members_partial_update"];
262
302
  trace?: never;
263
303
  };
304
+ "/workspaces/api/v1/{workspace_id}/members/{user_id}/name": {
305
+ parameters: {
306
+ query?: never;
307
+ header?: never;
308
+ path?: never;
309
+ cookie?: never;
310
+ };
311
+ get?: never;
312
+ put?: never;
313
+ post?: never;
314
+ delete?: never;
315
+ options?: never;
316
+ head?: never;
317
+ /**
318
+ * @description ``PATCH <ws>/members/<user_id>/name`` — correct a member's display name.
319
+ *
320
+ * Writes the CANONICAL name: stapel-profiles' ``Profile.display_name``,
321
+ * reached through this module's existing in-process profiles seam
322
+ * (``services.set_profile_display_name``), which also publishes
323
+ * ``profile.changed`` so every consumer of that name follows.
324
+ *
325
+ * NOT ``WorkspaceMember.display_name_hint``: the hint is a pre-profile
326
+ * placeholder, copied once at creation and dark from the moment a real
327
+ * profile exists (see its docstring in ``models.py``). Writing it would
328
+ * produce a correction the roster shows and nothing else in the product
329
+ * ever does — including, eventually, the roster.
330
+ *
331
+ * Where stapel-profiles does not run in this process there is nothing to
332
+ * write and no remote operation to call for it (that module publishes no
333
+ * write-somebody-else's-name endpoint and no comm Function), so the answer
334
+ * is an honest ``error.503.profiles_unavailable`` — never a 200 over a
335
+ * write that did not happen.
336
+ *
337
+ * Only an owner may rename an owner — the same hardcoded owner protection
338
+ * that role changes, removals and password resets carry. Renaming is not
339
+ * escalation, but an admin relabelling the owner of the organization on
340
+ * every screen in the product is close enough to the same act to answer
341
+ * the same way.
342
+ *
343
+ * **Permissions:** `IsAuthenticated`
344
+ */
345
+ patch: operations["workspaces_api_v1_members_name_partial_update"];
346
+ trace?: never;
347
+ };
264
348
  "/workspaces/api/v1/{workspace_id}/members/{user_id}/password/reset": {
265
349
  parameters: {
266
350
  query?: never;
@@ -403,6 +487,27 @@ export interface paths {
403
487
  patch?: never;
404
488
  trace?: never;
405
489
  };
490
+ "/workspaces/api/v1/instance": {
491
+ parameters: {
492
+ query?: never;
493
+ header?: never;
494
+ path?: never;
495
+ cookie?: never;
496
+ };
497
+ /**
498
+ * @description Instance shape: how a street signup lands (personal | none) and whether self-serve registration is open. Public, unauthenticated — the screen a kicked/left member sees depends on it.
499
+ *
500
+ * **Permissions:** `AllowAny`
501
+ */
502
+ get: operations["workspaces_api_v1_instance_retrieve"];
503
+ put?: never;
504
+ post?: never;
505
+ delete?: never;
506
+ options?: never;
507
+ head?: never;
508
+ patch?: never;
509
+ trace?: never;
510
+ };
406
511
  "/workspaces/api/v1/internal/{workspace_id}/members/{user_id}": {
407
512
  parameters: {
408
513
  query?: never;
@@ -585,6 +690,46 @@ export interface paths {
585
690
  export type webhooks = Record<string, never>;
586
691
  export interface components {
587
692
  schemas: {
693
+ /** @description What the name is after the write — the stored value, not the request. */
694
+ DisplayNameResponse: {
695
+ /**
696
+ * @description The stored name, trimmed and canon-checked. Empty string when the name was cleared
697
+ * @example Ada Lovelace
698
+ */
699
+ display_name: string;
700
+ };
701
+ /**
702
+ * @description How THIS instance is deployed — what the client needs to know before
703
+ * deciding what to show a person with no workspace.
704
+ *
705
+ * The ``landing`` axis (``STREET_LANDING_MODE``) has existed since
706
+ * 2026-08-03 but lived only in backend config, with nothing exposing it —
707
+ * a client had no way to tell a closed deployment from a public cloud one.
708
+ *
709
+ * This matters because the screen shown to someone who is no longer (or
710
+ * never was) a workspace member differs by deployment:
711
+ *
712
+ * * ``none`` — closed deployment: the person has no workspace and none is
713
+ * obtainable. An honest screen says "you're not a member, ask for an
714
+ * invite" and offers no path forward;
715
+ * * ``personal`` — public cloud: they have their own workspace, and the
716
+ * client can route them there.
717
+ *
718
+ * Offering "create a workspace" to someone the instance will never let
719
+ * create one is a dead end drawn as a button.
720
+ */
721
+ InstanceShapeResponse: {
722
+ /**
723
+ * @description What a "street" (no-invite) arrival lands on
724
+ * @example personal
725
+ */
726
+ landing: string;
727
+ /**
728
+ * @description Whether self-registration is available
729
+ * @example True
730
+ */
731
+ registration_open: boolean;
732
+ };
588
733
  /** @description Get-or-create result for a user's personal workspace (service-to-service). */
589
734
  InternalPersonalWorkspaceResponse: {
590
735
  /**
@@ -692,9 +837,19 @@ export interface components {
692
837
  * @example 0192a...
693
838
  */
694
839
  invited_by_id?: string | null;
840
+ /**
841
+ * @description Name hint typed for this invitee at invite time, if any; null otherwise
842
+ * @example Ada Lovelace
843
+ */
844
+ display_name?: string | null;
695
845
  };
696
846
  /** @description Invite payload. */
697
847
  MemberInviteRequest: {
848
+ /**
849
+ * @description Name hint for the invitee (this invite's "Name" field), applied to every email in this request. Optional — an invite without one behaves exactly as before. NOT written into stapel-profiles directly (that store is the invitee's own): held on the invitation, copied onto the member at accept time, and shown only until stapel-profiles has a real name for the person
850
+ * @example Ada Lovelace
851
+ */
852
+ display_name?: string | null;
698
853
  /**
699
854
  * @description One or more emails to invite
700
855
  * @example [
@@ -793,6 +948,11 @@ export interface components {
793
948
  suspended_at?: string | null;
794
949
  /** @description Why the membership is suspended (canonical value no_mfa); null while active */
795
950
  suspension_reason?: string | null;
951
+ /**
952
+ * @description Best-effort display name — a live lookup in stapel-profiles when it is installed and has one, else the name typed at invite/provision time (never both, never invented when neither exists). Null when nobody has one yet
953
+ * @example Ada Lovelace
954
+ */
955
+ display_name?: string | null;
796
956
  };
797
957
  PaginatedInvitationResponseList: {
798
958
  items: components["schemas"]["InvitationResponse"][];
@@ -820,6 +980,14 @@ export interface components {
820
980
  /** @description Number of items in current page */
821
981
  count: number;
822
982
  };
983
+ /** @description Roster-side name correction (one field, both name-edit endpoints). */
984
+ PatchedDisplayNameUpdateRequest: {
985
+ /**
986
+ * @description The name to show for this person. Held to stapel-profiles' display-name canon (validate_display_name) — this module never adds a second, differently-strict rule. Blank, whitespace-only, null and a missing key all mean the same thing: clear the name
987
+ * @example Ada Lovelace
988
+ */
989
+ display_name?: string | null;
990
+ };
823
991
  /** @description MemberUpdateRequest(role: str) */
824
992
  PatchedMemberUpdateRequest: {
825
993
  role?: string;
@@ -874,9 +1042,19 @@ export interface components {
874
1042
  /** @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 */
875
1043
  generated_password?: string | null;
876
1044
  };
877
- /** @description RoleListResponse(roles: List[stapel_workspaces.dto.RoleResponse] = <factory>) */
1045
+ /** @description The effective role registry (builtin four + STAPEL_WORKSPACES overlay). */
878
1046
  RoleListResponse: {
1047
+ /** @description One entry per effective role, descending rank */
879
1048
  roles?: components["schemas"]["RoleResponse"][];
1049
+ /**
1050
+ * @description Step-up level per namespaced capability string, builtin ``members.provision`` / ``members.password.reset`` / ``workspace.security.manage`` plus the deployment's ``STAPEL_WORKSPACES["CAPABILITY_LEVELS"]`` overlay. A capability absent from this map is ``"standard"`` — the client-side default. Lets a frontend stop hardcoding its own copy of this registry
1051
+ * @example {
1052
+ * "members.provision": "high"
1053
+ * }
1054
+ */
1055
+ capability_levels?: {
1056
+ [key: string]: string;
1057
+ };
880
1058
  };
881
1059
  /** @description One role of the effective registry (builtin + STAPEL_WORKSPACES overlay). */
882
1060
  RoleResponse: {
@@ -943,9 +1121,17 @@ export interface components {
943
1121
  */
944
1122
  type?: string;
945
1123
  };
946
- /** @description WorkspaceListResponse(workspaces: List[stapel_workspaces.dto.WorkspaceResponse] = <factory>) */
1124
+ /** @description The caller's own workspace list. */
947
1125
  WorkspaceListResponse: {
1126
+ /** @description The caller's active memberships (empty for a guest — see is_guest) */
948
1127
  workspaces?: components["schemas"]["WorkspaceResponse"][];
1128
+ /** @description True when the caller holds NO active mandate anywhere (permissions.is_guest) — the wire form of the mandate-model vardict's guest predicate (2026-08-03), so a frontend can render the "incomplete dashboard" without re-deriving it from workspaces == [] */
1129
+ is_guest?: boolean;
1130
+ /**
1131
+ * @description Which workspace a client should open when the person has expressed no choice — the instance's STAPEL_WORKSPACES["DEFAULT_WORKSPACE_ID"], echoed back ONLY when the caller actually holds an active membership in it, else "". Exists because clients that had to guess guessed badly: meettoday took `workspaces[0]` off a list ordered by last access, so a person in two spaces landed wherever they happened to have been last (measured 2026-08-06 as "the owner cannot see his own invitations" — they were in the OTHER space). A default, not a cage: an explicit choice still wins
1132
+ * @example a8bba7ae-5d2e-43e5-9911-0553e7df50b3
1133
+ */
1134
+ default_workspace_id?: string;
949
1135
  };
950
1136
  /** @description Workspace details. */
951
1137
  WorkspaceResponse: {
@@ -1163,6 +1349,34 @@ export interface operations {
1163
1349
  };
1164
1350
  };
1165
1351
  };
1352
+ workspaces_api_v1_invitations_name_partial_update: {
1353
+ parameters: {
1354
+ query?: never;
1355
+ header?: never;
1356
+ path: {
1357
+ invitation_id: string;
1358
+ workspace_id: string;
1359
+ };
1360
+ cookie?: never;
1361
+ };
1362
+ requestBody?: {
1363
+ content: {
1364
+ "application/json": components["schemas"]["PatchedDisplayNameUpdateRequest"];
1365
+ "application/x-www-form-urlencoded": components["schemas"]["PatchedDisplayNameUpdateRequest"];
1366
+ "multipart/form-data": components["schemas"]["PatchedDisplayNameUpdateRequest"];
1367
+ };
1368
+ };
1369
+ responses: {
1370
+ 200: {
1371
+ headers: {
1372
+ [name: string]: unknown;
1373
+ };
1374
+ content: {
1375
+ "application/json": components["schemas"]["DisplayNameResponse"];
1376
+ };
1377
+ };
1378
+ };
1379
+ };
1166
1380
  workspaces_api_v1_invitations_resend_create: {
1167
1381
  parameters: {
1168
1382
  query?: never;
@@ -1286,6 +1500,34 @@ export interface operations {
1286
1500
  };
1287
1501
  };
1288
1502
  };
1503
+ workspaces_api_v1_members_name_partial_update: {
1504
+ parameters: {
1505
+ query?: never;
1506
+ header?: never;
1507
+ path: {
1508
+ user_id: string;
1509
+ workspace_id: string;
1510
+ };
1511
+ cookie?: never;
1512
+ };
1513
+ requestBody?: {
1514
+ content: {
1515
+ "application/json": components["schemas"]["PatchedDisplayNameUpdateRequest"];
1516
+ "application/x-www-form-urlencoded": components["schemas"]["PatchedDisplayNameUpdateRequest"];
1517
+ "multipart/form-data": components["schemas"]["PatchedDisplayNameUpdateRequest"];
1518
+ };
1519
+ };
1520
+ responses: {
1521
+ 200: {
1522
+ headers: {
1523
+ [name: string]: unknown;
1524
+ };
1525
+ content: {
1526
+ "application/json": components["schemas"]["DisplayNameResponse"];
1527
+ };
1528
+ };
1529
+ };
1530
+ };
1289
1531
  workspaces_api_v1_members_password_reset_create: {
1290
1532
  parameters: {
1291
1533
  query?: never;
@@ -1368,6 +1610,25 @@ export interface operations {
1368
1610
  };
1369
1611
  };
1370
1612
  };
1613
+ workspaces_api_v1_instance_retrieve: {
1614
+ parameters: {
1615
+ query?: never;
1616
+ header?: never;
1617
+ path?: never;
1618
+ cookie?: never;
1619
+ };
1620
+ requestBody?: never;
1621
+ responses: {
1622
+ 200: {
1623
+ headers: {
1624
+ [name: string]: unknown;
1625
+ };
1626
+ content: {
1627
+ "application/json": components["schemas"]["InstanceShapeResponse"];
1628
+ };
1629
+ };
1630
+ };
1631
+ };
1371
1632
  workspaces_api_v1_internal_members_retrieve: {
1372
1633
  parameters: {
1373
1634
  query?: never;
package/src/api/types.ts CHANGED
@@ -53,6 +53,20 @@ export type MemberPasswordReset = Schemas["MemberPasswordResetRequest"];
53
53
  * — see {@link "../model/mutations.js".useResetMemberPassword}, which keeps it
54
54
  * out of the query cache. */
55
55
  export type MemberPasswordResetResult = Schemas["MemberPasswordResetResponse"];
56
+ /**
57
+ * PATCH /{id}/members/{userId}/name and PATCH
58
+ * /{id}/invitations/{invitationId}/name request body — the ONE field both
59
+ * name-edit endpoints take (stapel-workspaces ≥0.19.0). Blank,
60
+ * whitespace-only, `null` and a missing key all mean the same thing to the
61
+ * backend: clear the name. The value is held to stapel-profiles'
62
+ * `validate_display_name` canon (35-char ceiling as the serializer's
63
+ * `max_length`) — this module declares no second, differently-strict rule.
64
+ */
65
+ export type DisplayNameUpdate = Schemas["PatchedDisplayNameUpdateRequest"];
66
+ /** PATCH …/name 200 body — what the name IS after the write (the stored,
67
+ * trimmed, canon-checked value; empty string when the name was cleared), not
68
+ * an echo of the request. */
69
+ export type DisplayNameResult = Schemas["DisplayNameResponse"];
56
70
  /** POST /invitations/accept request body — the token from the email link. */
57
71
  export type InvitationAccept = Schemas["InvitationAcceptRequest"];
58
72
  /** GET /invitations/{token} 200 body — the public (AllowAny) preview the
@@ -68,6 +82,14 @@ export type RoleInfo = Schemas["RoleResponse"];
68
82
  /** GET /roles 200 body — the effective role registry, rank-descending. */
69
83
  export type RoleList = Schemas["RoleListResponse"];
70
84
 
85
+ /**
86
+ * Deployment shape: what a person landing "off the street" gets, and
87
+ * whether self-serve signup is open. A property of the INSTANCE, not of a
88
+ * workspace — and not a setting the customer can toggle: it's fixed at
89
+ * deployment time.
90
+ */
91
+ export type InstanceShape = Schemas["InstanceShapeResponse"];
92
+
71
93
  // ── documented corrections (drf-spectacular under-describes) ──────────────────
72
94
 
73
95
  /**
@@ -1,5 +1,7 @@
1
1
  import type { StapelClient, StapelRequestOptions } from "@stapel/core";
2
2
  import type {
3
+ DisplayNameResult,
4
+ DisplayNameUpdate,
3
5
  Invitation,
4
6
  InvitationPage,
5
7
  InvitationsParams,
@@ -14,6 +16,7 @@ import type {
14
16
  InvitationAccept,
15
17
  InvitationClaim,
16
18
  InvitationPreview,
19
+ InstanceShape,
17
20
  RoleList,
18
21
  Workspace,
19
22
  WorkspaceCreate,
@@ -92,6 +95,24 @@ export interface WorkspacesApi {
92
95
  /** Remove a member from a workspace (admin+; the last owner is protected). */
93
96
  removeMember(workspaceId: string, userId: string): Promise<void>;
94
97
 
98
+ /**
99
+ * Correct a member's display name (stapel-workspaces ≥0.19.0, capability
100
+ * `members.role.change`). Writes the CANONICAL name — stapel-profiles'
101
+ * `Profile.display_name` through this module's in-process profiles seam,
102
+ * which also publishes `profile.changed` — NOT the membership's
103
+ * `display_name_hint`, which is a pre-profile placeholder that goes dark the
104
+ * moment a real profile exists.
105
+ *
106
+ * Only an owner may rename an owner. Where stapel-profiles does not run in
107
+ * the deployment's process the answer is `error.503.profiles_unavailable`,
108
+ * never a 200 over a write that did not happen.
109
+ */
110
+ renameMember(
111
+ workspaceId: string,
112
+ userId: string,
113
+ body: DisplayNameUpdate
114
+ ): Promise<DisplayNameResult>;
115
+
95
116
  /**
96
117
  * Reset a member's password on the organization's order (stapel-workspaces
97
118
  * ≥0.14.0, #110). Capability `members.password.reset`, declared **high** —
@@ -140,6 +161,25 @@ export interface WorkspacesApi {
140
161
  workspaceId: string,
141
162
  invitationId: string
142
163
  ): Promise<Invitation>;
164
+ /**
165
+ * Fix a still-PENDING invitation's name hint (stapel-workspaces ≥0.19.0,
166
+ * capability `members.role.change` — the same one as {@link renameMember},
167
+ * because this hint IS the member's name after acceptance and splitting the
168
+ * two would let a role fix a name that reverts).
169
+ *
170
+ * The same correction as {@link renameMember}, one step earlier: the invitee
171
+ * has not accepted, so there is no profile of theirs to write and the name
172
+ * lives on the invitation. Before it, the only fix for a typo in an
173
+ * invitee's name was revoke-and-re-invite, which re-mails the person. A
174
+ * terminal invitation refuses with the keyed answers revoke gives
175
+ * (`error.400.invitation_already_used` / `_revoked` / `_declined` /
176
+ * `_expired`).
177
+ */
178
+ renameInvitation(
179
+ workspaceId: string,
180
+ invitationId: string,
181
+ body: DisplayNameUpdate
182
+ ): Promise<DisplayNameResult>;
143
183
 
144
184
  /** Accept an invitation by its token — returns the caller's new membership. */
145
185
  acceptInvitation(body: InvitationAccept): Promise<Member>;
@@ -171,6 +211,9 @@ export interface WorkspacesApi {
171
211
  /** The effective role registry (builtin + deployment overlay), for
172
212
  * RoleSelect-class UI — capability strings verbatim, rank-descending. */
173
213
  listRoles(): Promise<RoleList>;
214
+
215
+ /** Instance shape (GET /instance, unauthenticated). */
216
+ getInstanceShape(): Promise<InstanceShape>;
174
217
  }
175
218
 
176
219
  export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
@@ -226,6 +269,20 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
226
269
  mutating()
227
270
  ),
228
271
 
272
+ renameMember: (workspaceId, userId, body) =>
273
+ client.patch(
274
+ `/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(userId)}/name`,
275
+ body satisfies DisplayNameUpdate,
276
+ mutating()
277
+ ),
278
+
279
+ renameInvitation: (workspaceId, invitationId, body) =>
280
+ client.patch(
281
+ `/${encodeURIComponent(workspaceId)}/invitations/${encodeURIComponent(invitationId)}/name`,
282
+ body satisfies DisplayNameUpdate,
283
+ mutating()
284
+ ),
285
+
229
286
  resetMemberPassword: (workspaceId, userId, body) =>
230
287
  client.post(
231
288
  `/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(userId)}/password/reset`,
@@ -284,5 +341,7 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
284
341
  ),
285
342
 
286
343
  listRoles: () => client.get("/roles"),
344
+
345
+ getInstanceShape: () => client.get("/instance"),
287
346
  };
288
347
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `<MembersManager/>` — default skin for the "members & roles" settings
3
- * screen (owner directive: "настройки воркспейса (имя/участники/роли/
4
- * инвайты)"). Built entirely on this pair's EXISTING `Members` headless
3
+ * screen (owner directive: workspace settings — name/members/roles/
4
+ * invites). Built entirely on this pair's EXISTING `Members` headless
5
5
  * wrapper (`useMembers`/`useInviteMembers`/`useUpdateMemberRole`/
6
6
  * `useRemoveMember`) — no new backend surface.
7
7
  *
@@ -39,6 +39,10 @@ export const WORKSPACES_ERRORS = {
39
39
  "error.400.bad_request": { status: 400, params: [], remediation: "fix_input", en: "Bad request" },
40
40
  "error.400.captcha_invalid": { status: 400, params: [], remediation: "retry", en: "Captcha verification failed. Please try again." },
41
41
  "error.400.captcha_required": { status: 400, params: [], remediation: "retry", en: "Captcha token is required." },
42
+ "error.400.display_name_emoji": { status: 400, params: [], remediation: "fix_input", en: "Display name cannot contain emoji" },
43
+ "error.400.display_name_forbidden_chars": { status: 400, params: [], remediation: "fix_input", en: "Display name contains forbidden characters" },
44
+ "error.400.display_name_invisible_chars": { status: 400, params: [], remediation: "fix_input", en: "Display name contains invisible characters" },
45
+ "error.400.display_name_too_short": { status: 400, params: [], remediation: "fix_input", en: "Display name must be at least 2 characters" },
42
46
  "error.400.expected_list": { status: 400, params: [], remediation: "fix_input", en: "Expected a list of items" },
43
47
  "error.400.field.blank": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} may not be blank" },
44
48
  "error.400.field.does_not_exist": { status: 400, params: ["field"], remediation: "fix_input", en: "{field} does not exist" },
@@ -72,6 +76,7 @@ export const WORKSPACES_ERRORS = {
72
76
  "error.403.membership_suspended": { status: 403, params: ["reason"], remediation: "fix_input", en: "Your membership in this workspace is suspended ({reason})" },
73
77
  "error.403.missing_capability": { status: 403, params: ["capability"], remediation: "contact_support", en: "Your role does not include the {capability} capability in this workspace" },
74
78
  "error.403.network_blocked": { status: 403, params: [], remediation: "contact_support", en: "Requests from this network are not allowed" },
79
+ "error.403.role_exceeds_inviter_rank": { status: 403, params: ["role"], remediation: "fix_input", en: "You cannot grant a role that outranks your own ({role})" },
75
80
  "error.403.verification_enrollment_required": { status: 403, params: [], remediation: "verify", en: "Verification factor enrollment required" },
76
81
  "error.403.verification_required": { status: 403, params: [], remediation: "verify", en: "Additional verification required" },
77
82
  "error.404.ad_not_found": { status: 404, params: [], remediation: "retry", en: "Listing not found" },
@@ -95,6 +100,7 @@ export const WORKSPACES_ERRORS = {
95
100
  "error.429.too_many_requests": { status: 429, params: [], remediation: "wait_and_retry", en: "Too many requests. Please try again later." },
96
101
  "error.500.internal": { status: 500, params: [], remediation: "contact_support", en: "Something went wrong" },
97
102
  "error.503.auth_unavailable": { status: 503, params: [], remediation: "wait_and_retry", en: "The authentication service is unavailable; try again later" },
103
+ "error.503.profiles_unavailable": { status: 503, params: [], remediation: "wait_and_retry", en: "The profiles service is unavailable; try again later" },
98
104
  } as const;
99
105
 
100
106
  export type WorkspacesErrorCode = keyof typeof WORKSPACES_ERRORS;
@@ -105,6 +111,10 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
105
111
  "error.400.bad_request",
106
112
  "error.400.captcha_invalid",
107
113
  "error.400.captcha_required",
114
+ "error.400.display_name_emoji",
115
+ "error.400.display_name_forbidden_chars",
116
+ "error.400.display_name_invisible_chars",
117
+ "error.400.display_name_too_short",
108
118
  "error.400.expected_list",
109
119
  "error.400.field.blank",
110
120
  "error.400.field.does_not_exist",
@@ -138,6 +148,7 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
138
148
  "error.403.membership_suspended",
139
149
  "error.403.missing_capability",
140
150
  "error.403.network_blocked",
151
+ "error.403.role_exceeds_inviter_rank",
141
152
  "error.403.verification_enrollment_required",
142
153
  "error.403.verification_required",
143
154
  "error.404.ad_not_found",
@@ -161,6 +172,7 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
161
172
  "error.429.too_many_requests",
162
173
  "error.500.internal",
163
174
  "error.503.auth_unavailable",
175
+ "error.503.profiles_unavailable",
164
176
  ];
165
177
 
166
178
  /**
@@ -173,6 +185,10 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
173
185
  "error.400.bad_request": "Bad request",
174
186
  "error.400.captcha_invalid": "Captcha verification failed. Please try again.",
175
187
  "error.400.captcha_required": "Captcha token is required.",
188
+ "error.400.display_name_emoji": "Display name cannot contain emoji",
189
+ "error.400.display_name_forbidden_chars": "Display name contains forbidden characters",
190
+ "error.400.display_name_invisible_chars": "Display name contains invisible characters",
191
+ "error.400.display_name_too_short": "Display name must be at least 2 characters",
176
192
  "error.400.expected_list": "Expected a list of items",
177
193
  "error.400.field.blank": "{field} may not be blank",
178
194
  "error.400.field.does_not_exist": "{field} does not exist",
@@ -206,6 +222,7 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
206
222
  "error.403.membership_suspended": "Your membership in this workspace is suspended ({reason})",
207
223
  "error.403.missing_capability": "Your role does not include the {capability} capability in this workspace",
208
224
  "error.403.network_blocked": "Requests from this network are not allowed",
225
+ "error.403.role_exceeds_inviter_rank": "You cannot grant a role that outranks your own ({role})",
209
226
  "error.403.verification_enrollment_required": "Verification factor enrollment required",
210
227
  "error.403.verification_required": "Additional verification required",
211
228
  "error.404.ad_not_found": "Listing not found",
@@ -229,4 +246,5 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
229
246
  "error.429.too_many_requests": "Too many requests. Please try again later.",
230
247
  "error.500.internal": "Something went wrong",
231
248
  "error.503.auth_unavailable": "The authentication service is unavailable; try again later",
249
+ "error.503.profiles_unavailable": "The profiles service is unavailable; try again later",
232
250
  };
@@ -27,6 +27,34 @@
27
27
  "remediation": "retry",
28
28
  "en": "Captcha token is required."
29
29
  },
30
+ {
31
+ "code": "error.400.display_name_emoji",
32
+ "status": 400,
33
+ "params": [],
34
+ "remediation": "fix_input",
35
+ "en": "Display name cannot contain emoji"
36
+ },
37
+ {
38
+ "code": "error.400.display_name_forbidden_chars",
39
+ "status": 400,
40
+ "params": [],
41
+ "remediation": "fix_input",
42
+ "en": "Display name contains forbidden characters"
43
+ },
44
+ {
45
+ "code": "error.400.display_name_invisible_chars",
46
+ "status": 400,
47
+ "params": [],
48
+ "remediation": "fix_input",
49
+ "en": "Display name contains invisible characters"
50
+ },
51
+ {
52
+ "code": "error.400.display_name_too_short",
53
+ "status": 400,
54
+ "params": [],
55
+ "remediation": "fix_input",
56
+ "en": "Display name must be at least 2 characters"
57
+ },
30
58
  {
31
59
  "code": "error.400.expected_list",
32
60
  "status": 400,
@@ -290,6 +318,15 @@
290
318
  "remediation": "contact_support",
291
319
  "en": "Requests from this network are not allowed"
292
320
  },
321
+ {
322
+ "code": "error.403.role_exceeds_inviter_rank",
323
+ "status": 403,
324
+ "params": [
325
+ "role"
326
+ ],
327
+ "remediation": "fix_input",
328
+ "en": "You cannot grant a role that outranks your own ({role})"
329
+ },
293
330
  {
294
331
  "code": "error.403.verification_enrollment_required",
295
332
  "status": 403,
@@ -452,5 +489,12 @@
452
489
  "params": [],
453
490
  "remediation": "wait_and_retry",
454
491
  "en": "The authentication service is unavailable; try again later"
492
+ },
493
+ {
494
+ "code": "error.503.profiles_unavailable",
495
+ "status": 503,
496
+ "params": [],
497
+ "remediation": "wait_and_retry",
498
+ "en": "The profiles service is unavailable; try again later"
455
499
  }
456
500
  ]
@@ -16,6 +16,10 @@ export const workspacesErrorBundleRu: Record<WorkspacesErrorCode, string> = {
16
16
  "error.400.bad_request": "Некорректный запрос",
17
17
  "error.400.captcha_invalid": "Проверка капчи не пройдена. Пожалуйста, попробуйте ещё раз.",
18
18
  "error.400.captcha_required": "Требуется токен капчи.",
19
+ "error.400.display_name_emoji": "Отображаемое имя не может содержать эмодзи",
20
+ "error.400.display_name_forbidden_chars": "Отображаемое имя содержит запрещённые символы",
21
+ "error.400.display_name_invisible_chars": "Отображаемое имя содержит невидимые символы",
22
+ "error.400.display_name_too_short": "Отображаемое имя должно содержать не менее 2 символов",
19
23
  "error.400.expected_list": "Ожидался список элементов",
20
24
  "error.400.field.blank": "Поле «{field}» не может быть пустым",
21
25
  "error.400.field.does_not_exist": "«{field}» не существует",
@@ -49,6 +53,7 @@ export const workspacesErrorBundleRu: Record<WorkspacesErrorCode, string> = {
49
53
  "error.403.membership_suspended": "Ваше членство в этом рабочем пространстве приостановлено ({reason})",
50
54
  "error.403.missing_capability": "Ваша роль не включает право {capability} в этом рабочем пространстве",
51
55
  "error.403.network_blocked": "Запросы из этой сети не разрешены.",
56
+ "error.403.role_exceeds_inviter_rank": "Вы не можете выдать роль, которая превышает вашу собственную ({role})",
52
57
  "error.403.verification_enrollment_required": "Требуется регистрация фактора подтверждения.",
53
58
  "error.403.verification_required": "Требуется дополнительная проверка",
54
59
  "error.404.ad_not_found": "Объявление не найдено",
@@ -72,4 +77,5 @@ export const workspacesErrorBundleRu: Record<WorkspacesErrorCode, string> = {
72
77
  "error.429.too_many_requests": "Слишком много запросов. Пожалуйста, повторите попытку позже.",
73
78
  "error.500.internal": "Что-то пошло не так",
74
79
  "error.503.auth_unavailable": "Сервис аутентификации недоступен; повторите попытку позже",
80
+ "error.503.profiles_unavailable": "Сервис профилей недоступен; повторите попытку позже",
75
81
  };