@breeztech/breez-sdk-spark-react-native 0.15.0 → 0.16.1-dev1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/cpp/generated/breez_sdk_spark.cpp +4947 -2629
  2. package/cpp/generated/breez_sdk_spark.hpp +257 -110
  3. package/lib/commonjs/generated/breez_sdk_spark-ffi.js.map +1 -1
  4. package/lib/commonjs/generated/breez_sdk_spark.js +1304 -580
  5. package/lib/commonjs/generated/breez_sdk_spark.js.map +1 -1
  6. package/lib/commonjs/passkey-prf-provider.js +300 -0
  7. package/lib/commonjs/passkey-prf-provider.js.map +1 -0
  8. package/lib/module/generated/breez_sdk_spark-ffi.js.map +1 -1
  9. package/lib/module/generated/breez_sdk_spark.js +1304 -580
  10. package/lib/module/generated/breez_sdk_spark.js.map +1 -1
  11. package/lib/module/passkey-prf-provider.js +293 -0
  12. package/lib/module/passkey-prf-provider.js.map +1 -0
  13. package/lib/typescript/commonjs/src/generated/breez_sdk_spark-ffi.d.ts +199 -140
  14. package/lib/typescript/commonjs/src/generated/breez_sdk_spark-ffi.d.ts.map +1 -1
  15. package/lib/typescript/commonjs/src/generated/breez_sdk_spark.d.ts +6149 -3695
  16. package/lib/typescript/commonjs/src/generated/breez_sdk_spark.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/src/passkey-prf-provider.d.ts +135 -0
  18. package/lib/typescript/commonjs/src/passkey-prf-provider.d.ts.map +1 -0
  19. package/lib/typescript/module/src/generated/breez_sdk_spark-ffi.d.ts +199 -140
  20. package/lib/typescript/module/src/generated/breez_sdk_spark-ffi.d.ts.map +1 -1
  21. package/lib/typescript/module/src/generated/breez_sdk_spark.d.ts +6149 -3695
  22. package/lib/typescript/module/src/generated/breez_sdk_spark.d.ts.map +1 -1
  23. package/lib/typescript/module/src/passkey-prf-provider.d.ts +135 -0
  24. package/lib/typescript/module/src/passkey-prf-provider.d.ts.map +1 -0
  25. package/package.json +17 -5
  26. package/scripts/post-ubrn.js +227 -0
  27. package/src/generated/breez_sdk_spark-ffi.ts +366 -198
  28. package/src/generated/breez_sdk_spark.ts +12343 -7056
  29. package/src/passkey-prf-provider.ts +372 -0
@@ -0,0 +1,372 @@
1
+ import { NativeModules, Platform } from 'react-native';
2
+ import {
3
+ PasskeyClient as SdkPasskeyClient,
4
+ PasskeyProviderOptions,
5
+ type PasskeyConfig,
6
+ type PrfProvider,
7
+ } from './generated/breez_sdk_spark';
8
+
9
+ const { BreezSdkSparkPasskey } = NativeModules;
10
+
11
+ /**
12
+ * Diagnostic error for when the native passkey module isn't reachable. The
13
+ * common iOS cause is running below iOS 18, where the `@available(iOS 18.0, *)`
14
+ * Swift class cannot load; on Android a missing module means broken linking.
15
+ */
16
+ function passkeyModuleUnavailableError(operation: string): Error {
17
+ if (Platform.OS === 'ios') {
18
+ const version = parseFloat(String(Platform.Version));
19
+ if (!Number.isNaN(version) && version < 18) {
20
+ return new Error(
21
+ `Passkey PRF requires iOS 18.0 or later. ` +
22
+ `This device is running iOS ${Platform.Version}, where ` +
23
+ `ASAuthorizationPlatformPublicKeyCredentialPRFAssertionInput is not available. ` +
24
+ `${operation} is unsupported on this device.`
25
+ );
26
+ }
27
+ return new Error(
28
+ `Passkey PRF native module (BreezSdkSparkPasskey) failed to load on iOS. ` +
29
+ `This normally means the iOS deployment target is lower than 18.0 or ` +
30
+ `the pod was not linked. ${operation} is unavailable.`
31
+ );
32
+ }
33
+ if (Platform.OS === 'android') {
34
+ return new Error(
35
+ `Passkey PRF native module (BreezSdkSparkPasskey) is not registered. ` +
36
+ `Check that @breeztech/breez-sdk-spark-react-native is autolinked and ` +
37
+ `that BreezSdkSparkPasskeyModule appears in BreezSdkSparkReactNativePackage. ` +
38
+ `${operation} is unavailable.`
39
+ );
40
+ }
41
+ return new Error(
42
+ `Passkey PRF is only supported on iOS 18+ and Android 9+. ` +
43
+ `${operation} is not available on ${Platform.OS}.`
44
+ );
45
+ }
46
+
47
+ /**
48
+ * A passkey credential from a register or sign-in ceremony. `credentialId`
49
+ * is always set; the attestation fields are populated on registration and
50
+ * absent on sign-in (an assertion carries no attestation). Persist
51
+ * `credentialId` to drive `excludeCredentials` / `allowCredentials` on later
52
+ * calls. Treat `aaguid` as an unverified display hint, never a trust decision.
53
+ * `userId` is the user handle minted by the native plugin (never host-supplied).
54
+ */
55
+ export interface PasskeyCredential {
56
+ credentialId: Uint8Array;
57
+ userId: Uint8Array | null;
58
+ aaguid: Uint8Array | null;
59
+ backupEligible: boolean | null;
60
+ }
61
+
62
+ /**
63
+ * Result of {@link PasskeyProvider.checkDomainAssociation}. Switch on `kind`
64
+ * to handle each outcome.
65
+ */
66
+ export type DomainAssociation =
67
+ | { kind: 'Associated' }
68
+ | { kind: 'NotAssociated'; source: string; reason: string }
69
+ | { kind: 'Skipped'; reason: string };
70
+
71
+ /**
72
+ * Error thrown when a passkey operation fails, with a structured `code` for
73
+ * programmatic handling: `userCancelled`, `userTimedOut`, `prfNotSupported`,
74
+ * `noCredential`, `configuration`, `credentialAlreadyExists`, `unknown`.
75
+ * `userTimedOut` is the OS biometric inactivity timeout (distinct from the
76
+ * user dismissing the prompt), so hosts may safely auto-retry it.
77
+ */
78
+ export class PasskeyPrfException extends Error {
79
+ readonly code: string;
80
+
81
+ constructor(code: string, message: string) {
82
+ super(message);
83
+ this.name = 'PasskeyPrfException';
84
+ this.code = code;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Map a native bridge rejection (RN passes `{ code, message }` on the
90
+ * thrown error) into a typed [PasskeyPrfException].
91
+ */
92
+ function mapNativeError(err: unknown): PasskeyPrfException {
93
+ const anyErr = err as { code?: string; message?: string };
94
+ const message = anyErr?.message ?? 'Unknown passkey error';
95
+ switch (anyErr?.code) {
96
+ case 'ERR_USER_CANCELLED':
97
+ return new PasskeyPrfException('userCancelled', message);
98
+ case 'ERR_USER_TIMED_OUT':
99
+ return new PasskeyPrfException('userTimedOut', message);
100
+ case 'ERR_PRF_NOT_SUPPORTED':
101
+ return new PasskeyPrfException('prfNotSupported', message);
102
+ case 'ERR_NO_CREDENTIAL':
103
+ return new PasskeyPrfException('noCredential', message);
104
+ case 'ERR_CONFIGURATION':
105
+ return new PasskeyPrfException('configuration', message);
106
+ case 'ERR_CREDENTIAL_ALREADY_EXISTS':
107
+ return new PasskeyPrfException('credentialAlreadyExists', message);
108
+ case 'ERR_AUTHENTICATION_FAILED':
109
+ return new PasskeyPrfException('authenticationFailed', message);
110
+ case 'ERR_PRF_EVALUATION_FAILED':
111
+ return new PasskeyPrfException('prfEvaluationFailed', message);
112
+ default:
113
+ return new PasskeyPrfException('unknown', message);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Built-in React Native passkey PRF provider, backed by AuthenticationServices
119
+ * on iOS and Credential Manager on Android. The default {@link PrfProvider};
120
+ * inject a configured instance through {@link PasskeyClientBuilder}. Requires
121
+ * iOS 18+ or Android 14+ (API 34) plus the Associated Domains entitlement
122
+ * (iOS) or assetlinks.json (Android) for the RP domain.
123
+ */
124
+ export class PasskeyProvider {
125
+ /**
126
+ * Breez's shared `keys.breez.technology` RP. Pass as `rpId` to opt in
127
+ * (only valid for apps registered with Breez); apps with their own RP
128
+ * domain pass their own string.
129
+ */
130
+ static readonly BREEZ_RP_ID: string = 'keys.breez.technology';
131
+
132
+ /** Default `rpName` for the zero-config client when none is supplied. */
133
+ static readonly DEFAULT_RP_NAME: string = 'Breez';
134
+
135
+ private rpId: string;
136
+ private rpName: string;
137
+ private userName: string;
138
+ private userDisplayName: string;
139
+
140
+ constructor(options: PasskeyProviderOptions) {
141
+ this.rpId = options.rpId ?? PasskeyProvider.BREEZ_RP_ID;
142
+ this.rpName = options.rpName ?? PasskeyProvider.DEFAULT_RP_NAME;
143
+ this.userName = options.userName ?? this.rpName;
144
+ this.userDisplayName = options.userDisplayName ?? this.userName;
145
+ }
146
+
147
+ /**
148
+ * Derive one 32-byte seed per salt from passkey PRF, in as few OS prompts
149
+ * as the platform supports. `allowCredentials` restricts the assertion to
150
+ * specific credential IDs (mainly for reauthentication) when non-empty;
151
+ * `preferImmediatelyAvailableCredentials` overrides the platform default
152
+ * when set. Returns the seeds plus the asserted credential ID.
153
+ */
154
+ async deriveSeeds(request: {
155
+ salts: string[];
156
+ allowCredentials?: Uint8Array[];
157
+ preferImmediatelyAvailableCredentials?: boolean | null;
158
+ }): Promise<{ seeds: Uint8Array[]; credentialId?: Uint8Array }> {
159
+ if (!BreezSdkSparkPasskey) {
160
+ throw passkeyModuleUnavailableError('deriveSeeds');
161
+ }
162
+
163
+ const allowBase64 = (request.allowCredentials ?? []).map(id =>
164
+ uint8ArrayToBase64(id)
165
+ );
166
+ const preferImmediate = request.preferImmediatelyAvailableCredentials ?? null;
167
+
168
+ let result: { seeds: string[]; credentialId?: string | null };
169
+ try {
170
+ result = await BreezSdkSparkPasskey.deriveSeeds(
171
+ request.salts,
172
+ this.rpId,
173
+ this.rpName,
174
+ this.userName,
175
+ this.userDisplayName,
176
+ false,
177
+ allowBase64,
178
+ preferImmediate
179
+ );
180
+ if (!result || !Array.isArray(result.seeds)) {
181
+ throw new PasskeyPrfException('unknown', 'deriveSeeds returned an unexpected shape');
182
+ }
183
+ } catch (err) {
184
+ if (err instanceof PasskeyPrfException) {
185
+ throw err;
186
+ }
187
+ throw mapNativeError(err);
188
+ }
189
+
190
+ // The native module returns the asserted credential ID alongside the
191
+ // seeds; surface it so the SDK can pin the next derive to this exact
192
+ // credential.
193
+ return {
194
+ seeds: result.seeds.map(b64 => base64ToUint8Array(b64)),
195
+ credentialId: result.credentialId
196
+ ? base64ToUint8Array(result.credentialId)
197
+ : undefined,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Register a new PRF-capable passkey (one prompt, no seed derivation): use
203
+ * it to split credential creation from derivation in multi-step onboarding.
204
+ * `excludeCredentials` blocks re-registering a device that already holds a
205
+ * credential, surfaced as a `credentialAlreadyExists` failure. The returned
206
+ * user handle is minted fresh per call (never host-supplied).
207
+ */
208
+ async createPasskey(excludeCredentials: Uint8Array[] = []): Promise<PasskeyCredential> {
209
+ if (!BreezSdkSparkPasskey) {
210
+ throw passkeyModuleUnavailableError('createPasskey');
211
+ }
212
+
213
+ const excludeBase64 = excludeCredentials.map(id => uint8ArrayToBase64(id));
214
+
215
+ try {
216
+ const result: {
217
+ credentialId: string;
218
+ userId: string;
219
+ aaguid: string | null;
220
+ backupEligible: boolean | null;
221
+ } = await BreezSdkSparkPasskey.createPasskey(
222
+ this.rpId,
223
+ this.rpName,
224
+ this.userName,
225
+ this.userDisplayName,
226
+ excludeBase64
227
+ );
228
+
229
+ return {
230
+ credentialId: base64ToUint8Array(result.credentialId),
231
+ userId: base64ToUint8Array(result.userId),
232
+ aaguid: result.aaguid ? base64ToUint8Array(result.aaguid) : null,
233
+ backupEligible: result.backupEligible,
234
+ };
235
+ } catch (err) {
236
+ throw mapNativeError(err);
237
+ }
238
+ }
239
+
240
+ /** Whether this device supports passkeys with the PRF extension. */
241
+ async isSupported(): Promise<boolean> {
242
+ if (!BreezSdkSparkPasskey) {
243
+ return false;
244
+ }
245
+
246
+ return await BreezSdkSparkPasskey.isSupported();
247
+ }
248
+
249
+ /**
250
+ * Verify the app is associated with the configured `rpId` for WebAuthn.
251
+ * Android always returns `Skipped` rather than `NotAssociated`: Credential
252
+ * Manager runs its own check internally against fresher data.
253
+ */
254
+ async checkDomainAssociation(): Promise<DomainAssociation> {
255
+ if (!BreezSdkSparkPasskey) {
256
+ return {
257
+ kind: 'Skipped',
258
+ reason: 'Native passkey module unavailable on this platform',
259
+ };
260
+ }
261
+ try {
262
+ const result = await BreezSdkSparkPasskey.checkDomainAssociation(this.rpId);
263
+ const kind = result?.kind;
264
+ if (kind === 'Associated') {
265
+ return { kind: 'Associated' };
266
+ }
267
+ if (kind === 'NotAssociated') {
268
+ return {
269
+ kind: 'NotAssociated',
270
+ source: result?.source ?? 'unknown',
271
+ reason: result?.reason ?? '',
272
+ };
273
+ }
274
+ return { kind: 'Skipped', reason: result?.reason ?? '' };
275
+ } catch (err) {
276
+ const anyErr = err as { message?: string };
277
+ return {
278
+ kind: 'Skipped',
279
+ reason: anyErr?.message ?? 'Domain association probe failed',
280
+ };
281
+ }
282
+ }
283
+ }
284
+
285
+ /** Decode a base64 string to Uint8Array. */
286
+ function base64ToUint8Array(base64: string): Uint8Array {
287
+ const binaryString = atob(base64);
288
+ const bytes = new Uint8Array(binaryString.length);
289
+ for (let i = 0; i < binaryString.length; i++) {
290
+ bytes[i] = binaryString.charCodeAt(i);
291
+ }
292
+ return bytes;
293
+ }
294
+
295
+ /** Encode a Uint8Array to base64 string. */
296
+ function uint8ArrayToBase64(bytes: Uint8Array): string {
297
+ let binary = '';
298
+ for (const byte of bytes) {
299
+ binary += String.fromCharCode(byte);
300
+ }
301
+ return btoa(binary);
302
+ }
303
+
304
+ /**
305
+ * Builds a `PasskeyClient` backed by a caller-supplied provider. Use this
306
+ * for a custom PRF backend; omit the provider for the zero-config Breez-RP
307
+ * default and set `providerOptions` on the config to use your own RP.
308
+ */
309
+ export class PasskeyClientBuilder {
310
+ private provider?: PrfProvider;
311
+
312
+ /**
313
+ * @param breezApiKey Breez relay key for authenticated (NIP-42) label
314
+ * storage. Omit for public relays only.
315
+ * @param config Passkey client config. `providerOptions` configures the
316
+ * default provider (ignored when a provider is injected via
317
+ * {@link withPrfProvider}, which owns its RP); `defaultLabel` is the
318
+ * label-store default.
319
+ */
320
+ constructor(
321
+ private readonly breezApiKey?: string,
322
+ private readonly config?: PasskeyConfig
323
+ ) {}
324
+
325
+ /**
326
+ * Inject the provider the client derives seeds through: the built-in
327
+ * {@link PasskeyProvider} or any custom `PrfProvider` implementation.
328
+ * Supersedes the config's `providerOptions` (the injected provider owns
329
+ * its RP).
330
+ */
331
+ withPrfProvider(provider: PrfProvider): this {
332
+ this.provider = provider;
333
+ return this;
334
+ }
335
+
336
+ /**
337
+ * Construct the client. Falls back to a default {@link PasskeyProvider}
338
+ * on the config's `providerOptions` (default: the Breez RP) when no
339
+ * provider was injected.
340
+ */
341
+ build(): SdkPasskeyClient {
342
+ // The hand-written PasskeyProvider conforms structurally to the
343
+ // generated PrfProvider foreign interface.
344
+ const provider: PrfProvider =
345
+ this.provider ??
346
+ (new PasskeyProvider(
347
+ this.config?.providerOptions ?? PasskeyProviderOptions.create({})
348
+ ) as unknown as PrfProvider);
349
+ return new SdkPasskeyClient(provider, this.breezApiKey, this.config);
350
+ }
351
+ }
352
+
353
+ /** @internal Builds the zero-config client; exposed via {@link PasskeyClient}. */
354
+ function buildPasskeyClient(
355
+ breezApiKey?: string,
356
+ config?: PasskeyConfig
357
+ ): SdkPasskeyClient {
358
+ return new PasskeyClientBuilder(breezApiKey, config).build();
359
+ }
360
+
361
+ /**
362
+ * Zero-config passkey client on the Breez shared RP (`keys.breez.technology`),
363
+ * so a Breez-registered app needs only its relay key; set `providerOptions` on
364
+ * the config to use your own RP. For a custom PRF backend, build the provider
365
+ * and inject it via {@link PasskeyClientBuilder}.
366
+ */
367
+ export const PasskeyClient: {
368
+ new (breezApiKey?: string, config?: PasskeyConfig): SdkPasskeyClient;
369
+ } = buildPasskeyClient as unknown as {
370
+ new (breezApiKey?: string, config?: PasskeyConfig): SdkPasskeyClient;
371
+ };
372
+