@rakomi/react-native 0.1.0 → 0.3.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/COMPLIANCE.md +36 -6
- package/README.md +271 -6
- package/SECURITY.md +1 -1
- package/THIRD-PARTY-NOTICES +1 -1
- package/dist/index.cjs +728 -15
- package/dist/index.d.cts +97 -11
- package/dist/index.d.ts +97 -11
- package/dist/index.js +722 -19
- package/dist/native/index.cjs +265 -0
- package/dist/native/index.d.cts +33 -3
- package/dist/native/index.d.ts +33 -3
- package/dist/native/index.js +261 -1
- package/dist/{expo-adapter-CtPCEgue.d.cts → passkey-adapter-D_Z3Z7lV.d.cts} +172 -2
- package/dist/{expo-adapter-CtPCEgue.d.ts → passkey-adapter-D_Z3Z7lV.d.ts} +172 -2
- package/package.json +3 -3
- package/sbom.cdx.json +17 -11
package/COMPLIANCE.md
CHANGED
|
@@ -4,21 +4,24 @@ Regulatory cross-reference for tenants in EU-regulated industries.
|
|
|
4
4
|
|
|
5
5
|
## CRA — Reg. (EU) 2024/2847 (Cyber Resilience Act)
|
|
6
6
|
|
|
7
|
-
This package is a "product with digital elements" under CRA
|
|
7
|
+
This package is a "product with digital elements" under CRA Art. 3(1). Same posture as `@rakomi/node` and `@rakomi/react`.
|
|
8
8
|
|
|
9
9
|
| CRA reference | Compliance posture |
|
|
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. |
|
|
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
|
|
|
17
17
|
| Article | Posture |
|
|
18
18
|
|---|---|
|
|
19
|
-
| Art. 25 — Data protection by design | No PII collected client-side beyond what API requires; analytics opt-in via `onEvent` only. |
|
|
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). |
|
|
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
|
+
| 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
|
+
| 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.) |
|
|
22
25
|
|
|
23
26
|
## eIDAS 2 — Reg. (EU) 2024/1183 (EUDI Wallet)
|
|
24
27
|
|
|
@@ -26,7 +29,8 @@ End-of-2026 mandate. The `NativeAuthAdapter` exposes a typed forward-compat slot
|
|
|
26
29
|
|
|
27
30
|
## NIS2 — Dir. (EU) 2022/2555
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
NIS2 Art. 21 measures are obligations on the **entity**, and no supplier's posture discharges them. What
|
|
33
|
+
this SDK offers is *input* to your own risk assessment, not an attestation:
|
|
30
34
|
|
|
31
35
|
- Risk-management measures: cryptographic + access controls documented in `SECURITY.md`.
|
|
32
36
|
- Incident handling: statutory reporting of actively exploited vulnerabilities / severe incidents per CRA Art. 14 (early warning, notification, final report); see `SECURITY.md`.
|
|
@@ -34,7 +38,33 @@ Tenants in essential / important sectors can attest that their auth-provider mee
|
|
|
34
38
|
|
|
35
39
|
## DORA — Reg. (EU) 2022/2554
|
|
36
40
|
|
|
37
|
-
For financial-sector tenants
|
|
41
|
+
For financial-sector tenants, DORA governs ICT risk management (Art. 5–15) and ICT third-party risk
|
|
42
|
+
(Art. 28–30). What this SDK offers as *input* to those obligations, as one ICT component you consume:
|
|
43
|
+
|
|
44
|
+
- A CycloneDX SBOM and npm build provenance (SLSA Build L2) with every release — supporting your ICT
|
|
45
|
+
risk-management and supply-chain analysis. (Your Art. 28(3) register of information records the
|
|
46
|
+
**contractual arrangement** for the Rakomi service, not this library artefact.)
|
|
47
|
+
- Vulnerability handling per CRA Art. 13 + Annex I Part II, and statutory reporting per CRA Art. 14;
|
|
48
|
+
see `SECURITY.md`.
|
|
49
|
+
|
|
50
|
+
## Strong Customer Authentication (PSD2 / RTS 2018/389)
|
|
51
|
+
|
|
52
|
+
**This SDK makes no SCA claim and no assurance-level (AAL / eIDAS LoA) claim.** SCA is a property of a
|
|
53
|
+
payment flow, assessed by the payment service provider against its own deployment; a library cannot
|
|
54
|
+
satisfy it, and nothing in this document should be quoted as if it could. Two things are worth stating
|
|
55
|
+
because they are the parts a PSP most often assumes wrongly:
|
|
56
|
+
|
|
57
|
+
- **Dynamic linking (RTS Art. 5) is NOT provided.** This SDK authenticates a *user*, not a
|
|
58
|
+
*transaction*: it accepts no amount and no payee, and binds neither into the signed challenge. A
|
|
59
|
+
payment initiation requiring dynamic linking cannot be built on this SDK's passkey flow alone.
|
|
60
|
+
- **A platform passkey is not necessarily device-bound.** On both iOS and Android a passkey is, by
|
|
61
|
+
default, **synced** through the platform account (iCloud Keychain / Google Password Manager), and the
|
|
62
|
+
SDK cannot tell a synced credential from a device-bound one — the WebAuthn signals that would hint at
|
|
63
|
+
it are opaque relays we neither interpret nor vouch for. Any element analysis under RTS Art. 7 or Art. 9
|
|
64
|
+
(including whether the elements are independent on a multi-purpose device, per Art. 9(2)–(3)) is
|
|
65
|
+
therefore the PSP's assessment, on evidence this SDK does not supply.
|
|
66
|
+
|
|
67
|
+
PSD3/PSR are proposals and, as at 2026-07, are not in force; nothing here anticipates their final text.
|
|
38
68
|
|
|
39
69
|
## Apple App Store / Google Play Data Safety
|
|
40
70
|
|
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
|
|
6
|
-
>
|
|
7
|
-
>
|
|
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,16 +54,263 @@ export default function App() {
|
|
|
36
54
|
}
|
|
37
55
|
```
|
|
38
56
|
|
|
39
|
-
## What's included
|
|
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
|
|
43
|
-
- **Components:** `<SignIn />` (
|
|
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.
|
|
47
65
|
- **HKDF-style storage key derivation** — domain-separated per tenant + per purpose.
|
|
48
66
|
|
|
67
|
+
## Passkeys (WebAuthn)
|
|
68
|
+
|
|
69
|
+
`usePasskeys()` is the whole passkey surface: sign-in, step-up, registration, and management
|
|
70
|
+
(list / rename / delete). Passkeys give you **passwordless** sign-in. They are **phishing-resistant**
|
|
71
|
+
only because the operating system binds the credential to your app's associated domain — on iOS
|
|
72
|
+
through your **Associated Domains** entitlement, on Android through your **Digital Asset Links** file.
|
|
73
|
+
If either is missing or wrong the OS **refuses** the ceremony; the SDK cannot and does not substitute
|
|
74
|
+
for that configuration. (The reassuring half: a misconfiguration does not silently weaken anything —
|
|
75
|
+
it stops the ceremony outright.)
|
|
76
|
+
|
|
77
|
+
The flow is not "biometric sign-in", and calling it that in your UI would be untrue for a share of your
|
|
78
|
+
users: user verification is performed by the **device's screen lock — biometrics *or* passcode** — and
|
|
79
|
+
the SDK cannot distinguish them, nor does it want to.
|
|
80
|
+
|
|
81
|
+
The SDK does **not** ship a passkey library and does **not** depend on one. The OS ceremony
|
|
82
|
+
(the system passkey sheet) belongs to your app: you supply a native module, the SDK talks to it
|
|
83
|
+
through one small contract. That keeps a third-party module off the credential path, keeps Expo Go
|
|
84
|
+
working for apps that never use passkeys, and lets you pick whichever community wrapper you already
|
|
85
|
+
trust.
|
|
86
|
+
|
|
87
|
+
### Wiring the ceremony adapter
|
|
88
|
+
|
|
89
|
+
**With a native module of your own** (or a community wrapper's module):
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import {
|
|
93
|
+
createDefaultExpoAdapter,
|
|
94
|
+
createNativePasskeyAdapter,
|
|
95
|
+
RakomiProvider,
|
|
96
|
+
} from '@rakomi/react-native';
|
|
97
|
+
import { NativeModules } from 'react-native';
|
|
98
|
+
|
|
99
|
+
const passkeys = createNativePasskeyAdapter({ module: NativeModules.MyPasskeyModule });
|
|
100
|
+
|
|
101
|
+
// `nativeAdapter` is normally optional (it defaults to the Expo adapter). To ADD passkeys you must
|
|
102
|
+
// name it explicitly — spread the default and attach the slot. There is no `passkeys` shorthand prop.
|
|
103
|
+
<RakomiProvider
|
|
104
|
+
publishableKey={process.env.EXPO_PUBLIC_RAKOMI_KEY!}
|
|
105
|
+
baseUrl="https://api.rakomi.com"
|
|
106
|
+
redirectUri="myapp://callback"
|
|
107
|
+
nativeAdapter={{ ...createDefaultExpoAdapter(), passkeys }}
|
|
108
|
+
>
|
|
109
|
+
{/* … */}
|
|
110
|
+
</RakomiProvider>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**With a community wrapper whose API is not the module shape** — write a four-line shim; the contract
|
|
114
|
+
is deliberately tiny:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { createNativePasskeyAdapter } from '@rakomi/react-native';
|
|
118
|
+
import { Passkey } from 'some-passkey-wrapper'; // whichever wrapper you already trust
|
|
119
|
+
|
|
120
|
+
const passkeys = createNativePasskeyAdapter({
|
|
121
|
+
module: {
|
|
122
|
+
isPasskeySupported: () => Passkey.isSupported(),
|
|
123
|
+
// The SDK's contract is STRING in, STRING out. Most community wrappers are OBJECT in, OBJECT out —
|
|
124
|
+
// so the shim parses on the way in and serialises on the way out. Getting this backwards
|
|
125
|
+
// double-encodes the request (the wrapper receives a JSON string where it expects an object) and is
|
|
126
|
+
// the single most common wiring bug; check your wrapper's signature, do not copy this blindly.
|
|
127
|
+
createPasskey: (requestJson: string) =>
|
|
128
|
+
Passkey.create(JSON.parse(requestJson)).then((credential) => JSON.stringify(credential)),
|
|
129
|
+
getPasskey: (requestJson: string) =>
|
|
130
|
+
Passkey.get(JSON.parse(requestJson)).then((credential) => JSON.stringify(credential)),
|
|
131
|
+
// Implement this if your wrapper can dismiss an open sheet — it is what narrows the
|
|
132
|
+
// orphaned-credential window described below.
|
|
133
|
+
cancelPasskeyRequest: () => Passkey.cancel?.(),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Using the hook
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
const {
|
|
142
|
+
isSupported, // null until probed; false = this device cannot run a ceremony
|
|
143
|
+
signInWithPasskey, // no session needed
|
|
144
|
+
stepUpWithPasskey, // mints a step-up token — needs a session AND an existing passkey
|
|
145
|
+
registerPasskey, listPasskeys, renamePasskey, deletePasskey, // all need a step-up token
|
|
146
|
+
passkeys, // `undefined` until you call listPasskeys(); `[]` means "none"
|
|
147
|
+
error, isSigningIn, isRegistering, isSteppingUp, isLoading, isMutating,
|
|
148
|
+
} = usePasskeys();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`passkeys` starts as **`undefined`**, not `[]`. The difference is not pedantry: `[]` says "this user has
|
|
152
|
+
no passkeys" and `undefined` says "we have not looked". An empty-state UI keyed on `passkeys?.length === 0`
|
|
153
|
+
renders "no passkeys yet" before the first `listPasskeys()` has even run.
|
|
154
|
+
|
|
155
|
+
### Step-up tokens — read this before building an "Add a passkey" button
|
|
156
|
+
|
|
157
|
+
Every management action (`registerPasskey`, `listPasskeys`, `renamePasskey`, `deletePasskey`) requires a
|
|
158
|
+
**step-up token**: changing which keys can sign you in is itself a sensitive act, so the server demands a
|
|
159
|
+
fresh re-authentication for it. The SDK never mints or stores one for you.
|
|
160
|
+
|
|
161
|
+
**There is a chicken-and-egg here, and it will bite you first.** `stepUpWithPasskey()` re-authenticates
|
|
162
|
+
with an **existing passkey** — so it cannot mint the token needed to register the user's **first** one.
|
|
163
|
+
For that first registration, obtain a step-up token through another factor (your app's password or MFA
|
|
164
|
+
step-up endpoint), then pass it to `registerPasskey({ stepUpToken })`. Once the user has a passkey,
|
|
165
|
+
`stepUpWithPasskey()` is the smooth path for every subsequent management action.
|
|
166
|
+
|
|
167
|
+
Treat a step-up token as **single-use and short-lived**: mint a fresh one per gated action rather than
|
|
168
|
+
caching one across a management screen. (`stepUpWithPasskey()` returns its `expiresIn`.) The server's
|
|
169
|
+
reuse semantics are not something the SDK guarantees, so the defensive reading is the correct one.
|
|
170
|
+
|
|
171
|
+
### The bridge contract
|
|
172
|
+
|
|
173
|
+
| Module member | Required | Shape |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `isPasskeySupported()` | yes | `Promise<boolean>` — the device can run a passkey ceremony at all |
|
|
176
|
+
| `createPasskey(requestJson)` | yes | `Promise<string>` — the registration credential, serialised |
|
|
177
|
+
| `getPasskey(requestJson)` | yes | `Promise<string>` — the authentication credential, serialised |
|
|
178
|
+
| `isPlatformAuthenticatorAvailable()` | no | `Promise<boolean>` — is there a *platform* authenticator, not just any |
|
|
179
|
+
| `cancelPasskeyRequest()` | no | `Promise<void> \| void` — dismiss an open sheet when the ceremony is abandoned |
|
|
180
|
+
|
|
181
|
+
Implement `cancelPasskeyRequest` if your module can: without it, a ceremony abandoned by your app (a
|
|
182
|
+
screen unmounts, a timeout fires) can leave the OS sheet standing, and a registration abandoned after
|
|
183
|
+
the credential provider already created the credential leaves the user holding a passkey the server
|
|
184
|
+
never learned about (see *Orphaned credentials* below).
|
|
185
|
+
|
|
186
|
+
**Strings, not objects — and this is not a style preference.** The RN bridge serialises through JSON
|
|
187
|
+
and **drops `undefined`**, which silently deletes optional members. A request that arrives with
|
|
188
|
+
`residentKey` missing is a *different* request from one that arrives with `residentKey: 'required'`,
|
|
189
|
+
and the sheet the user sees changes accordingly. Passing the request as a string we build ourselves
|
|
190
|
+
is the only way to guarantee the bytes the OS gets are the bytes the server signed off on.
|
|
191
|
+
|
|
192
|
+
Every binary field is **base64url without padding** (`A–Z a–z 0–9 - _`, no `=`). Standard base64 —
|
|
193
|
+
with `+`, `/`, `=` — is rejected, and it is the single most common wiring bug: an authenticator that
|
|
194
|
+
returns standard base64 must be re-encoded in your shim, not "fixed" server-side.
|
|
195
|
+
|
|
196
|
+
### iOS ≠ Android
|
|
197
|
+
|
|
198
|
+
The two platforms fail differently, and the SDK maps both onto one vocabulary so your UI does not
|
|
199
|
+
have to branch:
|
|
200
|
+
|
|
201
|
+
| What happened | Code your UI handles |
|
|
202
|
+
|---|---|
|
|
203
|
+
| The user dismissed the sheet | `PASSKEY_CEREMONY_CANCELLED` |
|
|
204
|
+
| The device cannot do passkeys, or no adapter is wired | `PASSKEY_NOT_SUPPORTED` |
|
|
205
|
+
| The credential is already registered for this user | `PASSKEY_ALREADY_REGISTERED` |
|
|
206
|
+
| The action needs a fresh re-authentication first | `PASSKEY_STEP_UP_REQUIRED` |
|
|
207
|
+
| Your module broke the contract (returned a non-credential) | `PASSKEY_ADAPTER_ERROR` — an integration bug, never a retry |
|
|
208
|
+
| Sign-in succeeded but the tenant demands another factor | `PASSKEY_ADDITIONAL_STEP_REQUIRED` — **no tokens are issued**; route the user into your MFA flow, do not show "failed" |
|
|
209
|
+
| The user tried to delete their only sign-in method | `PASSKEY_LAST_METHOD` — "add another method first", not a generic failure |
|
|
210
|
+
| The phone was offline | `PASSKEY_NETWORK_ERROR` — retryable; do **not** sign the user out |
|
|
211
|
+
| The session is genuinely gone | `PASSKEY_SESSION_EXPIRED` — re-authenticate |
|
|
212
|
+
| Too many attempts | `PASSKEY_RATE_LIMITED` (`retryAfterMs` when the server sent one) |
|
|
213
|
+
| Anything else the OS reported | `PASSKEY_CEREMONY_FAILED` (retryable) |
|
|
214
|
+
|
|
215
|
+
Every error carries a `nextAction` (`retry` / `abort` / `none`) — branch on that rather than on the code
|
|
216
|
+
when all you need is "should the user try again". `PASSKEY_ADDITIONAL_STEP_REQUIRED` is the one most
|
|
217
|
+
likely to be mishandled: it arrives on a **successful** ceremony (HTTP 200) and simply means the tenant
|
|
218
|
+
requires a further factor before a session is issued.
|
|
219
|
+
|
|
220
|
+
Treat `PASSKEY_CEREMONY_CANCELLED` as a non-event — no error banner. It is by far the most frequent
|
|
221
|
+
outcome, and it also covers **system-initiated** dismissal (a swipe-down, an incoming call, the app
|
|
222
|
+
being backgrounded), not just a deliberate "Cancel". Users who dismissed a sheet on purpose do not
|
|
223
|
+
want to be told they failed.
|
|
224
|
+
|
|
225
|
+
**Mapping platform exceptions in your module.** Your native module reports failures with a `code`;
|
|
226
|
+
these are the codes the bridge understands, and everything else is re-thrown unchanged and surfaces
|
|
227
|
+
as `PASSKEY_CEREMONY_FAILED`:
|
|
228
|
+
|
|
229
|
+
| Platform exception | Your module's `code` |
|
|
230
|
+
|---|---|
|
|
231
|
+
| iOS `ASAuthorizationError.canceled` | `PASSKEY_CANCELLED` |
|
|
232
|
+
| iOS `ASAuthorizationError.failed` / `.invalidResponse` | *(re-throw unchanged)* |
|
|
233
|
+
| iOS — passkeys unavailable on this OS version | `PASSKEY_UNSUPPORTED` |
|
|
234
|
+
| Android `GetCredentialCancellationException` / `CreateCredentialCancellationException` | `PASSKEY_CANCELLED` |
|
|
235
|
+
| Android `GetCredentialUnsupportedException` / `CreateCredentialUnsupportedException` | `PASSKEY_UNSUPPORTED` |
|
|
236
|
+
| Android `CreateCredentialNoCreateOptionException` (no credential provider) | `PASSKEY_UNSUPPORTED` — fall back immediately, do not retry |
|
|
237
|
+
| Android `NoCredentialException` (the user has no passkey here) | `PASSKEY_NO_CREDENTIAL` |
|
|
238
|
+
| A community wrapper's own error codes | map them onto the three above, or re-throw |
|
|
239
|
+
| **Everything else** | **re-throw unchanged** → the SDK reports `PASSKEY_CEREMONY_FAILED` |
|
|
240
|
+
|
|
241
|
+
That last row is a known, documented compromise: the SDK's error vocabulary is closed and deliberately
|
|
242
|
+
small, so a platform-specific cause that has no code of its own arrives as a generic retryable failure.
|
|
243
|
+
Do **not** rewrite an error's `name` in your shim to force a different classification — the SDK reads
|
|
244
|
+
the `code`, and a renamed error is a lie the whole taxonomy then propagates.
|
|
245
|
+
|
|
246
|
+
**Development builds show request headers.** A host's network inspector / debugging interceptor can see
|
|
247
|
+
the passkey requests, including the `X-Step-Up-Token` header. That is a property of your development
|
|
248
|
+
build, not of the SDK: the token is held in memory for the duration of the call and never written to
|
|
249
|
+
device storage. Do not log headers in production builds.
|
|
250
|
+
|
|
251
|
+
### Deployment prerequisite — RP-ID binding
|
|
252
|
+
|
|
253
|
+
Passkeys are bound to a **relying-party identifier** (your domain), and the OS refuses to run a
|
|
254
|
+
ceremony for an app that cannot prove it belongs to that domain. Before a single passkey works you
|
|
255
|
+
must publish the association files and declare the entitlement:
|
|
256
|
+
|
|
257
|
+
- **iOS** — an `apple-app-site-association` file served over HTTPS at your domain, plus the
|
|
258
|
+
Associated Domains entitlement (`webcredentials:example.com`).
|
|
259
|
+
- **Android** — a Digital Asset Links (`assetlinks.json`) file at your domain listing your app's
|
|
260
|
+
signing-certificate fingerprint.
|
|
261
|
+
|
|
262
|
+
Until both are in place the ceremony fails on the device with no useful message. These two files are
|
|
263
|
+
the prerequisite that turns "passwordless" into "phishing-resistant": with the Associated Domains
|
|
264
|
+
entitlement and the Digital Asset Links file in place, the OS will only offer a credential to an app
|
|
265
|
+
it has verified against the domain that credential was created for — and without them it offers
|
|
266
|
+
nothing at all.
|
|
267
|
+
|
|
268
|
+
### Symptom → cause
|
|
269
|
+
|
|
270
|
+
| Symptom | Cause |
|
|
271
|
+
|---|---|
|
|
272
|
+
| The sheet never appears | No association file / entitlement (see above), or `isSupported()` is false |
|
|
273
|
+
| Every ceremony returns `PASSKEY_CEREMONY_FAILED` on a real device | The request is being mutated in your shim — pass the JSON string through untouched |
|
|
274
|
+
| It works in a debug build, not in a release build | Android: the release signing certificate is not in `assetlinks.json` |
|
|
275
|
+
| A field is missing on the OS side | Your wrapper passes objects, not the request string — `undefined` was dropped across the bridge |
|
|
276
|
+
| Sign-in succeeds on the device but the app stays signed out | The token submission was rejected — the hook reports this rather than claiming success |
|
|
277
|
+
|
|
278
|
+
A generic ceremony failure on a first integration is, in practice, almost always a missing or incorrect
|
|
279
|
+
associated-domains / `assetlinks.json` configuration. The error vocabulary cannot tell you that (the OS
|
|
280
|
+
does not tell *us*), so this line is the diagnostics: check the association files first, before you
|
|
281
|
+
suspect the SDK or your module.
|
|
282
|
+
|
|
283
|
+
**Two components, one sheet.** The passkey sheet is an operating-system modal: there is exactly one, for
|
|
284
|
+
the whole device. If a sign-in button in your header and a passkey screen in your settings both call
|
|
285
|
+
`usePasskeys()`, the SDK's lock lets only one of them open a ceremony — the second call is refused
|
|
286
|
+
locally with `PASSKEY_INVALID_INPUT` ("a ceremony is already in progress") and surfaces in that hook's
|
|
287
|
+
`error`. Note the busy flags (`isSigningIn`, `isSteppingUp`, …) are **per hook instance**: they tell you
|
|
288
|
+
about *your* component's call, not about the other one's. If you have two entry points on one screen,
|
|
289
|
+
drive both from a single `usePasskeys()` instance, or render one of them behind the other's busy state.
|
|
290
|
+
|
|
291
|
+
**Expo Go and emulators.** Expo Go cannot load a custom native module, so passkeys need a development
|
|
292
|
+
build. An Android emulator without Google Play services has no credential provider; an iOS simulator
|
|
293
|
+
needs a signed-in Apple account. A device is the only environment that proves the wiring.
|
|
294
|
+
|
|
295
|
+
**Orphaned credentials.** If registration succeeds on the device but the server never records it, the
|
|
296
|
+
authenticator holds a credential the server does not know about. Registering again with the same
|
|
297
|
+
authenticator returns `PASSKEY_ALREADY_REGISTERED` — that is the documented way out, not an error to
|
|
298
|
+
hide.
|
|
299
|
+
|
|
300
|
+
**Signing in as a different user — call `signOut()` first, and here is what happens if you don't.** A
|
|
301
|
+
passkey sign-in while another user's session is live is **not** an account switch. The token runtime
|
|
302
|
+
detects the change of subject and **clears the existing session** — user A is signed out — and the hook
|
|
303
|
+
then reports `PASSKEY_REQUEST_FAILED`. Its `nextAction` is `retry`, and a retry *will* succeed (the old
|
|
304
|
+
session is already gone), so from the user's seat it looks like "the first tap logged me out and did
|
|
305
|
+
nothing; the second tap worked". Gate your UI on the current session instead: sign out, then sign in.
|
|
306
|
+
|
|
307
|
+
**Bearer, not DPoP, on the passkey legs today.** The tokens a passkey sign-in returns are used exactly
|
|
308
|
+
like any other token this SDK issues; sender-constraining them is a separate concern from the ceremony.
|
|
309
|
+
|
|
310
|
+
**Expo web.** `usePasskeys()` on RN targets the native ceremony. There is no browser fallback here —
|
|
311
|
+
without a wired adapter the hook fails closed with `PASSKEY_NOT_SUPPORTED` rather than quietly
|
|
312
|
+
switching to a different ceremony. For the web, use `@rakomi/react`.
|
|
313
|
+
|
|
49
314
|
## Security defaults
|
|
50
315
|
|
|
51
316
|
- Refresh tokens stored in `expo-secure-store` with `keychainAccessible: AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` (no iCloud Keychain sync).
|
package/SECURITY.md
CHANGED
|
@@ -11,7 +11,7 @@ While these packages remain pre-1.0 (`0.x`), they carry **no stability or suppor
|
|
|
11
11
|
From version **1.0** onward, Rakomi maintains the current (N) and previous (N-1) MAJOR in parallel, with N-1 receiving
|
|
12
12
|
security-only fixes. The CRA support period for each MAJOR is determined in accordance with
|
|
13
13
|
**CRA Art. 13(8)** — at least five years, or the product's expected use time where shorter. The authoritative, machine-readable support windows are published at
|
|
14
|
-
[`https://rakomi.com/.well-known/sdk-support.json`](https://rakomi.com/.well-known/sdk-support.json)
|
|
14
|
+
[`https://api.rakomi.com/.well-known/sdk-support.json`](https://api.rakomi.com/.well-known/sdk-support.json)
|
|
15
15
|
and rendered for humans on the [SDK Support & Lifecycle page](https://rakomi.com/sdk-support). This
|
|
16
16
|
document points at that single source rather than re-typing dated rows.
|
|
17
17
|
|
package/THIRD-PARTY-NOTICES
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
|
|
2
2
|
|
|
3
|
-
For the npm package: @rakomi/react-native@0.
|
|
3
|
+
For the npm package: @rakomi/react-native@0.3.0
|
|
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
|