@stapel/workspaces-react 0.7.0 → 0.8.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 +42 -0
- package/dist/api/generated/schema.d.ts +390 -3
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/api/types.d.ts +84 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/workspacesApi.d.ts +33 -1
- package/dist/api/workspacesApi.d.ts.map +1 -1
- package/dist/api/workspacesApi.js +19 -0
- package/dist/api/workspacesApi.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/model/mutations.d.ts +85 -1
- package/dist/model/mutations.d.ts.map +1 -1
- package/dist/model/mutations.js +151 -0
- package/dist/model/mutations.js.map +1 -1
- package/dist/model/queries.d.ts +72 -2
- package/dist/model/queries.d.ts.map +1 -1
- package/dist/model/queries.js +90 -1
- package/dist/model/queries.js.map +1 -1
- package/dist/model/queryKeys.d.ts +10 -1
- package/dist/model/queryKeys.d.ts.map +1 -1
- package/dist/model/queryKeys.js +19 -0
- package/dist/model/queryKeys.js.map +1 -1
- package/dist/model/stepUp.d.ts +25 -0
- package/dist/model/stepUp.d.ts.map +1 -0
- package/dist/model/stepUp.js +79 -0
- package/dist/model/stepUp.js.map +1 -0
- package/llms.txt +11 -2
- package/manifest.json +123 -2
- package/package.json +4 -4
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +390 -3
- package/src/api/types.ts +93 -2
- package/src/api/workspacesApi.ts +87 -0
- package/src/index.ts +29 -2
- package/src/model/mutations.ts +192 -0
- package/src/model/queries.ts +132 -2
- package/src/model/queryKeys.ts +35 -1
- package/src/model/stepUp.ts +96 -0
|
@@ -89,6 +89,113 @@ export interface paths {
|
|
|
89
89
|
patch: operations["workspaces_api_v1_partial_update"];
|
|
90
90
|
trace?: never;
|
|
91
91
|
};
|
|
92
|
+
"/workspaces/api/v1/{workspace_id}/invitations": {
|
|
93
|
+
parameters: {
|
|
94
|
+
query?: never;
|
|
95
|
+
header?: never;
|
|
96
|
+
path?: never;
|
|
97
|
+
cookie?: never;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* @description Who was invited and has not accepted (#109).
|
|
101
|
+
*
|
|
102
|
+
* Before this endpoint an admin could send invitations and never see
|
|
103
|
+
* them again: acceptance produced a member row, and everything else —
|
|
104
|
+
* the unopened mail, the wrong address, the person who declined — was
|
|
105
|
+
* invisible from the product. The roster answered "who is in"; nothing
|
|
106
|
+
* answered "who is still out, and since when".
|
|
107
|
+
*
|
|
108
|
+
* Gated on ``members.invite``: the mandate that creates invitations is
|
|
109
|
+
* the mandate that sees and manages them. Reading the list is strictly
|
|
110
|
+
* less than sending one, so no separate capability is minted — a role
|
|
111
|
+
* that may invite but may not audit its own invitations would be a
|
|
112
|
+
* distinction without a use.
|
|
113
|
+
*
|
|
114
|
+
* The invite token is never in the response. It is a bearer credential;
|
|
115
|
+
* a list endpoint that carried it would hand every admin a working login
|
|
116
|
+
* link for every invited address.
|
|
117
|
+
*
|
|
118
|
+
* **Permissions:** `IsAuthenticated`
|
|
119
|
+
*/
|
|
120
|
+
get: operations["workspaces_api_v1_invitations_list"];
|
|
121
|
+
put?: never;
|
|
122
|
+
post?: never;
|
|
123
|
+
delete?: never;
|
|
124
|
+
options?: never;
|
|
125
|
+
head?: never;
|
|
126
|
+
patch?: never;
|
|
127
|
+
trace?: never;
|
|
128
|
+
};
|
|
129
|
+
"/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/resend": {
|
|
130
|
+
parameters: {
|
|
131
|
+
query?: never;
|
|
132
|
+
header?: never;
|
|
133
|
+
path?: never;
|
|
134
|
+
cookie?: never;
|
|
135
|
+
};
|
|
136
|
+
get?: never;
|
|
137
|
+
put?: never;
|
|
138
|
+
/**
|
|
139
|
+
* @description Send the invitation email again (#109).
|
|
140
|
+
*
|
|
141
|
+
* Accepts an **expired** invitation on purpose — a dead TTL is the most
|
|
142
|
+
* common reason to resend — and refuses the three stored terminal states,
|
|
143
|
+
* which are decisions rather than delivery failures. The token is
|
|
144
|
+
* rotated and the TTL restarts, so the fresh letter carries a fresh link
|
|
145
|
+
* and any stale copy of the old one stops working.
|
|
146
|
+
*
|
|
147
|
+
* Reviving an expired invitation re-reserves a seat, so the plan ceiling
|
|
148
|
+
* is re-checked here exactly as it is on invite: capability first ("may
|
|
149
|
+
* YOU", 403), then the org's plan ("may the ORG", 402). An invitation
|
|
150
|
+
* that is already pending costs no additional seat and is never blocked
|
|
151
|
+
* by that check.
|
|
152
|
+
*
|
|
153
|
+
* **Permissions:** `IsAuthenticated`
|
|
154
|
+
*/
|
|
155
|
+
post: operations["workspaces_api_v1_invitations_resend_create"];
|
|
156
|
+
delete?: never;
|
|
157
|
+
options?: never;
|
|
158
|
+
head?: never;
|
|
159
|
+
patch?: never;
|
|
160
|
+
trace?: never;
|
|
161
|
+
};
|
|
162
|
+
"/workspaces/api/v1/{workspace_id}/invitations/{invitation_id}/revoke": {
|
|
163
|
+
parameters: {
|
|
164
|
+
query?: never;
|
|
165
|
+
header?: never;
|
|
166
|
+
path?: never;
|
|
167
|
+
cookie?: never;
|
|
168
|
+
};
|
|
169
|
+
get?: never;
|
|
170
|
+
put?: never;
|
|
171
|
+
/**
|
|
172
|
+
* @description Withdraw a live invitation (#109).
|
|
173
|
+
*
|
|
174
|
+
* The workspace's terminal "no", the mirror of the invitee's decline —
|
|
175
|
+
* the two stay distinguishable in ``status`` forever. The seat the
|
|
176
|
+
* invitation reserved is freed on commit.
|
|
177
|
+
*
|
|
178
|
+
* Only a **pending** invitation is revocable: an accepted one produced a
|
|
179
|
+
* membership (remove the member instead), a declined or already-revoked
|
|
180
|
+
* one is terminal, and an expired one is already harmless. Each of those
|
|
181
|
+
* gets its own error key rather than a shrug.
|
|
182
|
+
*
|
|
183
|
+
* The transition is a compare-and-set under a row lock, not a blind
|
|
184
|
+
* write: an accept committing between this view's state check and the
|
|
185
|
+
* lock wins, and the revocation then fails honestly with
|
|
186
|
+
* ``error.400.invitation_already_used``. The reverse — a revocation
|
|
187
|
+
* committing between an accept's check and its lock — is the race fixed
|
|
188
|
+
* in 0.10.0, and it is the same predicate holding both ends.
|
|
189
|
+
*
|
|
190
|
+
* **Permissions:** `IsAuthenticated`
|
|
191
|
+
*/
|
|
192
|
+
post: operations["workspaces_api_v1_invitations_revoke_create"];
|
|
193
|
+
delete?: never;
|
|
194
|
+
options?: never;
|
|
195
|
+
head?: never;
|
|
196
|
+
patch?: never;
|
|
197
|
+
trace?: never;
|
|
198
|
+
};
|
|
92
199
|
"/workspaces/api/v1/{workspace_id}/members": {
|
|
93
200
|
parameters: {
|
|
94
201
|
query?: never;
|
|
@@ -154,6 +261,78 @@ export interface paths {
|
|
|
154
261
|
patch: operations["workspaces_api_v1_members_partial_update"];
|
|
155
262
|
trace?: never;
|
|
156
263
|
};
|
|
264
|
+
"/workspaces/api/v1/{workspace_id}/members/{user_id}/password/reset": {
|
|
265
|
+
parameters: {
|
|
266
|
+
query?: never;
|
|
267
|
+
header?: never;
|
|
268
|
+
path?: never;
|
|
269
|
+
cookie?: never;
|
|
270
|
+
};
|
|
271
|
+
get?: never;
|
|
272
|
+
put?: never;
|
|
273
|
+
/**
|
|
274
|
+
* @description Reset a member's password on the organization's order (#110).
|
|
275
|
+
*
|
|
276
|
+
* Five questions this endpoint has to answer out loud, because a
|
|
277
|
+
* password reset is an account takeover performed on purpose and every
|
|
278
|
+
* one of them is a way to get it wrong.
|
|
279
|
+
*
|
|
280
|
+
* **Who may do it.** A mandate, not a session: capability
|
|
281
|
+
* ``members.password.reset`` (builtin ``admin`` and ``owner``), declared
|
|
282
|
+
* ``high``, so ``@requires_verification(scope="sensitive")`` demands a
|
|
283
|
+
* fresh step-up on top — an ambient cookie is not enough to hand
|
|
284
|
+
* somebody else's account over. Only an owner may reset an OWNER's
|
|
285
|
+
* password, the same hardcoded owner protection role changes and
|
|
286
|
+
* removals carry: otherwise an admin resets the owner and inherits the
|
|
287
|
+
* organization. And auth refuses a staff/superuser target outright
|
|
288
|
+
* (``error.403.privileged_account``) — org admin is a role inside one
|
|
289
|
+
* workspace, deployment staff is a role above every workspace, and the
|
|
290
|
+
* first must never be a route to the second.
|
|
291
|
+
*
|
|
292
|
+
* **Whether the user finds out.** Always. A
|
|
293
|
+
* ``workspace.member_password_reset`` letter names the workspace and the
|
|
294
|
+
* admin who did it — a reset is indistinguishable from a takeover
|
|
295
|
+
* unless the account holder is told which one it was. ``notified`` in
|
|
296
|
+
* the response says honestly whether a channel existed; the letter
|
|
297
|
+
* never carries the new password (see
|
|
298
|
+
* :func:`~stapel_workspaces.services.reset_member_password`).
|
|
299
|
+
*
|
|
300
|
+
* **Whether the new password is temporary.** Yes — auth raises the
|
|
301
|
+
* workspace's ``provisioned_user_policies`` (#90), defaulting to
|
|
302
|
+
* ``password_change``. A password the admin knows must stop working the
|
|
303
|
+
* first time it is used, and since auth 0.15.0 that demand holds on all
|
|
304
|
+
* 19 session-issuance paths rather than only the password form. An org
|
|
305
|
+
* may pass an explicit ``[]`` to suppress it, and that lands in auth's
|
|
306
|
+
* audit row.
|
|
307
|
+
*
|
|
308
|
+
* **Whether it is an existence oracle.** No: a target that is not a
|
|
309
|
+
* resettable member of THIS workspace — an unknown UUID, a real account
|
|
310
|
+
* that is not a member, a member of a different workspace, or the
|
|
311
|
+
* caller's own id — all get one byte-identical 404. And the capability
|
|
312
|
+
* check runs before any target lookup, so a caller without the mandate
|
|
313
|
+
* learns nothing at all about anybody. ``tests/
|
|
314
|
+
* test_api_member_password_reset.py`` compares those responses byte for
|
|
315
|
+
* byte.
|
|
316
|
+
*
|
|
317
|
+
* **Whether it is logged with the actor.** Twice, on purpose:
|
|
318
|
+
* ``workspace.member_password_reset`` through the transactional outbox
|
|
319
|
+
* (the org's activity log) and auth's own ``AuthAuditLog`` row carrying
|
|
320
|
+
* ``actor_id`` and ``via=admin_reset`` (the deployment's security
|
|
321
|
+
* journal). Neither carries credential material.
|
|
322
|
+
*
|
|
323
|
+
* Own password: use auth's ``POST /password/change/``. This endpoint is
|
|
324
|
+
* for acting on somebody else, and answers about yourself with the same
|
|
325
|
+
* 404 as about a stranger.
|
|
326
|
+
*
|
|
327
|
+
* **Permissions:** `IsAuthenticated`
|
|
328
|
+
*/
|
|
329
|
+
post: operations["workspaces_api_v1_members_password_reset_create"];
|
|
330
|
+
delete?: never;
|
|
331
|
+
options?: never;
|
|
332
|
+
head?: never;
|
|
333
|
+
patch?: never;
|
|
334
|
+
trace?: never;
|
|
335
|
+
};
|
|
157
336
|
"/workspaces/api/v1/{workspace_id}/members/invite": {
|
|
158
337
|
parameters: {
|
|
159
338
|
query?: never;
|
|
@@ -454,18 +633,65 @@ export interface components {
|
|
|
454
633
|
*/
|
|
455
634
|
expires_at: string;
|
|
456
635
|
};
|
|
457
|
-
/**
|
|
636
|
+
/**
|
|
637
|
+
* @description One invitation row — the admin's "who has not accepted yet" table (#109).
|
|
638
|
+
*
|
|
639
|
+
* ``status`` is the derived label (``WorkspaceInvitation.status``), not a
|
|
640
|
+
* stored column: it is what the table renders and what the frontend
|
|
641
|
+
* routes the revoke/resend affordances on. The raw timestamps travel
|
|
642
|
+
* alongside it because "expires in 2 days" and "declined last Tuesday"
|
|
643
|
+
* are different rows on the same screen; the label alone cannot say
|
|
644
|
+
* when.
|
|
645
|
+
*
|
|
646
|
+
* The invite token is deliberately absent — it is a bearer secret that
|
|
647
|
+
* only ever leaves this service inside the invitation email.
|
|
648
|
+
*/
|
|
458
649
|
InvitationResponse: {
|
|
459
|
-
/**
|
|
650
|
+
/**
|
|
651
|
+
* Format: uuid
|
|
652
|
+
* @description Invitation UUID
|
|
653
|
+
* @example 0192f...
|
|
654
|
+
*/
|
|
460
655
|
id: string;
|
|
461
|
-
/**
|
|
656
|
+
/**
|
|
657
|
+
* Format: uuid
|
|
658
|
+
* @description Workspace UUID
|
|
659
|
+
*/
|
|
462
660
|
workspace_id: string;
|
|
661
|
+
/**
|
|
662
|
+
* @description Invited email address (normalized lowercase)
|
|
663
|
+
* @example alice@example.com
|
|
664
|
+
*/
|
|
463
665
|
email: string;
|
|
666
|
+
/**
|
|
667
|
+
* @description Role granted on acceptance
|
|
668
|
+
* @example member
|
|
669
|
+
*/
|
|
464
670
|
role: string;
|
|
671
|
+
/**
|
|
672
|
+
* @description Derived state. One of pending / accepted / declined / revoked / expired
|
|
673
|
+
* @example pending
|
|
674
|
+
*/
|
|
675
|
+
status: string;
|
|
676
|
+
/**
|
|
677
|
+
* @description ISO 8601 expiry time
|
|
678
|
+
* @example 2026-07-31T10:00:00Z
|
|
679
|
+
*/
|
|
465
680
|
expires_at: string;
|
|
681
|
+
/** @description ISO 8601 acceptance time; null unless accepted */
|
|
466
682
|
accepted_at: string | null;
|
|
683
|
+
/** @description ISO 8601 decline time (the invitee said no); null unless declined */
|
|
684
|
+
declined_at: string | null;
|
|
685
|
+
/** @description ISO 8601 revocation time (the workspace withdrew it); null unless revoked */
|
|
467
686
|
revoked_at: string | null;
|
|
687
|
+
/** @description ISO 8601 creation time */
|
|
468
688
|
created_at: string;
|
|
689
|
+
/**
|
|
690
|
+
* Format: uuid
|
|
691
|
+
* @description UUID of the admin who sent it; null when that account is gone
|
|
692
|
+
* @example 0192a...
|
|
693
|
+
*/
|
|
694
|
+
invited_by_id?: string | null;
|
|
469
695
|
};
|
|
470
696
|
/** @description Invite payload. */
|
|
471
697
|
MemberInviteRequest: {
|
|
@@ -486,6 +712,50 @@ export interface components {
|
|
|
486
712
|
MemberInviteResponse: {
|
|
487
713
|
invitations?: components["schemas"]["InvitationResponse"][];
|
|
488
714
|
};
|
|
715
|
+
/** @description Reset a member's password on the organization's order (#110). */
|
|
716
|
+
MemberPasswordResetRequest: {
|
|
717
|
+
/** @description New password chosen by the admin. Omitted: the server generates a crypto-strong one, returned once as generated_password */
|
|
718
|
+
password?: string | null;
|
|
719
|
+
/**
|
|
720
|
+
* @description First-login steps to demand of the member afterwards — any subset of password_change / mfa_enroll. Omitted: the workspace's own security policies, falling back to password_change. A password somebody else chose has to stop working at its first use, so an EMPTY list is a deliberate, auditable choice, not a shortcut
|
|
721
|
+
* @example [
|
|
722
|
+
* "password_change"
|
|
723
|
+
* ]
|
|
724
|
+
*/
|
|
725
|
+
first_login_policies?: string[] | null;
|
|
726
|
+
/**
|
|
727
|
+
* @description Free-text note recorded on auth's audit row (e.g. the ticket the request came in on)
|
|
728
|
+
* @example SUP-42
|
|
729
|
+
*/
|
|
730
|
+
reason?: string | null;
|
|
731
|
+
};
|
|
732
|
+
/** @description Result of an administrative password reset (#110). */
|
|
733
|
+
MemberPasswordResetResponse: {
|
|
734
|
+
/**
|
|
735
|
+
* Format: uuid
|
|
736
|
+
* @description UUID of the member whose password was reset
|
|
737
|
+
*/
|
|
738
|
+
user_id: string;
|
|
739
|
+
/**
|
|
740
|
+
* @description How many live sessions the reset ended. A reset that left them standing would not recover the account
|
|
741
|
+
* @example 2
|
|
742
|
+
*/
|
|
743
|
+
sessions_revoked?: number;
|
|
744
|
+
/** @description Server-generated password — returned exactly ONCE, only when the request omitted password. Hand it to the member out of band; it cannot be re-fetched. A credential: never log it */
|
|
745
|
+
generated_password?: string | null;
|
|
746
|
+
/**
|
|
747
|
+
* @description The steps now demanded of the member before their next session
|
|
748
|
+
* @example [
|
|
749
|
+
* "password_change"
|
|
750
|
+
* ]
|
|
751
|
+
*/
|
|
752
|
+
first_login_policies_applied?: string[];
|
|
753
|
+
/**
|
|
754
|
+
* @description Whether the member was told their password was reset. False means the account has no contact channel — the admin is the only one who can tell them
|
|
755
|
+
* @example true
|
|
756
|
+
*/
|
|
757
|
+
notified?: boolean;
|
|
758
|
+
};
|
|
489
759
|
/** @description Workspace member. */
|
|
490
760
|
MemberResponse: {
|
|
491
761
|
/**
|
|
@@ -524,6 +794,19 @@ export interface components {
|
|
|
524
794
|
/** @description Why the membership is suspended (canonical value no_mfa); null while active */
|
|
525
795
|
suspension_reason?: string | null;
|
|
526
796
|
};
|
|
797
|
+
PaginatedInvitationResponseList: {
|
|
798
|
+
items: components["schemas"]["InvitationResponse"][];
|
|
799
|
+
/** @description Anchor value for next page */
|
|
800
|
+
next_anchor?: string | null;
|
|
801
|
+
/** @description Anchor value for previous page */
|
|
802
|
+
prev_anchor?: string | null;
|
|
803
|
+
/** @description Whether there are more items after this page */
|
|
804
|
+
has_next: boolean;
|
|
805
|
+
/** @description Whether there are items before this page */
|
|
806
|
+
has_prev: boolean;
|
|
807
|
+
/** @description Number of items in current page */
|
|
808
|
+
count: number;
|
|
809
|
+
};
|
|
527
810
|
PaginatedMemberResponseList: {
|
|
528
811
|
items: components["schemas"]["MemberResponse"][];
|
|
529
812
|
/** @description Anchor value for next page */
|
|
@@ -848,6 +1131,82 @@ export interface operations {
|
|
|
848
1131
|
};
|
|
849
1132
|
};
|
|
850
1133
|
};
|
|
1134
|
+
workspaces_api_v1_invitations_list: {
|
|
1135
|
+
parameters: {
|
|
1136
|
+
query?: {
|
|
1137
|
+
/** @description Anchor value to paginate from (exclusive) */
|
|
1138
|
+
anchor?: string;
|
|
1139
|
+
/** @description Pagination direction */
|
|
1140
|
+
direction?: "next" | "prev" | "center";
|
|
1141
|
+
/** @description Number of items (default 100, max 500) */
|
|
1142
|
+
limit?: number;
|
|
1143
|
+
/** @description Case-insensitive substring filter on the invited email address. */
|
|
1144
|
+
search?: string;
|
|
1145
|
+
/** @description Which invitations to return. `pending` (default) — live, actionable, seat-reserving ones, i.e. who has not accepted yet. `never_accepted` — those plus the declined, revoked and expired ones. `all` — the full history, accepted rows included. */
|
|
1146
|
+
status?: "all" | "never_accepted" | "pending";
|
|
1147
|
+
};
|
|
1148
|
+
header?: never;
|
|
1149
|
+
path: {
|
|
1150
|
+
workspace_id: string;
|
|
1151
|
+
};
|
|
1152
|
+
cookie?: never;
|
|
1153
|
+
};
|
|
1154
|
+
requestBody?: never;
|
|
1155
|
+
responses: {
|
|
1156
|
+
200: {
|
|
1157
|
+
headers: {
|
|
1158
|
+
[name: string]: unknown;
|
|
1159
|
+
};
|
|
1160
|
+
content: {
|
|
1161
|
+
"application/json": components["schemas"]["PaginatedInvitationResponseList"];
|
|
1162
|
+
};
|
|
1163
|
+
};
|
|
1164
|
+
};
|
|
1165
|
+
};
|
|
1166
|
+
workspaces_api_v1_invitations_resend_create: {
|
|
1167
|
+
parameters: {
|
|
1168
|
+
query?: never;
|
|
1169
|
+
header?: never;
|
|
1170
|
+
path: {
|
|
1171
|
+
invitation_id: string;
|
|
1172
|
+
workspace_id: string;
|
|
1173
|
+
};
|
|
1174
|
+
cookie?: never;
|
|
1175
|
+
};
|
|
1176
|
+
requestBody?: never;
|
|
1177
|
+
responses: {
|
|
1178
|
+
200: {
|
|
1179
|
+
headers: {
|
|
1180
|
+
[name: string]: unknown;
|
|
1181
|
+
};
|
|
1182
|
+
content: {
|
|
1183
|
+
"application/json": components["schemas"]["InvitationResponse"];
|
|
1184
|
+
};
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
};
|
|
1188
|
+
workspaces_api_v1_invitations_revoke_create: {
|
|
1189
|
+
parameters: {
|
|
1190
|
+
query?: never;
|
|
1191
|
+
header?: never;
|
|
1192
|
+
path: {
|
|
1193
|
+
invitation_id: string;
|
|
1194
|
+
workspace_id: string;
|
|
1195
|
+
};
|
|
1196
|
+
cookie?: never;
|
|
1197
|
+
};
|
|
1198
|
+
requestBody?: never;
|
|
1199
|
+
responses: {
|
|
1200
|
+
200: {
|
|
1201
|
+
headers: {
|
|
1202
|
+
[name: string]: unknown;
|
|
1203
|
+
};
|
|
1204
|
+
content: {
|
|
1205
|
+
"application/json": components["schemas"]["InvitationResponse"];
|
|
1206
|
+
};
|
|
1207
|
+
};
|
|
1208
|
+
};
|
|
1209
|
+
};
|
|
851
1210
|
workspaces_api_v1_members_list: {
|
|
852
1211
|
parameters: {
|
|
853
1212
|
query?: {
|
|
@@ -927,6 +1286,34 @@ export interface operations {
|
|
|
927
1286
|
};
|
|
928
1287
|
};
|
|
929
1288
|
};
|
|
1289
|
+
workspaces_api_v1_members_password_reset_create: {
|
|
1290
|
+
parameters: {
|
|
1291
|
+
query?: never;
|
|
1292
|
+
header?: never;
|
|
1293
|
+
path: {
|
|
1294
|
+
user_id: string;
|
|
1295
|
+
workspace_id: string;
|
|
1296
|
+
};
|
|
1297
|
+
cookie?: never;
|
|
1298
|
+
};
|
|
1299
|
+
requestBody?: {
|
|
1300
|
+
content: {
|
|
1301
|
+
"application/json": components["schemas"]["MemberPasswordResetRequest"];
|
|
1302
|
+
"application/x-www-form-urlencoded": components["schemas"]["MemberPasswordResetRequest"];
|
|
1303
|
+
"multipart/form-data": components["schemas"]["MemberPasswordResetRequest"];
|
|
1304
|
+
};
|
|
1305
|
+
};
|
|
1306
|
+
responses: {
|
|
1307
|
+
200: {
|
|
1308
|
+
headers: {
|
|
1309
|
+
[name: string]: unknown;
|
|
1310
|
+
};
|
|
1311
|
+
content: {
|
|
1312
|
+
"application/json": components["schemas"]["MemberPasswordResetResponse"];
|
|
1313
|
+
};
|
|
1314
|
+
};
|
|
1315
|
+
};
|
|
1316
|
+
};
|
|
930
1317
|
workspaces_api_v1_members_invite_create: {
|
|
931
1318
|
parameters: {
|
|
932
1319
|
query?: never;
|
package/src/api/types.ts
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
* openapi-typescript under-describe the runtime, apply a small documented
|
|
10
10
|
* correction (see auth-react `api/types.ts` for the three canonical patterns).
|
|
11
11
|
*/
|
|
12
|
-
import type { components } from "./generated/schema.js";
|
|
12
|
+
import type { components, operations } from "./generated/schema.js";
|
|
13
13
|
|
|
14
14
|
/** The generated schema table — the one source of truth for wire shapes. */
|
|
15
15
|
export type Schemas = components["schemas"];
|
|
16
16
|
|
|
17
|
+
/** The generated operation table — the source of truth for QUERY parameters
|
|
18
|
+
* (drf-spectacular declares them per-operation, not as a component schema). */
|
|
19
|
+
export type Operations = operations;
|
|
20
|
+
|
|
17
21
|
// ── aliases (the stapel-workspaces schemas this pair uses) ────────────────────
|
|
18
22
|
|
|
19
23
|
/** One workspace — identity, storage, my role, member count (GET/POST/PATCH). */
|
|
@@ -34,8 +38,21 @@ export type MemberInvite = Schemas["MemberInviteRequest"];
|
|
|
34
38
|
export type MemberInviteResult = Schemas["MemberInviteResponse"];
|
|
35
39
|
/** PATCH /{id}/members/{userId} request body — the new role. */
|
|
36
40
|
export type MemberRoleUpdate = Schemas["PatchedMemberUpdateRequest"];
|
|
37
|
-
/** One pending/accepted invitation (a row of {@link MemberInviteResult}
|
|
41
|
+
/** One pending/accepted invitation (a row of {@link MemberInviteResult}, and
|
|
42
|
+
* a row of the admin's invitation table — GET /{id}/invitations). The invite
|
|
43
|
+
* TOKEN is deliberately absent from this shape: it is a bearer credential
|
|
44
|
+
* that only ever leaves the backend inside the invitation email. */
|
|
38
45
|
export type Invitation = Schemas["InvitationResponse"];
|
|
46
|
+
/** GET /{id}/invitations 200 body — an anchor-paginated page of {@link Invitation}s. */
|
|
47
|
+
export type InvitationPage = Schemas["PaginatedInvitationResponseList"];
|
|
48
|
+
/** POST /{id}/members/{userId}/password/reset request body — an optional
|
|
49
|
+
* admin-chosen password, the first-login demands to raise, and an audit note. */
|
|
50
|
+
export type MemberPasswordReset = Schemas["MemberPasswordResetRequest"];
|
|
51
|
+
/** POST /{id}/members/{userId}/password/reset 200 body. `generated_password`
|
|
52
|
+
* is a ONE-SHOT credential (present only when the request omitted `password`)
|
|
53
|
+
* — see {@link "../model/mutations.js".useResetMemberPassword}, which keeps it
|
|
54
|
+
* out of the query cache. */
|
|
55
|
+
export type MemberPasswordResetResult = Schemas["MemberPasswordResetResponse"];
|
|
39
56
|
/** POST /invitations/accept request body — the token from the email link. */
|
|
40
57
|
export type InvitationAccept = Schemas["InvitationAcceptRequest"];
|
|
41
58
|
/** GET /invitations/{token} 200 body — the public (AllowAny) preview the
|
|
@@ -88,3 +105,77 @@ export interface MembersParams {
|
|
|
88
105
|
/** Case-insensitive substring filter matched against a member's email or display name. */
|
|
89
106
|
readonly search?: string;
|
|
90
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Which invitations GET /{id}/invitations returns. Taken VERBATIM from the
|
|
111
|
+
* generated operation table (not re-typed by hand): `pending` (the backend
|
|
112
|
+
* default) — live, seat-reserving invitations, i.e. who has not accepted yet;
|
|
113
|
+
* `never_accepted` — those plus declined / revoked / expired; `all` — the full
|
|
114
|
+
* history, accepted rows included.
|
|
115
|
+
*/
|
|
116
|
+
export type InvitationStatusFilter = NonNullable<
|
|
117
|
+
NonNullable<
|
|
118
|
+
Operations["workspaces_api_v1_invitations_list"]["parameters"]["query"]
|
|
119
|
+
>["status"]
|
|
120
|
+
>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Anchor-pagination query for GET /{id}/invitations (core `AnchorPagination`,
|
|
124
|
+
* the same shape as {@link MembersParams} plus the `status` filter). All
|
|
125
|
+
* optional: no params fetches the newest page of PENDING invitations
|
|
126
|
+
* (default limit 100, max 500).
|
|
127
|
+
*
|
|
128
|
+
* ANCHOR, not page numbers: a page is addressed by the opaque `next_anchor`
|
|
129
|
+
* of the page before it. There is no "page 3" to jump to and no offset to
|
|
130
|
+
* skew when an invitation is revoked mid-scroll.
|
|
131
|
+
*/
|
|
132
|
+
export interface InvitationsParams {
|
|
133
|
+
/** Anchor value to paginate from (exclusive) — a page's `next_anchor`. */
|
|
134
|
+
readonly anchor?: string;
|
|
135
|
+
/** Pagination direction relative to `anchor`. */
|
|
136
|
+
readonly direction?: "next" | "prev" | "center";
|
|
137
|
+
/** Page size (default 100, max 500). */
|
|
138
|
+
readonly limit?: number;
|
|
139
|
+
/** Case-insensitive substring filter on the invited email address. */
|
|
140
|
+
readonly search?: string;
|
|
141
|
+
/** Which invitations to return (default `pending`). */
|
|
142
|
+
readonly status?: InvitationStatusFilter;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* One first-login demand an organization may raise on an account it admits
|
|
147
|
+
* (`settings.security.provisioned_user_policies`, backend dto
|
|
148
|
+
* `PROVISIONED_USER_POLICIES`). INDEPENDENT checkboxes, not alternatives — an
|
|
149
|
+
* org may demand both. NOT GENERATED: the security block lives inside the
|
|
150
|
+
* free-form `Workspace.settings` JSON, so drf-spectacular types it as an
|
|
151
|
+
* untyped object; the backend serializer validates exactly these two values.
|
|
152
|
+
*/
|
|
153
|
+
export type ProvisionedUserPolicy = "password_change" | "mfa_enroll";
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Typed shape of `Workspace.settings.security` (backend dto
|
|
157
|
+
* `WorkspaceSecuritySettings`, org-program §C3). A documented correction for
|
|
158
|
+
* the same reason as {@link WorkspaceRole}: the block is stored inside the
|
|
159
|
+
* free-form settings JSON (no schema migration on the backend), so the
|
|
160
|
+
* generated types say `object` and this is the canon of the known keys.
|
|
161
|
+
* Unknown keys are preserved verbatim by the backend — hence the index
|
|
162
|
+
* signature, and hence why the update hook MERGES rather than replaces.
|
|
163
|
+
*/
|
|
164
|
+
export interface WorkspaceSecuritySettings {
|
|
165
|
+
/** Whether membership requires a strong second factor. Turning it on
|
|
166
|
+
* sweeps current members and suspends those without one (reason `no_mfa`). */
|
|
167
|
+
readonly require_mfa?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* First-login steps demanded of accounts the org admits — a LIST since
|
|
170
|
+
* stapel-workspaces 0.13.0 (#90). The pre-0.13 singular
|
|
171
|
+
* `provisioned_user_policy` string is still read by the backend for old
|
|
172
|
+
* rows, but this pair only ever WRITES the list: a client that keeps
|
|
173
|
+
* writing the singular spelling silently caps the org at one demand.
|
|
174
|
+
*/
|
|
175
|
+
readonly provisioned_user_policies?: readonly ProvisionedUserPolicy[];
|
|
176
|
+
/** Derived, read-only: whether the org actually set the policies (as
|
|
177
|
+
* opposed to inheriting the `password_change` default). Never sent. */
|
|
178
|
+
readonly policies_configured?: boolean;
|
|
179
|
+
/** Extra keys pass through the backend verbatim (client extension). */
|
|
180
|
+
readonly [extra: string]: unknown;
|
|
181
|
+
}
|
package/src/api/workspacesApi.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { StapelClient, StapelRequestOptions } from "@stapel/core";
|
|
2
2
|
import type {
|
|
3
|
+
Invitation,
|
|
4
|
+
InvitationPage,
|
|
5
|
+
InvitationsParams,
|
|
3
6
|
Member,
|
|
4
7
|
MemberInvite,
|
|
5
8
|
MemberInviteResult,
|
|
6
9
|
MemberPage,
|
|
10
|
+
MemberPasswordReset,
|
|
11
|
+
MemberPasswordResetResult,
|
|
7
12
|
MemberRoleUpdate,
|
|
8
13
|
MembersParams,
|
|
9
14
|
InvitationAccept,
|
|
@@ -87,6 +92,55 @@ export interface WorkspacesApi {
|
|
|
87
92
|
/** Remove a member from a workspace (admin+; the last owner is protected). */
|
|
88
93
|
removeMember(workspaceId: string, userId: string): Promise<void>;
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Reset a member's password on the organization's order (stapel-workspaces
|
|
97
|
+
* ≥0.14.0, #110). Capability `members.password.reset`, declared **high** —
|
|
98
|
+
* the backend wraps it in `requires_verification(scope="sensitive")`, so an
|
|
99
|
+
* ambient cookie is not enough and a step-up (or factor ENROLLMENT) 403 is
|
|
100
|
+
* the normal first answer. A missing/wrong target answers one
|
|
101
|
+
* byte-identical 404 (never an existence oracle).
|
|
102
|
+
*
|
|
103
|
+
* `generated_password` in the reply is a ONE-SHOT credential: present only
|
|
104
|
+
* when the request omitted `password`, never re-fetchable, never logged.
|
|
105
|
+
*/
|
|
106
|
+
resetMemberPassword(
|
|
107
|
+
workspaceId: string,
|
|
108
|
+
userId: string,
|
|
109
|
+
body?: MemberPasswordReset
|
|
110
|
+
): Promise<MemberPasswordResetResult>;
|
|
111
|
+
|
|
112
|
+
// ── invitation administration (#109, stapel-workspaces ≥0.12.0) ────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The workspace's invitations — the admin's "who has not accepted yet"
|
|
116
|
+
* table (capability `members.invite`). An ANCHOR-paginated page, filtered
|
|
117
|
+
* by `status` (default `pending`) and `search` (invited email). The invite
|
|
118
|
+
* token is never in the response.
|
|
119
|
+
*/
|
|
120
|
+
listInvitations(
|
|
121
|
+
workspaceId: string,
|
|
122
|
+
params?: InvitationsParams
|
|
123
|
+
): Promise<InvitationPage>;
|
|
124
|
+
/**
|
|
125
|
+
* Withdraw a live invitation — the workspace's terminal "no", the mirror of
|
|
126
|
+
* the invitee's decline (the two stay distinguishable in `status` forever).
|
|
127
|
+
* Only a `pending` one is revocable; the reply is the updated row.
|
|
128
|
+
*/
|
|
129
|
+
revokeInvitation(
|
|
130
|
+
workspaceId: string,
|
|
131
|
+
invitationId: string
|
|
132
|
+
): Promise<Invitation>;
|
|
133
|
+
/**
|
|
134
|
+
* Send the invitation email again. Accepts an EXPIRED invitation on purpose
|
|
135
|
+
* (a dead TTL is the commonest reason to resend) and refuses the three
|
|
136
|
+
* terminal states. Rotates the token and restarts the TTL — every earlier
|
|
137
|
+
* link stops working — so the reply is the updated row, not the old one.
|
|
138
|
+
*/
|
|
139
|
+
resendInvitation(
|
|
140
|
+
workspaceId: string,
|
|
141
|
+
invitationId: string
|
|
142
|
+
): Promise<Invitation>;
|
|
143
|
+
|
|
90
144
|
/** Accept an invitation by its token — returns the caller's new membership. */
|
|
91
145
|
acceptInvitation(body: InvitationAccept): Promise<Member>;
|
|
92
146
|
|
|
@@ -172,6 +226,39 @@ export function createWorkspacesApi(client: StapelClient): WorkspacesApi {
|
|
|
172
226
|
mutating()
|
|
173
227
|
),
|
|
174
228
|
|
|
229
|
+
resetMemberPassword: (workspaceId, userId, body) =>
|
|
230
|
+
client.post(
|
|
231
|
+
`/${encodeURIComponent(workspaceId)}/members/${encodeURIComponent(userId)}/password/reset`,
|
|
232
|
+
(body ?? {}) satisfies MemberPasswordReset,
|
|
233
|
+
mutating()
|
|
234
|
+
),
|
|
235
|
+
|
|
236
|
+
listInvitations: (workspaceId, params) => {
|
|
237
|
+
const query: Record<string, string | number> = {};
|
|
238
|
+
if (params?.anchor !== undefined) query.anchor = params.anchor;
|
|
239
|
+
if (params?.direction !== undefined) query.direction = params.direction;
|
|
240
|
+
if (params?.limit !== undefined) query.limit = params.limit;
|
|
241
|
+
if (params?.search !== undefined) query.search = params.search;
|
|
242
|
+
if (params?.status !== undefined) query.status = params.status;
|
|
243
|
+
return client.get(`/${encodeURIComponent(workspaceId)}/invitations`, {
|
|
244
|
+
query,
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
revokeInvitation: (workspaceId, invitationId) =>
|
|
249
|
+
client.post(
|
|
250
|
+
`/${encodeURIComponent(workspaceId)}/invitations/${encodeURIComponent(invitationId)}/revoke`,
|
|
251
|
+
undefined,
|
|
252
|
+
mutating()
|
|
253
|
+
),
|
|
254
|
+
|
|
255
|
+
resendInvitation: (workspaceId, invitationId) =>
|
|
256
|
+
client.post(
|
|
257
|
+
`/${encodeURIComponent(workspaceId)}/invitations/${encodeURIComponent(invitationId)}/resend`,
|
|
258
|
+
undefined,
|
|
259
|
+
mutating()
|
|
260
|
+
),
|
|
261
|
+
|
|
175
262
|
acceptInvitation: (body) =>
|
|
176
263
|
client.post(
|
|
177
264
|
"/invitations/accept",
|