@stapel/auth-react 1.0.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 (191) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/MODULE.md +147 -0
  4. package/README.md +116 -0
  5. package/dist/api/authApi.d.ts +68 -0
  6. package/dist/api/authApi.d.ts.map +1 -0
  7. package/dist/api/authApi.js +90 -0
  8. package/dist/api/authApi.js.map +1 -0
  9. package/dist/api/types.d.ts +238 -0
  10. package/dist/api/types.d.ts.map +1 -0
  11. package/dist/api/types.js +14 -0
  12. package/dist/api/types.js.map +1 -0
  13. package/dist/api/urls.d.ts +41 -0
  14. package/dist/api/urls.d.ts.map +1 -0
  15. package/dist/api/urls.js +86 -0
  16. package/dist/api/urls.js.map +1 -0
  17. package/dist/flows/anonymousFlow.d.ts +33 -0
  18. package/dist/flows/anonymousFlow.d.ts.map +1 -0
  19. package/dist/flows/anonymousFlow.js +26 -0
  20. package/dist/flows/anonymousFlow.js.map +1 -0
  21. package/dist/flows/authenticatorChangeFlow.d.ts +67 -0
  22. package/dist/flows/authenticatorChangeFlow.d.ts.map +1 -0
  23. package/dist/flows/authenticatorChangeFlow.js +79 -0
  24. package/dist/flows/authenticatorChangeFlow.js.map +1 -0
  25. package/dist/flows/createFlowMachine.d.ts +55 -0
  26. package/dist/flows/createFlowMachine.d.ts.map +1 -0
  27. package/dist/flows/createFlowMachine.js +56 -0
  28. package/dist/flows/createFlowMachine.js.map +1 -0
  29. package/dist/flows/errors.d.ts +15 -0
  30. package/dist/flows/errors.d.ts.map +1 -0
  31. package/dist/flows/errors.js +17 -0
  32. package/dist/flows/errors.js.map +1 -0
  33. package/dist/flows/magicLinkFlow.d.ts +41 -0
  34. package/dist/flows/magicLinkFlow.d.ts.map +1 -0
  35. package/dist/flows/magicLinkFlow.js +29 -0
  36. package/dist/flows/magicLinkFlow.js.map +1 -0
  37. package/dist/flows/oauthFlow.d.ts +58 -0
  38. package/dist/flows/oauthFlow.d.ts.map +1 -0
  39. package/dist/flows/oauthFlow.js +53 -0
  40. package/dist/flows/oauthFlow.js.map +1 -0
  41. package/dist/flows/otpFlow.d.ts +74 -0
  42. package/dist/flows/otpFlow.d.ts.map +1 -0
  43. package/dist/flows/otpFlow.js +68 -0
  44. package/dist/flows/otpFlow.js.map +1 -0
  45. package/dist/flows/passkeyFlow.d.ts +75 -0
  46. package/dist/flows/passkeyFlow.d.ts.map +1 -0
  47. package/dist/flows/passkeyFlow.js +100 -0
  48. package/dist/flows/passkeyFlow.js.map +1 -0
  49. package/dist/flows/passwordChangeFlow.d.ts +53 -0
  50. package/dist/flows/passwordChangeFlow.d.ts.map +1 -0
  51. package/dist/flows/passwordChangeFlow.js +51 -0
  52. package/dist/flows/passwordChangeFlow.js.map +1 -0
  53. package/dist/flows/passwordLoginFlow.d.ts +62 -0
  54. package/dist/flows/passwordLoginFlow.d.ts.map +1 -0
  55. package/dist/flows/passwordLoginFlow.js +55 -0
  56. package/dist/flows/passwordLoginFlow.js.map +1 -0
  57. package/dist/flows/passwordResetFlow.d.ts +56 -0
  58. package/dist/flows/passwordResetFlow.d.ts.map +1 -0
  59. package/dist/flows/passwordResetFlow.js +57 -0
  60. package/dist/flows/passwordResetFlow.js.map +1 -0
  61. package/dist/flows/qrLoginFlow.d.ts +55 -0
  62. package/dist/flows/qrLoginFlow.d.ts.map +1 -0
  63. package/dist/flows/qrLoginFlow.js +91 -0
  64. package/dist/flows/qrLoginFlow.js.map +1 -0
  65. package/dist/flows/ssoFlow.d.ts +46 -0
  66. package/dist/flows/ssoFlow.d.ts.map +1 -0
  67. package/dist/flows/ssoFlow.js +34 -0
  68. package/dist/flows/ssoFlow.js.map +1 -0
  69. package/dist/flows/totpSetupFlow.d.ts +49 -0
  70. package/dist/flows/totpSetupFlow.d.ts.map +1 -0
  71. package/dist/flows/totpSetupFlow.js +47 -0
  72. package/dist/flows/totpSetupFlow.js.map +1 -0
  73. package/dist/flows/useFlow.d.ts +9 -0
  74. package/dist/flows/useFlow.d.ts.map +1 -0
  75. package/dist/flows/useFlow.js +11 -0
  76. package/dist/flows/useFlow.js.map +1 -0
  77. package/dist/flows/verificationFlow.d.ts +108 -0
  78. package/dist/flows/verificationFlow.d.ts.map +1 -0
  79. package/dist/flows/verificationFlow.js +195 -0
  80. package/dist/flows/verificationFlow.js.map +1 -0
  81. package/dist/headless/AuthProvider.d.ts +18 -0
  82. package/dist/headless/AuthProvider.d.ts.map +1 -0
  83. package/dist/headless/AuthProvider.js +22 -0
  84. package/dist/headless/AuthProvider.js.map +1 -0
  85. package/dist/headless/Passkey.d.ts +31 -0
  86. package/dist/headless/Passkey.d.ts.map +1 -0
  87. package/dist/headless/Passkey.js +51 -0
  88. package/dist/headless/Passkey.js.map +1 -0
  89. package/dist/headless/PasswordChange.d.ts +20 -0
  90. package/dist/headless/PasswordChange.d.ts.map +1 -0
  91. package/dist/headless/PasswordChange.js +30 -0
  92. package/dist/headless/PasswordChange.js.map +1 -0
  93. package/dist/headless/PasswordLogin.d.ts +17 -0
  94. package/dist/headless/PasswordLogin.d.ts.map +1 -0
  95. package/dist/headless/PasswordLogin.js +31 -0
  96. package/dist/headless/PasswordLogin.js.map +1 -0
  97. package/dist/headless/PasswordReset.d.ts +19 -0
  98. package/dist/headless/PasswordReset.d.ts.map +1 -0
  99. package/dist/headless/PasswordReset.js +34 -0
  100. package/dist/headless/PasswordReset.js.map +1 -0
  101. package/dist/headless/PasswordlessLogin.d.ts +28 -0
  102. package/dist/headless/PasswordlessLogin.d.ts.map +1 -0
  103. package/dist/headless/PasswordlessLogin.js +42 -0
  104. package/dist/headless/PasswordlessLogin.js.map +1 -0
  105. package/dist/headless/QrLogin.d.ts +19 -0
  106. package/dist/headless/QrLogin.d.ts.map +1 -0
  107. package/dist/headless/QrLogin.js +32 -0
  108. package/dist/headless/QrLogin.js.map +1 -0
  109. package/dist/headless/TotpSetup.d.ts +17 -0
  110. package/dist/headless/TotpSetup.d.ts.map +1 -0
  111. package/dist/headless/TotpSetup.js +26 -0
  112. package/dist/headless/TotpSetup.js.map +1 -0
  113. package/dist/headless/VerificationChallenge.d.ts +37 -0
  114. package/dist/headless/VerificationChallenge.d.ts.map +1 -0
  115. package/dist/headless/VerificationChallenge.js +40 -0
  116. package/dist/headless/VerificationChallenge.js.map +1 -0
  117. package/dist/headless/misc.d.ts +47 -0
  118. package/dist/headless/misc.d.ts.map +1 -0
  119. package/dist/headless/misc.js +84 -0
  120. package/dist/headless/misc.js.map +1 -0
  121. package/dist/i18n/keys.d.ts +34 -0
  122. package/dist/i18n/keys.d.ts.map +1 -0
  123. package/dist/i18n/keys.js +83 -0
  124. package/dist/i18n/keys.js.map +1 -0
  125. package/dist/index.d.ts +73 -0
  126. package/dist/index.d.ts.map +1 -0
  127. package/dist/index.js +48 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/model/context.d.ts +22 -0
  130. package/dist/model/context.d.ts.map +1 -0
  131. package/dist/model/context.js +34 -0
  132. package/dist/model/context.js.map +1 -0
  133. package/dist/model/mutations.d.ts +28 -0
  134. package/dist/model/mutations.d.ts.map +1 -0
  135. package/dist/model/mutations.js +108 -0
  136. package/dist/model/mutations.js.map +1 -0
  137. package/dist/model/queries.d.ts +30 -0
  138. package/dist/model/queries.d.ts.map +1 -0
  139. package/dist/model/queries.js +87 -0
  140. package/dist/model/queries.js.map +1 -0
  141. package/dist/model/queryKeys.d.ts +13 -0
  142. package/dist/model/queryKeys.d.ts.map +1 -0
  143. package/dist/model/queryKeys.js +21 -0
  144. package/dist/model/queryKeys.js.map +1 -0
  145. package/dist/model/runtime.d.ts +39 -0
  146. package/dist/model/runtime.d.ts.map +1 -0
  147. package/dist/model/runtime.js +44 -0
  148. package/dist/model/runtime.js.map +1 -0
  149. package/dist/model/session.d.ts +50 -0
  150. package/dist/model/session.d.ts.map +1 -0
  151. package/dist/model/session.js +124 -0
  152. package/dist/model/session.js.map +1 -0
  153. package/package.json +68 -0
  154. package/src/api/authApi.ts +332 -0
  155. package/src/api/types.ts +291 -0
  156. package/src/api/urls.ts +99 -0
  157. package/src/flows/anonymousFlow.ts +57 -0
  158. package/src/flows/authenticatorChangeFlow.ts +160 -0
  159. package/src/flows/createFlowMachine.ts +126 -0
  160. package/src/flows/errors.ts +29 -0
  161. package/src/flows/magicLinkFlow.ts +68 -0
  162. package/src/flows/oauthFlow.ts +114 -0
  163. package/src/flows/otpFlow.ts +156 -0
  164. package/src/flows/passkeyFlow.ts +191 -0
  165. package/src/flows/passwordChangeFlow.ts +114 -0
  166. package/src/flows/passwordLoginFlow.ts +122 -0
  167. package/src/flows/passwordResetFlow.ts +123 -0
  168. package/src/flows/qrLoginFlow.ts +158 -0
  169. package/src/flows/ssoFlow.ts +84 -0
  170. package/src/flows/totpSetupFlow.ts +96 -0
  171. package/src/flows/useFlow.ts +16 -0
  172. package/src/flows/verificationFlow.ts +341 -0
  173. package/src/headless/AuthProvider.tsx +30 -0
  174. package/src/headless/Passkey.tsx +97 -0
  175. package/src/headless/PasswordChange.tsx +46 -0
  176. package/src/headless/PasswordLogin.tsx +49 -0
  177. package/src/headless/PasswordReset.tsx +51 -0
  178. package/src/headless/PasswordlessLogin.tsx +60 -0
  179. package/src/headless/QrLogin.tsx +52 -0
  180. package/src/headless/TotpSetup.tsx +40 -0
  181. package/src/headless/VerificationChallenge.tsx +54 -0
  182. package/src/headless/misc.tsx +151 -0
  183. package/src/i18n/keys.ts +94 -0
  184. package/src/index.ts +229 -0
  185. package/src/model/context.tsx +51 -0
  186. package/src/model/mutations.ts +152 -0
  187. package/src/model/queries.ts +130 -0
  188. package/src/model/queryKeys.ts +32 -0
  189. package/src/model/runtime.ts +93 -0
  190. package/src/model/session.ts +188 -0
  191. package/tsconfig.json +26 -0
@@ -0,0 +1,52 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { QrType } from "../api/types.js";
4
+ import { createQrLoginFlow } from "../flows/qrLoginFlow.js";
5
+ import type { QrLoginState } from "../flows/qrLoginFlow.js";
6
+ import { useFlow } from "../flows/useFlow.js";
7
+ import { useAuthAnalytics, useAuthApi, useAuthSession } from "../model/context.js";
8
+
9
+ export interface QrLoginBag {
10
+ readonly state: QrLoginState;
11
+ start(
12
+ type: QrType,
13
+ redirectUrl: string,
14
+ allowUnauthenticatedScanner?: boolean
15
+ ): void;
16
+ dispose(): void;
17
+ }
18
+
19
+ /**
20
+ * Headless QR authentication with background polling (auth-sa.md §8). Render
21
+ * the `awaitingScan` state's `scanUrl` as a QR image. The poll loop starts on
22
+ * `start()` and is torn down on unmount. For `login_request`, delivered tokens
23
+ * are adopted into the session automatically.
24
+ */
25
+ export function QrLogin(props: {
26
+ children: (bag: QrLoginBag) => ReactNode;
27
+ pollIntervalMs?: number;
28
+ }): ReactNode {
29
+ const api = useAuthApi();
30
+ const analytics = useAuthAnalytics();
31
+ const session = useAuthSession();
32
+ const { pollIntervalMs } = props;
33
+ const flow = useMemo(
34
+ () =>
35
+ createQrLoginFlow({
36
+ api,
37
+ analytics,
38
+ onAuthenticated: (tokens) => session.setTokens(tokens),
39
+ ...(pollIntervalMs !== undefined ? { pollIntervalMs } : {}),
40
+ }),
41
+ [api, analytics, session, pollIntervalMs]
42
+ );
43
+ const state = useFlow(flow.machine);
44
+ useEffect(() => () => flow.dispose(), [flow]);
45
+ return props.children({
46
+ state,
47
+ start: (type, redirectUrl, allow) => {
48
+ void flow.start(type, redirectUrl, allow);
49
+ },
50
+ dispose: flow.dispose,
51
+ });
52
+ }
@@ -0,0 +1,40 @@
1
+ import { useMemo } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { createTotpSetupFlow } from "../flows/totpSetupFlow.js";
4
+ import type { TotpSetupState } from "../flows/totpSetupFlow.js";
5
+ import { useFlow } from "../flows/useFlow.js";
6
+ import { useAuthAnalytics, useAuthApi } from "../model/context.js";
7
+
8
+ export interface TotpSetupBag {
9
+ readonly state: TotpSetupState;
10
+ start(): void;
11
+ confirm(code: string): void;
12
+ reset(): void;
13
+ }
14
+
15
+ /**
16
+ * Headless TOTP enrollment (auth-sa.md §11). Render the `enrolling` state's
17
+ * `qrUri` as a QR image and `secret` for manual entry; on `done`, surface the
18
+ * one-time `backupCodes` with a copy/warn affordance (shown ONCE).
19
+ */
20
+ export function TotpSetup(props: {
21
+ children: (bag: TotpSetupBag) => ReactNode;
22
+ }): ReactNode {
23
+ const api = useAuthApi();
24
+ const analytics = useAuthAnalytics();
25
+ const flow = useMemo(
26
+ () => createTotpSetupFlow({ api, analytics }),
27
+ [api, analytics]
28
+ );
29
+ const state = useFlow(flow.machine);
30
+ return props.children({
31
+ state,
32
+ start: () => {
33
+ void flow.start();
34
+ },
35
+ confirm: (code) => {
36
+ void flow.confirm(code);
37
+ },
38
+ reset: flow.reset,
39
+ });
40
+ }
@@ -0,0 +1,54 @@
1
+ import type { ReactNode } from "react";
2
+ import type { VerificationFactorId } from "../api/types.js";
3
+ import { useFlow } from "../flows/useFlow.js";
4
+ import type { VerificationState } from "../flows/verificationFlow.js";
5
+ import { useVerification } from "../model/context.js";
6
+
7
+ export interface VerificationChallengeBag {
8
+ readonly state: VerificationState;
9
+ chooseFactor(factor: VerificationFactorId): void;
10
+ submitCode(proof: { code?: string; backup_code?: string }): void;
11
+ submitPasskey(credential: unknown): void;
12
+ cancel(): void;
13
+ }
14
+
15
+ /**
16
+ * THE FLAGSHIP headless component (frontend-standard §2). Mount it ONCE at the
17
+ * app root. It renders the app's step-up modal by driving the singleton
18
+ * {@link VerificationController} that `createAuthRuntime` wired into
19
+ * `client.onVerificationChallenge`. When any sensitive request 403s with a
20
+ * verification envelope, `state.step` leaves `"idle"` and this render prop
21
+ * fires — pick a factor, submit the proof, and core transparently retries the
22
+ * original request with `X-Verification-Token`.
23
+ *
24
+ * Renders nothing while idle, so it is safe to leave mounted:
25
+ * ```tsx
26
+ * <VerificationChallenge>
27
+ * {({ state, chooseFactor, submitCode, cancel }) => (
28
+ * <Modal open={state.step !== "idle"}>… </Modal>
29
+ * )}
30
+ * </VerificationChallenge>
31
+ * ```
32
+ */
33
+ export function VerificationChallenge(props: {
34
+ children: (bag: VerificationChallengeBag) => ReactNode;
35
+ /** Render even when idle (default false — returns null while idle). */
36
+ renderWhenIdle?: boolean;
37
+ }): ReactNode {
38
+ const controller = useVerification();
39
+ const state = useFlow(controller.machine);
40
+ if (state.step === "idle" && props.renderWhenIdle !== true) return null;
41
+ return props.children({
42
+ state,
43
+ chooseFactor: (factor) => {
44
+ void controller.chooseFactor(factor);
45
+ },
46
+ submitCode: (proof) => {
47
+ void controller.submitCode(proof);
48
+ },
49
+ submitPasskey: (credential) => {
50
+ void controller.submitPasskey(credential);
51
+ },
52
+ cancel: controller.cancel,
53
+ });
54
+ }
@@ -0,0 +1,151 @@
1
+ import { useMemo } from "react";
2
+ import type { ReactNode } from "react";
3
+ import type { OtpChannel } from "../api/types.js";
4
+ import { createAnonymousFlow } from "../flows/anonymousFlow.js";
5
+ import type { AnonymousState } from "../flows/anonymousFlow.js";
6
+ import { createAuthenticatorChangeFlow } from "../flows/authenticatorChangeFlow.js";
7
+ import type { AuthenticatorChangeState } from "../flows/authenticatorChangeFlow.js";
8
+ import { createMagicLinkFlow } from "../flows/magicLinkFlow.js";
9
+ import type { MagicLinkState } from "../flows/magicLinkFlow.js";
10
+ import { createSsoFlow } from "../flows/ssoFlow.js";
11
+ import type { SsoState } from "../flows/ssoFlow.js";
12
+ import { useFlow } from "../flows/useFlow.js";
13
+ import { useAuthAnalytics, useAuthApi, useAuthSession } from "../model/context.js";
14
+
15
+ // ── Magic link ──────────────────────────────────────────────────────────────
16
+
17
+ export interface MagicLinkBag {
18
+ readonly state: MagicLinkState;
19
+ request(email: string, redirectUrl?: string): void;
20
+ reset(): void;
21
+ }
22
+
23
+ /** Headless magic-link request (auth-sa.md §15). */
24
+ export function MagicLink(props: {
25
+ children: (bag: MagicLinkBag) => ReactNode;
26
+ }): ReactNode {
27
+ const api = useAuthApi();
28
+ const analytics = useAuthAnalytics();
29
+ const flow = useMemo(
30
+ () => createMagicLinkFlow({ api, analytics }),
31
+ [api, analytics]
32
+ );
33
+ const state = useFlow(flow.machine);
34
+ return props.children({
35
+ state,
36
+ request: (email, redirectUrl) => {
37
+ void flow.request(email, redirectUrl);
38
+ },
39
+ reset: flow.reset,
40
+ });
41
+ }
42
+
43
+ // ── Anonymous ────────────────────────────────────────────────────────────────
44
+
45
+ export interface AnonymousBag {
46
+ readonly state: AnonymousState;
47
+ create(deviceId?: string): void;
48
+ reset(): void;
49
+ }
50
+
51
+ /** Headless anonymous session (auth-sa.md §6). */
52
+ export function AnonymousSession(props: {
53
+ children: (bag: AnonymousBag) => ReactNode;
54
+ }): ReactNode {
55
+ const api = useAuthApi();
56
+ const analytics = useAuthAnalytics();
57
+ const session = useAuthSession();
58
+ const flow = useMemo(
59
+ () =>
60
+ createAnonymousFlow({
61
+ api,
62
+ analytics,
63
+ onAuthenticated: (r) => session.adopt(r),
64
+ }),
65
+ [api, analytics, session]
66
+ );
67
+ const state = useFlow(flow.machine);
68
+ return props.children({
69
+ state,
70
+ create: (deviceId) => {
71
+ void flow.create(deviceId);
72
+ },
73
+ reset: flow.reset,
74
+ });
75
+ }
76
+
77
+ // ── SSO discovery ────────────────────────────────────────────────────────────
78
+
79
+ export interface SsoDiscoveryBag {
80
+ readonly state: SsoState;
81
+ lookup(domain: string): void;
82
+ beginLogin(orgSlug: string): void;
83
+ reset(): void;
84
+ }
85
+
86
+ /** Headless SSO domain discovery + login redirect (auth-sa.md §18). */
87
+ export function SsoDiscovery(props: {
88
+ children: (bag: SsoDiscoveryBag) => ReactNode;
89
+ }): ReactNode {
90
+ const api = useAuthApi();
91
+ const analytics = useAuthAnalytics();
92
+ const flow = useMemo(
93
+ () => createSsoFlow({ api, analytics }),
94
+ [api, analytics]
95
+ );
96
+ const state = useFlow(flow.machine);
97
+ return props.children({
98
+ state,
99
+ lookup: (domain) => {
100
+ void flow.lookup(domain);
101
+ },
102
+ beginLogin: flow.beginLogin,
103
+ reset: flow.reset,
104
+ });
105
+ }
106
+
107
+ // ── Authenticator change (instant) ───────────────────────────────────────────
108
+
109
+ export interface AuthenticatorChangeBag {
110
+ readonly state: AuthenticatorChangeState;
111
+ startInstant(channel: OtpChannel): void;
112
+ submitOldCode(code: string): void;
113
+ requestNew(newValue: string): void;
114
+ submitNewCode(code: string): void;
115
+ reset(): void;
116
+ }
117
+
118
+ /** Headless instant email/phone change (auth-sa.md §9). */
119
+ export function AuthenticatorChange(props: {
120
+ children: (bag: AuthenticatorChangeBag) => ReactNode;
121
+ }): ReactNode {
122
+ const api = useAuthApi();
123
+ const analytics = useAuthAnalytics();
124
+ const session = useAuthSession();
125
+ const flow = useMemo(
126
+ () =>
127
+ createAuthenticatorChangeFlow({
128
+ api,
129
+ analytics,
130
+ onAuthenticated: (r) => session.adopt(r),
131
+ }),
132
+ [api, analytics, session]
133
+ );
134
+ const state = useFlow(flow.machine);
135
+ return props.children({
136
+ state,
137
+ startInstant: (channel) => {
138
+ void flow.startInstant(channel);
139
+ },
140
+ submitOldCode: (code) => {
141
+ void flow.submitOldCode(code);
142
+ },
143
+ requestNew: (newValue) => {
144
+ void flow.requestNew(newValue);
145
+ },
146
+ submitNewCode: (code) => {
147
+ void flow.submitNewCode(code);
148
+ },
149
+ reset: flow.reset,
150
+ });
151
+ }
@@ -0,0 +1,94 @@
1
+ import type { I18nDictionary, I18nEngine } from "@stapel/core";
2
+
3
+ /**
4
+ * auth-react's own translation KEYS (frontend-standard §4.2): headless
5
+ * components never render literal strings — hosts resolve these via core's
6
+ * i18n engine (`useT`). Backend error codes (auth-sa.md "Error reference")
7
+ * flow through the SAME contour: a `StapelApiError.code` like
8
+ * `error.400.code_expired` is already a key, so the default bundle below ships
9
+ * English fallbacks for both the backend error codes and auth-react's UI keys.
10
+ * Point core's `loadLocale` at stapel-translate to override per locale.
11
+ */
12
+ export const AUTH_I18N_KEYS = {
13
+ // Flow UI keys (auth-react-owned)
14
+ otpEnterCode: "auth.otp.enter_code",
15
+ otpResend: "auth.otp.resend",
16
+ otpSentTo: "auth.otp.sent_to",
17
+ passwordLabel: "auth.password.label",
18
+ totpEnterCode: "auth.totp.enter_code",
19
+ totpUseBackup: "auth.totp.use_backup",
20
+ verificationChoose: "auth.verification.choose_factor",
21
+ verificationSuccess: "auth.verification.success",
22
+ sessionThisDevice: "auth.session.this_device",
23
+ sessionSuspicious: "auth.session.suspicious",
24
+ passkeyNoCredentials: "auth.passkey.no_credentials",
25
+ unknownError: "auth.error.unknown",
26
+ } as const;
27
+
28
+ export type AuthI18nKey =
29
+ (typeof AUTH_I18N_KEYS)[keyof typeof AUTH_I18N_KEYS];
30
+
31
+ /** English fallback bundle for auth-react UI keys + backend auth error codes. */
32
+ export const authI18nBundleEn: I18nDictionary = {
33
+ // auth-react UI
34
+ "auth.otp.enter_code": "Enter the code we sent you",
35
+ "auth.otp.resend": "Resend code",
36
+ "auth.otp.sent_to": "Code sent to {target}",
37
+ "auth.password.label": "Password",
38
+ "auth.totp.enter_code": "Enter your 6-digit code",
39
+ "auth.totp.use_backup": "Use a backup code",
40
+ "auth.verification.choose_factor": "Verify it's you",
41
+ "auth.verification.success": "Verified",
42
+ "auth.session.this_device": "This device",
43
+ "auth.session.suspicious": "Unrecognized sign-in",
44
+ "auth.passkey.no_credentials":
45
+ "Couldn't sign in with a passkey on this device. Add one in Security settings after signing in another way, or pick a different sign-in method below.",
46
+ "auth.error.unknown": "Something went wrong. Please try again.",
47
+
48
+ // Backend error codes (auth-sa.md "Error reference")
49
+ "error.401.invalid_credentials": "Incorrect email or password.",
50
+ "error.401.account_disabled": "This account has been disabled.",
51
+ "error.401.refresh_revoked": "Your session ended. Please sign in again.",
52
+ "error.400.code_expired": "That code has expired. Request a new one.",
53
+ "error.400.invalid_code": "That code is incorrect.",
54
+ "error.400.invalid_code_attempts":
55
+ "That code is incorrect. {attempts_remaining} attempts left.",
56
+ "error.422.blocked": "Too many attempts. Try again in {retry_after_minutes} min.",
57
+ "error.429.rate_limit": "Please wait before requesting another code.",
58
+ "error.400.no_verified_contact": "No verified contact for this method.",
59
+ "error.400.wrong_password": "Your current password is incorrect.",
60
+ "error.400.no_password": "This account has no password set.",
61
+ "error.404.user_for_reset": "No account found for that email or phone.",
62
+ "error.403.mock_otp_admin": "Admin accounts can't use codes in this environment.",
63
+ "error.400.code_required": "A code is required.",
64
+ "error.400.totp_not_pending": "Start two-factor setup first.",
65
+ "error.423.account_locked":
66
+ "Account locked. Try again in {retry_after_minutes} min.",
67
+ "error.429.magic_link_rate": "Too many login-link requests. Try again later.",
68
+ "error.400.passkey_invalid": "Passkey verification failed.",
69
+ "error.400.passkey_challenge_expired": "Passkey request expired. Try again.",
70
+ "error.409.passkey_already_registered": "This passkey is already registered.",
71
+ "error.400.last_auth_method": "You can't remove your last sign-in method.",
72
+ "error.400.invalid_redirect_url": "Invalid redirect.",
73
+ "error.400.magic_link_invalid": "This login link is invalid or has expired.",
74
+ "error.400.captcha_required": "Please complete the captcha.",
75
+ "error.400.captcha_invalid": "Captcha verification failed.",
76
+ "error.403.verification_required": "Additional verification required.",
77
+ "error.404.verification_challenge_not_found":
78
+ "This verification expired. Please retry the action.",
79
+ "error.400.verification_invalid_factor": "That verification option isn't available.",
80
+ "error.400.verification_failed": "Verification failed. Try again.",
81
+ "error.423.verification_locked": "Too many attempts. Retry the action later.",
82
+ "error.404.sso_org_not_found": "Organization not found.",
83
+ "error.400.sso_not_configured": "SSO isn't configured for this organization.",
84
+ "error.403.sso_required": "This account must sign in with SSO.",
85
+ };
86
+
87
+ /**
88
+ * Register auth-react's key bundle into a core i18n engine (call once at
89
+ * startup). Registers under the given locale (default `"en"`); a later
90
+ * `loadLocale` from stapel-translate can layer localized overrides.
91
+ */
92
+ export function registerAuthI18n(engine: I18nEngine, locale = "en"): void {
93
+ engine.registerBundle(locale, authI18nBundleEn);
94
+ }
package/src/index.ts ADDED
@@ -0,0 +1,229 @@
1
+ /**
2
+ * `@stapel/auth-react` — the headless React flow pair for stapel-auth
3
+ * (frontend-standard §2). Business + state only, zero visual opinion. Built on
4
+ * `@stapel/core`'s StapelClient (verification-403 interception, token refresh,
5
+ * i18n, analytics, query layer).
6
+ */
7
+
8
+ // ── api ──────────────────────────────────────────────────────────────────────
9
+ export { createAuthApi } from "./api/authApi.js";
10
+ export type { AuthApi } from "./api/authApi.js";
11
+ export {
12
+ authUrls,
13
+ validRedirectUrl,
14
+ safeNextPath,
15
+ safeScanRedirect,
16
+ } from "./api/urls.js";
17
+ export type { AuthUrls } from "./api/urls.js";
18
+ export { isTotpChallenge } from "./api/types.js";
19
+ export type {
20
+ AuthStatus,
21
+ StapelUser,
22
+ AuthTokens,
23
+ AuthResponse,
24
+ TOTPChallengeResponse,
25
+ LoginResponse,
26
+ OtpRequestResponse,
27
+ StatusResponse,
28
+ OtpChannel,
29
+ OAuthProviderInfo,
30
+ RegistrationCapabilities,
31
+ LoginCapabilities,
32
+ Capabilities,
33
+ PasswordChangeMethod,
34
+ PasswordMethodEntry,
35
+ PasswordMethods,
36
+ SecurityStatus,
37
+ SessionDeviceType,
38
+ AuthSession as AuthSessionRecord,
39
+ TotpSetupResponse,
40
+ TotpSetupConfirmResponse,
41
+ TotpDisableRequest,
42
+ VerificationFactorId,
43
+ VerificationEnvelope,
44
+ VerificationInitiateResponse,
45
+ VerificationCompleteResponse,
46
+ QrType,
47
+ QrGenerateResponse,
48
+ QrStatusValue,
49
+ QrStatusResponse,
50
+ Passkey,
51
+ PasskeyRegisterBeginResponse,
52
+ PasskeyAuthenticateBeginResponse,
53
+ ChangeOldVerifiedResponse,
54
+ DelayedChangeInitiatedResponse,
55
+ DelayedChangeStatus,
56
+ SsoLookupResponse,
57
+ AuditEvent,
58
+ AuditPage,
59
+ RefreshResponse,
60
+ } from "./api/types.js";
61
+
62
+ // ── flows (the reusable machine pattern — first instance) ────────────────────
63
+ export { createFlowMachine } from "./flows/createFlowMachine.js";
64
+ export type {
65
+ FlowMachine,
66
+ FlowMachineOptions,
67
+ FlowStateBase,
68
+ } from "./flows/createFlowMachine.js";
69
+ export { useFlow } from "./flows/useFlow.js";
70
+ export { toFlowError, isErrorCode } from "./flows/errors.js";
71
+ export type { FlowError } from "./flows/errors.js";
72
+
73
+ export { createOtpFlow } from "./flows/otpFlow.js";
74
+ export type { OtpFlow, OtpFlowDeps, OtpState } from "./flows/otpFlow.js";
75
+ export { createPasswordLoginFlow } from "./flows/passwordLoginFlow.js";
76
+ export type {
77
+ PasswordLoginFlow,
78
+ PasswordLoginFlowDeps,
79
+ PasswordLoginState,
80
+ TotpProof,
81
+ } from "./flows/passwordLoginFlow.js";
82
+ export { createPasswordChangeFlow } from "./flows/passwordChangeFlow.js";
83
+ export type {
84
+ PasswordChangeFlow,
85
+ PasswordChangeFlowDeps,
86
+ PasswordChangeState,
87
+ } from "./flows/passwordChangeFlow.js";
88
+ export { createPasswordResetFlow } from "./flows/passwordResetFlow.js";
89
+ export type {
90
+ PasswordResetFlow,
91
+ PasswordResetFlowDeps,
92
+ PasswordResetState,
93
+ } from "./flows/passwordResetFlow.js";
94
+ export { createVerificationController } from "./flows/verificationFlow.js";
95
+ export type {
96
+ VerificationController,
97
+ VerificationControllerDeps,
98
+ VerificationState,
99
+ } from "./flows/verificationFlow.js";
100
+ export { createTotpSetupFlow } from "./flows/totpSetupFlow.js";
101
+ export type {
102
+ TotpSetupFlow,
103
+ TotpSetupFlowDeps,
104
+ TotpSetupState,
105
+ } from "./flows/totpSetupFlow.js";
106
+ export { createOAuthFlow } from "./flows/oauthFlow.js";
107
+ export type { OAuthFlow, OAuthFlowDeps, OAuthState } from "./flows/oauthFlow.js";
108
+ export { createQrLoginFlow } from "./flows/qrLoginFlow.js";
109
+ export type {
110
+ QrLoginFlow,
111
+ QrLoginFlowDeps,
112
+ QrLoginState,
113
+ } from "./flows/qrLoginFlow.js";
114
+ export {
115
+ createPasskeyRegistrationFlow,
116
+ createPasskeyLoginFlow,
117
+ } from "./flows/passkeyFlow.js";
118
+ export type {
119
+ PasskeyRegistrationFlow,
120
+ PasskeyRegistrationFlowDeps,
121
+ PasskeyRegisterState,
122
+ PasskeyLoginFlow,
123
+ PasskeyLoginFlowDeps,
124
+ PasskeyLoginState,
125
+ } from "./flows/passkeyFlow.js";
126
+ export { createMagicLinkFlow } from "./flows/magicLinkFlow.js";
127
+ export type {
128
+ MagicLinkFlow,
129
+ MagicLinkFlowDeps,
130
+ MagicLinkState,
131
+ } from "./flows/magicLinkFlow.js";
132
+ export { createAnonymousFlow } from "./flows/anonymousFlow.js";
133
+ export type {
134
+ AnonymousFlow,
135
+ AnonymousFlowDeps,
136
+ AnonymousState,
137
+ } from "./flows/anonymousFlow.js";
138
+ export { createSsoFlow } from "./flows/ssoFlow.js";
139
+ export type { SsoFlow, SsoFlowDeps, SsoState } from "./flows/ssoFlow.js";
140
+ export { createAuthenticatorChangeFlow } from "./flows/authenticatorChangeFlow.js";
141
+ export type {
142
+ AuthenticatorChangeFlow,
143
+ AuthenticatorChangeFlowDeps,
144
+ AuthenticatorChangeState,
145
+ } from "./flows/authenticatorChangeFlow.js";
146
+
147
+ // ── model (runtime wiring, session, query hooks) ─────────────────────────────
148
+ export { createAuthRuntime } from "./model/runtime.js";
149
+ export type { AuthRuntime, CreateAuthRuntimeOptions } from "./model/runtime.js";
150
+ export { createAuthSession } from "./model/session.js";
151
+ export type {
152
+ AuthSession,
153
+ AuthSessionState,
154
+ AuthSessionOptions,
155
+ TeardownReason,
156
+ } from "./model/session.js";
157
+ export {
158
+ AuthRuntimeContext,
159
+ useAuthRuntime,
160
+ useAuthApi,
161
+ useAuthSession,
162
+ useVerification,
163
+ useAuthAnalytics,
164
+ useAuthSessionState,
165
+ } from "./model/context.js";
166
+ export { authQueryKeys } from "./model/queryKeys.js";
167
+ export {
168
+ useCapabilities,
169
+ useMe,
170
+ useSecurityStatus,
171
+ usePasswordMethods,
172
+ useSessions,
173
+ usePasskeys,
174
+ useAuditLog,
175
+ useDelayedChangeStatus,
176
+ useSsoLookup,
177
+ } from "./model/queries.js";
178
+ export {
179
+ useLogout,
180
+ useRevokeSession,
181
+ useRevokeOtherSessions,
182
+ useConfirmSession,
183
+ useRemovePasskey,
184
+ useDisableTotp,
185
+ useCancelDelayedChange,
186
+ } from "./model/mutations.js";
187
+
188
+ // ── headless (renderless components) ─────────────────────────────────────────
189
+ export { AuthProvider } from "./headless/AuthProvider.js";
190
+ export { PasswordlessLogin } from "./headless/PasswordlessLogin.js";
191
+ export type { PasswordlessLoginBag } from "./headless/PasswordlessLogin.js";
192
+ export { PasswordLogin } from "./headless/PasswordLogin.js";
193
+ export type { PasswordLoginBag } from "./headless/PasswordLogin.js";
194
+ export { PasswordReset } from "./headless/PasswordReset.js";
195
+ export type { PasswordResetBag } from "./headless/PasswordReset.js";
196
+ export { PasswordChange } from "./headless/PasswordChange.js";
197
+ export type { PasswordChangeBag } from "./headless/PasswordChange.js";
198
+ export { VerificationChallenge } from "./headless/VerificationChallenge.js";
199
+ export type { VerificationChallengeBag } from "./headless/VerificationChallenge.js";
200
+ export { TotpSetup } from "./headless/TotpSetup.js";
201
+ export type { TotpSetupBag } from "./headless/TotpSetup.js";
202
+ export { QrLogin } from "./headless/QrLogin.js";
203
+ export type { QrLoginBag } from "./headless/QrLogin.js";
204
+ export { PasskeyRegistration, PasskeyLogin } from "./headless/Passkey.js";
205
+ export type {
206
+ WebauthnBinding,
207
+ PasskeyRegistrationBag,
208
+ PasskeyLoginBag,
209
+ } from "./headless/Passkey.js";
210
+ export {
211
+ MagicLink,
212
+ AnonymousSession,
213
+ SsoDiscovery,
214
+ AuthenticatorChange,
215
+ } from "./headless/misc.js";
216
+ export type {
217
+ MagicLinkBag,
218
+ AnonymousBag,
219
+ SsoDiscoveryBag,
220
+ AuthenticatorChangeBag,
221
+ } from "./headless/misc.js";
222
+
223
+ // ── i18n ─────────────────────────────────────────────────────────────────────
224
+ export {
225
+ AUTH_I18N_KEYS,
226
+ authI18nBundleEn,
227
+ registerAuthI18n,
228
+ } from "./i18n/keys.js";
229
+ export type { AuthI18nKey } from "./i18n/keys.js";
@@ -0,0 +1,51 @@
1
+ import { createContext, useContext, useSyncExternalStore } from "react";
2
+ import type { Context } from "react";
3
+ import type { Analytics } from "@stapel/core";
4
+ import type { AuthApi } from "../api/authApi.js";
5
+ import type { VerificationController } from "../flows/verificationFlow.js";
6
+ import type { AuthRuntime } from "./runtime.js";
7
+ import type { AuthSession, AuthSessionState } from "./session.js";
8
+
9
+ /**
10
+ * The auth runtime shared through React context by `<AuthProvider>`. Hooks in
11
+ * `model/` and `headless/` read the wired singletons from here; per-flow
12
+ * machines are created by the headless components using `useAuthApi()` +
13
+ * `useAuthAnalytics()`.
14
+ */
15
+ export const AuthRuntimeContext: Context<AuthRuntime | null> =
16
+ createContext<AuthRuntime | null>(null);
17
+
18
+ export function useAuthRuntime(): AuthRuntime {
19
+ const runtime = useContext(AuthRuntimeContext);
20
+ if (runtime === null) {
21
+ throw new Error("Auth hooks must be used within an <AuthProvider>");
22
+ }
23
+ return runtime;
24
+ }
25
+
26
+ export function useAuthApi(): AuthApi {
27
+ return useAuthRuntime().api;
28
+ }
29
+
30
+ export function useAuthSession(): AuthSession {
31
+ return useAuthRuntime().session;
32
+ }
33
+
34
+ /** The singleton step-up verification controller (one modal per app). */
35
+ export function useVerification(): VerificationController {
36
+ return useAuthRuntime().verification;
37
+ }
38
+
39
+ export function useAuthAnalytics(): Analytics | null {
40
+ return useAuthRuntime().analytics;
41
+ }
42
+
43
+ /** Reactive session state (user / tokens / status). Re-renders on change. */
44
+ export function useAuthSessionState(): AuthSessionState {
45
+ const session = useAuthSession();
46
+ return useSyncExternalStore(
47
+ session.subscribe,
48
+ session.getState,
49
+ session.getState
50
+ );
51
+ }