@rakomi/react-native 0.0.0 → 0.2.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 +90 -0
- package/LICENSE +21 -0
- package/README.md +327 -2
- package/SECURITY.md +206 -0
- package/THIRD-PARTY-NOTICES +11 -0
- package/dist/index.cjs +2788 -0
- package/dist/index.d.cts +698 -0
- package/dist/index.d.ts +698 -0
- package/dist/index.js +2681 -0
- package/dist/native/index.cjs +646 -0
- package/dist/native/index.d.cts +143 -0
- package/dist/native/index.d.ts +143 -0
- package/dist/native/index.js +637 -0
- package/dist/passkey-adapter-D_Z3Z7lV.d.cts +469 -0
- package/dist/passkey-adapter-D_Z3Z7lV.d.ts +469 -0
- package/package.json +96 -5
- package/sbom.cdx.json +72 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { CryptoProvider, KeyValueStore, PasskeyCeremonyAdapter } from '@rakomi/sdk-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `NativeAuthAdapter` — single typed surface that swaps every
|
|
5
|
+
* browser-coupled primitive in `@rakomi/react` for an RN/Expo equivalent.
|
|
6
|
+
*
|
|
7
|
+
* It is the source of truth for which native module backs each capability.
|
|
8
|
+
* The default impl in `./expo-adapter.ts` wires Expo modules; bare-RN consumers
|
|
9
|
+
* wire their own via `<RakomiProvider nativeAdapter={...}>`.
|
|
10
|
+
*
|
|
11
|
+
* Forward-compat slots (typed, optional, no default impl):
|
|
12
|
+
* - `verifiers` for EUDI Wallet attestation (eIDAS 2 / Reg. 2024/1183, end-2026 mandate).
|
|
13
|
+
* - `dpopProver` for RFC 9449 DPoP.
|
|
14
|
+
* - `pushAuthorizationRequest` for RFC 9126 PAR.
|
|
15
|
+
*
|
|
16
|
+
* Hardening rules:
|
|
17
|
+
* - The provider MUST `Object.freeze` the adapter on first use.
|
|
18
|
+
* - Adapter methods MUST NOT log token values.
|
|
19
|
+
* - Adapter is the ONLY layer touching expo-* modules — keeps SDK core platform-neutral.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface BrowserAuthSessionOptions {
|
|
23
|
+
/**
|
|
24
|
+
* iOS-only. Defaults to `true` for stricter privacy
|
|
25
|
+
* (no shared cookies). Consumer can opt out via `<RakomiProvider browserPreferEphemeralSession={false}>`.
|
|
26
|
+
*/
|
|
27
|
+
preferEphemeralSession?: boolean;
|
|
28
|
+
}
|
|
29
|
+
type BrowserAuthSessionResult = {
|
|
30
|
+
type: 'success';
|
|
31
|
+
url: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'cancel';
|
|
34
|
+
} | {
|
|
35
|
+
type: 'dismiss';
|
|
36
|
+
} | {
|
|
37
|
+
type: 'locked';
|
|
38
|
+
};
|
|
39
|
+
interface SystemBrowser {
|
|
40
|
+
/**
|
|
41
|
+
* Open `authUrl` in the OS system browser (ASWebAuthenticationSession on iOS,
|
|
42
|
+
* Custom Tabs on Android). The OS returns a `redirectUri`-prefixed URL when
|
|
43
|
+
* the OAuth provider redirects.
|
|
44
|
+
*
|
|
45
|
+
* Implementations MUST NOT use any kind of WebView (RFC 8252).
|
|
46
|
+
*/
|
|
47
|
+
openAuthSession(authUrl: string, redirectUri: string, options?: BrowserAuthSessionOptions): Promise<BrowserAuthSessionResult>;
|
|
48
|
+
}
|
|
49
|
+
interface DeepLinkSubscription {
|
|
50
|
+
/** Stop receiving callbacks. Idempotent. */
|
|
51
|
+
remove(): void;
|
|
52
|
+
}
|
|
53
|
+
interface DeepLinkProvider {
|
|
54
|
+
/**
|
|
55
|
+
* Return the URL the app was launched with, if any. Cold start case.
|
|
56
|
+
*/
|
|
57
|
+
getInitialUrl(): Promise<string | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to incoming deep links. Listener is called with each URL.
|
|
60
|
+
* Implementations MUST debounce duplicate URLs within ~60s (single-use guard)
|
|
61
|
+
* — but typically the SDK layer enforces this, not the adapter.
|
|
62
|
+
*/
|
|
63
|
+
addListener(listener: (url: string) => void): DeepLinkSubscription;
|
|
64
|
+
}
|
|
65
|
+
type BiometricResult = {
|
|
66
|
+
success: true;
|
|
67
|
+
} | {
|
|
68
|
+
success: false;
|
|
69
|
+
reason: 'cancelled' | 'lockout' | 'not_enrolled' | 'unavailable' | 'unknown';
|
|
70
|
+
};
|
|
71
|
+
interface BiometricGate {
|
|
72
|
+
/**
|
|
73
|
+
* Return `true` if the device has hardware AND the user has enrolled biometrics
|
|
74
|
+
* (or a passcode, when `disableDeviceFallback === false`).
|
|
75
|
+
*/
|
|
76
|
+
isAvailable(): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Prompt the user for biometric authentication.
|
|
79
|
+
*
|
|
80
|
+
* `strict: true` disables passcode fallback (`disableDeviceFallback: true`).
|
|
81
|
+
* Default is `false` (allow device passcode).
|
|
82
|
+
*/
|
|
83
|
+
authenticate(options: {
|
|
84
|
+
promptMessage: string;
|
|
85
|
+
strict?: boolean;
|
|
86
|
+
}): Promise<BiometricResult>;
|
|
87
|
+
}
|
|
88
|
+
type AppStateValue = 'active' | 'background' | 'inactive' | 'unknown';
|
|
89
|
+
interface AppStateSubscription {
|
|
90
|
+
remove(): void;
|
|
91
|
+
}
|
|
92
|
+
interface AppLifecycle {
|
|
93
|
+
/**
|
|
94
|
+
* Subscribe to AppState transitions.
|
|
95
|
+
*
|
|
96
|
+
* Android Q+ fires `AppState` twice on keyboard
|
|
97
|
+
* dismiss — the SDK layer (not the adapter) MUST debounce ~300ms.
|
|
98
|
+
*/
|
|
99
|
+
addStateChangeListener(listener: (next: AppStateValue) => void): AppStateSubscription;
|
|
100
|
+
/** Read current state synchronously. */
|
|
101
|
+
getCurrent(): AppStateValue;
|
|
102
|
+
}
|
|
103
|
+
interface NetInfoSubscription {
|
|
104
|
+
remove(): void;
|
|
105
|
+
}
|
|
106
|
+
interface ConnectivityProvider {
|
|
107
|
+
/** Resolve once with the current connectivity state. */
|
|
108
|
+
isConnected(): Promise<boolean>;
|
|
109
|
+
/** Subscribe to connectivity transitions. */
|
|
110
|
+
addListener(listener: (isConnected: boolean) => void): NetInfoSubscription;
|
|
111
|
+
}
|
|
112
|
+
/** Reserved for EUDI Wallet attestation verifiers (eIDAS 2). */
|
|
113
|
+
interface AttestationVerifier {
|
|
114
|
+
readonly id: string;
|
|
115
|
+
verify(presentation: unknown): Promise<{
|
|
116
|
+
ok: boolean;
|
|
117
|
+
reason?: string;
|
|
118
|
+
}>;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* RFC 9449 DPoP proof generator — the canonical cross-port prover contract.
|
|
122
|
+
* `@rakomi/node`'s `createDpopProver` is the conformance oracle the proof
|
|
123
|
+
* *shape* matches byte-for-byte in structure.
|
|
124
|
+
*
|
|
125
|
+
* SECURITY — the production RN prover MUST bridge signing to the native
|
|
126
|
+
* keystore (iOS Keychain / Secure Enclave, Android Keystore/StrongBox) and MUST
|
|
127
|
+
* NEVER sign in the JS bundle: only `{jti, iat, nonce?}` + the signature cross
|
|
128
|
+
* the bridge; the private key never leaves the secure element. The prover OWNS
|
|
129
|
+
* every security-relevant proof field — `jti` (CSPRNG UUID, fresh per call),
|
|
130
|
+
* `iat` (seconds NumericDate), `typ: "dpop+jwt"`, the public-only `jwk`, and the
|
|
131
|
+
* pinned `alg` (`ES256` — the cross-port baseline, Secure-Enclave-eligible;
|
|
132
|
+
* NEVER derived from a key or any input). The caller supplies ONLY the canonical
|
|
133
|
+
* request binding (`htm`/`htu`) and the optional server `nonce`; it can never
|
|
134
|
+
* influence the algorithm or inject claims, and the proof carries NO PII
|
|
135
|
+
* (exactly `{htm, htu, jti, iat}` body + optional `nonce`).
|
|
136
|
+
*/
|
|
137
|
+
interface DpopProofInput {
|
|
138
|
+
/** HTTP method of the bound request — `"POST"` for the refresh call. */
|
|
139
|
+
htm: string;
|
|
140
|
+
/**
|
|
141
|
+
* The ALREADY-canonical `htu` (RFC 9449 §4.3 — scheme+host+path, query/fragment
|
|
142
|
+
* stripped, default port stripped). The SDK decision layer canonicalizes; the
|
|
143
|
+
* prover signs it verbatim and MUST NOT re-derive or mutate it.
|
|
144
|
+
*/
|
|
145
|
+
htu: string;
|
|
146
|
+
/** Optional RFC 9449 §8 server nonce, echoed into the proof on a retry. */
|
|
147
|
+
nonce?: string;
|
|
148
|
+
}
|
|
149
|
+
interface DpopProver {
|
|
150
|
+
/**
|
|
151
|
+
* Build a compact-serialized DPoP-proof JWT for the bound request. Resolves to
|
|
152
|
+
* the `DPoP` header value. The prover signs with the session's single native
|
|
153
|
+
* keypair (one keypair per session — never re-generated per call). MUST reject
|
|
154
|
+
* (never return an empty/falsy string) if the native signer is unavailable so
|
|
155
|
+
* the SDK surfaces `dpop_prover_unavailable` instead of a silent Bearer call.
|
|
156
|
+
*/
|
|
157
|
+
createProof(input: DpopProofInput): Promise<string>;
|
|
158
|
+
/**
|
|
159
|
+
* RFC 7638 SHA-256 thumbprint of the prover's public JWK — lets the SDK's
|
|
160
|
+
* bound-state cross-check pair `token_type === "DPoP"` with a `jkt` sanity
|
|
161
|
+
* check (`jkt`-continuity). Resolves the SAME value for the life
|
|
162
|
+
* of the session's keypair.
|
|
163
|
+
*/
|
|
164
|
+
jktHint(): Promise<string>;
|
|
165
|
+
}
|
|
166
|
+
/** Reserved for RFC 9126 PAR (Pushed Authorization Requests). */
|
|
167
|
+
interface ParClient {
|
|
168
|
+
push(authorizationRequest: Record<string, string>): Promise<{
|
|
169
|
+
requestUri: string;
|
|
170
|
+
expiresIn: number;
|
|
171
|
+
}>;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Background-fetch contract — best-effort token refresh while the app is suspended
|
|
175
|
+
* (Expo Task Manager / expo-background-fetch on Expo, BGTaskScheduler on iOS / WorkManager on Android
|
|
176
|
+
* for bare-RN consumers).
|
|
177
|
+
*
|
|
178
|
+
* The OS controls scheduling; the SDK only registers the work and provides the handler.
|
|
179
|
+
* best-effort, NOT guaranteed delivery.
|
|
180
|
+
*/
|
|
181
|
+
interface BackgroundTask {
|
|
182
|
+
/**
|
|
183
|
+
* Register a periodic refresh task. The handler is called by the OS at OS-decided intervals
|
|
184
|
+
* (≥15 min on Android; OS-throttled on iOS). Returns an unregister function.
|
|
185
|
+
*/
|
|
186
|
+
register(handler: () => Promise<void>, options?: {
|
|
187
|
+
minimumIntervalSeconds?: number;
|
|
188
|
+
}): Promise<() => Promise<void>>;
|
|
189
|
+
/** Resolve `true` if the OS reports background-fetch as available + not user-disabled. */
|
|
190
|
+
isAvailable(): Promise<boolean>;
|
|
191
|
+
}
|
|
192
|
+
interface NativeAuthAdapter {
|
|
193
|
+
readonly crypto: CryptoProvider;
|
|
194
|
+
readonly storage: KeyValueStore;
|
|
195
|
+
readonly browser: SystemBrowser;
|
|
196
|
+
readonly deepLink: DeepLinkProvider;
|
|
197
|
+
readonly biometric: BiometricGate;
|
|
198
|
+
readonly appLifecycle: AppLifecycle;
|
|
199
|
+
readonly connectivity: ConnectivityProvider;
|
|
200
|
+
readonly verifiers?: readonly AttestationVerifier[];
|
|
201
|
+
readonly dpopProver?: DpopProver;
|
|
202
|
+
readonly par?: ParClient;
|
|
203
|
+
/** Optional best-effort background refresh hook. */
|
|
204
|
+
readonly backgroundTask?: BackgroundTask;
|
|
205
|
+
/**
|
|
206
|
+
* The passkey ceremony, delegated to the platform.
|
|
207
|
+
*
|
|
208
|
+
* The contract is `@rakomi/sdk-core`'s `PasskeyCeremonyAdapter` — imported, never re-declared
|
|
209
|
+
* here. A second interface for the same thing would let the browser binding and the native
|
|
210
|
+
* binding drift, and the conformance suite both of them run could only ever test one of them.
|
|
211
|
+
*
|
|
212
|
+
* Build one with `createNativePasskeyAdapter({ module })` from `@rakomi/react-native/native`.
|
|
213
|
+
* On Expo web a host may put a browser adapter in this same slot — the slot is platform-agnostic.
|
|
214
|
+
*
|
|
215
|
+
* A future SECOND ceremony type (a device-bound key, a hybrid/CTAP flow) gets its OWN optional
|
|
216
|
+
* slot; this one never becomes a type union. A union would destroy the totality of the shared
|
|
217
|
+
* conformance suite, which validates exactly ONE contract.
|
|
218
|
+
*/
|
|
219
|
+
readonly passkeys?: PasskeyCeremonyAdapter;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* `tokenCache` injection sugar (shorthand).
|
|
223
|
+
*
|
|
224
|
+
* If a consumer just wants to override storage without re-implementing the full adapter,
|
|
225
|
+
* they pass `tokenCache` and the SDK constructs an adapter that delegates only
|
|
226
|
+
* `storage` to `tokenCache` while inheriting the rest from the default Expo adapter.
|
|
227
|
+
*/
|
|
228
|
+
interface TokenCache {
|
|
229
|
+
getToken(key: string): Promise<string | null>;
|
|
230
|
+
saveToken(key: string, value: string): Promise<void>;
|
|
231
|
+
clearToken(key: string): Promise<void>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The raw native-module surface a RN host exposes for DPoP signing — a
|
|
236
|
+
* TurboModule / legacy NativeModule whose methods take POSITIONAL arguments and
|
|
237
|
+
* return Promises (RN bridge convention: no `undefined` across the bridge — an
|
|
238
|
+
* absent nonce is passed as `null`).
|
|
239
|
+
*
|
|
240
|
+
* contract the native implementation MUST honour:
|
|
241
|
+
* - Generate ONE session keypair in the secure element on first use and hold it
|
|
242
|
+
* for the module instance's lifetime (one keypair per session); NEVER
|
|
243
|
+
* regenerate per call (a fresh key ⇒ `jkt` mismatch ⇒ 401 on every refresh).
|
|
244
|
+
* - `createDpopProof` builds + signs the FULL compact proof JWT natively and
|
|
245
|
+
* resolves the `DPoP` header value; it owns `jti`/`iat`/`typ`/`jwk`/`alg`.
|
|
246
|
+
* - Reject (Promise rejection) when the secure element is unavailable — NEVER
|
|
247
|
+
* resolve an empty string or a Bearer-shaped placeholder.
|
|
248
|
+
*/
|
|
249
|
+
interface NativeDpopModuleSpec {
|
|
250
|
+
/**
|
|
251
|
+
* Build + sign the compact DPoP-proof JWT in the secure element for the bound
|
|
252
|
+
* request. `nonce` is `null` unless this is the RFC 9449 §8 nonce retry.
|
|
253
|
+
* Resolves the `DPoP` header value.
|
|
254
|
+
*/
|
|
255
|
+
createDpopProof(htm: string, htu: string, nonce: string | null): Promise<string>;
|
|
256
|
+
/** RFC 7638 SHA-256 thumbprint of the session public key (stable for the key's life). */
|
|
257
|
+
dpopJktThumbprint(): Promise<string>;
|
|
258
|
+
}
|
|
259
|
+
interface CreateNativeDpopProverOptions {
|
|
260
|
+
/**
|
|
261
|
+
* The host's native DPoP module. On bare RN this is the linked native module
|
|
262
|
+
* (`NativeModules.RakomiDpop`); on Expo it is the Expo module's JS surface.
|
|
263
|
+
* The module MUST be backed by the platform secure element.
|
|
264
|
+
*/
|
|
265
|
+
module: NativeDpopModuleSpec;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Adapt a host's native DPoP signing module to the canonical {@link DpopProver}
|
|
269
|
+
* contract. Wire the result into the consumer's `NativeAuthAdapter.dpopProver`
|
|
270
|
+
* slot; `<RakomiProvider>` then auto-constructs the session-scoped `DpopSession`.
|
|
271
|
+
*
|
|
272
|
+
* The adapter enforces the no-silent-downgrade invariant: if the native
|
|
273
|
+
* signer rejects OR returns a falsy proof, `createProof` throws so the refresh
|
|
274
|
+
* path surfaces `auth/dpop_prover_unavailable` and makes NO proof-less network
|
|
275
|
+
* call — it never falls back to an empty/Bearer request.
|
|
276
|
+
*
|
|
277
|
+
* @public — additive-only.
|
|
278
|
+
*/
|
|
279
|
+
declare function createNativeDpopProver(options: CreateNativeDpopProverOptions): DpopProver;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Default Expo adapter — wires the `NativeAuthAdapter` interface to Expo modules.
|
|
283
|
+
*
|
|
284
|
+
* Modules are imported dynamically inside the factory so:
|
|
285
|
+
* - Bare-RN consumers who pass a custom adapter don't pay the Expo bundle cost.
|
|
286
|
+
* - Consumers who never use biometric don't pay the `expo-local-authentication`
|
|
287
|
+
* cost (lazy-load).
|
|
288
|
+
*
|
|
289
|
+
* Hardening:
|
|
290
|
+
* - The returned adapter is `Object.freeze`-able by callers; this module returns
|
|
291
|
+
* a plain object — `<RakomiProvider>` freezes it on mount.
|
|
292
|
+
* - No `console.log` of token values anywhere in this file.
|
|
293
|
+
*
|
|
294
|
+
* Native-passkey integration (`react-native-passkey`, ASAuthorization) is
|
|
295
|
+
* out-of-scope here.
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Build the default Expo-backed `NativeAuthAdapter`.
|
|
300
|
+
*
|
|
301
|
+
* Each capability is lazily resolved on first use to keep the cold-import bundle
|
|
302
|
+
* small (perf budget). The resolution is memoised internally.
|
|
303
|
+
*/
|
|
304
|
+
declare function createDefaultExpoAdapter(options?: CreateDefaultExpoAdapterOptions): NativeAuthAdapter;
|
|
305
|
+
interface CreateDefaultExpoAdapterOptions {
|
|
306
|
+
/**
|
|
307
|
+
* Override storage with the consumer's own token cache (sugar).
|
|
308
|
+
* When provided, `crypto`/`browser`/`deepLink`/`biometric`/`appLifecycle`/`connectivity`
|
|
309
|
+
* still come from Expo defaults; only storage is replaced.
|
|
310
|
+
*/
|
|
311
|
+
tokenCache?: TokenCache;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The raw native-module surface a host exposes for passkey ceremonies.
|
|
316
|
+
*
|
|
317
|
+
* It is a structural duck-type: no brand, no symbol, no base class. **Any module satisfying this
|
|
318
|
+
* spec is a drop-in replacement; the SDK does not know which one you used** — your own Swift/Kotlin
|
|
319
|
+
* module, a community package, or a future first-party Expo module.
|
|
320
|
+
*
|
|
321
|
+
* ## What crosses the bridge
|
|
322
|
+
*
|
|
323
|
+
* Both ceremony methods take the server's options as a **JSON string** and resolve the finished
|
|
324
|
+
* credential as a **JSON string**. See the file header for why this is not an object.
|
|
325
|
+
*
|
|
326
|
+
* ## What the module MUST return (W3C WebAuthn **Level 3** JSON serialization, §5.1.8 / §5.1.9 —
|
|
327
|
+
* Candidate Recommendation; these JSON dictionaries do not exist in Level 2)
|
|
328
|
+
*
|
|
329
|
+
* `createPasskey` (registration):
|
|
330
|
+
*
|
|
331
|
+
* | member | required |
|
|
332
|
+
* |---|---|
|
|
333
|
+
* | `id`, `rawId`, `type: "public-key"` | MUST |
|
|
334
|
+
* | `response.clientDataJSON`, `response.attestationObject` | MUST |
|
|
335
|
+
* | `response.authenticatorData`, `response.publicKeyAlgorithm` | MUST |
|
|
336
|
+
* | `response.transports` | MUST (omitting it silently degrades later autofill) |
|
|
337
|
+
* | `clientExtensionResults` | MUST |
|
|
338
|
+
* | `authenticatorAttachment`, `response.publicKey` | optional |
|
|
339
|
+
*
|
|
340
|
+
* `getPasskey` (assertion):
|
|
341
|
+
*
|
|
342
|
+
* | member | required |
|
|
343
|
+
* |---|---|
|
|
344
|
+
* | `id`, `rawId`, `type: "public-key"` | MUST |
|
|
345
|
+
* | `response.clientDataJSON`, `response.authenticatorData`, `response.signature` | MUST |
|
|
346
|
+
* | `clientExtensionResults` | MUST |
|
|
347
|
+
* | `response.userHandle` | optional |
|
|
348
|
+
*
|
|
349
|
+
* **ENCODING — the single most common integration failure.** Every binary member is
|
|
350
|
+
* **base64url WITHOUT padding** (RFC 4648 **§5**). On iOS, `Data.base64EncodedString()` emits
|
|
351
|
+
* *standard* base64 (RFC 4648 **§4**, with `+`, `/` and `=`) — that is WRONG and the bridge rejects
|
|
352
|
+
* it with a message naming the field.
|
|
353
|
+
*
|
|
354
|
+
* (The bridge ENFORCES a subset of the table above — the fields the SDK's own fixtures carry and the
|
|
355
|
+
* server actually consumes. The rest are a MUST for you, not a MUST for the validator; sending them
|
|
356
|
+
* is what keeps your integration correct as the server tightens.)
|
|
357
|
+
*
|
|
358
|
+
* ## Rejection vocabulary — CLOSED
|
|
359
|
+
*
|
|
360
|
+
* Reject with an object carrying a `code`:
|
|
361
|
+
* - `PASSKEY_CANCELLED` — the user dismissed the sheet, or the OS cancelled.
|
|
362
|
+
* - `PASSKEY_UNSUPPORTED` — no passkey support on this OS version, or no credential provider.
|
|
363
|
+
* - `PASSKEY_NO_CREDENTIAL` — the user has no passkey for this relying party.
|
|
364
|
+
*
|
|
365
|
+
* Any other rejection is a genuine failure and is passed through untouched. **Do NOT set a
|
|
366
|
+
* rejection's `name` to `AbortError` / `NotAllowedError` / `TimeoutError` unless it really is a
|
|
367
|
+
* cancellation** — the SDK matches on those names and will read your intent as "the user cancelled".
|
|
368
|
+
*
|
|
369
|
+
* ## Stability (this is a SemVer surface even though we never compile your module)
|
|
370
|
+
*
|
|
371
|
+
* Your module is Swift or Kotlin: our typecheck will never see it, so a breaking change to this spec
|
|
372
|
+
* breaks every existing module **at runtime**, not at compile time — and the SDK's 24-month support
|
|
373
|
+
* policy means a module written today must still satisfy this spec two years from now. Therefore:
|
|
374
|
+
* - a new **optional** member → MINOR;
|
|
375
|
+
* - a new **optional trailing parameter** on an existing method → MINOR (a lower-arity function
|
|
376
|
+
* stays assignable — which is exactly why we do not reserve parameters "just in case");
|
|
377
|
+
* - a **required** method, a parameter-type change, or a change to the rejection vocabulary → MAJOR;
|
|
378
|
+
* - the rejection vocabulary is CLOSED: a new code is first a change in `@rakomi/sdk-core`, and only
|
|
379
|
+
* then a lane here.
|
|
380
|
+
*
|
|
381
|
+
* ## Orphan credentials — a residual you should know about
|
|
382
|
+
*
|
|
383
|
+
* If a registration ceremony is abandoned *after* the platform provider (iCloud Keychain / Google
|
|
384
|
+
* Password Manager) already created the credential, the user holds a passkey for your app that the
|
|
385
|
+
* server never learned about; their next sign-in with it fails. Implementing
|
|
386
|
+
* {@link NativePasskeyModuleSpec.cancelPasskeyRequest} narrows that window. The SDK does not
|
|
387
|
+
* reconcile orphans.
|
|
388
|
+
*/
|
|
389
|
+
interface NativePasskeyModuleSpec {
|
|
390
|
+
/**
|
|
391
|
+
* Whether this platform can run a passkey ceremony at all.
|
|
392
|
+
*
|
|
393
|
+
* **Advisory, not a verdict.** Every module in the ecosystem answers this from the OS version, so
|
|
394
|
+
* it says `true` on an Android emulator with no credential provider and on an iOS version where
|
|
395
|
+
* the platform provider does not exist. The SDK treats a throw or a non-`true` value as `false`
|
|
396
|
+
* (fail-closed) but cannot catch an honest `true` with no provider behind it — the authoritative
|
|
397
|
+
* verdict is the ceremony's own outcome.
|
|
398
|
+
*/
|
|
399
|
+
isPasskeySupported(): Promise<boolean>;
|
|
400
|
+
/** Run the registration ceremony. `requestJson` is the server's options, verbatim. */
|
|
401
|
+
createPasskey(requestJson: string): Promise<string>;
|
|
402
|
+
/** Run the assertion ceremony. `requestJson` is the server's options, verbatim. */
|
|
403
|
+
getPasskey(requestJson: string): Promise<string>;
|
|
404
|
+
/**
|
|
405
|
+
* Optional: is a *platform* authenticator (Face ID / fingerprint / screen lock) available?
|
|
406
|
+
*
|
|
407
|
+
* A UI hint only — never a gate. A synced passkey on another device is still a passkey, so a
|
|
408
|
+
* `false` here does not mean "no passkeys". Omit it and the SDK reports `null` (unknown) rather
|
|
409
|
+
* than guessing.
|
|
410
|
+
*/
|
|
411
|
+
isPlatformAuthenticatorAvailable?(): Promise<boolean>;
|
|
412
|
+
/**
|
|
413
|
+
* Optional: dismiss the in-flight native sheet (iOS `ASAuthorizationController.cancel()`;
|
|
414
|
+
* Android's `CredentialManager` calls are cancellable). Best-effort — a throw is ignored.
|
|
415
|
+
*
|
|
416
|
+
* It takes no request id because the native side has nothing to correlate against: the passkey sheet
|
|
417
|
+
* is an **OS-global modal**, and a cancel dismisses whatever is on screen. The SDK does NOT serialise
|
|
418
|
+
* ceremonies — nothing here can, since the sheet is owned by the OS — so the bridge instead fires the
|
|
419
|
+
* cancel only while the aborting ceremony is still the one in flight, making a stale abort a no-op
|
|
420
|
+
* rather than a teardown of somebody else's sheet. Preventing a second ceremony from being STARTED is
|
|
421
|
+
* a UI concern and belongs to the provider.
|
|
422
|
+
*/
|
|
423
|
+
cancelPasskeyRequest?(): void | Promise<void>;
|
|
424
|
+
}
|
|
425
|
+
interface CreateNativePasskeyAdapterOptions {
|
|
426
|
+
/**
|
|
427
|
+
* The host's native passkey module. On bare RN this is the linked native module
|
|
428
|
+
* (`NativeModules.RakomiPasskey`); on Expo it is the Expo module's JS surface; it may also be a
|
|
429
|
+
* community package wrapped to satisfy {@link NativePasskeyModuleSpec}.
|
|
430
|
+
*/
|
|
431
|
+
module: NativePasskeyModuleSpec;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Adapt a host's native passkey module to the canonical `PasskeyCeremonyAdapter` contract. Wire the
|
|
435
|
+
* result into `NativeAuthAdapter.passkeys`.
|
|
436
|
+
*
|
|
437
|
+
* The module's SHAPE is validated here, at wiring time, and a missing method throws loudly — in the
|
|
438
|
+
* host's adapter-construction code, where the bug is, rather than degrading to "passkeys are not
|
|
439
|
+
* supported on this device", which is a lie that hides the host's bug. An ABSENT adapter and a
|
|
440
|
+
* BROKEN adapter are different diagnoses and must not be collapsed.
|
|
441
|
+
*
|
|
442
|
+
* @public — additive-only.
|
|
443
|
+
*/
|
|
444
|
+
/**
|
|
445
|
+
* The core's ceremony contract, plus the one native-only capability the hook needs and the contract
|
|
446
|
+
* has no place for: the platform-authenticator hint. It is attached HERE rather than plumbed through
|
|
447
|
+
* the host, because the probe needs the MODULE and the hook only ever sees the adapter.
|
|
448
|
+
*
|
|
449
|
+
* Structurally a `PasskeyCeremonyAdapter`, so it drops into `nativeAdapter.passkeys` unchanged.
|
|
450
|
+
*/
|
|
451
|
+
interface NativePasskeyCeremonyAdapter extends PasskeyCeremonyAdapter {
|
|
452
|
+
/** `true` / `false` / `null` — and `null` means UNKNOWN, never "no". */
|
|
453
|
+
hasPlatformAuthenticator(): Promise<boolean | null>;
|
|
454
|
+
}
|
|
455
|
+
declare function createNativePasskeyAdapter(options: CreateNativePasskeyAdapterOptions): NativePasskeyCeremonyAdapter;
|
|
456
|
+
/**
|
|
457
|
+
* Ask the host's module whether a *platform* authenticator is available.
|
|
458
|
+
*
|
|
459
|
+
* `null` means **unknown**, and it is `null` in all three shapes of "the module does not really
|
|
460
|
+
* implement this", because in React Native a missing method has three shapes, not one: it is absent
|
|
461
|
+
* (a legacy `NativeModules` object), or it exists but throws synchronously (a TurboModule whose
|
|
462
|
+
* codegen compiled the spec but whose native side did not implement it), or it exists but rejects
|
|
463
|
+
* (an Expo module proxy). None of them may answer `false` — that would be the lie "this device has
|
|
464
|
+
* no platform authenticator" — and none of them may be inferred from `isSupported()`, because a
|
|
465
|
+
* synced passkey is a passkey without a local platform authenticator.
|
|
466
|
+
*/
|
|
467
|
+
declare function probePlatformAuthenticator(mod: NativePasskeyModuleSpec): Promise<boolean | null>;
|
|
468
|
+
|
|
469
|
+
export { type AppLifecycle as A, type BackgroundTask as B, type ConnectivityProvider as C, type DpopProver as D, type NativeAuthAdapter as N, type ParClient as P, type SystemBrowser as S, type TokenCache as T, type AppStateValue as a, type AttestationVerifier as b, type BiometricGate as c, type BiometricResult as d, type BrowserAuthSessionOptions as e, type BrowserAuthSessionResult as f, type CreateDefaultExpoAdapterOptions as g, type CreateNativeDpopProverOptions as h, type CreateNativePasskeyAdapterOptions as i, type DeepLinkProvider as j, type DpopProofInput as k, type NativeDpopModuleSpec as l, type NativePasskeyCeremonyAdapter as m, type NativePasskeyModuleSpec as n, createDefaultExpoAdapter as o, createNativeDpopProver as p, createNativePasskeyAdapter as q, type AppStateSubscription as r, type DeepLinkSubscription as s, type NetInfoSubscription as t, probePlatformAuthenticator as u };
|
package/package.json
CHANGED
|
@@ -1,7 +1,98 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rakomi/react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React Native / Expo SDK for Rakomi authentication.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rakomi",
|
|
7
|
+
"authentication",
|
|
8
|
+
"auth",
|
|
9
|
+
"oauth",
|
|
10
|
+
"oidc",
|
|
11
|
+
"react-native",
|
|
12
|
+
"expo",
|
|
13
|
+
"sdk"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/rakomidev/rakomi-js.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/rakomidev/rakomi-js/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/rakomidev/rakomi-js#readme",
|
|
23
|
+
"rakomi": {
|
|
24
|
+
"apiVersion": "2026-03-01"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"provenance": true
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"sideEffects": false,
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"./native": {
|
|
44
|
+
"import": {
|
|
45
|
+
"types": "./dist/native/index.d.ts",
|
|
46
|
+
"default": "./dist/native/index.js"
|
|
47
|
+
},
|
|
48
|
+
"require": {
|
|
49
|
+
"types": "./dist/native/index.d.cts",
|
|
50
|
+
"default": "./dist/native/index.cjs"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./package.json": "./package.json"
|
|
54
|
+
},
|
|
55
|
+
"main": "./dist/index.cjs",
|
|
56
|
+
"module": "./dist/index.js",
|
|
57
|
+
"types": "./dist/index.d.ts",
|
|
58
|
+
"author": "CRE8EVE Sp. z o.o.",
|
|
59
|
+
"files": [
|
|
60
|
+
"dist",
|
|
61
|
+
"!dist/metafile-*.json",
|
|
62
|
+
"README.md",
|
|
63
|
+
"LICENSE",
|
|
64
|
+
"SECURITY.md",
|
|
65
|
+
"COMPLIANCE.md",
|
|
66
|
+
"THIRD-PARTY-NOTICES",
|
|
67
|
+
"sbom.cdx.json"
|
|
68
|
+
],
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"expo": ">=54",
|
|
71
|
+
"react": ">=18",
|
|
72
|
+
"react-native": ">=0.81"
|
|
73
|
+
},
|
|
74
|
+
"peerDependenciesMeta": {
|
|
75
|
+
"expo": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"react-native": {
|
|
79
|
+
"optional": false
|
|
80
|
+
},
|
|
81
|
+
"react": {
|
|
82
|
+
"optional": false
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@rakomi/react": "0.2.0"
|
|
87
|
+
},
|
|
88
|
+
"dependencies": {
|
|
89
|
+
"jose": "^6.2.3",
|
|
90
|
+
"@rakomi/sdk-core": "^0.2.0"
|
|
91
|
+
},
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "NODE_OPTIONS=--max-old-space-size=4096 tsup",
|
|
94
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.typetest.json",
|
|
95
|
+
"lint": "eslint src/ test/ --max-warnings=0",
|
|
96
|
+
"test": "vitest run"
|
|
97
|
+
}
|
|
98
|
+
}
|
package/sbom.cdx.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bomFormat": "CycloneDX",
|
|
3
|
+
"specVersion": "1.6",
|
|
4
|
+
"version": 1,
|
|
5
|
+
"metadata": {
|
|
6
|
+
"supplier": {
|
|
7
|
+
"name": "CRE8EVE Sp. z o.o.",
|
|
8
|
+
"url": [
|
|
9
|
+
"https://rakomi.com"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"authors": [
|
|
13
|
+
{
|
|
14
|
+
"name": "CRE8EVE Sp. z o.o."
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"tools": {
|
|
18
|
+
"components": [
|
|
19
|
+
{
|
|
20
|
+
"type": "application",
|
|
21
|
+
"group": "rakomi",
|
|
22
|
+
"name": "generate-sbom",
|
|
23
|
+
"version": "sha256:9472206f8105"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"component": {
|
|
28
|
+
"type": "library",
|
|
29
|
+
"bom-ref": "pkg:npm/%40rakomi%2Freact-native@0.2.0",
|
|
30
|
+
"name": "@rakomi/react-native",
|
|
31
|
+
"version": "0.2.0",
|
|
32
|
+
"purl": "pkg:npm/%40rakomi%2Freact-native@0.2.0",
|
|
33
|
+
"licenses": [
|
|
34
|
+
{
|
|
35
|
+
"license": {
|
|
36
|
+
"id": "MIT"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"components": [
|
|
43
|
+
{
|
|
44
|
+
"type": "library",
|
|
45
|
+
"bom-ref": "pkg:npm/@rakomi/sdk-core@0.2.0",
|
|
46
|
+
"name": "@rakomi/sdk-core",
|
|
47
|
+
"version": "0.2.0",
|
|
48
|
+
"purl": "pkg:npm/@rakomi/sdk-core@0.2.0",
|
|
49
|
+
"licenses": [
|
|
50
|
+
{
|
|
51
|
+
"license": {
|
|
52
|
+
"id": "MIT"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"type": "library",
|
|
59
|
+
"bom-ref": "pkg:npm/jose@6.2.9",
|
|
60
|
+
"name": "jose",
|
|
61
|
+
"version": "6.2.9",
|
|
62
|
+
"purl": "pkg:npm/jose@6.2.9",
|
|
63
|
+
"licenses": [
|
|
64
|
+
{
|
|
65
|
+
"license": {
|
|
66
|
+
"id": "MIT"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|