@stapel/workspaces-react 0.6.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/api/generated/schema.d.ts +774 -7
  3. package/dist/api/generated/schema.d.ts.map +1 -1
  4. package/dist/api/types.d.ts +104 -7
  5. package/dist/api/types.d.ts.map +1 -1
  6. package/dist/api/workspacesApi.d.ts +55 -1
  7. package/dist/api/workspacesApi.d.ts.map +1 -1
  8. package/dist/api/workspacesApi.js +23 -0
  9. package/dist/api/workspacesApi.js.map +1 -1
  10. package/dist/default/InviteAcceptPage.d.ts +28 -0
  11. package/dist/default/InviteAcceptPage.d.ts.map +1 -0
  12. package/dist/default/InviteAcceptPage.js +141 -0
  13. package/dist/default/InviteAcceptPage.js.map +1 -0
  14. package/dist/default/MembersManager.d.ts +4 -4
  15. package/dist/default/MembersManager.d.ts.map +1 -1
  16. package/dist/default/MembersManager.js +61 -56
  17. package/dist/default/MembersManager.js.map +1 -1
  18. package/dist/default/index.d.ts +2 -0
  19. package/dist/default/index.d.ts.map +1 -1
  20. package/dist/default/index.js +1 -0
  21. package/dist/default/index.js.map +1 -1
  22. package/dist/flows/inviteAcceptFlow.d.ts +155 -0
  23. package/dist/flows/inviteAcceptFlow.d.ts.map +1 -0
  24. package/dist/flows/inviteAcceptFlow.js +126 -0
  25. package/dist/flows/inviteAcceptFlow.js.map +1 -0
  26. package/dist/headless/Can.d.ts +40 -0
  27. package/dist/headless/Can.d.ts.map +1 -0
  28. package/dist/headless/Can.js +30 -0
  29. package/dist/headless/Can.js.map +1 -0
  30. package/dist/headless/InviteAcceptFlow.d.ts +44 -0
  31. package/dist/headless/InviteAcceptFlow.d.ts.map +1 -0
  32. package/dist/headless/InviteAcceptFlow.js +67 -0
  33. package/dist/headless/InviteAcceptFlow.js.map +1 -0
  34. package/dist/headless/RoleSelect.d.ts +41 -0
  35. package/dist/headless/RoleSelect.d.ts.map +1 -0
  36. package/dist/headless/RoleSelect.js +34 -0
  37. package/dist/headless/RoleSelect.js.map +1 -0
  38. package/dist/i18n/generated/errors.gen.d.ts +48 -0
  39. package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
  40. package/dist/i18n/generated/errors.gen.js +24 -0
  41. package/dist/i18n/generated/errors.gen.js.map +1 -1
  42. package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -1
  43. package/dist/i18n/generated/errors.ru.gen.js +8 -0
  44. package/dist/i18n/generated/errors.ru.gen.js.map +1 -1
  45. package/dist/i18n/keys.d.ts +28 -0
  46. package/dist/i18n/keys.d.ts.map +1 -1
  47. package/dist/i18n/keys.js +64 -0
  48. package/dist/i18n/keys.js.map +1 -1
  49. package/dist/i18n/ru.d.ts.map +1 -1
  50. package/dist/i18n/ru.js +30 -0
  51. package/dist/i18n/ru.js.map +1 -1
  52. package/dist/index.d.ts +17 -4
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +10 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/model/capabilities.d.ts +21 -0
  57. package/dist/model/capabilities.d.ts.map +1 -0
  58. package/dist/model/capabilities.js +32 -0
  59. package/dist/model/capabilities.js.map +1 -0
  60. package/dist/model/emailMask.d.ts +22 -0
  61. package/dist/model/emailMask.d.ts.map +1 -0
  62. package/dist/model/emailMask.js +36 -0
  63. package/dist/model/emailMask.js.map +1 -0
  64. package/dist/model/mutations.d.ts +107 -3
  65. package/dist/model/mutations.d.ts.map +1 -1
  66. package/dist/model/mutations.js +190 -1
  67. package/dist/model/mutations.js.map +1 -1
  68. package/dist/model/queries.d.ts +108 -2
  69. package/dist/model/queries.d.ts.map +1 -1
  70. package/dist/model/queries.js +139 -1
  71. package/dist/model/queries.js.map +1 -1
  72. package/dist/model/queryKeys.d.ts +12 -1
  73. package/dist/model/queryKeys.d.ts.map +1 -1
  74. package/dist/model/queryKeys.js +25 -0
  75. package/dist/model/queryKeys.js.map +1 -1
  76. package/dist/model/stepUp.d.ts +25 -0
  77. package/dist/model/stepUp.d.ts.map +1 -0
  78. package/dist/model/stepUp.js +79 -0
  79. package/dist/model/stepUp.js.map +1 -0
  80. package/llms.txt +49 -15
  81. package/manifest.json +346 -3
  82. package/package.json +5 -5
  83. package/src/analytics/generated/events.json +1 -1
  84. package/src/api/generated/schema.ts +774 -7
  85. package/src/api/types.ts +113 -7
  86. package/src/api/workspacesApi.ts +137 -0
  87. package/src/default/InviteAcceptPage.tsx +373 -0
  88. package/src/default/MembersManager.tsx +155 -145
  89. package/src/default/index.ts +2 -0
  90. package/src/flows/inviteAcceptFlow.ts +289 -0
  91. package/src/headless/Can.tsx +49 -0
  92. package/src/headless/InviteAcceptFlow.tsx +105 -0
  93. package/src/headless/RoleSelect.tsx +58 -0
  94. package/src/i18n/generated/errors.gen.ts +24 -0
  95. package/src/i18n/generated/errors.json +62 -0
  96. package/src/i18n/generated/errors.ru.gen.ts +8 -0
  97. package/src/i18n/keys.ts +68 -0
  98. package/src/i18n/ru.ts +34 -0
  99. package/src/index.ts +54 -1
  100. package/src/model/capabilities.ts +37 -0
  101. package/src/model/emailMask.ts +37 -0
  102. package/src/model/mutations.ts +248 -4
  103. package/src/model/queries.ts +205 -2
  104. package/src/model/queryKeys.ts +45 -1
  105. package/src/model/stepUp.ts +96 -0
@@ -1,4 +1,4 @@
1
- import type { MembersParams } from "../api/types.js";
1
+ import type { InvitationsParams, MembersParams } from "../api/types.js";
2
2
 
3
3
  /**
4
4
  * Namespaced TanStack Query keys (frontend-standard §2 — "ключи неймспейснуты").
@@ -18,6 +18,25 @@ export const workspacesQueryKeys: {
18
18
  workspaceId: string,
19
19
  params: MembersParams
20
20
  ): readonly ["workspaces", "members", string, MembersParams];
21
+ invitations(workspaceId: string): readonly ["workspaces", "invitations", string];
22
+ invitationsPage(
23
+ workspaceId: string,
24
+ params: InvitationsParams
25
+ ): readonly ["workspaces", "invitations", string, InvitationsParams];
26
+ invitationsInfinite(
27
+ workspaceId: string,
28
+ filters: InvitationsParams
29
+ ): readonly [
30
+ "workspaces",
31
+ "invitations",
32
+ string,
33
+ "infinite",
34
+ InvitationsParams,
35
+ ];
36
+ roles(): readonly ["workspaces", "roles"];
37
+ invitationPreview(
38
+ token: string
39
+ ): readonly ["workspaces", "invitation-preview", string];
21
40
  } = {
22
41
  all: [ROOT],
23
42
  list: () => [ROOT, "list"],
@@ -26,4 +45,29 @@ export const workspacesQueryKeys: {
26
45
  // invalidating `members(workspaceId)` (mutations.ts) drops every page.
27
46
  members: (workspaceId) => [ROOT, "members", workspaceId],
28
47
  membersPage: (workspaceId, params) => [ROOT, "members", workspaceId, params],
48
+ // Same prefix trick as `members`: the bare 3-tuple invalidates every page
49
+ // (and the infinite list, which keys on it) after a revoke / resend.
50
+ invitations: (workspaceId) => [ROOT, "invitations", workspaceId],
51
+ invitationsPage: (workspaceId, params) => [
52
+ ROOT,
53
+ "invitations",
54
+ workspaceId,
55
+ params,
56
+ ],
57
+ // The infinite list carries its own marker segment: its cached value is an
58
+ // `InfiniteData` envelope, not a page, so it must never collide with a
59
+ // single-page key that happens to hold the same filters.
60
+ invitationsInfinite: (workspaceId, filters) => [
61
+ ROOT,
62
+ "invitations",
63
+ workspaceId,
64
+ "infinite",
65
+ filters,
66
+ ],
67
+ roles: () => [ROOT, "roles"],
68
+ // NOTE: the key carries the invite TOKEN (a secret). Core's query persist
69
+ // scope is per-user and the preview response is already public-by-design
70
+ // (masked email only) — but hosts logging query keys should treat this one
71
+ // as sensitive.
72
+ invitationPreview: (token) => [ROOT, "invitation-preview", token],
29
73
  };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Step-up ahead of the button, not behind it (org-program §A3).
3
+ *
4
+ * Some workspace operations are declared **high**: the backend gates them
5
+ * with `stapel_core`'s `@requires_verification(scope="sensitive")` on top of
6
+ * the capability check, so an ambient cookie is never enough. A client that
7
+ * only learns this from the 403 shows the admin a button, a spinner and a
8
+ * refusal; this module lets it know BEFORE the click.
9
+ *
10
+ * Two things travel from the backend, and they are not the same thing:
11
+ *
12
+ * 1. **A step-up CHALLENGE** — `verification.challenge_id` is present. This
13
+ * one nobody has to handle here: `@stapel/core`'s client intercepts it,
14
+ * hands it to the app's verification controller (auth-react's factor
15
+ * machines) and replays the original request with
16
+ * `X-Verification-Token`. It is a detour, not a failure.
17
+ * 2. **An ENROLLMENT demand** — `verification.enroll === true` and there is
18
+ * NO challenge id, because the user holds no factor that could be
19
+ * challenged. Core deliberately does not intercept this one (there is
20
+ * nothing to drive), so it surfaces as a plain 403 and the app must send
21
+ * the user to factor enrollment first. {@link readVerificationEnrollment}
22
+ * is how a hook says "this is that, and these are the factors".
23
+ *
24
+ * KEEP IN SYNC with stapel-workspaces `capabilities.BUILTIN_CAPABILITY_LEVELS`
25
+ * — a PORT for the same reason `capabilityMatches` is one: `GET /roles`
26
+ * publishes each role's capabilities but not their levels, so the level map
27
+ * cannot be read off the wire. A deployment may raise further capabilities via
28
+ * `STAPEL_WORKSPACES["CAPABILITY_LEVELS"]`; those are additive, so treating an
29
+ * unlisted capability as `standard` can only ever under-promise (the backend
30
+ * still challenges, and core still drives it).
31
+ */
32
+ import { isStapelApiError } from "@stapel/core";
33
+
34
+ /** The verification scope every `high` workspace capability is gated under. */
35
+ export const SENSITIVE_SCOPE = "sensitive";
36
+
37
+ /** Step-up level of a capability. */
38
+ export type CapabilityLevel = "standard" | "high";
39
+
40
+ /** PORT of `BUILTIN_CAPABILITY_LEVELS` — everything unlisted is `standard`. */
41
+ export const BUILTIN_CAPABILITY_LEVELS: Readonly<Record<string, "high">> = {
42
+ "members.provision": "high",
43
+ "members.password.reset": "high",
44
+ "workspace.security.manage": "high",
45
+ };
46
+
47
+ /** The declared step-up level of one capability string. */
48
+ export function capabilityLevel(capability: string): CapabilityLevel {
49
+ return BUILTIN_CAPABILITY_LEVELS[capability] ?? "standard";
50
+ }
51
+
52
+ /**
53
+ * The 403 ENROLLMENT envelope: the caller must first enroll one of `factors`
54
+ * — there is no challenge to complete, so core cannot drive it.
55
+ */
56
+ export interface VerificationEnrollment {
57
+ /** What the demand protects — `"sensitive"` for every `high` capability. */
58
+ readonly scope: string;
59
+ /** Interchangeable factors, any ONE of which unblocks the operation. */
60
+ readonly factors: readonly string[];
61
+ }
62
+
63
+ function asRecord(value: unknown): Record<string, unknown> | null {
64
+ return typeof value === "object" && value !== null && !Array.isArray(value)
65
+ ? (value as Record<string, unknown>)
66
+ : null;
67
+ }
68
+
69
+ /**
70
+ * Read the enrollment demand out of a rejected request, or `null` when the
71
+ * error is anything else (including a step-up CHALLENGE, which carries a
72
+ * `challenge_id` and is core's business, not the consumer's).
73
+ */
74
+ export function readVerificationEnrollment(
75
+ error: unknown
76
+ ): VerificationEnrollment | null {
77
+ // Both dialects, narrowed through the imported predicate (never through a
78
+ // shape this call site invented): the typed error carries the parsed body,
79
+ // a rethrown raw envelope IS the body.
80
+ const body = asRecord(isStapelApiError(error) ? error.body : error);
81
+ if (body === null) return null;
82
+ const verification = asRecord(body["verification"]);
83
+ if (verification === null) return null;
84
+ if (verification["enroll"] !== true) return null;
85
+ if (typeof verification["challenge_id"] === "string") return null;
86
+ const factors = verification["factors"];
87
+ return {
88
+ scope:
89
+ typeof verification["scope"] === "string"
90
+ ? verification["scope"]
91
+ : SENSITIVE_SCOPE,
92
+ factors: Array.isArray(factors)
93
+ ? factors.filter((f): f is string => typeof f === "string")
94
+ : [],
95
+ };
96
+ }