@rakomi/react-native 0.2.0 → 0.3.1

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/COMPLIANCE.md CHANGED
@@ -10,7 +10,7 @@ This package is a "product with digital elements" under CRA Art. 3(1). Same post
10
10
  |---|---|
11
11
  | Art. 13 — Vulnerability handling | Documented secure vulnerability-handling process; vulnerability records retained with the technical documentation. Internal fix targets (e.g. 14 days for High severity) are an operational SLA, **not** a statutory deadline. See `SECURITY.md`. |
12
12
  | Art. 14 — Reporting of actively exploited vulnerabilities & severe incidents | Statutory reporting to the designated authority on the CRA timeline (early warning → notification → final report) and informing impacted users; published advisories via GitHub Security Advisories. See `SECURITY.md`. |
13
- | Annex I — Essential cybersecurity requirements | Secure defaults: fixed asymmetric token-signature verification, PKCE with S256 challenge (RFC 7636), biometric opt-in, ephemeral session, minimal attack surface. **Passkeys:** no passkey library is bundled or depended on by **this package** — the OS ceremony is reached through a module the host supplies. Read that precisely: it removes third-party ceremony code from *our* dependency tree and therefore from *our* SBOM; the module you wire **is** on the credential path and is **your** supply-chain surface, so account for it in yours. The bridge itself carries no bearer token, emits nothing to telemetry or the console, and an unwired adapter fails closed (`PASSKEY_NOT_SUPPORTED`) rather than falling back to a different ceremony. Proving test: `test/passkey-native-adapter.test.ts` (`@asserts CTRL-CRA-ANNEXI-SECURE-DEFAULT`). |
13
+ | Annex I — Essential cybersecurity requirements | Secure defaults: fixed asymmetric token-signature verification, PKCE with S256 challenge (RFC 7636), biometric opt-in, ephemeral session, minimal attack surface. **Passkeys:** no passkey library is bundled or depended on by **this package** — the OS ceremony is reached through a module the host supplies. Read that precisely: it removes third-party ceremony code from *our* dependency tree and therefore from *our* SBOM; the module you wire **is** on the credential path and is **your** supply-chain surface, so account for it in yours. The bridge itself carries no bearer token, emits nothing to telemetry or the console, and an unwired adapter fails closed (`PASSKEY_NOT_SUPPORTED`) rather than falling back to a different ceremony. |
14
14
 
15
15
  ## GDPR — Reg. (EU) 2016/679
16
16
 
@@ -18,8 +18,8 @@ This package is a "product with digital elements" under CRA Art. 3(1). Same post
18
18
  |---|---|
19
19
  | Art. 25 — Data protection by design | No PII collected client-side beyond what API requires; analytics opt-in via `onEvent` only. **Passkeys — sign-in:** identified sign-in takes an opaque, server-issued user handle, never an email or a username. **Passkeys — registration (state this plainly):** the registration options the server issues carry the account's `user.name` / `user.displayName`, which today is the user's **email address**. It crosses the bridge to the host-supplied module and is passed to the platform credential provider, which stores it with the credential and may **sync it to the user's platform account** (iCloud Keychain / Google Password Manager). This is inherent to WebAuthn — the value is what the user is shown when they pick a passkey — not a choice this SDK makes, and it is disclosed here rather than glossed. The SDK adds no identifier of its own. **Errors:** the credential-envelope validator names the failing FIELD, never its VALUE; error messages relayed from the server or from the host's own callbacks are passed through verbatim and are the tenant's surface to control. |
20
20
  | Art. 28 — Processor obligations | SDK is a data-processor adjunct; tenant is data controller. DPA covers the relationship. |
21
- | Art. 32 — Security of processing | Pseudonymisation + encryption at rest (Keychain) + access control (biometric opt-in). **Passkeys — key material:** the private key is created and held by the platform credential provider; it never enters SDK memory and the SDK holds no passkey material at rest. Proving test: `test/use-passkeys.test.tsx` (`@asserts CTRL-GDPR-32-NO-SECRET-TO-ADAPTER` — no access token, step-up token or bearer secret reaches the ceremony adapter). |
22
- | Art. 32 — Security of processing (session integrity) | A completed ceremony whose session was **not** actually stored is reported as a failure, never as a success — the SDK re-reads the token runtime and matches the session identity before it reports a sign-in. Proving test: `test/use-passkeys.test.tsx` (the session-confirmation polarities). |
21
+ | Art. 32 — Security of processing | Pseudonymisation + encryption at rest (Keychain) + access control (biometric opt-in). **Passkeys — key material:** the private key is created and held by the platform credential provider; it never enters SDK memory and the SDK holds no passkey material at rest. No access token, step-up token or bearer secret reaches the ceremony adapter. |
22
+ | Art. 32 — Security of processing (session integrity) | A completed ceremony whose session was **not** actually stored is reported as a failure, never as a success — the SDK re-reads the token runtime and matches the session identity before it reports a sign-in. |
23
23
  | Art. 32 — Security of processing (re-authentication gate) | Management actions (list / rename / delete) are re-authentication-gated. **The gate is enforced by the API, not by this SDK** — the SDK transmits the step-up token it is given and does not itself decide whether one is sufficient. Stated here because an SDK compliance doc must not claim credit for a server-side control. |
24
24
  | Art. 9 — Special categories (negative scope) | **No biometric data is processed by this SDK.** A fingerprint or face is consumed by the operating system to unlock the authenticator; the SDK receives only the resulting assertion. It never reads, stores, transmits or infers a biometric template, so Art. 9's special-category regime is not engaged by anything this library does. (Whether YOUR app processes biometrics for other purposes is your assessment.) |
25
25
 
package/README.md CHANGED
@@ -2,9 +2,27 @@
2
2
 
3
3
  React Native / Expo SDK for [Rakomi](https://rakomi.com) — EU-native auth-as-a-service.
4
4
 
5
- > **Status:** `0.1.0` — initial. API surface frozen for parity with `@rakomi/react`.
6
- > Token-manager runtime, JWKS verification, social-provider deep-link auto-handler,
7
- > bare-RN adapter example, and the demo app land in subsequent 0.x patches.
5
+ > **Status: PREVIEW.** `@rakomi/react-native` is a pre-1.0 package — no stability or support
6
+ > guarantee (SemVer 2.0.0 §4). The exported hook and component **surface** is frozen for parity with
7
+ > [`@rakomi/react`](https://www.npmjs.com/package/@rakomi/react), and every name below is exported
8
+ > and type-checked, but not every export's **behavior** is wired to the network yet.
9
+ >
10
+ > **Works today:** social sign-in via the system browser (`<SignIn providers={[...]} />` /
11
+ > `startSocialSignIn()`), magic link, email OTP, registration, MFA (TOTP), passkeys
12
+ > (sign-in/step-up/register/list/rename/delete), session management (`useAuth`, `useSession`,
13
+ > `getToken`), `<UserButton>`, `<SignedIn>`/`<SignedOut>`, `<Protect>`, `useOrganization` /
14
+ > `useOrganizationList` (JWT-claim reads), `useAuthConfig`, `useBranding`, `useBaasPlans`,
15
+ > `useBaasSubscription`.
16
+ >
17
+ > **Not yet wired — calls resolve/return an error placeholder, no network request is made:**
18
+ > `useAuth().signIn()` (direct email+password) and `useAuth().switchOrganization()`;
19
+ > `useLinkedAccounts().link()`/`.unlink()`; `useAnonymousSignin().signInAnonymously()`. `<SignIn />`
20
+ > renders social providers and MFA TOTP entry only — there is no password field in the pre-built
21
+ > component. `<UserProfile>` and `<Feature>` are marked preview individually below. Each of these is
22
+ > also called out at its own symbol in the [full reference docs](https://docs.rakomi.dev/sdk/react-native/authentication/#what-works-today).
23
+ >
24
+ > Token-manager runtime, JWKS verification, social-provider deep-link auto-handler, a bare-RN adapter
25
+ > example, and a demo app land in subsequent 0.x releases.
8
26
 
9
27
  ## Install
10
28
 
@@ -36,11 +54,11 @@ export default function App() {
36
54
  }
37
55
  ```
38
56
 
39
- ## What's included in 0.1.0
57
+ ## What's included
40
58
 
41
59
  - **`<RakomiProvider>`** — context provider. Freezes the native adapter on mount. AppState debounce 300ms. Connectivity transitions wired.
42
- - **Hooks (parity-locked):** `useAuth`, `useUser`, `useSession`, `useFlag`, `useOrganization`, `useOrganizationList`, `useLinkedAccounts`, `useTranslation`, `useAuthConfig`, `useBranding`, `useAnonymousSignin`, `useBaasPlans`, `useBaasSubscription`. Type-level parity with `@rakomi/react` is enforced by CI.
43
- - **Components:** `<SignIn />` (password + social + MFA TOTP), `<SignUp />`, `<UserButton />`, `<UserProfile />` (preview), `<SignedIn>`, `<SignedOut>`, `<Protect>`, `<Feature>`. RN primitives only — no HTML, no WebView.
60
+ - **Hooks (parity-locked):** `useAuth`, `useUser`, `useSession`, `useFlag`, `useOrganization`, `useOrganizationList`, `useLinkedAccounts` (preview), `useTranslation`, `useAuthConfig`, `useBranding`, `useAnonymousSignin` (preview), `useBaasPlans`, `useBaasSubscription`. Type-level parity with `@rakomi/react` is enforced by CI — see the status banner above for which hooks make real network calls today.
61
+ - **Components:** `<SignIn />` (social + MFA TOTP — no password field), `<SignUp />`, `<UserButton />`, `<UserProfile />` (preview), `<SignedIn>`, `<SignedOut>`, `<Protect>`, `<Feature>` (preview — reads the JWT directly, no live evaluation). RN primitives only — no HTML, no WebView.
44
62
  - **OAuth (RFC 8252):** PKCE S256, 32-byte state with single-use 60s TTL + constant-time comparison, system browser via `expo-web-browser` (`preferEphemeralSession: true` default), confused-deputy guard on callback ingest.
45
63
  - **Native adapter contract:** `NativeAuthAdapter` interface with forward-compat slots (`verifiers` for EUDI, `dpopProver` for DPoP, `par` for RFC 9126 PAR).
46
64
  - **`tokenCache` injection sugar** — replace storage without re-implementing the full adapter.
@@ -1,6 +1,6 @@
1
1
  THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
2
2
 
3
- For the npm package: @rakomi/react-native@0.2.0
3
+ For the npm package: @rakomi/react-native@0.3.1
4
4
 
5
5
  This package's published bundle (`dist/`) INLINES the third-party components listed below.
6
6
  Their copyright and permission notices are reproduced verbatim, as required by their licenses
package/dist/index.cjs CHANGED
@@ -1013,6 +1013,21 @@ function RakomiProvider(props) {
1013
1013
  return Object.freeze(built);
1014
1014
  }, [props.nativeAdapter, props.tokenCache]);
1015
1015
  const http = react.useMemo(() => createRnHttpClient({ baseUrl: props.baseUrl }), [props.baseUrl]);
1016
+ const authorizationEndpointCache = react.useMemo(
1017
+ () => sdkCore.createAuthorizationEndpointCache({
1018
+ fetchDiscoveryDocument: async () => {
1019
+ const response = await http.fetch("/.well-known/openid-configuration", {
1020
+ method: "GET",
1021
+ headers: { Accept: "application/json" }
1022
+ });
1023
+ if (!response.ok) {
1024
+ throw new Error(`discovery fetch failed: HTTP ${response.status}`);
1025
+ }
1026
+ return response.json();
1027
+ }
1028
+ }),
1029
+ [http]
1030
+ );
1016
1031
  const events = react.useMemo(() => new sdkCore.EventLog(props.onEvent), [props.onEvent]);
1017
1032
  const [snapshot, dispatch] = react.useReducer(sdkCore.reduce, sdkCore.INITIAL_SNAPSHOT);
1018
1033
  const passkeyCeremonyLock = react.useRef(createCeremonyLock()).current;
@@ -1148,6 +1163,7 @@ function RakomiProvider(props) {
1148
1163
  redirectUri: props.redirectUri,
1149
1164
  adapter,
1150
1165
  http,
1166
+ authorizationEndpointCache,
1151
1167
  snapshot,
1152
1168
  state: snapshot.state,
1153
1169
  isSignedIn: sdkCore.isSignedIn(snapshot),
@@ -1163,7 +1179,7 @@ function RakomiProvider(props) {
1163
1179
  beginAuthFlow,
1164
1180
  dpopSession
1165
1181
  }),
1166
- [adapter, http, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession, passkeyCeremonyLock]
1182
+ [adapter, http, authorizationEndpointCache, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession, passkeyCeremonyLock]
1167
1183
  );
1168
1184
  return /* @__PURE__ */ jsxRuntime.jsx(RakomiContext.Provider, { value, children: props.children });
1169
1185
  }
@@ -1620,14 +1636,24 @@ function SignIn(props) {
1620
1636
  const [step, setStep] = react.useState({ kind: "methods" });
1621
1637
  const [code, setCode] = react.useState("");
1622
1638
  const [busy, setBusy] = react.useState(false);
1623
- const tokenEndpoint = props.tokenEndpoint ?? "/v1/auth/oauth/callback";
1624
- const authorizationEndpoint = props.authorizationEndpoint ?? "/oauth/authorize";
1625
- const totpVerifyEndpoint = props.totpVerifyEndpoint ?? "/v1/auth/mfa/totp/verify";
1639
+ const tokenEndpoint = props.tokenEndpoint ?? "/oauth/token";
1640
+ const totpVerifyEndpoint = props.totpVerifyEndpoint ?? "/v1/auth/mfa/verify-login";
1626
1641
  const providers = props.providers ?? ["google"];
1627
1642
  async function onProviderPress(provider) {
1628
1643
  if (busy) return;
1629
1644
  setBusy(true);
1630
1645
  ctx.events.push({ type: "sign_in_attempted", severity: "info", metadata: { provider } });
1646
+ let authorizationEndpoint = props.authorizationEndpoint;
1647
+ if (!authorizationEndpoint) {
1648
+ const resolved = await ctx.authorizationEndpointCache.resolve(ctx.baseUrl);
1649
+ if (!resolved.ok) {
1650
+ ctx.events.push({ type: "sign_in_failed", severity: "warning", error: resolved.error });
1651
+ setStep({ kind: "error", error: resolved.error });
1652
+ setBusy(false);
1653
+ return;
1654
+ }
1655
+ authorizationEndpoint = resolved.authorizationEndpoint;
1656
+ }
1631
1657
  const outcome = await startSocialSignIn({
1632
1658
  adapter: ctx.adapter,
1633
1659
  http: ctx.http,
@@ -1833,7 +1859,7 @@ function UserButton(props) {
1833
1859
  function UserProfile(props) {
1834
1860
  react.useEffect(() => {
1835
1861
  if (typeof __DEV__ !== "undefined" && __DEV__) {
1836
- console.warn("[@rakomi/react-native] <UserProfile /> is in preview until 0.2.0 \u2014 feedback welcome.");
1862
+ console.warn("[@rakomi/react-native] <UserProfile /> is in preview \u2014 feedback welcome.");
1837
1863
  }
1838
1864
  }, []);
1839
1865
  const ctx = useRakomiContext();
@@ -2660,7 +2686,7 @@ function useMfa() {
2660
2686
  const ctx = useRakomiContext();
2661
2687
  return {
2662
2688
  verifyTotp: async (challengeToken, code, endpoint) => {
2663
- const url = endpoint ?? `${ctx.baseUrl}/v1/auth/mfa/totp/verify`;
2689
+ const url = endpoint ?? `${ctx.baseUrl}/v1/auth/mfa/verify-login`;
2664
2690
  const result = await sdkCore.verifyTotp({ http: ctx.http, endpoint: url, challengeToken, code });
2665
2691
  if (result.ok) {
2666
2692
  const nonce = await ctx.beginAuthFlow();
@@ -2691,6 +2717,10 @@ Object.defineProperty(exports, "base64url", {
2691
2717
  enumerable: true,
2692
2718
  get: function () { return sdkCore.base64url; }
2693
2719
  });
2720
+ Object.defineProperty(exports, "createAuthorizationEndpointCache", {
2721
+ enumerable: true,
2722
+ get: function () { return sdkCore.createAuthorizationEndpointCache; }
2723
+ });
2694
2724
  Object.defineProperty(exports, "createTranslator", {
2695
2725
  enumerable: true,
2696
2726
  get: function () { return sdkCore.createTranslator; }
@@ -2707,6 +2737,10 @@ Object.defineProperty(exports, "decodeUser", {
2707
2737
  enumerable: true,
2708
2738
  get: function () { return sdkCore.decodeUser; }
2709
2739
  });
2740
+ Object.defineProperty(exports, "deriveAuthorizationEndpointFallback", {
2741
+ enumerable: true,
2742
+ get: function () { return sdkCore.deriveAuthorizationEndpointFallback; }
2743
+ });
2710
2744
  Object.defineProperty(exports, "generatePkce", {
2711
2745
  enumerable: true,
2712
2746
  get: function () { return sdkCore.generatePkce; }
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { AuthEvent, Locale, Translations, AuthMachineState, SignInOptions, SignInResult, TokenResult, AuthError, UserResource, HasParams, SwitchOrgResult, SessionResource, AuthConfig, BrandingConfig, OrgMembershipClaim, TranslationFn, OAuthTokenResponse, HttpClient, OpaqueUserHandle, PasskeyCeremonyAdapter, PasskeySummary, PasskeyError, AssertPasskeyResult, StepUpWithPasskeyResult, RegisterPasskeyResult, ListPasskeysResult, PasskeySummaryResult, DeletePasskeyResult, RequestResult, VerifyResult } from '@rakomi/sdk-core';
3
- export { AssertPasskeyResult, AuthError, AuthEvent, AuthMachineState, CryptoProvider, DeletePasskeyResult, HasParams, HttpClient, KeyValueStore, ListPasskeysResult, Locale, MfaStepUpRequiredError, MfaStepUpUnavailableError, OpaqueUserHandle, PasskeyCeremonyAdapter, PasskeyError, PasskeyErrorCode, PasskeySummary, PasskeySummaryResult, PasskeyTokens, PkceChallenge, RegisterPasskeyResult, RegisterResult, SessionResource, SignInOptions, SignInResult, StepUpWithPasskeyResult, SwitchOrgResult, TokenResult, Translations, UserResource, base64url, createTranslator, decodeJwtPayload, decodeSession, decodeUser, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
3
+ export { AssertPasskeyResult, AuthError, AuthEvent, AuthMachineState, AuthorizationEndpointCache, AuthorizationEndpointCacheOptions, CryptoProvider, DeletePasskeyResult, HasParams, HttpClient, KeyValueStore, ListPasskeysResult, Locale, MfaStepUpRequiredError, MfaStepUpUnavailableError, OpaqueUserHandle, PasskeyCeremonyAdapter, PasskeyError, PasskeyErrorCode, PasskeySummary, PasskeySummaryResult, PasskeyTokens, PkceChallenge, RegisterPasskeyResult, RegisterResult, SessionResource, SignInOptions, SignInResult, StepUpWithPasskeyResult, SwitchOrgResult, TokenResult, Translations, UserResource, base64url, createAuthorizationEndpointCache, createTranslator, decodeJwtPayload, decodeSession, decodeUser, deriveAuthorizationEndpointFallback, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
4
4
  import { D as DpopProver, N as NativeAuthAdapter, T as TokenCache } from './passkey-adapter-D_Z3Z7lV.cjs';
5
5
  export { A as AppLifecycle, a as AppStateValue, b as AttestationVerifier, B as BackgroundTask, c as BiometricGate, d as BiometricResult, e as BrowserAuthSessionOptions, f as BrowserAuthSessionResult, C as ConnectivityProvider, g as CreateDefaultExpoAdapterOptions, h as CreateNativeDpopProverOptions, i as CreateNativePasskeyAdapterOptions, j as DeepLinkProvider, k as DpopProofInput, l as NativeDpopModuleSpec, m as NativePasskeyCeremonyAdapter, n as NativePasskeyModuleSpec, P as ParClient, S as SystemBrowser, o as createDefaultExpoAdapter, p as createNativeDpopProver, q as createNativePasskeyAdapter } from './passkey-adapter-D_Z3Z7lV.cjs';
6
6
 
@@ -134,6 +134,11 @@ declare function RakomiProvider(props: RakomiProviderProps): ReactNode;
134
134
  * Discriminated-union AuthState — identical to `@rakomi/react`'s `AuthState`.
135
135
  *
136
136
  * `state: AuthMachineState` is the addition.
137
+ *
138
+ * @experimental Preview — `signIn()` and `switchOrganization()` are present for type parity with
139
+ * `@rakomi/react` but always resolve `{ status: 'error', error: { code: 'INVALID_CONFIG', … } }` in the
140
+ * current release; nothing is wired to the network yet. Use one of the working sign-in paths instead
141
+ * (social, magic link, email OTP, passkeys — see the package README / docs).
137
142
  */
138
143
  type AuthState = {
139
144
  isLoaded: false;
@@ -239,13 +244,19 @@ type LinkedMethods = LinkedMethod[];
239
244
  interface UseLinkedAccountsResult {
240
245
  isLoaded: boolean;
241
246
  methods: LinkedMethods;
247
+ /** @experimental Preview — always resolves `{ status: 'error' }` in the current release; no network call is made. */
242
248
  link: (provider: LinkProvider) => Promise<{
243
249
  status: 'redirect' | 'error';
244
250
  }>;
251
+ /** @experimental Preview — always resolves `{ status: 'error' }` in the current release; no network call is made. */
245
252
  unlink: (provider: LinkProvider | 'password' | 'magic_link' | 'email_otp' | 'passkey') => Promise<{
246
253
  status: 'complete' | 'error';
247
254
  }>;
248
255
  }
256
+ /**
257
+ * @experimental Preview — `methods` reads the JWT's `linked_methods` claim (live), but `link()` and
258
+ * `unlink()` are not yet wired to the network in this release; see the return type's per-field notes.
259
+ */
249
260
  declare function useLinkedAccounts(): UseLinkedAccountsResult;
250
261
  declare function useTranslation(): {
251
262
  t: TranslationFn;
@@ -263,12 +274,14 @@ interface UseBrandingReturn {
263
274
  declare function useBranding(): UseBrandingReturn;
264
275
  interface UseAnonymousSigninResult {
265
276
  isLoaded: boolean;
277
+ /** @experimental Preview — always resolves `{ status: 'error' }` in the current release; no network call is made. */
266
278
  signInAnonymously: (options?: {
267
279
  publicMetadata?: Record<string, unknown>;
268
280
  }) => Promise<{
269
281
  status: 'complete' | 'error';
270
282
  }>;
271
283
  }
284
+ /** @experimental Preview — `signInAnonymously()` is not yet wired to the network in this release; see the return type's field note. */
272
285
  declare function useAnonymousSignin(): UseAnonymousSigninResult;
273
286
  interface BaasPlanPublicItem {
274
287
  id: string;
@@ -354,15 +367,19 @@ interface FeatureProps {
354
367
  declare function Feature({ flag, children, fallback }: FeatureProps): ReactNode;
355
368
 
356
369
  /**
357
- * `<SignIn />` — RN port.
370
+ * `<SignIn />` — RN port. **Preview:** `@rakomi/react-native` is a pre-1.0 preview package — see the
371
+ * package README for the full "what's wired today" boundary.
358
372
  *
359
373
  * Renders a sign-in flow using ONLY RN primitives (`<View>`, `<Text>`,
360
374
  * `<TextInput>`, `<Pressable>`) — no HTML. Supports:
361
- * - Password (direct) sign-in.
362
375
  * - Social provider list (Google, GitHub, …) via `startSocialSignIn`.
363
376
  * - MFA TOTP code entry view (`keyboardType="number-pad"`,
364
377
  * `textContentType="oneTimeCode"`, `autoComplete="sms-otp"`).
365
378
  *
379
+ * There is no email/password field in this component — direct email+password sign-in is not wired in
380
+ * the current release. Use `useRegister()` for account creation, or one of the other working sign-in
381
+ * paths (magic link, email OTP, passkeys).
382
+ *
366
383
  * Accessibility:
367
384
  * - Every interactive element has `accessibilityLabel` + `accessibilityRole`.
368
385
  * - i18n keys come from `useRakomiContext.translate`.
@@ -370,11 +387,16 @@ declare function Feature({ flag, children, fallback }: FeatureProps): ReactNode;
370
387
  */
371
388
 
372
389
  interface SignInProps {
373
- /** Authorization endpoint, default `${baseUrl}/oauth/authorize`. */
390
+ /**
391
+ * Full authorization-endpoint URL to open the system browser to. Default: resolved via OIDC
392
+ * discovery against `baseUrl` (cached, with a host-naming fallback) — see `useRakomiContext()`'s
393
+ * `authorizationEndpointCache`. A bare `${baseUrl}/oauth/authorize` is NOT a valid default: that
394
+ * path on the API host returns JSON, not the hosted login UI.
395
+ */
374
396
  authorizationEndpoint?: string;
375
- /** Token endpoint, default `${baseUrl}/v1/auth/oauth/callback`. */
397
+ /** RFC 6749 §3.2 token endpoint used for the PKCE code→token exchange, default `${baseUrl}/oauth/token`. */
376
398
  tokenEndpoint?: string;
377
- /** MFA TOTP verify endpoint, default `${baseUrl}/v1/auth/mfa/totp/verify`. */
399
+ /** MFA TOTP verify endpoint, default `${baseUrl}/v1/auth/mfa/verify-login`. */
378
400
  totpVerifyEndpoint?: string;
379
401
  /** Social providers to render. Default: ['google']. */
380
402
  providers?: string[];
@@ -424,8 +446,8 @@ declare function UserButton(props: UserButtonProps): ReactNode;
424
446
  /**
425
447
  * `<UserProfile />` — RN port (PREVIEW).
426
448
  *
427
- * Marked `'preview'` until 0.2.0; on first mount in `__DEV__` builds we log a
428
- * warning so consumers don't accidentally ship it to production.
449
+ * In preview — on first mount in `__DEV__` builds we log a warning so consumers don't accidentally
450
+ * ship it to production ahead of graduation.
429
451
  */
430
452
 
431
453
  interface UserProfileProps {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from 'react';
2
2
  import { AuthEvent, Locale, Translations, AuthMachineState, SignInOptions, SignInResult, TokenResult, AuthError, UserResource, HasParams, SwitchOrgResult, SessionResource, AuthConfig, BrandingConfig, OrgMembershipClaim, TranslationFn, OAuthTokenResponse, HttpClient, OpaqueUserHandle, PasskeyCeremonyAdapter, PasskeySummary, PasskeyError, AssertPasskeyResult, StepUpWithPasskeyResult, RegisterPasskeyResult, ListPasskeysResult, PasskeySummaryResult, DeletePasskeyResult, RequestResult, VerifyResult } from '@rakomi/sdk-core';
3
- export { AssertPasskeyResult, AuthError, AuthEvent, AuthMachineState, CryptoProvider, DeletePasskeyResult, HasParams, HttpClient, KeyValueStore, ListPasskeysResult, Locale, MfaStepUpRequiredError, MfaStepUpUnavailableError, OpaqueUserHandle, PasskeyCeremonyAdapter, PasskeyError, PasskeyErrorCode, PasskeySummary, PasskeySummaryResult, PasskeyTokens, PkceChallenge, RegisterPasskeyResult, RegisterResult, SessionResource, SignInOptions, SignInResult, StepUpWithPasskeyResult, SwitchOrgResult, TokenResult, Translations, UserResource, base64url, createTranslator, decodeJwtPayload, decodeSession, decodeUser, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
3
+ export { AssertPasskeyResult, AuthError, AuthEvent, AuthMachineState, AuthorizationEndpointCache, AuthorizationEndpointCacheOptions, CryptoProvider, DeletePasskeyResult, HasParams, HttpClient, KeyValueStore, ListPasskeysResult, Locale, MfaStepUpRequiredError, MfaStepUpUnavailableError, OpaqueUserHandle, PasskeyCeremonyAdapter, PasskeyError, PasskeyErrorCode, PasskeySummary, PasskeySummaryResult, PasskeyTokens, PkceChallenge, RegisterPasskeyResult, RegisterResult, SessionResource, SignInOptions, SignInResult, StepUpWithPasskeyResult, SwitchOrgResult, TokenResult, Translations, UserResource, base64url, createAuthorizationEndpointCache, createTranslator, decodeJwtPayload, decodeSession, decodeUser, deriveAuthorizationEndpointFallback, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
4
4
  import { D as DpopProver, N as NativeAuthAdapter, T as TokenCache } from './passkey-adapter-D_Z3Z7lV.js';
5
5
  export { A as AppLifecycle, a as AppStateValue, b as AttestationVerifier, B as BackgroundTask, c as BiometricGate, d as BiometricResult, e as BrowserAuthSessionOptions, f as BrowserAuthSessionResult, C as ConnectivityProvider, g as CreateDefaultExpoAdapterOptions, h as CreateNativeDpopProverOptions, i as CreateNativePasskeyAdapterOptions, j as DeepLinkProvider, k as DpopProofInput, l as NativeDpopModuleSpec, m as NativePasskeyCeremonyAdapter, n as NativePasskeyModuleSpec, P as ParClient, S as SystemBrowser, o as createDefaultExpoAdapter, p as createNativeDpopProver, q as createNativePasskeyAdapter } from './passkey-adapter-D_Z3Z7lV.js';
6
6
 
@@ -134,6 +134,11 @@ declare function RakomiProvider(props: RakomiProviderProps): ReactNode;
134
134
  * Discriminated-union AuthState — identical to `@rakomi/react`'s `AuthState`.
135
135
  *
136
136
  * `state: AuthMachineState` is the addition.
137
+ *
138
+ * @experimental Preview — `signIn()` and `switchOrganization()` are present for type parity with
139
+ * `@rakomi/react` but always resolve `{ status: 'error', error: { code: 'INVALID_CONFIG', … } }` in the
140
+ * current release; nothing is wired to the network yet. Use one of the working sign-in paths instead
141
+ * (social, magic link, email OTP, passkeys — see the package README / docs).
137
142
  */
138
143
  type AuthState = {
139
144
  isLoaded: false;
@@ -239,13 +244,19 @@ type LinkedMethods = LinkedMethod[];
239
244
  interface UseLinkedAccountsResult {
240
245
  isLoaded: boolean;
241
246
  methods: LinkedMethods;
247
+ /** @experimental Preview — always resolves `{ status: 'error' }` in the current release; no network call is made. */
242
248
  link: (provider: LinkProvider) => Promise<{
243
249
  status: 'redirect' | 'error';
244
250
  }>;
251
+ /** @experimental Preview — always resolves `{ status: 'error' }` in the current release; no network call is made. */
245
252
  unlink: (provider: LinkProvider | 'password' | 'magic_link' | 'email_otp' | 'passkey') => Promise<{
246
253
  status: 'complete' | 'error';
247
254
  }>;
248
255
  }
256
+ /**
257
+ * @experimental Preview — `methods` reads the JWT's `linked_methods` claim (live), but `link()` and
258
+ * `unlink()` are not yet wired to the network in this release; see the return type's per-field notes.
259
+ */
249
260
  declare function useLinkedAccounts(): UseLinkedAccountsResult;
250
261
  declare function useTranslation(): {
251
262
  t: TranslationFn;
@@ -263,12 +274,14 @@ interface UseBrandingReturn {
263
274
  declare function useBranding(): UseBrandingReturn;
264
275
  interface UseAnonymousSigninResult {
265
276
  isLoaded: boolean;
277
+ /** @experimental Preview — always resolves `{ status: 'error' }` in the current release; no network call is made. */
266
278
  signInAnonymously: (options?: {
267
279
  publicMetadata?: Record<string, unknown>;
268
280
  }) => Promise<{
269
281
  status: 'complete' | 'error';
270
282
  }>;
271
283
  }
284
+ /** @experimental Preview — `signInAnonymously()` is not yet wired to the network in this release; see the return type's field note. */
272
285
  declare function useAnonymousSignin(): UseAnonymousSigninResult;
273
286
  interface BaasPlanPublicItem {
274
287
  id: string;
@@ -354,15 +367,19 @@ interface FeatureProps {
354
367
  declare function Feature({ flag, children, fallback }: FeatureProps): ReactNode;
355
368
 
356
369
  /**
357
- * `<SignIn />` — RN port.
370
+ * `<SignIn />` — RN port. **Preview:** `@rakomi/react-native` is a pre-1.0 preview package — see the
371
+ * package README for the full "what's wired today" boundary.
358
372
  *
359
373
  * Renders a sign-in flow using ONLY RN primitives (`<View>`, `<Text>`,
360
374
  * `<TextInput>`, `<Pressable>`) — no HTML. Supports:
361
- * - Password (direct) sign-in.
362
375
  * - Social provider list (Google, GitHub, …) via `startSocialSignIn`.
363
376
  * - MFA TOTP code entry view (`keyboardType="number-pad"`,
364
377
  * `textContentType="oneTimeCode"`, `autoComplete="sms-otp"`).
365
378
  *
379
+ * There is no email/password field in this component — direct email+password sign-in is not wired in
380
+ * the current release. Use `useRegister()` for account creation, or one of the other working sign-in
381
+ * paths (magic link, email OTP, passkeys).
382
+ *
366
383
  * Accessibility:
367
384
  * - Every interactive element has `accessibilityLabel` + `accessibilityRole`.
368
385
  * - i18n keys come from `useRakomiContext.translate`.
@@ -370,11 +387,16 @@ declare function Feature({ flag, children, fallback }: FeatureProps): ReactNode;
370
387
  */
371
388
 
372
389
  interface SignInProps {
373
- /** Authorization endpoint, default `${baseUrl}/oauth/authorize`. */
390
+ /**
391
+ * Full authorization-endpoint URL to open the system browser to. Default: resolved via OIDC
392
+ * discovery against `baseUrl` (cached, with a host-naming fallback) — see `useRakomiContext()`'s
393
+ * `authorizationEndpointCache`. A bare `${baseUrl}/oauth/authorize` is NOT a valid default: that
394
+ * path on the API host returns JSON, not the hosted login UI.
395
+ */
374
396
  authorizationEndpoint?: string;
375
- /** Token endpoint, default `${baseUrl}/v1/auth/oauth/callback`. */
397
+ /** RFC 6749 §3.2 token endpoint used for the PKCE code→token exchange, default `${baseUrl}/oauth/token`. */
376
398
  tokenEndpoint?: string;
377
- /** MFA TOTP verify endpoint, default `${baseUrl}/v1/auth/mfa/totp/verify`. */
399
+ /** MFA TOTP verify endpoint, default `${baseUrl}/v1/auth/mfa/verify-login`. */
378
400
  totpVerifyEndpoint?: string;
379
401
  /** Social providers to render. Default: ['google']. */
380
402
  providers?: string[];
@@ -424,8 +446,8 @@ declare function UserButton(props: UserButtonProps): ReactNode;
424
446
  /**
425
447
  * `<UserProfile />` — RN port (PREVIEW).
426
448
  *
427
- * Marked `'preview'` until 0.2.0; on first mount in `__DEV__` builds we log a
428
- * warning so consumers don't accidentally ship it to production.
449
+ * In preview — on first mount in `__DEV__` builds we log a warning so consumers don't accidentally
450
+ * ship it to production ahead of graduation.
429
451
  */
430
452
 
431
453
  interface UserProfileProps {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createContext, useMemo, useReducer, useRef, useEffect, useState, useCallback, useContext } from 'react';
2
- import { EventLog, reduce, INITIAL_SNAPSHOT, createTranslator, isSignedIn, hasPermission, hasRole, extractOrgClaims, parseAuthConfigResponse, generatePkce, issueState, buildAuthorizationUrl, consumeState, exchangeAuthCode, parseOAuthCallbackError, scorePassword, isSafeUrl, passkeyError, decodeUser, assertPasskey, stepUpWithPasskey, registerPasskey, listPasskeys, renamePasskey, deletePasskey, verifyMagicLink, requestMagicLink, verifyEmailOtp, requestEmailOtp, register, verifyTotp, MfaStepUpUnavailableError, decodeSession, deriveTenantStorageKey, createJwksCache, refreshAccessToken, decodeJwtPayload, PasskeyCeremonyError } from '@rakomi/sdk-core';
3
- export { MfaStepUpRequiredError, MfaStepUpUnavailableError, base64url, createTranslator, decodeJwtPayload, decodeSession, decodeUser, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
2
+ import { createAuthorizationEndpointCache, EventLog, reduce, INITIAL_SNAPSHOT, createTranslator, isSignedIn, hasPermission, hasRole, extractOrgClaims, parseAuthConfigResponse, generatePkce, issueState, buildAuthorizationUrl, consumeState, exchangeAuthCode, parseOAuthCallbackError, scorePassword, isSafeUrl, passkeyError, decodeUser, assertPasskey, stepUpWithPasskey, registerPasskey, listPasskeys, renamePasskey, deletePasskey, verifyMagicLink, requestMagicLink, verifyEmailOtp, requestEmailOtp, register, verifyTotp, MfaStepUpUnavailableError, decodeSession, deriveTenantStorageKey, createJwksCache, refreshAccessToken, decodeJwtPayload, PasskeyCeremonyError } from '@rakomi/sdk-core';
3
+ export { MfaStepUpRequiredError, MfaStepUpUnavailableError, base64url, createAuthorizationEndpointCache, createTranslator, decodeJwtPayload, decodeSession, decodeUser, deriveAuthorizationEndpointFallback, generatePkce, getErrorMessage, hasPermission, hasRole, isSafeUrl, networkError, parseOAuthCallbackError, parseTokenEndpointError, scorePassword, selectPluralForm } from '@rakomi/sdk-core';
4
4
  import { jwtVerify } from 'jose';
5
5
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
6
6
 
@@ -1012,6 +1012,21 @@ function RakomiProvider(props) {
1012
1012
  return Object.freeze(built);
1013
1013
  }, [props.nativeAdapter, props.tokenCache]);
1014
1014
  const http = useMemo(() => createRnHttpClient({ baseUrl: props.baseUrl }), [props.baseUrl]);
1015
+ const authorizationEndpointCache = useMemo(
1016
+ () => createAuthorizationEndpointCache({
1017
+ fetchDiscoveryDocument: async () => {
1018
+ const response = await http.fetch("/.well-known/openid-configuration", {
1019
+ method: "GET",
1020
+ headers: { Accept: "application/json" }
1021
+ });
1022
+ if (!response.ok) {
1023
+ throw new Error(`discovery fetch failed: HTTP ${response.status}`);
1024
+ }
1025
+ return response.json();
1026
+ }
1027
+ }),
1028
+ [http]
1029
+ );
1015
1030
  const events = useMemo(() => new EventLog(props.onEvent), [props.onEvent]);
1016
1031
  const [snapshot, dispatch] = useReducer(reduce, INITIAL_SNAPSHOT);
1017
1032
  const passkeyCeremonyLock = useRef(createCeremonyLock()).current;
@@ -1147,6 +1162,7 @@ function RakomiProvider(props) {
1147
1162
  redirectUri: props.redirectUri,
1148
1163
  adapter,
1149
1164
  http,
1165
+ authorizationEndpointCache,
1150
1166
  snapshot,
1151
1167
  state: snapshot.state,
1152
1168
  isSignedIn: isSignedIn(snapshot),
@@ -1162,7 +1178,7 @@ function RakomiProvider(props) {
1162
1178
  beginAuthFlow,
1163
1179
  dpopSession
1164
1180
  }),
1165
- [adapter, http, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession, passkeyCeremonyLock]
1181
+ [adapter, http, authorizationEndpointCache, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession, passkeyCeremonyLock]
1166
1182
  );
1167
1183
  return /* @__PURE__ */ jsx(RakomiContext.Provider, { value, children: props.children });
1168
1184
  }
@@ -1619,14 +1635,24 @@ function SignIn(props) {
1619
1635
  const [step, setStep] = useState({ kind: "methods" });
1620
1636
  const [code, setCode] = useState("");
1621
1637
  const [busy, setBusy] = useState(false);
1622
- const tokenEndpoint = props.tokenEndpoint ?? "/v1/auth/oauth/callback";
1623
- const authorizationEndpoint = props.authorizationEndpoint ?? "/oauth/authorize";
1624
- const totpVerifyEndpoint = props.totpVerifyEndpoint ?? "/v1/auth/mfa/totp/verify";
1638
+ const tokenEndpoint = props.tokenEndpoint ?? "/oauth/token";
1639
+ const totpVerifyEndpoint = props.totpVerifyEndpoint ?? "/v1/auth/mfa/verify-login";
1625
1640
  const providers = props.providers ?? ["google"];
1626
1641
  async function onProviderPress(provider) {
1627
1642
  if (busy) return;
1628
1643
  setBusy(true);
1629
1644
  ctx.events.push({ type: "sign_in_attempted", severity: "info", metadata: { provider } });
1645
+ let authorizationEndpoint = props.authorizationEndpoint;
1646
+ if (!authorizationEndpoint) {
1647
+ const resolved = await ctx.authorizationEndpointCache.resolve(ctx.baseUrl);
1648
+ if (!resolved.ok) {
1649
+ ctx.events.push({ type: "sign_in_failed", severity: "warning", error: resolved.error });
1650
+ setStep({ kind: "error", error: resolved.error });
1651
+ setBusy(false);
1652
+ return;
1653
+ }
1654
+ authorizationEndpoint = resolved.authorizationEndpoint;
1655
+ }
1630
1656
  const outcome = await startSocialSignIn({
1631
1657
  adapter: ctx.adapter,
1632
1658
  http: ctx.http,
@@ -1832,7 +1858,7 @@ function UserButton(props) {
1832
1858
  function UserProfile(props) {
1833
1859
  useEffect(() => {
1834
1860
  if (typeof __DEV__ !== "undefined" && __DEV__) {
1835
- console.warn("[@rakomi/react-native] <UserProfile /> is in preview until 0.2.0 \u2014 feedback welcome.");
1861
+ console.warn("[@rakomi/react-native] <UserProfile /> is in preview \u2014 feedback welcome.");
1836
1862
  }
1837
1863
  }, []);
1838
1864
  const ctx = useRakomiContext();
@@ -2659,7 +2685,7 @@ function useMfa() {
2659
2685
  const ctx = useRakomiContext();
2660
2686
  return {
2661
2687
  verifyTotp: async (challengeToken, code, endpoint) => {
2662
- const url = endpoint ?? `${ctx.baseUrl}/v1/auth/mfa/totp/verify`;
2688
+ const url = endpoint ?? `${ctx.baseUrl}/v1/auth/mfa/verify-login`;
2663
2689
  const result = await verifyTotp({ http: ctx.http, endpoint: url, challengeToken, code });
2664
2690
  if (result.ok) {
2665
2691
  const nonce = await ctx.beginAuthFlow();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rakomi/react-native",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "React Native / Expo SDK for Rakomi authentication.",
5
5
  "keywords": [
6
6
  "rakomi",
@@ -83,11 +83,11 @@
83
83
  }
84
84
  },
85
85
  "devDependencies": {
86
- "@rakomi/react": "0.2.0"
86
+ "@rakomi/react": "0.3.1"
87
87
  },
88
88
  "dependencies": {
89
89
  "jose": "^6.2.3",
90
- "@rakomi/sdk-core": "^0.2.0"
90
+ "@rakomi/sdk-core": "^0.3.1"
91
91
  },
92
92
  "scripts": {
93
93
  "build": "NODE_OPTIONS=--max-old-space-size=4096 tsup",
package/sbom.cdx.json CHANGED
@@ -20,16 +20,16 @@
20
20
  "type": "application",
21
21
  "group": "rakomi",
22
22
  "name": "generate-sbom",
23
- "version": "sha256:9472206f8105"
23
+ "version": "sha256:ffabbf3f1638"
24
24
  }
25
25
  ]
26
26
  },
27
27
  "component": {
28
28
  "type": "library",
29
- "bom-ref": "pkg:npm/%40rakomi%2Freact-native@0.2.0",
29
+ "bom-ref": "pkg:npm/%40rakomi%2Freact-native@0.3.1",
30
30
  "name": "@rakomi/react-native",
31
- "version": "0.2.0",
32
- "purl": "pkg:npm/%40rakomi%2Freact-native@0.2.0",
31
+ "version": "0.3.1",
32
+ "purl": "pkg:npm/%40rakomi%2Freact-native@0.3.1",
33
33
  "licenses": [
34
34
  {
35
35
  "license": {
@@ -42,10 +42,10 @@
42
42
  "components": [
43
43
  {
44
44
  "type": "library",
45
- "bom-ref": "pkg:npm/@rakomi/sdk-core@0.2.0",
45
+ "bom-ref": "pkg:npm/@rakomi/sdk-core@0.3.1",
46
46
  "name": "@rakomi/sdk-core",
47
- "version": "0.2.0",
48
- "purl": "pkg:npm/@rakomi/sdk-core@0.2.0",
47
+ "version": "0.3.1",
48
+ "purl": "pkg:npm/@rakomi/sdk-core@0.3.1",
49
49
  "licenses": [
50
50
  {
51
51
  "license": {
@@ -56,10 +56,10 @@
56
56
  },
57
57
  {
58
58
  "type": "library",
59
- "bom-ref": "pkg:npm/jose@6.2.9",
59
+ "bom-ref": "pkg:npm/jose@6.2.10",
60
60
  "name": "jose",
61
- "version": "6.2.9",
62
- "purl": "pkg:npm/jose@6.2.9",
61
+ "version": "6.2.10",
62
+ "purl": "pkg:npm/jose@6.2.10",
63
63
  "licenses": [
64
64
  {
65
65
  "license": {