@rakomi/react-native 0.0.0 → 0.1.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 +60 -0
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/SECURITY.md +206 -0
- package/THIRD-PARTY-NOTICES +11 -0
- package/dist/expo-adapter-CtPCEgue.d.cts +299 -0
- package/dist/expo-adapter-CtPCEgue.d.ts +299 -0
- package/dist/index.cjs +2109 -0
- package/dist/index.d.cts +634 -0
- package/dist/index.d.ts +634 -0
- package/dist/index.js +2004 -0
- package/dist/native/index.cjs +381 -0
- package/dist/native/index.d.cts +113 -0
- package/dist/native/index.d.ts +113 -0
- package/dist/native/index.js +377 -0
- package/package.json +96 -5
- package/sbom.cdx.json +66 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { CryptoProvider, KeyValueStore } 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
|
+
/**
|
|
207
|
+
* `tokenCache` injection sugar (shorthand).
|
|
208
|
+
*
|
|
209
|
+
* If a consumer just wants to override storage without re-implementing the full adapter,
|
|
210
|
+
* they pass `tokenCache` and the SDK constructs an adapter that delegates only
|
|
211
|
+
* `storage` to `tokenCache` while inheriting the rest from the default Expo adapter.
|
|
212
|
+
*/
|
|
213
|
+
interface TokenCache {
|
|
214
|
+
getToken(key: string): Promise<string | null>;
|
|
215
|
+
saveToken(key: string, value: string): Promise<void>;
|
|
216
|
+
clearToken(key: string): Promise<void>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The raw native-module surface a RN host exposes for DPoP signing — a
|
|
221
|
+
* TurboModule / legacy NativeModule whose methods take POSITIONAL arguments and
|
|
222
|
+
* return Promises (RN bridge convention: no `undefined` across the bridge — an
|
|
223
|
+
* absent nonce is passed as `null`).
|
|
224
|
+
*
|
|
225
|
+
* contract the native implementation MUST honour:
|
|
226
|
+
* - Generate ONE session keypair in the secure element on first use and hold it
|
|
227
|
+
* for the module instance's lifetime (one keypair per session); NEVER
|
|
228
|
+
* regenerate per call (a fresh key ⇒ `jkt` mismatch ⇒ 401 on every refresh).
|
|
229
|
+
* - `createDpopProof` builds + signs the FULL compact proof JWT natively and
|
|
230
|
+
* resolves the `DPoP` header value; it owns `jti`/`iat`/`typ`/`jwk`/`alg`.
|
|
231
|
+
* - Reject (Promise rejection) when the secure element is unavailable — NEVER
|
|
232
|
+
* resolve an empty string or a Bearer-shaped placeholder.
|
|
233
|
+
*/
|
|
234
|
+
interface NativeDpopModuleSpec {
|
|
235
|
+
/**
|
|
236
|
+
* Build + sign the compact DPoP-proof JWT in the secure element for the bound
|
|
237
|
+
* request. `nonce` is `null` unless this is the RFC 9449 §8 nonce retry.
|
|
238
|
+
* Resolves the `DPoP` header value.
|
|
239
|
+
*/
|
|
240
|
+
createDpopProof(htm: string, htu: string, nonce: string | null): Promise<string>;
|
|
241
|
+
/** RFC 7638 SHA-256 thumbprint of the session public key (stable for the key's life). */
|
|
242
|
+
dpopJktThumbprint(): Promise<string>;
|
|
243
|
+
}
|
|
244
|
+
interface CreateNativeDpopProverOptions {
|
|
245
|
+
/**
|
|
246
|
+
* The host's native DPoP module. On bare RN this is the linked native module
|
|
247
|
+
* (`NativeModules.RakomiDpop`); on Expo it is the Expo module's JS surface.
|
|
248
|
+
* The module MUST be backed by the platform secure element.
|
|
249
|
+
*/
|
|
250
|
+
module: NativeDpopModuleSpec;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Adapt a host's native DPoP signing module to the canonical {@link DpopProver}
|
|
254
|
+
* contract. Wire the result into the consumer's `NativeAuthAdapter.dpopProver`
|
|
255
|
+
* slot; `<RakomiProvider>` then auto-constructs the session-scoped `DpopSession`.
|
|
256
|
+
*
|
|
257
|
+
* The adapter enforces the no-silent-downgrade invariant: if the native
|
|
258
|
+
* signer rejects OR returns a falsy proof, `createProof` throws so the refresh
|
|
259
|
+
* path surfaces `auth/dpop_prover_unavailable` and makes NO proof-less network
|
|
260
|
+
* call — it never falls back to an empty/Bearer request.
|
|
261
|
+
*
|
|
262
|
+
* @public — additive-only.
|
|
263
|
+
*/
|
|
264
|
+
declare function createNativeDpopProver(options: CreateNativeDpopProverOptions): DpopProver;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Default Expo adapter — wires the `NativeAuthAdapter` interface to Expo modules.
|
|
268
|
+
*
|
|
269
|
+
* Modules are imported dynamically inside the factory so:
|
|
270
|
+
* - Bare-RN consumers who pass a custom adapter don't pay the Expo bundle cost.
|
|
271
|
+
* - Consumers who never use biometric don't pay the `expo-local-authentication`
|
|
272
|
+
* cost (lazy-load).
|
|
273
|
+
*
|
|
274
|
+
* Hardening:
|
|
275
|
+
* - The returned adapter is `Object.freeze`-able by callers; this module returns
|
|
276
|
+
* a plain object — `<RakomiProvider>` freezes it on mount.
|
|
277
|
+
* - No `console.log` of token values anywhere in this file.
|
|
278
|
+
*
|
|
279
|
+
* Native-passkey integration (`react-native-passkey`, ASAuthorization) is
|
|
280
|
+
* out-of-scope here.
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build the default Expo-backed `NativeAuthAdapter`.
|
|
285
|
+
*
|
|
286
|
+
* Each capability is lazily resolved on first use to keep the cold-import bundle
|
|
287
|
+
* small (perf budget). The resolution is memoised internally.
|
|
288
|
+
*/
|
|
289
|
+
declare function createDefaultExpoAdapter(options?: CreateDefaultExpoAdapterOptions): NativeAuthAdapter;
|
|
290
|
+
interface CreateDefaultExpoAdapterOptions {
|
|
291
|
+
/**
|
|
292
|
+
* Override storage with the consumer's own token cache (sugar).
|
|
293
|
+
* When provided, `crypto`/`browser`/`deepLink`/`biometric`/`appLifecycle`/`connectivity`
|
|
294
|
+
* still come from Expo defaults; only storage is replaced.
|
|
295
|
+
*/
|
|
296
|
+
tokenCache?: TokenCache;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
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 DeepLinkProvider as i, type DpopProofInput as j, type NativeDpopModuleSpec as k, createDefaultExpoAdapter as l, createNativeDpopProver as m, type AppStateSubscription as n, type DeepLinkSubscription as o, type NetInfoSubscription as p };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { CryptoProvider, KeyValueStore } 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
|
+
/**
|
|
207
|
+
* `tokenCache` injection sugar (shorthand).
|
|
208
|
+
*
|
|
209
|
+
* If a consumer just wants to override storage without re-implementing the full adapter,
|
|
210
|
+
* they pass `tokenCache` and the SDK constructs an adapter that delegates only
|
|
211
|
+
* `storage` to `tokenCache` while inheriting the rest from the default Expo adapter.
|
|
212
|
+
*/
|
|
213
|
+
interface TokenCache {
|
|
214
|
+
getToken(key: string): Promise<string | null>;
|
|
215
|
+
saveToken(key: string, value: string): Promise<void>;
|
|
216
|
+
clearToken(key: string): Promise<void>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The raw native-module surface a RN host exposes for DPoP signing — a
|
|
221
|
+
* TurboModule / legacy NativeModule whose methods take POSITIONAL arguments and
|
|
222
|
+
* return Promises (RN bridge convention: no `undefined` across the bridge — an
|
|
223
|
+
* absent nonce is passed as `null`).
|
|
224
|
+
*
|
|
225
|
+
* contract the native implementation MUST honour:
|
|
226
|
+
* - Generate ONE session keypair in the secure element on first use and hold it
|
|
227
|
+
* for the module instance's lifetime (one keypair per session); NEVER
|
|
228
|
+
* regenerate per call (a fresh key ⇒ `jkt` mismatch ⇒ 401 on every refresh).
|
|
229
|
+
* - `createDpopProof` builds + signs the FULL compact proof JWT natively and
|
|
230
|
+
* resolves the `DPoP` header value; it owns `jti`/`iat`/`typ`/`jwk`/`alg`.
|
|
231
|
+
* - Reject (Promise rejection) when the secure element is unavailable — NEVER
|
|
232
|
+
* resolve an empty string or a Bearer-shaped placeholder.
|
|
233
|
+
*/
|
|
234
|
+
interface NativeDpopModuleSpec {
|
|
235
|
+
/**
|
|
236
|
+
* Build + sign the compact DPoP-proof JWT in the secure element for the bound
|
|
237
|
+
* request. `nonce` is `null` unless this is the RFC 9449 §8 nonce retry.
|
|
238
|
+
* Resolves the `DPoP` header value.
|
|
239
|
+
*/
|
|
240
|
+
createDpopProof(htm: string, htu: string, nonce: string | null): Promise<string>;
|
|
241
|
+
/** RFC 7638 SHA-256 thumbprint of the session public key (stable for the key's life). */
|
|
242
|
+
dpopJktThumbprint(): Promise<string>;
|
|
243
|
+
}
|
|
244
|
+
interface CreateNativeDpopProverOptions {
|
|
245
|
+
/**
|
|
246
|
+
* The host's native DPoP module. On bare RN this is the linked native module
|
|
247
|
+
* (`NativeModules.RakomiDpop`); on Expo it is the Expo module's JS surface.
|
|
248
|
+
* The module MUST be backed by the platform secure element.
|
|
249
|
+
*/
|
|
250
|
+
module: NativeDpopModuleSpec;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Adapt a host's native DPoP signing module to the canonical {@link DpopProver}
|
|
254
|
+
* contract. Wire the result into the consumer's `NativeAuthAdapter.dpopProver`
|
|
255
|
+
* slot; `<RakomiProvider>` then auto-constructs the session-scoped `DpopSession`.
|
|
256
|
+
*
|
|
257
|
+
* The adapter enforces the no-silent-downgrade invariant: if the native
|
|
258
|
+
* signer rejects OR returns a falsy proof, `createProof` throws so the refresh
|
|
259
|
+
* path surfaces `auth/dpop_prover_unavailable` and makes NO proof-less network
|
|
260
|
+
* call — it never falls back to an empty/Bearer request.
|
|
261
|
+
*
|
|
262
|
+
* @public — additive-only.
|
|
263
|
+
*/
|
|
264
|
+
declare function createNativeDpopProver(options: CreateNativeDpopProverOptions): DpopProver;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Default Expo adapter — wires the `NativeAuthAdapter` interface to Expo modules.
|
|
268
|
+
*
|
|
269
|
+
* Modules are imported dynamically inside the factory so:
|
|
270
|
+
* - Bare-RN consumers who pass a custom adapter don't pay the Expo bundle cost.
|
|
271
|
+
* - Consumers who never use biometric don't pay the `expo-local-authentication`
|
|
272
|
+
* cost (lazy-load).
|
|
273
|
+
*
|
|
274
|
+
* Hardening:
|
|
275
|
+
* - The returned adapter is `Object.freeze`-able by callers; this module returns
|
|
276
|
+
* a plain object — `<RakomiProvider>` freezes it on mount.
|
|
277
|
+
* - No `console.log` of token values anywhere in this file.
|
|
278
|
+
*
|
|
279
|
+
* Native-passkey integration (`react-native-passkey`, ASAuthorization) is
|
|
280
|
+
* out-of-scope here.
|
|
281
|
+
*/
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build the default Expo-backed `NativeAuthAdapter`.
|
|
285
|
+
*
|
|
286
|
+
* Each capability is lazily resolved on first use to keep the cold-import bundle
|
|
287
|
+
* small (perf budget). The resolution is memoised internally.
|
|
288
|
+
*/
|
|
289
|
+
declare function createDefaultExpoAdapter(options?: CreateDefaultExpoAdapterOptions): NativeAuthAdapter;
|
|
290
|
+
interface CreateDefaultExpoAdapterOptions {
|
|
291
|
+
/**
|
|
292
|
+
* Override storage with the consumer's own token cache (sugar).
|
|
293
|
+
* When provided, `crypto`/`browser`/`deepLink`/`biometric`/`appLifecycle`/`connectivity`
|
|
294
|
+
* still come from Expo defaults; only storage is replaced.
|
|
295
|
+
*/
|
|
296
|
+
tokenCache?: TokenCache;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
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 DeepLinkProvider as i, type DpopProofInput as j, type NativeDpopModuleSpec as k, createDefaultExpoAdapter as l, createNativeDpopProver as m, type AppStateSubscription as n, type DeepLinkSubscription as o, type NetInfoSubscription as p };
|