@stapel/workspaces-react 0.9.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 (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/api/generated/schema.d.ts +173 -17
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +14 -0
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/workspacesApi.d.ts +29 -1
  7. package/dist/api/workspacesApi.d.ts.map +1 -1
  8. package/dist/api/workspacesApi.js +2 -0
  9. package/dist/api/workspacesApi.js.map +1 -1
  10. package/dist/i18n/generated/errors.gen.d.ts +30 -0
  11. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  12. package/dist/i18n/generated/errors.gen.js +15 -0
  13. package/dist/i18n/generated/errors.gen.js.map +1 -1
  14. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  15. package/dist/i18n/generated/errors.ru.gen.js +5 -0
  16. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  17. package/dist/index.d.ts +3 -3
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/model/mutations.d.ts +83 -1
  22. package/dist/model/mutations.d.ts.map +1 -1
  23. package/dist/model/mutations.js +93 -0
  24. package/dist/model/mutations.js.map +1 -1
  25. package/dist/model/queryKeys.d.ts +1 -0
  26. package/dist/model/queryKeys.d.ts.map +1 -1
  27. package/dist/model/queryKeys.js +7 -0
  28. package/dist/model/queryKeys.js.map +1 -1
  29. package/llms.txt +7 -3
  30. package/manifest.json +73 -2
  31. package/package.json +3 -3
  32. package/src/analytics/generated/events.json +1 -1
  33. package/src/api/generated/schema.ts +173 -17
  34. package/src/api/types.ts +14 -0
  35. package/src/api/workspacesApi.ts +53 -0
  36. package/src/i18n/generated/errors.gen.ts +15 -0
  37. package/src/i18n/generated/errors.json +35 -0
  38. package/src/i18n/generated/errors.ru.gen.ts +5 -0
  39. package/src/index.ts +6 -0
  40. package/src/model/mutations.ts +127 -0
  41. package/src/model/queryKeys.ts +8 -0
package/manifest.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "$generated": "by scripts/gen-manifest.mjs — do not edit; drift-gated (pnpm gen:manifest:check)",
3
3
  "package": "@stapel/workspaces-react",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "backend": {
6
6
  "module": "stapel-workspaces",
7
- "contract": ">=0.18 <0.19"
7
+ "contract": ">=0.19 <0.20"
8
8
  },
9
9
  "layers": [
10
10
  "api",
@@ -81,6 +81,13 @@
81
81
  "request": null,
82
82
  "response": "PaginatedInvitationResponseList"
83
83
  },
84
+ "workspaces_api_v1_invitations_name_partial_update": {
85
+ "method": "PATCH",
86
+ "path": "/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/name",
87
+ "tag": "Members",
88
+ "request": "PatchedDisplayNameUpdateRequest",
89
+ "response": "DisplayNameResponse"
90
+ },
84
91
  "workspaces_api_v1_invitations_resend_create": {
85
92
  "method": "POST",
86
93
  "path": "/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/resend",
@@ -123,6 +130,13 @@
123
130
  "request": null,
124
131
  "response": "PaginatedMemberResponseList"
125
132
  },
133
+ "workspaces_api_v1_members_name_partial_update": {
134
+ "method": "PATCH",
135
+ "path": "/workspaces/api/v1/{workspace_id}/members/{user_id}/name",
136
+ "tag": "Members",
137
+ "request": "PatchedDisplayNameUpdateRequest",
138
+ "response": "DisplayNameResponse"
139
+ },
126
140
  "workspaces_api_v1_members_partial_update": {
127
141
  "method": "PATCH",
128
142
  "path": "/workspaces/api/v1/{workspace_id}/members/{user_id}",
@@ -288,6 +302,27 @@
288
302
  ]
289
303
  ]
290
304
  },
305
+ "useRenameInvitation": {
306
+ "kind": "mutation",
307
+ "operation": "renameInvitation",
308
+ "invalidates": [
309
+ [
310
+ "workspaces",
311
+ "invitations",
312
+ "{workspaceId}"
313
+ ]
314
+ ]
315
+ },
316
+ "useRenameMember": {
317
+ "kind": "mutation",
318
+ "operation": "renameMember",
319
+ "invalidates": [
320
+ [
321
+ "workspaces",
322
+ "members"
323
+ ]
324
+ ]
325
+ },
291
326
  "useResendInvitation": {
292
327
  "kind": "mutation",
293
328
  "operation": "resendInvitation",
@@ -422,6 +457,26 @@
422
457
  "params": [],
423
458
  "remediation": "retry"
424
459
  },
460
+ "error.400.display_name_emoji": {
461
+ "status": 400,
462
+ "params": [],
463
+ "remediation": "fix_input"
464
+ },
465
+ "error.400.display_name_forbidden_chars": {
466
+ "status": 400,
467
+ "params": [],
468
+ "remediation": "fix_input"
469
+ },
470
+ "error.400.display_name_invisible_chars": {
471
+ "status": 400,
472
+ "params": [],
473
+ "remediation": "fix_input"
474
+ },
475
+ "error.400.display_name_too_short": {
476
+ "status": 400,
477
+ "params": [],
478
+ "remediation": "fix_input"
479
+ },
425
480
  "error.400.expected_list": {
426
481
  "status": 400,
427
482
  "params": [],
@@ -742,6 +797,11 @@
742
797
  "status": 503,
743
798
  "params": [],
744
799
  "remediation": "wait_and_retry"
800
+ },
801
+ "error.503.profiles_unavailable": {
802
+ "status": 503,
803
+ "params": [],
804
+ "remediation": "wait_and_retry"
745
805
  }
746
806
  },
747
807
  "events": {
@@ -852,6 +912,10 @@
852
912
  "error.400.bad_request",
853
913
  "error.400.captcha_invalid",
854
914
  "error.400.captcha_required",
915
+ "error.400.display_name_emoji",
916
+ "error.400.display_name_forbidden_chars",
917
+ "error.400.display_name_invisible_chars",
918
+ "error.400.display_name_too_short",
855
919
  "error.400.expected_list",
856
920
  "error.400.field.blank",
857
921
  "error.400.field.does_not_exist",
@@ -909,6 +973,7 @@
909
973
  "error.429.too_many_requests",
910
974
  "error.500.internal",
911
975
  "error.503.auth_unavailable",
976
+ "error.503.profiles_unavailable",
912
977
  "workspaces.accept.accept",
913
978
  "workspaces.accept.accepted",
914
979
  "workspaces.accept.accepting",
@@ -1018,6 +1083,8 @@
1018
1083
  "useInviteMembers",
1019
1084
  "useMembers",
1020
1085
  "useRemoveMember",
1086
+ "useRenameInvitation",
1087
+ "useRenameMember",
1021
1088
  "useResendInvitation",
1022
1089
  "useResetMemberPassword",
1023
1090
  "useRevokeInvitation",
@@ -1041,6 +1108,8 @@
1041
1108
  "CapabilityGate",
1042
1109
  "CapabilityLevel",
1043
1110
  "CreateWorkspacesRuntimeOptions",
1111
+ "DisplayNameResult",
1112
+ "DisplayNameUpdate",
1044
1113
  "FlowEndpoint",
1045
1114
  "FlowError",
1046
1115
  "FlowMachine",
@@ -1051,6 +1120,7 @@
1051
1120
  "InvitationClaim",
1052
1121
  "InvitationListData",
1053
1122
  "InvitationPreview",
1123
+ "InvitationRename",
1054
1124
  "InvitationStatusFilter",
1055
1125
  "InvitationsParams",
1056
1126
  "InviteAcceptFlowBag",
@@ -1064,6 +1134,7 @@
1064
1134
  "MemberPasswordReset",
1065
1135
  "MemberPasswordResetResult",
1066
1136
  "MemberPasswordResetVars",
1137
+ "MemberRename",
1067
1138
  "MemberRoleChange",
1068
1139
  "MemberRoleUpdate",
1069
1140
  "MembersBag",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stapel/workspaces-react",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Headless React flow pair for stapel-workspaces: typed API client, TanStack Query hooks, flow machines, headless components, and i18n keys. Zero visual opinion.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -82,9 +82,9 @@
82
82
  "size-limit": "^11.2.0",
83
83
  "typescript": "^5.8.3",
84
84
  "vitest": "^3.2.4",
85
- "@stapel/core": "^0.10.0",
85
+ "@stapel/tokens": "^0.5.0",
86
86
  "@stapel/showcase": "^0.2.0",
87
- "@stapel/tokens": "^0.5.0"
87
+ "@stapel/core": "^0.10.0"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=22"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$generated": "by scripts/gen-events.mjs — do not edit; drift-gated (pnpm gen:events:check)",
3
3
  "package": "@stapel/workspaces-react",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "defined": [],
6
6
  "flows": []
7
7
  }
@@ -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;
@@ -606,34 +690,42 @@ export interface paths {
606
690
  export type webhooks = Record<string, never>;
607
691
  export interface components {
608
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
+ };
609
701
  /**
610
- * @description Как развёрнут ЭТОТ инстанс — то, что клиенту нужно знать ДО того, как
611
- * он решит, что показать человеку без пространства.
702
+ * @description How THIS instance is deployed — what the client needs to know before
703
+ * deciding what to show a person with no workspace.
612
704
  *
613
- * Ось ``landing`` (``STREET_LANDING_MODE``) существовала с 03.08.2026, но
614
- * жила только в окружении бэкенда: наружу не отдавалась ничем, и отличить
615
- * закрытый контур от публичного облака на клиенте было НЕЧЕМ.
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.
616
708
  *
617
- * Почему это не косметика. Экран, который видит человек, выброшенный из
618
- * Спейса (или вышедший сам), в этих двух мирах разный по существу:
709
+ * This matters because the screen shown to someone who is no longer (or
710
+ * never was) a workspace member differs by deployment:
619
711
  *
620
- * * ``none`` — закрытый контур: своего пространства у человека нет и
621
- * взяться неоткуда. Честный экран говорит «вы не участник, попросите
622
- * приглашение» и никуда не зовёт;
623
- * * ``personal`` — публичное облако: у него есть собственное
624
- * пространство, и туда можно вести.
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.
625
717
  *
626
- * Показать «создайте встречу» тому, кому инстанс её создать не даст, —
627
- * это тупик, нарисованный кнопкой.
718
+ * Offering "create a workspace" to someone the instance will never let
719
+ * create one is a dead end drawn as a button.
628
720
  */
629
721
  InstanceShapeResponse: {
630
722
  /**
631
- * @description С чем приземляется человек «с улицы»
723
+ * @description What a "street" (no-invite) arrival lands on
632
724
  * @example personal
633
725
  */
634
726
  landing: string;
635
727
  /**
636
- * @description Можно ли завести учётку самому
728
+ * @description Whether self-registration is available
637
729
  * @example True
638
730
  */
639
731
  registration_open: boolean;
@@ -754,7 +846,7 @@ export interface components {
754
846
  /** @description Invite payload. */
755
847
  MemberInviteRequest: {
756
848
  /**
757
- * @description Name hint for the invitee (this invite's "Имя" 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
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
758
850
  * @example Ada Lovelace
759
851
  */
760
852
  display_name?: string | null;
@@ -888,6 +980,14 @@ export interface components {
888
980
  /** @description Number of items in current page */
889
981
  count: number;
890
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
+ };
891
991
  /** @description MemberUpdateRequest(role: str) */
892
992
  PatchedMemberUpdateRequest: {
893
993
  role?: string;
@@ -1249,6 +1349,34 @@ export interface operations {
1249
1349
  };
1250
1350
  };
1251
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
+ };
1252
1380
  workspaces_api_v1_invitations_resend_create: {
1253
1381
  parameters: {
1254
1382
  query?: never;
@@ -1372,6 +1500,34 @@ export interface operations {
1372
1500
  };
1373
1501
  };
1374
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
+ };
1375
1531
  workspaces_api_v1_members_password_reset_create: {
1376
1532
  parameters: {
1377
1533
  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
@@ -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,
@@ -93,6 +95,24 @@ export interface WorkspacesApi {
93
95
  /** Remove a member from a workspace (admin+; the last owner is protected). */
94
96
  removeMember(workspaceId: string, userId: string): Promise<void>;
95
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
+
96
116
  /**
97
117
  * Reset a member's password on the organization's order (stapel-workspaces
98
118
  * ≥0.14.0, #110). Capability `members.password.reset`, declared **high** —
@@ -141,6 +161,25 @@ export interface WorkspacesApi {
141
161
  workspaceId: string,
142
162
  invitationId: string
143
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>;
144
183
 
145
184
  /** Accept an invitation by its token — returns the caller's new membership. */
146
185
  acceptInvitation(body: InvitationAccept): Promise<Member>;
@@ -230,6 +269,20 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
230
269
  mutating()
231
270
  ),
232
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
+
233
286
  resetMemberPassword: (workspaceId, userId, body) =>
234
287
  client.post(
235
288
  `/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(userId)}/password/reset`,
@@ -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" },
@@ -96,6 +100,7 @@ export const WORKSPACES_ERRORS = {
96
100
  "error.429.too_many_requests": { status: 429, params: [], remediation: "wait_and_retry", en: "Too many requests. Please try again later." },
97
101
  "error.500.internal": { status: 500, params: [], remediation: "contact_support", en: "Something went wrong" },
98
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" },
99
104
  } as const;
100
105
 
101
106
  export type WorkspacesErrorCode = keyof typeof WORKSPACES_ERRORS;
@@ -106,6 +111,10 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
106
111
  "error.400.bad_request",
107
112
  "error.400.captcha_invalid",
108
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",
109
118
  "error.400.expected_list",
110
119
  "error.400.field.blank",
111
120
  "error.400.field.does_not_exist",
@@ -163,6 +172,7 @@ export const WORKSPACES_ERROR_CODES: readonly WorkspacesErrorCode[] = [
163
172
  "error.429.too_many_requests",
164
173
  "error.500.internal",
165
174
  "error.503.auth_unavailable",
175
+ "error.503.profiles_unavailable",
166
176
  ];
167
177
 
168
178
  /**
@@ -175,6 +185,10 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
175
185
  "error.400.bad_request": "Bad request",
176
186
  "error.400.captcha_invalid": "Captcha verification failed. Please try again.",
177
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",
178
192
  "error.400.expected_list": "Expected a list of items",
179
193
  "error.400.field.blank": "{field} may not be blank",
180
194
  "error.400.field.does_not_exist": "{field} does not exist",
@@ -232,4 +246,5 @@ export const workspacesErrorBundleEn: Record<WorkspacesErrorCode, string> = {
232
246
  "error.429.too_many_requests": "Too many requests. Please try again later.",
233
247
  "error.500.internal": "Something went wrong",
234
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",
235
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,
@@ -461,5 +489,12 @@
461
489
  "params": [],
462
490
  "remediation": "wait_and_retry",
463
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"
464
499
  }
465
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}» не существует",
@@ -73,4 +77,5 @@ export const workspacesErrorBundleRu: Record<WorkspacesErrorCode, string> = {
73
77
  "error.429.too_many_requests": "Слишком много запросов. Пожалуйста, повторите попытку позже.",
74
78
  "error.500.internal": "Что-то пошло не так",
75
79
  "error.503.auth_unavailable": "Сервис аутентификации недоступен; повторите попытку позже",
80
+ "error.503.profiles_unavailable": "Сервис профилей недоступен; повторите попытку позже",
76
81
  };