@rakomi/react-native 0.1.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 +36 -6
- package/README.md +247 -0
- package/SECURITY.md +1 -1
- package/THIRD-PARTY-NOTICES +1 -1
- package/dist/index.cjs +692 -13
- package/dist/index.d.cts +70 -6
- package/dist/index.d.ts +70 -6
- package/dist/index.js +693 -16
- 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/dist/native/index.cjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var sdkCore = require('@rakomi/sdk-core');
|
|
4
|
+
|
|
3
5
|
// src/native/bare-rn-example.ts
|
|
4
6
|
function createBareRnAdapter(options) {
|
|
5
7
|
const servicePrefix = options.keychainServicePrefix ?? "rakomi";
|
|
@@ -376,6 +378,269 @@ function createNetInfoConnectivity() {
|
|
|
376
378
|
};
|
|
377
379
|
}
|
|
378
380
|
|
|
381
|
+
// src/native/passkey-envelope.ts
|
|
382
|
+
var BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
383
|
+
var RULES = {
|
|
384
|
+
registration: {
|
|
385
|
+
required: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
|
|
386
|
+
base64url: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
|
|
387
|
+
requiredStringArray: ["response.transports"],
|
|
388
|
+
requiredObject: ["clientExtensionResults"],
|
|
389
|
+
keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
|
|
390
|
+
keepInResponse: [
|
|
391
|
+
"clientDataJSON",
|
|
392
|
+
"attestationObject",
|
|
393
|
+
"authenticatorData",
|
|
394
|
+
"publicKey",
|
|
395
|
+
"publicKeyAlgorithm"
|
|
396
|
+
],
|
|
397
|
+
keepStringArrayInResponse: ["transports"]
|
|
398
|
+
},
|
|
399
|
+
assertion: {
|
|
400
|
+
required: [
|
|
401
|
+
"id",
|
|
402
|
+
"rawId",
|
|
403
|
+
"response.clientDataJSON",
|
|
404
|
+
"response.authenticatorData",
|
|
405
|
+
"response.signature"
|
|
406
|
+
],
|
|
407
|
+
base64url: [
|
|
408
|
+
"id",
|
|
409
|
+
"rawId",
|
|
410
|
+
"response.clientDataJSON",
|
|
411
|
+
"response.authenticatorData",
|
|
412
|
+
"response.signature",
|
|
413
|
+
"response.userHandle"
|
|
414
|
+
],
|
|
415
|
+
requiredStringArray: [],
|
|
416
|
+
requiredObject: ["clientExtensionResults"],
|
|
417
|
+
keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
|
|
418
|
+
keepInResponse: ["clientDataJSON", "authenticatorData", "signature", "userHandle"],
|
|
419
|
+
keepStringArrayInResponse: []
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
var CredentialEnvelopeError = class extends Error {
|
|
423
|
+
constructor(message) {
|
|
424
|
+
super(message);
|
|
425
|
+
this.name = "CredentialEnvelopeError";
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
function isRecord(value) {
|
|
429
|
+
return typeof value === "object" && value !== null;
|
|
430
|
+
}
|
|
431
|
+
function readPath(envelope, path) {
|
|
432
|
+
const dot = path.indexOf(".");
|
|
433
|
+
if (dot === -1) return envelope[path];
|
|
434
|
+
const parent = envelope[path.slice(0, dot)];
|
|
435
|
+
return isRecord(parent) ? parent[path.slice(dot + 1)] : void 0;
|
|
436
|
+
}
|
|
437
|
+
function assertW3cCredentialEnvelope(kind, parsed) {
|
|
438
|
+
if (!isRecord(parsed)) {
|
|
439
|
+
throw new CredentialEnvelopeError(
|
|
440
|
+
`the native module returned a ${parsed === null ? "null" : typeof parsed}; the contract is a JSON object`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
if (parsed["type"] !== "public-key") {
|
|
444
|
+
throw new CredentialEnvelopeError(`'type' must be the string "public-key" (W3C WebAuthn L3)`);
|
|
445
|
+
}
|
|
446
|
+
if (!isRecord(parsed["response"])) {
|
|
447
|
+
throw new CredentialEnvelopeError(`'response' is missing or is not an object`);
|
|
448
|
+
}
|
|
449
|
+
const rule = RULES[kind];
|
|
450
|
+
for (const path of rule.required) {
|
|
451
|
+
const value = readPath(parsed, path);
|
|
452
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
453
|
+
throw new CredentialEnvelopeError(
|
|
454
|
+
`'${path}' is required on a ${kind} response and must be a non-empty string (W3C WebAuthn L3 JSON serialization)`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
for (const path of rule.requiredStringArray) {
|
|
459
|
+
const value = readPath(parsed, path);
|
|
460
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
461
|
+
throw new CredentialEnvelopeError(
|
|
462
|
+
`'${path}' is required on a ${kind} response and must be an array of strings (W3C WebAuthn L3). The server stores it and replays it into allowCredentials \u2014 omitting it silently breaks cross-device sign-in later, on another device.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
for (const path of rule.requiredObject) {
|
|
467
|
+
const value = readPath(parsed, path);
|
|
468
|
+
if (!isRecord(value)) {
|
|
469
|
+
throw new CredentialEnvelopeError(
|
|
470
|
+
`'${path}' is required on a ${kind} response and must be an object (an empty object is fine)`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const path of rule.base64url) {
|
|
475
|
+
const value = readPath(parsed, path);
|
|
476
|
+
if (value === void 0 || value === null) continue;
|
|
477
|
+
if (typeof value !== "string" || !BASE64URL.test(value)) {
|
|
478
|
+
throw new CredentialEnvelopeError(
|
|
479
|
+
`'${path}' is not unpadded base64url (RFC 4648 \xA75). On iOS, Data.base64EncodedString() emits standard base64 (RFC 4648 \xA74, with '+', '/' and '=' padding) \u2014 use the URL-safe, unpadded variant.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function sanitizeCredentialEnvelope(kind, parsed) {
|
|
485
|
+
assertW3cCredentialEnvelope(kind, parsed);
|
|
486
|
+
const envelope = parsed;
|
|
487
|
+
const source = envelope["response"];
|
|
488
|
+
const rule = RULES[kind];
|
|
489
|
+
const response = /* @__PURE__ */ Object.create(null);
|
|
490
|
+
for (const key of rule.keepInResponse) {
|
|
491
|
+
const value = source[key];
|
|
492
|
+
if (typeof value === "string" || typeof value === "number") response[key] = value;
|
|
493
|
+
}
|
|
494
|
+
for (const key of rule.keepStringArrayInResponse) {
|
|
495
|
+
const value = source[key];
|
|
496
|
+
if (Array.isArray(value)) response[key] = value.filter((entry) => typeof entry === "string");
|
|
497
|
+
}
|
|
498
|
+
const projected = /* @__PURE__ */ Object.create(null);
|
|
499
|
+
for (const key of rule.keep) {
|
|
500
|
+
const value = envelope[key];
|
|
501
|
+
if (value !== void 0) projected[key] = value;
|
|
502
|
+
}
|
|
503
|
+
projected["response"] = response;
|
|
504
|
+
return projected;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/native/passkey-adapter.ts
|
|
508
|
+
var NATIVE_CODE_TO_KIND = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
509
|
+
PASSKEY_CANCELLED: "cancelled",
|
|
510
|
+
PASSKEY_UNSUPPORTED: "unsupported",
|
|
511
|
+
PASSKEY_NO_CREDENTIAL: "cancelled"
|
|
512
|
+
});
|
|
513
|
+
function isRecord2(value) {
|
|
514
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
515
|
+
}
|
|
516
|
+
function normalizeNativeRejection(err) {
|
|
517
|
+
const code = isRecord2(err) ? err["code"] : void 0;
|
|
518
|
+
if (typeof code === "string") {
|
|
519
|
+
const kind = NATIVE_CODE_TO_KIND[code];
|
|
520
|
+
if (kind !== void 0) {
|
|
521
|
+
throw new sdkCore.PasskeyCeremonyError(kind);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
function assertNever(value) {
|
|
527
|
+
throw new sdkCore.PasskeyCeremonyError("failed", `unhandled ceremony-error kind: ${String(value)}`);
|
|
528
|
+
}
|
|
529
|
+
function describeKind(kind) {
|
|
530
|
+
switch (kind) {
|
|
531
|
+
case "cancelled":
|
|
532
|
+
return "the user dismissed the passkey sheet";
|
|
533
|
+
case "unsupported":
|
|
534
|
+
return "this platform cannot run a passkey ceremony";
|
|
535
|
+
case "failed":
|
|
536
|
+
return "the passkey ceremony failed";
|
|
537
|
+
default:
|
|
538
|
+
return assertNever(kind);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function createNativePasskeyAdapter(options) {
|
|
542
|
+
const mod = options.module;
|
|
543
|
+
let inFlight = null;
|
|
544
|
+
const candidate = mod;
|
|
545
|
+
const missing = ["isPasskeySupported", "createPasskey", "getPasskey"].filter(
|
|
546
|
+
(method) => typeof candidate?.[method] !== "function"
|
|
547
|
+
);
|
|
548
|
+
if (missing.length > 0) {
|
|
549
|
+
throw new Error(
|
|
550
|
+
`native passkey module is missing: ${missing.join(", ")} \u2014 it does not satisfy NativePasskeyModuleSpec`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
async function runCeremony(kind, requestJson, ceremonyOptions) {
|
|
554
|
+
const signal = ceremonyOptions?.signal;
|
|
555
|
+
if (signal?.aborted === true) {
|
|
556
|
+
throw new sdkCore.PasskeyCeremonyError("cancelled", describeKind("cancelled"));
|
|
557
|
+
}
|
|
558
|
+
const ticket = /* @__PURE__ */ Symbol("passkey-ceremony");
|
|
559
|
+
inFlight = ticket;
|
|
560
|
+
const onAbort = () => {
|
|
561
|
+
if (inFlight !== ticket) return;
|
|
562
|
+
try {
|
|
563
|
+
void Promise.resolve(mod.cancelPasskeyRequest?.()).catch(() => void 0);
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
568
|
+
let raw;
|
|
569
|
+
try {
|
|
570
|
+
raw = await (kind === "registration" ? mod.createPasskey(requestJson) : mod.getPasskey(requestJson));
|
|
571
|
+
} catch (err) {
|
|
572
|
+
normalizeNativeRejection(err);
|
|
573
|
+
} finally {
|
|
574
|
+
signal?.removeEventListener("abort", onAbort);
|
|
575
|
+
if (inFlight === ticket) inFlight = null;
|
|
576
|
+
}
|
|
577
|
+
if (typeof raw !== "string") {
|
|
578
|
+
throw new CredentialEnvelopeError(
|
|
579
|
+
`the native module returned ${raw === null ? "null" : `a ${typeof raw}`}; the contract is a JSON string \u2014 wrap a module with an object API so it stringifies its argument and its result`
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
let parsed;
|
|
583
|
+
try {
|
|
584
|
+
parsed = JSON.parse(raw);
|
|
585
|
+
} catch {
|
|
586
|
+
throw new CredentialEnvelopeError(
|
|
587
|
+
"the native module returned a string that is not valid JSON (a double-encoded response does this)"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
return sanitizeCredentialEnvelope(kind, parsed);
|
|
591
|
+
}
|
|
592
|
+
async function ceremonyOrMalformed(kind, requestJson, ceremonyOptions) {
|
|
593
|
+
try {
|
|
594
|
+
return await runCeremony(kind, requestJson, ceremonyOptions);
|
|
595
|
+
} catch (err) {
|
|
596
|
+
if (err instanceof CredentialEnvelopeError) {
|
|
597
|
+
return { rakomiEnvelopeViolation: err.message };
|
|
598
|
+
}
|
|
599
|
+
throw err;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
async isSupported() {
|
|
604
|
+
try {
|
|
605
|
+
return await mod.isPasskeySupported() === true;
|
|
606
|
+
} catch {
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
hasPlatformAuthenticator() {
|
|
611
|
+
return probePlatformAuthenticator(mod);
|
|
612
|
+
},
|
|
613
|
+
createCredential(creationOptions, ceremonyOptions) {
|
|
614
|
+
return ceremonyOrMalformed(
|
|
615
|
+
"registration",
|
|
616
|
+
JSON.stringify(creationOptions),
|
|
617
|
+
ceremonyOptions
|
|
618
|
+
);
|
|
619
|
+
},
|
|
620
|
+
getCredential(requestOptions, ceremonyOptions) {
|
|
621
|
+
return ceremonyOrMalformed(
|
|
622
|
+
"assertion",
|
|
623
|
+
JSON.stringify(requestOptions),
|
|
624
|
+
ceremonyOptions
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
async function probePlatformAuthenticator(mod) {
|
|
630
|
+
if (typeof mod.isPlatformAuthenticatorAvailable !== "function") return null;
|
|
631
|
+
try {
|
|
632
|
+
const available = await mod.isPlatformAuthenticatorAvailable();
|
|
633
|
+
return typeof available === "boolean" ? available : null;
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
exports.CredentialEnvelopeError = CredentialEnvelopeError;
|
|
640
|
+
exports.assertW3cCredentialEnvelope = assertW3cCredentialEnvelope;
|
|
379
641
|
exports.createBareRnAdapter = createBareRnAdapter;
|
|
380
642
|
exports.createDefaultExpoAdapter = createDefaultExpoAdapter;
|
|
381
643
|
exports.createNativeDpopProver = createNativeDpopProver;
|
|
644
|
+
exports.createNativePasskeyAdapter = createNativePasskeyAdapter;
|
|
645
|
+
exports.probePlatformAuthenticator = probePlatformAuthenticator;
|
|
646
|
+
exports.sanitizeCredentialEnvelope = sanitizeCredentialEnvelope;
|
package/dist/native/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as NativeAuthAdapter } from '../
|
|
2
|
-
export { A as AppLifecycle,
|
|
1
|
+
import { N as NativeAuthAdapter } from '../passkey-adapter-D_Z3Z7lV.cjs';
|
|
2
|
+
export { A as AppLifecycle, r as AppStateSubscription, a as AppStateValue, b as AttestationVerifier, B as BackgroundTask, c as BiometricGate, d as BiometricResult, e as BrowserAuthSessionOptions, f as BrowserAuthSessionResult, C as ConnectivityProvider, g as CreateDefaultExpoAdapterOptions, h as CreateNativeDpopProverOptions, i as CreateNativePasskeyAdapterOptions, j as DeepLinkProvider, s as DeepLinkSubscription, k as DpopProofInput, D as DpopProver, l as NativeDpopModuleSpec, m as NativePasskeyCeremonyAdapter, n as NativePasskeyModuleSpec, t as NetInfoSubscription, P as ParClient, S as SystemBrowser, T as TokenCache, o as createDefaultExpoAdapter, p as createNativeDpopProver, q as createNativePasskeyAdapter, u as probePlatformAuthenticator } from '../passkey-adapter-D_Z3Z7lV.cjs';
|
|
3
3
|
import '@rakomi/sdk-core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -110,4 +110,34 @@ interface CreateBareRnAdapterOptions {
|
|
|
110
110
|
}
|
|
111
111
|
declare function createBareRnAdapter(options: CreateBareRnAdapterOptions): NativeAuthAdapter;
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/** Which ceremony produced this envelope. The required-set differs per direction — deliberately. */
|
|
114
|
+
type CredentialEnvelopeKind = 'registration' | 'assertion';
|
|
115
|
+
/** The envelope broke the W3C contract. Terminal: the module is broken, retrying changes nothing. */
|
|
116
|
+
declare class CredentialEnvelopeError extends Error {
|
|
117
|
+
constructor(message: string);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate a parsed native response against the W3C envelope for `kind`.
|
|
121
|
+
*
|
|
122
|
+
* Throws {@link CredentialEnvelopeError} naming the field and the violated rule — never the field's
|
|
123
|
+
* VALUE. A `PasskeyError` message reaches logs and crash reporters; a credential id and a
|
|
124
|
+
* `clientDataJSON` are durable user identifiers, so the diagnosis must be free of them.
|
|
125
|
+
*/
|
|
126
|
+
declare function assertW3cCredentialEnvelope(kind: CredentialEnvelopeKind, parsed: unknown): void;
|
|
127
|
+
/**
|
|
128
|
+
* Validate, then **re-project** the envelope onto the members the contract knows.
|
|
129
|
+
*
|
|
130
|
+
* Validating without projecting is a check, not a boundary. The bridge used to forward the whole parsed
|
|
131
|
+
* object, and the server accepts the credential as an opaque blob — so every extra member a buggy or
|
|
132
|
+
* hostile module invented rode straight through to persistence. A `transports` that is a *string*
|
|
133
|
+
* instead of an array, or a five-thousand-element array, lands in the database and then poisons every
|
|
134
|
+
* subsequent `allowCredentials` for that user. Dropping unknown members costs nothing (the server reads
|
|
135
|
+
* none of them) and closes the class outright, instead of blacklisting the shapes we happened to think
|
|
136
|
+
* of.
|
|
137
|
+
*
|
|
138
|
+
* `Object.create(null)` for the projection, so a `"__proto__"` key that survived `JSON.parse` as an own
|
|
139
|
+
* property cannot be carried into an object with a real prototype further down.
|
|
140
|
+
*/
|
|
141
|
+
declare function sanitizeCredentialEnvelope(kind: CredentialEnvelopeKind, parsed: unknown): Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
export { type CreateBareRnAdapterOptions, CredentialEnvelopeError, type CredentialEnvelopeKind, NativeAuthAdapter, assertW3cCredentialEnvelope, createBareRnAdapter, sanitizeCredentialEnvelope };
|
package/dist/native/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { N as NativeAuthAdapter } from '../
|
|
2
|
-
export { A as AppLifecycle,
|
|
1
|
+
import { N as NativeAuthAdapter } from '../passkey-adapter-D_Z3Z7lV.js';
|
|
2
|
+
export { A as AppLifecycle, r as AppStateSubscription, a as AppStateValue, b as AttestationVerifier, B as BackgroundTask, c as BiometricGate, d as BiometricResult, e as BrowserAuthSessionOptions, f as BrowserAuthSessionResult, C as ConnectivityProvider, g as CreateDefaultExpoAdapterOptions, h as CreateNativeDpopProverOptions, i as CreateNativePasskeyAdapterOptions, j as DeepLinkProvider, s as DeepLinkSubscription, k as DpopProofInput, D as DpopProver, l as NativeDpopModuleSpec, m as NativePasskeyCeremonyAdapter, n as NativePasskeyModuleSpec, t as NetInfoSubscription, P as ParClient, S as SystemBrowser, T as TokenCache, o as createDefaultExpoAdapter, p as createNativeDpopProver, q as createNativePasskeyAdapter, u as probePlatformAuthenticator } from '../passkey-adapter-D_Z3Z7lV.js';
|
|
3
3
|
import '@rakomi/sdk-core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -110,4 +110,34 @@ interface CreateBareRnAdapterOptions {
|
|
|
110
110
|
}
|
|
111
111
|
declare function createBareRnAdapter(options: CreateBareRnAdapterOptions): NativeAuthAdapter;
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
/** Which ceremony produced this envelope. The required-set differs per direction — deliberately. */
|
|
114
|
+
type CredentialEnvelopeKind = 'registration' | 'assertion';
|
|
115
|
+
/** The envelope broke the W3C contract. Terminal: the module is broken, retrying changes nothing. */
|
|
116
|
+
declare class CredentialEnvelopeError extends Error {
|
|
117
|
+
constructor(message: string);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate a parsed native response against the W3C envelope for `kind`.
|
|
121
|
+
*
|
|
122
|
+
* Throws {@link CredentialEnvelopeError} naming the field and the violated rule — never the field's
|
|
123
|
+
* VALUE. A `PasskeyError` message reaches logs and crash reporters; a credential id and a
|
|
124
|
+
* `clientDataJSON` are durable user identifiers, so the diagnosis must be free of them.
|
|
125
|
+
*/
|
|
126
|
+
declare function assertW3cCredentialEnvelope(kind: CredentialEnvelopeKind, parsed: unknown): void;
|
|
127
|
+
/**
|
|
128
|
+
* Validate, then **re-project** the envelope onto the members the contract knows.
|
|
129
|
+
*
|
|
130
|
+
* Validating without projecting is a check, not a boundary. The bridge used to forward the whole parsed
|
|
131
|
+
* object, and the server accepts the credential as an opaque blob — so every extra member a buggy or
|
|
132
|
+
* hostile module invented rode straight through to persistence. A `transports` that is a *string*
|
|
133
|
+
* instead of an array, or a five-thousand-element array, lands in the database and then poisons every
|
|
134
|
+
* subsequent `allowCredentials` for that user. Dropping unknown members costs nothing (the server reads
|
|
135
|
+
* none of them) and closes the class outright, instead of blacklisting the shapes we happened to think
|
|
136
|
+
* of.
|
|
137
|
+
*
|
|
138
|
+
* `Object.create(null)` for the projection, so a `"__proto__"` key that survived `JSON.parse` as an own
|
|
139
|
+
* property cannot be carried into an object with a real prototype further down.
|
|
140
|
+
*/
|
|
141
|
+
declare function sanitizeCredentialEnvelope(kind: CredentialEnvelopeKind, parsed: unknown): Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
export { type CreateBareRnAdapterOptions, CredentialEnvelopeError, type CredentialEnvelopeKind, NativeAuthAdapter, assertW3cCredentialEnvelope, createBareRnAdapter, sanitizeCredentialEnvelope };
|
package/dist/native/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PasskeyCeremonyError } from '@rakomi/sdk-core';
|
|
2
|
+
|
|
1
3
|
// src/native/bare-rn-example.ts
|
|
2
4
|
function createBareRnAdapter(options) {
|
|
3
5
|
const servicePrefix = options.keychainServicePrefix ?? "rakomi";
|
|
@@ -374,4 +376,262 @@ function createNetInfoConnectivity() {
|
|
|
374
376
|
};
|
|
375
377
|
}
|
|
376
378
|
|
|
377
|
-
|
|
379
|
+
// src/native/passkey-envelope.ts
|
|
380
|
+
var BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
381
|
+
var RULES = {
|
|
382
|
+
registration: {
|
|
383
|
+
required: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
|
|
384
|
+
base64url: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
|
|
385
|
+
requiredStringArray: ["response.transports"],
|
|
386
|
+
requiredObject: ["clientExtensionResults"],
|
|
387
|
+
keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
|
|
388
|
+
keepInResponse: [
|
|
389
|
+
"clientDataJSON",
|
|
390
|
+
"attestationObject",
|
|
391
|
+
"authenticatorData",
|
|
392
|
+
"publicKey",
|
|
393
|
+
"publicKeyAlgorithm"
|
|
394
|
+
],
|
|
395
|
+
keepStringArrayInResponse: ["transports"]
|
|
396
|
+
},
|
|
397
|
+
assertion: {
|
|
398
|
+
required: [
|
|
399
|
+
"id",
|
|
400
|
+
"rawId",
|
|
401
|
+
"response.clientDataJSON",
|
|
402
|
+
"response.authenticatorData",
|
|
403
|
+
"response.signature"
|
|
404
|
+
],
|
|
405
|
+
base64url: [
|
|
406
|
+
"id",
|
|
407
|
+
"rawId",
|
|
408
|
+
"response.clientDataJSON",
|
|
409
|
+
"response.authenticatorData",
|
|
410
|
+
"response.signature",
|
|
411
|
+
"response.userHandle"
|
|
412
|
+
],
|
|
413
|
+
requiredStringArray: [],
|
|
414
|
+
requiredObject: ["clientExtensionResults"],
|
|
415
|
+
keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
|
|
416
|
+
keepInResponse: ["clientDataJSON", "authenticatorData", "signature", "userHandle"],
|
|
417
|
+
keepStringArrayInResponse: []
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
var CredentialEnvelopeError = class extends Error {
|
|
421
|
+
constructor(message) {
|
|
422
|
+
super(message);
|
|
423
|
+
this.name = "CredentialEnvelopeError";
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
function isRecord(value) {
|
|
427
|
+
return typeof value === "object" && value !== null;
|
|
428
|
+
}
|
|
429
|
+
function readPath(envelope, path) {
|
|
430
|
+
const dot = path.indexOf(".");
|
|
431
|
+
if (dot === -1) return envelope[path];
|
|
432
|
+
const parent = envelope[path.slice(0, dot)];
|
|
433
|
+
return isRecord(parent) ? parent[path.slice(dot + 1)] : void 0;
|
|
434
|
+
}
|
|
435
|
+
function assertW3cCredentialEnvelope(kind, parsed) {
|
|
436
|
+
if (!isRecord(parsed)) {
|
|
437
|
+
throw new CredentialEnvelopeError(
|
|
438
|
+
`the native module returned a ${parsed === null ? "null" : typeof parsed}; the contract is a JSON object`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
if (parsed["type"] !== "public-key") {
|
|
442
|
+
throw new CredentialEnvelopeError(`'type' must be the string "public-key" (W3C WebAuthn L3)`);
|
|
443
|
+
}
|
|
444
|
+
if (!isRecord(parsed["response"])) {
|
|
445
|
+
throw new CredentialEnvelopeError(`'response' is missing or is not an object`);
|
|
446
|
+
}
|
|
447
|
+
const rule = RULES[kind];
|
|
448
|
+
for (const path of rule.required) {
|
|
449
|
+
const value = readPath(parsed, path);
|
|
450
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
451
|
+
throw new CredentialEnvelopeError(
|
|
452
|
+
`'${path}' is required on a ${kind} response and must be a non-empty string (W3C WebAuthn L3 JSON serialization)`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
for (const path of rule.requiredStringArray) {
|
|
457
|
+
const value = readPath(parsed, path);
|
|
458
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
459
|
+
throw new CredentialEnvelopeError(
|
|
460
|
+
`'${path}' is required on a ${kind} response and must be an array of strings (W3C WebAuthn L3). The server stores it and replays it into allowCredentials \u2014 omitting it silently breaks cross-device sign-in later, on another device.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const path of rule.requiredObject) {
|
|
465
|
+
const value = readPath(parsed, path);
|
|
466
|
+
if (!isRecord(value)) {
|
|
467
|
+
throw new CredentialEnvelopeError(
|
|
468
|
+
`'${path}' is required on a ${kind} response and must be an object (an empty object is fine)`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
for (const path of rule.base64url) {
|
|
473
|
+
const value = readPath(parsed, path);
|
|
474
|
+
if (value === void 0 || value === null) continue;
|
|
475
|
+
if (typeof value !== "string" || !BASE64URL.test(value)) {
|
|
476
|
+
throw new CredentialEnvelopeError(
|
|
477
|
+
`'${path}' is not unpadded base64url (RFC 4648 \xA75). On iOS, Data.base64EncodedString() emits standard base64 (RFC 4648 \xA74, with '+', '/' and '=' padding) \u2014 use the URL-safe, unpadded variant.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function sanitizeCredentialEnvelope(kind, parsed) {
|
|
483
|
+
assertW3cCredentialEnvelope(kind, parsed);
|
|
484
|
+
const envelope = parsed;
|
|
485
|
+
const source = envelope["response"];
|
|
486
|
+
const rule = RULES[kind];
|
|
487
|
+
const response = /* @__PURE__ */ Object.create(null);
|
|
488
|
+
for (const key of rule.keepInResponse) {
|
|
489
|
+
const value = source[key];
|
|
490
|
+
if (typeof value === "string" || typeof value === "number") response[key] = value;
|
|
491
|
+
}
|
|
492
|
+
for (const key of rule.keepStringArrayInResponse) {
|
|
493
|
+
const value = source[key];
|
|
494
|
+
if (Array.isArray(value)) response[key] = value.filter((entry) => typeof entry === "string");
|
|
495
|
+
}
|
|
496
|
+
const projected = /* @__PURE__ */ Object.create(null);
|
|
497
|
+
for (const key of rule.keep) {
|
|
498
|
+
const value = envelope[key];
|
|
499
|
+
if (value !== void 0) projected[key] = value;
|
|
500
|
+
}
|
|
501
|
+
projected["response"] = response;
|
|
502
|
+
return projected;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/native/passkey-adapter.ts
|
|
506
|
+
var NATIVE_CODE_TO_KIND = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
507
|
+
PASSKEY_CANCELLED: "cancelled",
|
|
508
|
+
PASSKEY_UNSUPPORTED: "unsupported",
|
|
509
|
+
PASSKEY_NO_CREDENTIAL: "cancelled"
|
|
510
|
+
});
|
|
511
|
+
function isRecord2(value) {
|
|
512
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
513
|
+
}
|
|
514
|
+
function normalizeNativeRejection(err) {
|
|
515
|
+
const code = isRecord2(err) ? err["code"] : void 0;
|
|
516
|
+
if (typeof code === "string") {
|
|
517
|
+
const kind = NATIVE_CODE_TO_KIND[code];
|
|
518
|
+
if (kind !== void 0) {
|
|
519
|
+
throw new PasskeyCeremonyError(kind);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
throw err;
|
|
523
|
+
}
|
|
524
|
+
function assertNever(value) {
|
|
525
|
+
throw new PasskeyCeremonyError("failed", `unhandled ceremony-error kind: ${String(value)}`);
|
|
526
|
+
}
|
|
527
|
+
function describeKind(kind) {
|
|
528
|
+
switch (kind) {
|
|
529
|
+
case "cancelled":
|
|
530
|
+
return "the user dismissed the passkey sheet";
|
|
531
|
+
case "unsupported":
|
|
532
|
+
return "this platform cannot run a passkey ceremony";
|
|
533
|
+
case "failed":
|
|
534
|
+
return "the passkey ceremony failed";
|
|
535
|
+
default:
|
|
536
|
+
return assertNever(kind);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function createNativePasskeyAdapter(options) {
|
|
540
|
+
const mod = options.module;
|
|
541
|
+
let inFlight = null;
|
|
542
|
+
const candidate = mod;
|
|
543
|
+
const missing = ["isPasskeySupported", "createPasskey", "getPasskey"].filter(
|
|
544
|
+
(method) => typeof candidate?.[method] !== "function"
|
|
545
|
+
);
|
|
546
|
+
if (missing.length > 0) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`native passkey module is missing: ${missing.join(", ")} \u2014 it does not satisfy NativePasskeyModuleSpec`
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
async function runCeremony(kind, requestJson, ceremonyOptions) {
|
|
552
|
+
const signal = ceremonyOptions?.signal;
|
|
553
|
+
if (signal?.aborted === true) {
|
|
554
|
+
throw new PasskeyCeremonyError("cancelled", describeKind("cancelled"));
|
|
555
|
+
}
|
|
556
|
+
const ticket = /* @__PURE__ */ Symbol("passkey-ceremony");
|
|
557
|
+
inFlight = ticket;
|
|
558
|
+
const onAbort = () => {
|
|
559
|
+
if (inFlight !== ticket) return;
|
|
560
|
+
try {
|
|
561
|
+
void Promise.resolve(mod.cancelPasskeyRequest?.()).catch(() => void 0);
|
|
562
|
+
} catch {
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
566
|
+
let raw;
|
|
567
|
+
try {
|
|
568
|
+
raw = await (kind === "registration" ? mod.createPasskey(requestJson) : mod.getPasskey(requestJson));
|
|
569
|
+
} catch (err) {
|
|
570
|
+
normalizeNativeRejection(err);
|
|
571
|
+
} finally {
|
|
572
|
+
signal?.removeEventListener("abort", onAbort);
|
|
573
|
+
if (inFlight === ticket) inFlight = null;
|
|
574
|
+
}
|
|
575
|
+
if (typeof raw !== "string") {
|
|
576
|
+
throw new CredentialEnvelopeError(
|
|
577
|
+
`the native module returned ${raw === null ? "null" : `a ${typeof raw}`}; the contract is a JSON string \u2014 wrap a module with an object API so it stringifies its argument and its result`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
let parsed;
|
|
581
|
+
try {
|
|
582
|
+
parsed = JSON.parse(raw);
|
|
583
|
+
} catch {
|
|
584
|
+
throw new CredentialEnvelopeError(
|
|
585
|
+
"the native module returned a string that is not valid JSON (a double-encoded response does this)"
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return sanitizeCredentialEnvelope(kind, parsed);
|
|
589
|
+
}
|
|
590
|
+
async function ceremonyOrMalformed(kind, requestJson, ceremonyOptions) {
|
|
591
|
+
try {
|
|
592
|
+
return await runCeremony(kind, requestJson, ceremonyOptions);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
if (err instanceof CredentialEnvelopeError) {
|
|
595
|
+
return { rakomiEnvelopeViolation: err.message };
|
|
596
|
+
}
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
async isSupported() {
|
|
602
|
+
try {
|
|
603
|
+
return await mod.isPasskeySupported() === true;
|
|
604
|
+
} catch {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
hasPlatformAuthenticator() {
|
|
609
|
+
return probePlatformAuthenticator(mod);
|
|
610
|
+
},
|
|
611
|
+
createCredential(creationOptions, ceremonyOptions) {
|
|
612
|
+
return ceremonyOrMalformed(
|
|
613
|
+
"registration",
|
|
614
|
+
JSON.stringify(creationOptions),
|
|
615
|
+
ceremonyOptions
|
|
616
|
+
);
|
|
617
|
+
},
|
|
618
|
+
getCredential(requestOptions, ceremonyOptions) {
|
|
619
|
+
return ceremonyOrMalformed(
|
|
620
|
+
"assertion",
|
|
621
|
+
JSON.stringify(requestOptions),
|
|
622
|
+
ceremonyOptions
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
async function probePlatformAuthenticator(mod) {
|
|
628
|
+
if (typeof mod.isPlatformAuthenticatorAvailable !== "function") return null;
|
|
629
|
+
try {
|
|
630
|
+
const available = await mod.isPlatformAuthenticatorAvailable();
|
|
631
|
+
return typeof available === "boolean" ? available : null;
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export { CredentialEnvelopeError, assertW3cCredentialEnvelope, createBareRnAdapter, createDefaultExpoAdapter, createNativeDpopProver, createNativePasskeyAdapter, probePlatformAuthenticator, sanitizeCredentialEnvelope };
|