@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.
- package/CHANGELOG.md +51 -0
- package/dist/api/generated/schema.d.ts +263 -2
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +21 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/workspacesApi.d.ts +31 -1
- package/dist/api/workspacesApi.d.ts.map +1 -1
- package/dist/api/workspacesApi.js +3 -0
- package/dist/api/workspacesApi.js.map +1 -1
- package/dist/default/MembersManager.js +2 -2
- package/dist/i18n/generated/errors.gen.d.ts +36 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +18 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.js +6 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/model/mutations.d.ts +84 -2
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +94 -1
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/queries.d.ts +19 -1
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +25 -0
- package/dist/model/queries.js.map +1 -1
- package/dist/model/queryKeys.d.ts +2 -0
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +9 -1
- package/dist/model/queryKeys.js.map +1 -1
- package/llms.txt +10 -3
- package/manifest.json +97 -2
- package/package.json +3 -3
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +263 -2
- package/src/api/types.ts +22 -0
- package/src/api/workspacesApi.ts +59 -0
- package/src/default/MembersManager.tsx +2 -2
- package/src/i18n/generated/errors.gen.ts +18 -0
- package/src/i18n/generated/errors.json +44 -0
- package/src/i18n/generated/errors.ru.gen.ts +6 -0
- package/src/index.ts +7 -0
- package/src/model/mutations.ts +128 -1
- package/src/model/queries.ts +30 -0
- 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
|
|
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
|
|
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
|
/**
|
package/src/api/workspacesApi.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
};
|