@yielded/auth-react-native 0.1.0-beta.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel van der Merwe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @yielded/auth-react-native
2
+
3
+ Client-local iOS passkey registration, authentication, and capability checks for
4
+ `@yielded/auth`. Import `* as ReactNativePasskey` from this package and construct
5
+ the adapter with `ReactNativePasskey.make()`, or provide `ReactNativePasskey.layer`.
6
+
7
+ Install this package and its `react-native` and `react-native-passkey` peers only
8
+ in the native application. `@yielded/auth` does not depend on React Native or
9
+ this adapter. The adapter consumes core's public passkey schemas and returns the
10
+ same flow IDs and redacted completion responses as the browser adapter.
11
+
12
+ Applications own begin → prompt → complete, HTTP, binding, persistence, sessions,
13
+ and associated domains. Registration creates ES256 platform passkeys; authentication
14
+ also supports existing security-key credentials. iOS 16+ is supported; Android and
15
+ conditional mediation are unsupported. Interruption discards delivery but cannot dismiss the system
16
+ prompt, and the shared busy guard stays held until the native promise settles.
17
+
18
+ See the [iOS setup guide](../../docs/src/content/docs/guide/passkeys.md#prompt-in-an-ios-react-native-app)
19
+ and [API reference](../../docs/src/content/docs/reference/passkey-react-native.md).
@@ -0,0 +1,4 @@
1
+ import { PasskeyReactNativeAuthentication, PasskeyReactNativeBusy, PasskeyReactNativeCapabilities, PasskeyReactNativeFailure, PasskeyReactNativeInputRejected, PasskeyReactNativeInvalidResponse, PasskeyReactNativeNotCompleted, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable, PasskeyReactNativeUnsupported } from "./internal/models.mjs";
2
+ import { PasskeyReactNative } from "./internal/PasskeyReactNative.mjs";
3
+ import { layer, make } from "./internal/react-native-passkey.mjs";
4
+ export { PasskeyReactNative, PasskeyReactNativeAuthentication, PasskeyReactNativeBusy, PasskeyReactNativeCapabilities, PasskeyReactNativeFailure, PasskeyReactNativeInputRejected, PasskeyReactNativeInvalidResponse, PasskeyReactNativeNotCompleted, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable, PasskeyReactNativeUnsupported, layer, make };
package/dist/index.mjs ADDED
@@ -0,0 +1,4 @@
1
+ import { PasskeyReactNative } from "./internal/PasskeyReactNative.mjs";
2
+ import { PasskeyReactNativeAuthentication, PasskeyReactNativeBusy, PasskeyReactNativeCapabilities, PasskeyReactNativeFailure, PasskeyReactNativeInputRejected, PasskeyReactNativeInvalidResponse, PasskeyReactNativeNotCompleted, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable, PasskeyReactNativeUnsupported } from "./internal/models.mjs";
3
+ import { layer, make } from "./internal/react-native-passkey.mjs";
4
+ export { PasskeyReactNative, PasskeyReactNativeAuthentication, PasskeyReactNativeBusy, PasskeyReactNativeCapabilities, PasskeyReactNativeFailure, PasskeyReactNativeInputRejected, PasskeyReactNativeInvalidResponse, PasskeyReactNativeNotCompleted, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable, PasskeyReactNativeUnsupported, layer, make };
@@ -0,0 +1,21 @@
1
+ import { PasskeyReactNativeAuthentication, PasskeyReactNativeCapabilities, PasskeyReactNativeFailure, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable } from "./models.mjs";
2
+ import { Context, Effect } from "effect";
3
+ import { PasskeyAuthenticationStarted, PasskeyRegistrationStarted } from "@yielded/auth/Passkey";
4
+ //#region src/internal/PasskeyReactNative.d.ts
5
+ declare const PasskeyReactNative_base: Context.ServiceClass<PasskeyReactNative, "effect-auth/PasskeyReactNative", {
6
+ readonly capabilities: Effect.Effect<PasskeyReactNativeCapabilities, PasskeyReactNativeUnavailable>;
7
+ /** Registers a platform passkey; requested algorithms must include ES256 (-7). */
8
+ readonly register: (started: typeof PasskeyRegistrationStarted.Type) => Effect.Effect<PasskeyReactNativeRegistration, PasskeyReactNativeFailure>;
9
+ /** Conditional mediation is accepted for port compatibility but fails Unsupported. */
10
+ readonly authenticate: (input: {
11
+ readonly started: typeof PasskeyAuthenticationStarted.Type;
12
+ readonly mediation: "required" | "conditional";
13
+ }) => Effect.Effect<PasskeyReactNativeAuthentication, PasskeyReactNativeFailure>;
14
+ }>;
15
+ /** Client-local iOS ceremonies. Applications own Begin/Complete and request binding.
16
+ * Interruption stops delivery, but cannot dismiss OS UI. A started native request
17
+ * retains the shared busy guard until its promise settles, even after interruption.
18
+ * The calling fiber owns failure diagnostics; late settlement only clears the guard. */
19
+ export declare class PasskeyReactNative extends PasskeyReactNative_base {}
20
+ //#endregion
21
+ //# sourceMappingURL=PasskeyReactNative.d.mts.map
@@ -0,0 +1,11 @@
1
+ import { Context } from "effect";
2
+ //#region src/internal/PasskeyReactNative.ts
3
+ /** Client-local iOS ceremonies. Applications own Begin/Complete and request binding.
4
+ * Interruption stops delivery, but cannot dismiss OS UI. A started native request
5
+ * retains the shared busy guard until its promise settles, even after interruption.
6
+ * The calling fiber owns failure diagnostics; late settlement only clears the guard. */
7
+ var PasskeyReactNative = class extends Context.Service()("effect-auth/PasskeyReactNative") {};
8
+ //#endregion
9
+ export { PasskeyReactNative };
10
+
11
+ //# sourceMappingURL=PasskeyReactNative.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PasskeyReactNative.mjs","names":[],"sources":["../../src/internal/PasskeyReactNative.ts"],"sourcesContent":["import type {\n PasskeyAuthenticationStarted,\n PasskeyRegistrationStarted,\n} from \"@yielded/auth/Passkey\";\nimport { Context, type Effect } from \"effect\";\n\nimport type {\n PasskeyReactNativeAuthentication,\n PasskeyReactNativeCapabilities,\n PasskeyReactNativeFailure,\n PasskeyReactNativeRegistration,\n PasskeyReactNativeUnavailable,\n} from \"./models\";\n\n/** Client-local iOS ceremonies. Applications own Begin/Complete and request binding.\n * Interruption stops delivery, but cannot dismiss OS UI. A started native request\n * retains the shared busy guard until its promise settles, even after interruption.\n * The calling fiber owns failure diagnostics; late settlement only clears the guard. */\nexport class PasskeyReactNative extends Context.Service<\n PasskeyReactNative,\n {\n readonly capabilities: Effect.Effect<\n PasskeyReactNativeCapabilities,\n PasskeyReactNativeUnavailable\n >;\n /** Registers a platform passkey; requested algorithms must include ES256 (-7). */\n readonly register: (\n started: typeof PasskeyRegistrationStarted.Type,\n ) => Effect.Effect<PasskeyReactNativeRegistration, PasskeyReactNativeFailure>;\n /** Conditional mediation is accepted for port compatibility but fails Unsupported. */\n readonly authenticate: (input: {\n readonly started: typeof PasskeyAuthenticationStarted.Type;\n readonly mediation: \"required\" | \"conditional\";\n }) => Effect.Effect<PasskeyReactNativeAuthentication, PasskeyReactNativeFailure>;\n }\n>()(\"effect-auth/PasskeyReactNative\") {}\n"],"mappings":";;;;;;AAkBA,IAAa,qBAAb,cAAwC,QAAQ,QAiB9C,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { Schema } from "effect";
2
+ //#region src/internal/models.d.ts
3
+ /** OS availability only; does not attest to entitlements, linking, or a saved key. */
4
+ export declare const PasskeyReactNativeCapabilities: Schema.Struct<{
5
+ readonly supported: Schema.Boolean;
6
+ readonly conditionalGet: Schema.Literal<"unsupported">;
7
+ readonly cancellation: Schema.Literal<"unsupported">;
8
+ }>;
9
+ export type PasskeyReactNativeCapabilities = typeof PasskeyReactNativeCapabilities.Type;
10
+ export declare const PasskeyReactNativeRegistration: Schema.Struct<{
11
+ readonly flowId: Schema.brand<Schema.String, "effect-auth/RequestBindingFlowId">;
12
+ readonly response: Schema.RedactedFromValue<Schema.NonEmptyString>;
13
+ }>;
14
+ export type PasskeyReactNativeRegistration = typeof PasskeyReactNativeRegistration.Type;
15
+ export declare const PasskeyReactNativeAuthentication: Schema.Struct<{
16
+ readonly flowId: Schema.brand<Schema.String, "effect-auth/RequestBindingFlowId">;
17
+ readonly response: Schema.RedactedFromValue<Schema.NonEmptyString>;
18
+ }>;
19
+ export type PasskeyReactNativeAuthentication = typeof PasskeyReactNativeAuthentication.Type;
20
+ declare const PasskeyReactNativeInputRejected_base: Schema.Class<PasskeyReactNativeInputRejected, Schema.TaggedStruct<"PasskeyReactNativeInputRejected", {}>, import("effect/Cause").YieldableError>;
21
+ export declare class PasskeyReactNativeInputRejected extends PasskeyReactNativeInputRejected_base {}
22
+ declare const PasskeyReactNativeUnsupported_base: Schema.Class<PasskeyReactNativeUnsupported, Schema.TaggedStruct<"PasskeyReactNativeUnsupported", {}>, import("effect/Cause").YieldableError>;
23
+ export declare class PasskeyReactNativeUnsupported extends PasskeyReactNativeUnsupported_base {}
24
+ declare const PasskeyReactNativeBusy_base: Schema.Class<PasskeyReactNativeBusy, Schema.TaggedStruct<"PasskeyReactNativeBusy", {}>, import("effect/Cause").YieldableError>;
25
+ export declare class PasskeyReactNativeBusy extends PasskeyReactNativeBusy_base {}
26
+ declare const PasskeyReactNativeNotCompleted_base: Schema.Class<PasskeyReactNativeNotCompleted, Schema.TaggedStruct<"PasskeyReactNativeNotCompleted", {
27
+ readonly reason: Schema.Literals<readonly ["cancelled", "no-credentials", "credential-exists", "interrupted", "timed-out", "request-failed"]>;
28
+ }>, import("effect/Cause").YieldableError>;
29
+ /** A local outcome, never evidence that a credential was not created. */
30
+ export declare class PasskeyReactNativeNotCompleted extends PasskeyReactNativeNotCompleted_base {}
31
+ declare const PasskeyReactNativeInvalidResponse_base: Schema.Class<PasskeyReactNativeInvalidResponse, Schema.TaggedStruct<"PasskeyReactNativeInvalidResponse", {}>, import("effect/Cause").YieldableError>;
32
+ export declare class PasskeyReactNativeInvalidResponse extends PasskeyReactNativeInvalidResponse_base {}
33
+ declare const PasskeyReactNativeUnavailable_base: Schema.Class<PasskeyReactNativeUnavailable, Schema.TaggedStruct<"PasskeyReactNativeUnavailable", {}>, import("effect/Cause").YieldableError>;
34
+ /** Configuration failures, native defects, and unrecognized failures; no native message/cause. */
35
+ export declare class PasskeyReactNativeUnavailable extends PasskeyReactNativeUnavailable_base {}
36
+ export declare const PasskeyReactNativeFailure: Schema.Union<readonly [typeof PasskeyReactNativeInputRejected, typeof PasskeyReactNativeUnsupported, typeof PasskeyReactNativeBusy, typeof PasskeyReactNativeNotCompleted, typeof PasskeyReactNativeInvalidResponse, typeof PasskeyReactNativeUnavailable]>;
37
+ export type PasskeyReactNativeFailure = typeof PasskeyReactNativeFailure.Type;
38
+ //#endregion
39
+ //# sourceMappingURL=models.d.mts.map
@@ -0,0 +1,44 @@
1
+ import { Schema } from "effect";
2
+ import { PasskeyAssertion, PasskeyAttestation, PasskeyAuthenticationStarted } from "@yielded/auth/Passkey";
3
+ //#region src/internal/models.ts
4
+ /** OS availability only; does not attest to entitlements, linking, or a saved key. */
5
+ const PasskeyReactNativeCapabilities = Schema.Struct({
6
+ supported: Schema.Boolean,
7
+ conditionalGet: Schema.Literal("unsupported"),
8
+ cancellation: Schema.Literal("unsupported")
9
+ });
10
+ const PasskeyReactNativeRegistration = Schema.Struct({
11
+ flowId: PasskeyAuthenticationStarted.fields.flowId,
12
+ response: PasskeyAttestation
13
+ });
14
+ const PasskeyReactNativeAuthentication = Schema.Struct({
15
+ flowId: PasskeyAuthenticationStarted.fields.flowId,
16
+ response: PasskeyAssertion
17
+ });
18
+ var PasskeyReactNativeInputRejected = class extends Schema.TaggedError()("PasskeyReactNativeInputRejected", {}) {};
19
+ var PasskeyReactNativeUnsupported = class extends Schema.TaggedError()("PasskeyReactNativeUnsupported", {}) {};
20
+ var PasskeyReactNativeBusy = class extends Schema.TaggedError()("PasskeyReactNativeBusy", {}) {};
21
+ /** A local outcome, never evidence that a credential was not created. */
22
+ var PasskeyReactNativeNotCompleted = class extends Schema.TaggedError()("PasskeyReactNativeNotCompleted", { reason: Schema.Literals([
23
+ "cancelled",
24
+ "no-credentials",
25
+ "credential-exists",
26
+ "interrupted",
27
+ "timed-out",
28
+ "request-failed"
29
+ ]) }) {};
30
+ var PasskeyReactNativeInvalidResponse = class extends Schema.TaggedError()("PasskeyReactNativeInvalidResponse", {}) {};
31
+ /** Configuration failures, native defects, and unrecognized failures; no native message/cause. */
32
+ var PasskeyReactNativeUnavailable = class extends Schema.TaggedError()("PasskeyReactNativeUnavailable", {}) {};
33
+ const PasskeyReactNativeFailure = Schema.Union([
34
+ PasskeyReactNativeInputRejected,
35
+ PasskeyReactNativeUnsupported,
36
+ PasskeyReactNativeBusy,
37
+ PasskeyReactNativeNotCompleted,
38
+ PasskeyReactNativeInvalidResponse,
39
+ PasskeyReactNativeUnavailable
40
+ ]);
41
+ //#endregion
42
+ export { PasskeyReactNativeAuthentication, PasskeyReactNativeBusy, PasskeyReactNativeCapabilities, PasskeyReactNativeFailure, PasskeyReactNativeInputRejected, PasskeyReactNativeInvalidResponse, PasskeyReactNativeNotCompleted, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable, PasskeyReactNativeUnsupported };
43
+
44
+ //# sourceMappingURL=models.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.mjs","names":[],"sources":["../../src/internal/models.ts"],"sourcesContent":["import {\n PasskeyAssertion,\n PasskeyAttestation,\n PasskeyAuthenticationStarted,\n} from \"@yielded/auth/Passkey\";\nimport { Schema } from \"effect\";\n\n/** OS availability only; does not attest to entitlements, linking, or a saved key. */\nexport const PasskeyReactNativeCapabilities = Schema.Struct({\n supported: Schema.Boolean,\n conditionalGet: Schema.Literal(\"unsupported\"),\n cancellation: Schema.Literal(\"unsupported\"),\n});\n\nexport type PasskeyReactNativeCapabilities = typeof PasskeyReactNativeCapabilities.Type;\n\nexport const PasskeyReactNativeRegistration = Schema.Struct({\n flowId: PasskeyAuthenticationStarted.fields.flowId,\n response: PasskeyAttestation,\n});\n\nexport type PasskeyReactNativeRegistration = typeof PasskeyReactNativeRegistration.Type;\n\nexport const PasskeyReactNativeAuthentication = Schema.Struct({\n flowId: PasskeyAuthenticationStarted.fields.flowId,\n response: PasskeyAssertion,\n});\n\nexport type PasskeyReactNativeAuthentication = typeof PasskeyReactNativeAuthentication.Type;\n\nexport class PasskeyReactNativeInputRejected extends Schema.TaggedError<PasskeyReactNativeInputRejected>()(\n \"PasskeyReactNativeInputRejected\",\n {},\n) {}\n\nexport class PasskeyReactNativeUnsupported extends Schema.TaggedError<PasskeyReactNativeUnsupported>()(\n \"PasskeyReactNativeUnsupported\",\n {},\n) {}\n\nexport class PasskeyReactNativeBusy extends Schema.TaggedError<PasskeyReactNativeBusy>()(\n \"PasskeyReactNativeBusy\",\n {},\n) {}\n\n/** A local outcome, never evidence that a credential was not created. */\nexport class PasskeyReactNativeNotCompleted extends Schema.TaggedError<PasskeyReactNativeNotCompleted>()(\n \"PasskeyReactNativeNotCompleted\",\n {\n reason: Schema.Literals([\n \"cancelled\",\n \"no-credentials\",\n \"credential-exists\",\n \"interrupted\",\n \"timed-out\",\n \"request-failed\",\n ]),\n },\n) {}\n\nexport class PasskeyReactNativeInvalidResponse extends Schema.TaggedError<PasskeyReactNativeInvalidResponse>()(\n \"PasskeyReactNativeInvalidResponse\",\n {},\n) {}\n\n/** Configuration failures, native defects, and unrecognized failures; no native message/cause. */\nexport class PasskeyReactNativeUnavailable extends Schema.TaggedError<PasskeyReactNativeUnavailable>()(\n \"PasskeyReactNativeUnavailable\",\n {},\n) {}\n\nexport const PasskeyReactNativeFailure = Schema.Union([\n PasskeyReactNativeInputRejected,\n PasskeyReactNativeUnsupported,\n PasskeyReactNativeBusy,\n PasskeyReactNativeNotCompleted,\n PasskeyReactNativeInvalidResponse,\n PasskeyReactNativeUnavailable,\n]);\n\nexport type PasskeyReactNativeFailure = typeof PasskeyReactNativeFailure.Type;\n"],"mappings":";;;;AAQA,MAAa,iCAAiC,OAAO,OAAO;CAC1D,WAAW,OAAO;CAClB,gBAAgB,OAAO,QAAQ,aAAa;CAC5C,cAAc,OAAO,QAAQ,aAAa;AAC5C,CAAC;AAID,MAAa,iCAAiC,OAAO,OAAO;CAC1D,QAAQ,6BAA6B,OAAO;CAC5C,UAAU;AACZ,CAAC;AAID,MAAa,mCAAmC,OAAO,OAAO;CAC5D,QAAQ,6BAA6B,OAAO;CAC5C,UAAU;AACZ,CAAC;AAID,IAAa,kCAAb,cAAqD,OAAO,YAA6C,CAAC,CACxG,mCACA,CAAC,CACH,CAAC,CAAC,CAAC;AAEH,IAAa,gCAAb,cAAmD,OAAO,YAA2C,CAAC,CACpG,iCACA,CAAC,CACH,CAAC,CAAC,CAAC;AAEH,IAAa,yBAAb,cAA4C,OAAO,YAAoC,CAAC,CACtF,0BACA,CAAC,CACH,CAAC,CAAC,CAAC;;AAGH,IAAa,iCAAb,cAAoD,OAAO,YAA4C,CAAC,CACtG,kCACA,EACE,QAAQ,OAAO,SAAS;CACtB;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,EACH,CACF,CAAC,CAAC,CAAC;AAEH,IAAa,oCAAb,cAAuD,OAAO,YAA+C,CAAC,CAC5G,qCACA,CAAC,CACH,CAAC,CAAC,CAAC;;AAGH,IAAa,gCAAb,cAAmD,OAAO,YAA2C,CAAC,CACpG,iCACA,CAAC,CACH,CAAC,CAAC,CAAC;AAEH,MAAa,4BAA4B,OAAO,MAAM;CACpD;CACA;CACA;CACA;CACA;CACA;AACF,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { PasskeyReactNative } from "./PasskeyReactNative.mjs";
2
+ import { Effect, Layer } from "effect";
3
+ //#region src/internal/react-native-passkey.d.ts
4
+ type Native = PasskeyReactNative["Service"];
5
+ /** Creates the iOS adapter. Ceremonies own their scopes; construction performs no native I/O. */
6
+ export declare const make: () => Effect.Effect<Native>;
7
+ export declare const layer: Layer.Layer<PasskeyReactNative>;
8
+ //#endregion
9
+ //# sourceMappingURL=react-native-passkey.d.mts.map
@@ -0,0 +1,200 @@
1
+ import { PasskeyReactNative } from "./PasskeyReactNative.mjs";
2
+ import { PasskeyReactNativeAuthentication, PasskeyReactNativeBusy, PasskeyReactNativeInputRejected, PasskeyReactNativeInvalidResponse, PasskeyReactNativeNotCompleted, PasskeyReactNativeRegistration, PasskeyReactNativeUnavailable, PasskeyReactNativeUnsupported } from "./models.mjs";
3
+ import { authenticationWire, nativeAuthentication, nativeRegistration, registrationWire } from "./wire.mjs";
4
+ import { Cause, DateTime, Effect, Layer, Predicate, Redacted, Schema } from "effect";
5
+ import { PasskeyAuthenticationStarted, PasskeyRegistrationStarted, snapshotPasskey } from "@yielded/auth/Passkey";
6
+ import { reportAuthFailure } from "@yielded/auth/Persistence";
7
+ import { Platform } from "react-native";
8
+ import { Passkey } from "react-native-passkey";
9
+ //#region src/internal/react-native-passkey.ts
10
+ const unavailable = () => new PasskeyReactNativeUnavailable({});
11
+ const rejected = () => new PasskeyReactNativeInputRejected({});
12
+ const invalid = () => new PasskeyReactNativeInvalidResponse({});
13
+ const timedOut = () => new PasskeyReactNativeNotCompleted({ reason: "timed-out" });
14
+ const redactDefects = (failure) => (effect) => effect.pipe(Effect.catchCause((cause) => Cause.hasDies(cause) ? reportAuthFailure("passkey-react-native", cause).pipe(Effect.andThen(Effect.fail(failure()))) : Effect.failCause(cause)));
15
+ const unexpected = redactDefects(unavailable);
16
+ const timeout = Schema.Int.check(Schema.isBetween({
17
+ minimum: 1,
18
+ maximum: 3e5
19
+ }));
20
+ const registrationInput = Schema.Struct({
21
+ ...PasskeyRegistrationStarted.fields,
22
+ options: Schema.Struct({
23
+ ...PasskeyRegistrationStarted.fields.options.fields,
24
+ timeout
25
+ })
26
+ });
27
+ const authenticationInput = Schema.Struct({
28
+ started: Schema.Struct({
29
+ ...PasskeyAuthenticationStarted.fields,
30
+ options: Schema.Struct({
31
+ ...PasskeyAuthenticationStarted.fields.options.fields,
32
+ timeout
33
+ })
34
+ }),
35
+ mediation: Schema.Literals(["required", "conditional"])
36
+ });
37
+ const environment = Effect.sync(() => {
38
+ if (Platform.OS !== "ios") return {
39
+ supported: false,
40
+ exclusions: false
41
+ };
42
+ const [major = 0, minor = 0] = Schema.decodeUnknownSync(Schema.String.check(Schema.isPattern(/^\d+(?:\.\d+){0,2}$/)))(Platform.Version).split(".").map(Number);
43
+ const supported = Schema.decodeUnknownSync(Schema.Boolean)(Passkey.isSupported());
44
+ return {
45
+ supported: major >= 16 && supported,
46
+ exclusions: major > 17 || major === 17 && minor >= 4
47
+ };
48
+ }).pipe(unexpected);
49
+ let activeLease;
50
+ const settle = (lease) => {
51
+ lease.phase = "settled";
52
+ if (activeLease === lease) activeLease = void 0;
53
+ };
54
+ const acquire = Effect.gen(function* () {
55
+ if (activeLease !== void 0) return yield* new PasskeyReactNativeBusy({});
56
+ const lease = {
57
+ phase: "pre-native",
58
+ listening: true
59
+ };
60
+ activeLease = lease;
61
+ return lease;
62
+ });
63
+ const release = (lease) => Effect.sync(() => {
64
+ lease.listening = false;
65
+ if (lease.phase === "pre-native") settle(lease);
66
+ });
67
+ const nativeFailure = (value) => Effect.suspend(() => {
68
+ if (!Predicate.isObject(value)) return Effect.die(value);
69
+ const code = Schema.decodeUnknownOption(Schema.String)(value.error);
70
+ if (code._tag === "None") return Effect.die(value);
71
+ switch (code.value) {
72
+ case "NotSupported": return Effect.fail(new PasskeyReactNativeUnsupported({}));
73
+ case "InvalidChallenge":
74
+ case "InvalidUserId": return Effect.fail(rejected());
75
+ case "UserCancelled": return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "cancelled" }));
76
+ case "NoCredentials": return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "no-credentials" }));
77
+ case "CredentialAlreadyExists": return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "credential-exists" }));
78
+ case "Interrupted": return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "interrupted" }));
79
+ case "TimedOut": return Effect.fail(timedOut());
80
+ case "RequestFailed": return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "request-failed" }));
81
+ default: return Effect.die(value);
82
+ }
83
+ });
84
+ const native = (lease, call) => Effect.callback((resume, signal) => {
85
+ if (signal.aborted || !lease.listening || lease.phase !== "pre-native" || activeLease !== lease) {
86
+ resume(Effect.die(/* @__PURE__ */ new Error("Invalid passkey native lease")));
87
+ return;
88
+ }
89
+ lease.phase = "native";
90
+ try {
91
+ Promise.resolve(call()).then((value) => {
92
+ try {
93
+ settle(lease);
94
+ if (!signal.aborted && lease.listening) resume(Effect.succeed(value));
95
+ } catch {}
96
+ }, (error) => {
97
+ try {
98
+ settle(lease);
99
+ if (!signal.aborted && lease.listening) resume(nativeFailure(error));
100
+ } catch {}
101
+ });
102
+ } catch (error) {
103
+ settle(lease);
104
+ resume(Effect.die(error));
105
+ }
106
+ });
107
+ const remaining = Effect.fnUntraced(function* (deadline) {
108
+ const duration = deadline - DateTime.toEpochMillis(yield* DateTime.now);
109
+ if (duration <= 0) return yield* timedOut();
110
+ return Math.max(1, Math.floor(duration));
111
+ });
112
+ const runCeremony = Effect.fnUntraced(function* (started, use) {
113
+ const now = DateTime.toEpochMillis(yield* DateTime.now);
114
+ if (started.expiresAtMillis <= now) return yield* rejected();
115
+ const deadline = Math.min(started.expiresAtMillis, now + started.options.timeout);
116
+ return yield* Effect.scoped(Effect.gen(function* () {
117
+ const lease = yield* Effect.acquireRelease(acquire, release);
118
+ const duration = yield* remaining(deadline);
119
+ return yield* use(lease, deadline).pipe(Effect.timeoutOrElse({
120
+ duration,
121
+ orElse: () => Effect.fail(timedOut())
122
+ }));
123
+ }));
124
+ });
125
+ const decodeResponse = (schema, value) => Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(invalid), redactDefects(invalid));
126
+ /** Creates the iOS adapter. Ceremonies own their scopes; construction performs no native I/O. */
127
+ const make = () => Effect.sync(() => {
128
+ const register = Effect.fnUntraced(function* (input) {
129
+ const started = yield* snapshotPasskey(registrationInput, input).pipe(Effect.mapError(rejected));
130
+ const env = yield* environment;
131
+ if (!env.supported || !started.options.pubKeyCredParams.some(({ alg }) => alg === -7) || started.options.excludeCredentials.length > 0 && !env.exclusions) return yield* new PasskeyReactNativeUnsupported({});
132
+ return yield* runCeremony(started, (lease, deadline) => Effect.gen(function* () {
133
+ const options = started.options;
134
+ const request = {
135
+ ...options,
136
+ rp: { ...options.rp },
137
+ user: { ...options.user },
138
+ authenticatorSelection: { ...options.authenticatorSelection },
139
+ pubKeyCredParams: options.pubKeyCredParams.map((value) => ({ ...value })),
140
+ excludeCredentials: options.excludeCredentials.map((value) => ({ ...value })),
141
+ timeout: yield* remaining(deadline)
142
+ };
143
+ const credential = yield* decodeResponse(nativeRegistration, yield* native(lease, () => Passkey.createPlatformKey(request)));
144
+ const response = yield* Schema.encodeEffect(registrationWire)({
145
+ id: credential.id,
146
+ rawId: credential.rawId,
147
+ type: "public-key",
148
+ response: credential.response,
149
+ clientExtensionResults: {}
150
+ }).pipe(Effect.mapError(invalid));
151
+ yield* remaining(deadline);
152
+ return yield* snapshotPasskey(PasskeyReactNativeRegistration, {
153
+ flowId: started.flowId,
154
+ response: Redacted.make(response)
155
+ }).pipe(Effect.mapError(invalid));
156
+ }));
157
+ }, unexpected);
158
+ const authenticate = Effect.fnUntraced(function* (input) {
159
+ const { started, mediation } = yield* snapshotPasskey(authenticationInput, input).pipe(Effect.mapError(rejected));
160
+ if (mediation === "conditional" || !(yield* environment).supported) return yield* new PasskeyReactNativeUnsupported({});
161
+ return yield* runCeremony(started, (lease, deadline) => Effect.gen(function* () {
162
+ const request = {
163
+ ...started.options,
164
+ allowCredentials: started.options.allowCredentials.map((value) => ({ ...value })),
165
+ timeout: yield* remaining(deadline)
166
+ };
167
+ const credential = yield* decodeResponse(nativeAuthentication, yield* native(lease, () => Passkey.get(request)));
168
+ const { userHandle, ...responseFields } = credential.response;
169
+ const response = yield* Schema.encodeEffect(authenticationWire)({
170
+ id: credential.id,
171
+ rawId: credential.rawId ?? credential.id,
172
+ type: "public-key",
173
+ response: {
174
+ ...responseFields,
175
+ ...userHandle === null || userHandle === void 0 ? {} : { userHandle }
176
+ },
177
+ clientExtensionResults: {}
178
+ }).pipe(Effect.mapError(invalid));
179
+ yield* remaining(deadline);
180
+ return yield* snapshotPasskey(PasskeyReactNativeAuthentication, {
181
+ flowId: started.flowId,
182
+ response: Redacted.make(response)
183
+ }).pipe(Effect.mapError(invalid));
184
+ }));
185
+ }, unexpected);
186
+ return PasskeyReactNative.of({
187
+ register,
188
+ authenticate,
189
+ capabilities: environment.pipe(Effect.map(({ supported }) => ({
190
+ supported,
191
+ conditionalGet: "unsupported",
192
+ cancellation: "unsupported"
193
+ })))
194
+ });
195
+ });
196
+ const layer = Layer.effect(PasskeyReactNative, make());
197
+ //#endregion
198
+ export { layer, make };
199
+
200
+ //# sourceMappingURL=react-native-passkey.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-native-passkey.mjs","names":[],"sources":["../../src/internal/react-native-passkey.ts"],"sourcesContent":["import {\n PasskeyAuthenticationStarted,\n PasskeyRegistrationStarted,\n snapshotPasskey,\n} from \"@yielded/auth/Passkey\";\nimport { reportAuthFailure } from \"@yielded/auth/Persistence\";\nimport { Cause, DateTime, Effect, Layer, Predicate, Redacted, Schema } from \"effect\";\nimport { Platform } from \"react-native\";\nimport { Passkey, type PasskeyCreateRequest, type PasskeyGetRequest } from \"react-native-passkey\";\n\nimport {\n PasskeyReactNativeAuthentication,\n PasskeyReactNativeBusy,\n type PasskeyReactNativeFailure,\n PasskeyReactNativeInputRejected,\n PasskeyReactNativeInvalidResponse,\n PasskeyReactNativeNotCompleted,\n PasskeyReactNativeRegistration,\n PasskeyReactNativeUnavailable,\n PasskeyReactNativeUnsupported,\n} from \"./models\";\nimport { PasskeyReactNative } from \"./PasskeyReactNative\";\nimport {\n authenticationWire,\n nativeAuthentication,\n nativeRegistration,\n registrationWire,\n} from \"./wire\";\n\ntype Native = PasskeyReactNative[\"Service\"];\nconst unavailable = () => new PasskeyReactNativeUnavailable({});\nconst rejected = () => new PasskeyReactNativeInputRejected({});\nconst invalid = () => new PasskeyReactNativeInvalidResponse({});\nconst timedOut = () => new PasskeyReactNativeNotCompleted({ reason: \"timed-out\" });\n\nconst redactDefects =\n <Failure>(failure: () => Failure) =>\n <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n effect.pipe(\n Effect.catchCause((cause): Effect.Effect<never, E | Failure> =>\n Cause.hasDies(cause)\n ? reportAuthFailure(\"passkey-react-native\", cause).pipe(\n Effect.andThen(Effect.fail(failure())),\n )\n : Effect.failCause(cause),\n ),\n );\n\nconst unexpected = redactDefects(unavailable);\n\nconst timeout = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 300000 }));\n\nconst registrationInput = Schema.Struct({\n ...PasskeyRegistrationStarted.fields,\n options: Schema.Struct({ ...PasskeyRegistrationStarted.fields.options.fields, timeout }),\n});\n\nconst authenticationInput = Schema.Struct({\n started: Schema.Struct({\n ...PasskeyAuthenticationStarted.fields,\n options: Schema.Struct({ ...PasskeyAuthenticationStarted.fields.options.fields, timeout }),\n }),\n mediation: Schema.Literals([\"required\", \"conditional\"]),\n});\n\nconst environment = Effect.sync(() => {\n if (Platform.OS !== \"ios\") return { supported: false, exclusions: false };\n\n // The maintained helper only checks OS version (and includes iOS 15).\n // Decode platform metadata before applying this adapter's iOS 16 minimum.\n // eslint-disable-next-line no-restricted-properties\n const version = Schema.decodeUnknownSync(\n Schema.String.check(Schema.isPattern(/^\\d+(?:\\.\\d+){0,2}$/)),\n )(Platform.Version);\n\n const [major = 0, minor = 0] = version.split(\".\").map(Number);\n // eslint-disable-next-line no-restricted-properties\n const supported = Schema.decodeUnknownSync(Schema.Boolean)(Passkey.isSupported());\n\n return {\n supported: major >= 16 && supported,\n exclusions: major > 17 || (major === 17 && minor >= 4),\n };\n}).pipe(unexpected);\n\ninterface Lease {\n phase: \"pre-native\" | \"native\" | \"settled\";\n listening: boolean;\n}\n// The peer has one native resolver. Coordinate every instance of this installed\n// module; callers must not also use the peer directly or load duplicate copies.\nlet activeLease: Lease | undefined;\n\nconst settle = (lease: Lease): void => {\n lease.phase = \"settled\";\n if (activeLease === lease) activeLease = undefined;\n};\n\nconst acquire = Effect.gen(function* () {\n if (activeLease !== undefined) return yield* new PasskeyReactNativeBusy({});\n const lease: Lease = { phase: \"pre-native\", listening: true };\n\n activeLease = lease;\n\n return lease;\n});\n\nconst release = (lease: Lease) =>\n Effect.sync(() => {\n lease.listening = false;\n // There is no cancel API. Free only a request that has not reached native.\n if (lease.phase === \"pre-native\") settle(lease);\n });\n\nconst nativeFailure = (value: unknown): Effect.Effect<never, PasskeyReactNativeFailure> =>\n Effect.suspend((): Effect.Effect<never, PasskeyReactNativeFailure> => {\n if (!Predicate.isObject(value)) return Effect.die(value);\n // Inspect only the normalized code; never copy message, native stack, or cause.\n // eslint-disable-next-line no-restricted-properties\n const code = Schema.decodeUnknownOption(Schema.String)(value.error);\n\n if (code._tag === \"None\") return Effect.die(value);\n switch (code.value) {\n case \"NotSupported\":\n return Effect.fail(new PasskeyReactNativeUnsupported({}));\n case \"InvalidChallenge\":\n case \"InvalidUserId\":\n return Effect.fail(rejected());\n case \"UserCancelled\":\n return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: \"cancelled\" }));\n case \"NoCredentials\":\n return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: \"no-credentials\" }));\n case \"CredentialAlreadyExists\":\n return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: \"credential-exists\" }));\n case \"Interrupted\":\n return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: \"interrupted\" }));\n case \"TimedOut\":\n return Effect.fail(timedOut());\n case \"RequestFailed\":\n return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: \"request-failed\" }));\n default:\n return Effect.die(value);\n }\n });\n\nconst native = (lease: Lease, call: () => Promise<unknown>) =>\n Effect.callback<unknown, PasskeyReactNativeFailure>((resume, signal) => {\n if (\n signal.aborted ||\n !lease.listening ||\n lease.phase !== \"pre-native\" ||\n activeLease !== lease\n ) {\n resume(Effect.die(new Error(\"Invalid passkey native lease\")));\n\n return;\n }\n lease.phase = \"native\";\n try {\n // The caller owns classification and diagnostic reporting. Callbacks only\n // settle the guard and resume a live caller. After interruption they discard\n // the outcome without inspecting it or retaining/running a scoped logger.\n void Promise.resolve(call()).then(\n (value) => {\n try {\n settle(lease);\n if (!signal.aborted && lease.listening) resume(Effect.succeed(value));\n } catch {\n /* A detached handler must not reject. */\n }\n },\n (error) => {\n try {\n settle(lease);\n if (!signal.aborted && lease.listening) resume(nativeFailure(error));\n } catch {\n /* A detached handler must not reject. */\n }\n },\n );\n } catch (error) {\n settle(lease);\n resume(Effect.die(error));\n }\n });\n\nconst remaining = Effect.fnUntraced(function* (deadline: number) {\n const duration = deadline - DateTime.toEpochMillis(yield* DateTime.now);\n\n if (duration <= 0) return yield* timedOut();\n\n return Math.max(1, Math.floor(duration));\n});\n\nconst runCeremony = Effect.fnUntraced(function* <A>(\n started: { readonly expiresAtMillis: number; readonly options: { readonly timeout: number } },\n use: (lease: Lease, deadline: number) => Effect.Effect<A, PasskeyReactNativeFailure>,\n) {\n const now = DateTime.toEpochMillis(yield* DateTime.now);\n\n if (started.expiresAtMillis <= now) return yield* rejected();\n const deadline = Math.min(started.expiresAtMillis, now + started.options.timeout);\n\n return yield* Effect.scoped(\n Effect.gen(function* () {\n const lease = yield* Effect.acquireRelease(acquire, release);\n const duration = yield* remaining(deadline);\n\n return yield* use(lease, deadline).pipe(\n Effect.timeoutOrElse({ duration, orElse: () => Effect.fail(timedOut()) }),\n );\n }),\n );\n});\n\nconst decodeResponse = <S extends Schema.Codec<unknown, unknown, never, never>>(\n schema: S,\n value: unknown,\n) =>\n // Peer declarations are not validation. Parse bounded base64url fields and\n // discard unknown fields before constructing the server's JSON envelope.\n // eslint-disable-next-line no-restricted-properties\n Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(invalid), redactDefects(invalid));\n\n/** Creates the iOS adapter. Ceremonies own their scopes; construction performs no native I/O. */\nexport const make = (): Effect.Effect<Native> =>\n Effect.sync(() => {\n const register = Effect.fnUntraced(function* (input: Parameters<Native[\"register\"]>[0]) {\n const started = yield* snapshotPasskey(registrationInput, input).pipe(\n Effect.mapError(rejected),\n );\n\n const env = yield* environment;\n\n if (\n !env.supported ||\n !started.options.pubKeyCredParams.some(({ alg }) => alg === -7) ||\n (started.options.excludeCredentials.length > 0 && !env.exclusions)\n )\n return yield* new PasskeyReactNativeUnsupported({});\n\n return yield* runCeremony(started, (lease, deadline) =>\n Effect.gen(function* () {\n const options = started.options;\n\n const request: PasskeyCreateRequest = {\n ...options,\n rp: { ...options.rp },\n user: { ...options.user },\n authenticatorSelection: { ...options.authenticatorSelection },\n pubKeyCredParams: options.pubKeyCredParams.map((value) => ({ ...value })),\n excludeCredentials: options.excludeCredentials.map((value) => ({ ...value })),\n timeout: yield* remaining(deadline),\n };\n\n // The platform path preserves the required attestation: \"none\" policy.\n const credential = yield* decodeResponse(\n nativeRegistration,\n yield* native(lease, () => Passkey.createPlatformKey(request)),\n );\n\n const response = yield* Schema.encodeEffect(registrationWire)({\n id: credential.id,\n rawId: credential.rawId,\n type: \"public-key\",\n response: credential.response,\n clientExtensionResults: {},\n }).pipe(Effect.mapError(invalid));\n\n yield* remaining(deadline);\n\n return yield* snapshotPasskey(PasskeyReactNativeRegistration, {\n flowId: started.flowId,\n response: Redacted.make(response),\n }).pipe(Effect.mapError(invalid));\n }),\n );\n }, unexpected);\n\n const authenticate = Effect.fnUntraced(function* (\n input: Parameters<Native[\"authenticate\"]>[0],\n ) {\n const { started, mediation } = yield* snapshotPasskey(authenticationInput, input).pipe(\n Effect.mapError(rejected),\n );\n\n if (mediation === \"conditional\" || !(yield* environment).supported)\n return yield* new PasskeyReactNativeUnsupported({});\n\n return yield* runCeremony(started, (lease, deadline) =>\n Effect.gen(function* () {\n const request: PasskeyGetRequest = {\n ...started.options,\n allowCredentials: started.options.allowCredentials.map((value) => ({ ...value })),\n timeout: yield* remaining(deadline),\n };\n\n const credential = yield* decodeResponse(\n nativeAuthentication,\n yield* native(lease, () => Passkey.get(request)),\n );\n\n const { userHandle, ...responseFields } = credential.response;\n\n const response = yield* Schema.encodeEffect(authenticationWire)({\n id: credential.id,\n rawId: credential.rawId ?? credential.id,\n type: \"public-key\",\n response: {\n ...responseFields,\n ...(userHandle === null || userHandle === undefined ? {} : { userHandle }),\n },\n clientExtensionResults: {},\n }).pipe(Effect.mapError(invalid));\n\n yield* remaining(deadline);\n\n return yield* snapshotPasskey(PasskeyReactNativeAuthentication, {\n flowId: started.flowId,\n response: Redacted.make(response),\n }).pipe(Effect.mapError(invalid));\n }),\n );\n }, unexpected);\n\n return PasskeyReactNative.of({\n register,\n authenticate,\n capabilities: environment.pipe(\n Effect.map(({ supported }) => ({\n supported,\n conditionalGet: \"unsupported\" as const,\n cancellation: \"unsupported\" as const,\n })),\n ),\n });\n });\n\nexport const layer: Layer.Layer<PasskeyReactNative> = Layer.effect(PasskeyReactNative, make());\n"],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { Schema } from "effect";
2
+ import { PasskeyProtocolCredentialId, PasskeyUserHandle } from "@yielded/auth/Passkey";
3
+ import { Base64Url } from "effect/encoding";
4
+ //#region src/internal/wire.ts
5
+ const bytes = (minimum, maximum) => Schema.String.check(Schema.isMaxLength(Math.ceil(maximum * 4 / 3)), Schema.makeFilter((value) => {
6
+ const decoded = Base64Url.decode(value);
7
+ return decoded._tag === "Success" && decoded.success.length >= minimum && decoded.success.length <= maximum && Base64Url.encode(decoded.success) === value;
8
+ }));
9
+ const envelope = {
10
+ id: PasskeyProtocolCredentialId,
11
+ rawId: Schema.optionalKey(PasskeyProtocolCredentialId),
12
+ type: Schema.optionalKey(Schema.Literal("public-key"))
13
+ };
14
+ const matchingId = Schema.makeFilter((value) => value.rawId === void 0 || value.rawId === value.id);
15
+ const nativeRegistration = Schema.Struct({
16
+ ...envelope,
17
+ rawId: PasskeyProtocolCredentialId,
18
+ response: Schema.Struct({
19
+ clientDataJSON: bytes(1, 8192),
20
+ attestationObject: bytes(1, 65536)
21
+ })
22
+ }).check(matchingId);
23
+ const nativeAuthentication = Schema.Struct({
24
+ ...envelope,
25
+ response: Schema.Struct({
26
+ clientDataJSON: bytes(1, 8192),
27
+ authenticatorData: bytes(37, 16384),
28
+ signature: bytes(1, 1024),
29
+ userHandle: Schema.optionalKey(Schema.NullOr(PasskeyUserHandle))
30
+ })
31
+ }).check(matchingId);
32
+ const wireEnvelope = {
33
+ id: PasskeyProtocolCredentialId,
34
+ rawId: PasskeyProtocolCredentialId,
35
+ type: Schema.Literal("public-key"),
36
+ clientExtensionResults: Schema.Struct({})
37
+ };
38
+ const registrationWire = Schema.fromJsonString(Schema.Struct({
39
+ ...wireEnvelope,
40
+ response: nativeRegistration.fields.response
41
+ }));
42
+ const authenticationWire = Schema.fromJsonString(Schema.Struct({
43
+ ...wireEnvelope,
44
+ response: Schema.Struct({
45
+ ...nativeAuthentication.fields.response.fields,
46
+ userHandle: Schema.optionalKey(PasskeyUserHandle)
47
+ })
48
+ }));
49
+ //#endregion
50
+ export { authenticationWire, nativeAuthentication, nativeRegistration, registrationWire };
51
+
52
+ //# sourceMappingURL=wire.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire.mjs","names":[],"sources":["../../src/internal/wire.ts"],"sourcesContent":["import { PasskeyProtocolCredentialId, PasskeyUserHandle } from \"@yielded/auth/Passkey\";\nimport { Schema } from \"effect\";\nimport { Base64Url } from \"effect/encoding\";\n\nconst bytes = (minimum: number, maximum: number) =>\n Schema.String.check(\n Schema.isMaxLength(Math.ceil((maximum * 4) / 3)),\n Schema.makeFilter((value) => {\n const decoded = Base64Url.decode(value);\n\n return (\n decoded._tag === \"Success\" &&\n decoded.success.length >= minimum &&\n decoded.success.length <= maximum &&\n Base64Url.encode(decoded.success) === value\n );\n }),\n );\n\n// The peer allows omitted type/rawId in assertions. Normalize only missing fields;\n// a supplied mismatch must fail. Never forward unsolicited extensions or metadata.\nconst envelope = {\n id: PasskeyProtocolCredentialId,\n rawId: Schema.optionalKey(PasskeyProtocolCredentialId),\n type: Schema.optionalKey(Schema.Literal(\"public-key\")),\n};\n\nconst matchingId = Schema.makeFilter<{ readonly id: string; readonly rawId?: string }>(\n (value) => value.rawId === undefined || value.rawId === value.id,\n);\n\nexport const nativeRegistration = Schema.Struct({\n ...envelope,\n rawId: PasskeyProtocolCredentialId,\n response: Schema.Struct({\n clientDataJSON: bytes(1, 8192),\n attestationObject: bytes(1, 65536),\n }),\n}).check(matchingId);\n\nexport const nativeAuthentication = Schema.Struct({\n ...envelope,\n response: Schema.Struct({\n clientDataJSON: bytes(1, 8192),\n authenticatorData: bytes(37, 16384),\n signature: bytes(1, 1024),\n userHandle: Schema.optionalKey(Schema.NullOr(PasskeyUserHandle)),\n }),\n}).check(matchingId);\n\nconst wireEnvelope = {\n id: PasskeyProtocolCredentialId,\n rawId: PasskeyProtocolCredentialId,\n type: Schema.Literal(\"public-key\"),\n clientExtensionResults: Schema.Struct({}),\n};\n\nexport const registrationWire = Schema.fromJsonString(\n Schema.Struct({\n ...wireEnvelope,\n response: nativeRegistration.fields.response,\n }),\n);\n\nexport const authenticationWire = Schema.fromJsonString(\n Schema.Struct({\n ...wireEnvelope,\n response: Schema.Struct({\n ...nativeAuthentication.fields.response.fields,\n userHandle: Schema.optionalKey(PasskeyUserHandle),\n }),\n }),\n);\n"],"mappings":";;;;AAIA,MAAM,SAAS,SAAiB,YAC9B,OAAO,OAAO,MACZ,OAAO,YAAY,KAAK,KAAM,UAAU,IAAK,CAAC,CAAC,GAC/C,OAAO,YAAY,UAAU;CAC3B,MAAM,UAAU,UAAU,OAAO,KAAK;CAEtC,OACE,QAAQ,SAAS,aACjB,QAAQ,QAAQ,UAAU,WAC1B,QAAQ,QAAQ,UAAU,WAC1B,UAAU,OAAO,QAAQ,OAAO,MAAM;AAE1C,CAAC,CACH;AAIF,MAAM,WAAW;CACf,IAAI;CACJ,OAAO,OAAO,YAAY,2BAA2B;CACrD,MAAM,OAAO,YAAY,OAAO,QAAQ,YAAY,CAAC;AACvD;AAEA,MAAM,aAAa,OAAO,YACvB,UAAU,MAAM,UAAU,KAAA,KAAa,MAAM,UAAU,MAAM,EAChE;AAEA,MAAa,qBAAqB,OAAO,OAAO;CAC9C,GAAG;CACH,OAAO;CACP,UAAU,OAAO,OAAO;EACtB,gBAAgB,MAAM,GAAG,IAAI;EAC7B,mBAAmB,MAAM,GAAG,KAAK;CACnC,CAAC;AACH,CAAC,CAAC,CAAC,MAAM,UAAU;AAEnB,MAAa,uBAAuB,OAAO,OAAO;CAChD,GAAG;CACH,UAAU,OAAO,OAAO;EACtB,gBAAgB,MAAM,GAAG,IAAI;EAC7B,mBAAmB,MAAM,IAAI,KAAK;EAClC,WAAW,MAAM,GAAG,IAAI;EACxB,YAAY,OAAO,YAAY,OAAO,OAAO,iBAAiB,CAAC;CACjE,CAAC;AACH,CAAC,CAAC,CAAC,MAAM,UAAU;AAEnB,MAAM,eAAe;CACnB,IAAI;CACJ,OAAO;CACP,MAAM,OAAO,QAAQ,YAAY;CACjC,wBAAwB,OAAO,OAAO,CAAC,CAAC;AAC1C;AAEA,MAAa,mBAAmB,OAAO,eACrC,OAAO,OAAO;CACZ,GAAG;CACH,UAAU,mBAAmB,OAAO;AACtC,CAAC,CACH;AAEA,MAAa,qBAAqB,OAAO,eACvC,OAAO,OAAO;CACZ,GAAG;CACH,UAAU,OAAO,OAAO;EACtB,GAAG,qBAAqB,OAAO,SAAS;EACxC,YAAY,OAAO,YAAY,iBAAiB;CAClD,CAAC;AACH,CAAC,CACH"}
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name":"@yielded/auth-react-native","version":"0.1.0-beta.12","dependencies":{"@yielded/auth":"0.1.0-beta.12"},"devDependencies":{"@effect/vitest":"4.0.0","effect":"4.0.0","react-native":"0.81.4","react-native-passkey":"3.6.2","typescript":"7.0.2","vite-plus":"0.3.3"},"peerDependencies":{"effect":"^4.0.0","react-native":">=0.81 <1","react-native-passkey":"~3.6.2"},"exports":{".":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"}},"description":"iOS passkey ceremonies for Yielded Auth and React Native.","homepage":"https://yielded.dev/auth/","bugs":{"url":"https://github.com/yielded-dev/auth/issues"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/yielded-dev/auth.git","directory":"packages/auth-react-native"},"files":["dist","src"],"type":"module","sideEffects":[],"publishConfig":{"access":"public"},"scripts":{"build":"vp pack","check":"tsc --noEmit -p tsconfig.json","test":"vp test --passWithNoTests"}}
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { PasskeyReactNative } from "./internal/PasskeyReactNative";
2
+
3
+ export {
4
+ PasskeyReactNativeAuthentication,
5
+ PasskeyReactNativeBusy,
6
+ PasskeyReactNativeCapabilities,
7
+ PasskeyReactNativeFailure,
8
+ PasskeyReactNativeInputRejected,
9
+ PasskeyReactNativeInvalidResponse,
10
+ PasskeyReactNativeNotCompleted,
11
+ PasskeyReactNativeRegistration,
12
+ PasskeyReactNativeUnavailable,
13
+ PasskeyReactNativeUnsupported,
14
+ } from "./internal/models";
15
+
16
+ export { make, layer } from "./internal/react-native-passkey";
@@ -0,0 +1,36 @@
1
+ import type {
2
+ PasskeyAuthenticationStarted,
3
+ PasskeyRegistrationStarted,
4
+ } from "@yielded/auth/Passkey";
5
+ import { Context, type Effect } from "effect";
6
+
7
+ import type {
8
+ PasskeyReactNativeAuthentication,
9
+ PasskeyReactNativeCapabilities,
10
+ PasskeyReactNativeFailure,
11
+ PasskeyReactNativeRegistration,
12
+ PasskeyReactNativeUnavailable,
13
+ } from "./models";
14
+
15
+ /** Client-local iOS ceremonies. Applications own Begin/Complete and request binding.
16
+ * Interruption stops delivery, but cannot dismiss OS UI. A started native request
17
+ * retains the shared busy guard until its promise settles, even after interruption.
18
+ * The calling fiber owns failure diagnostics; late settlement only clears the guard. */
19
+ export class PasskeyReactNative extends Context.Service<
20
+ PasskeyReactNative,
21
+ {
22
+ readonly capabilities: Effect.Effect<
23
+ PasskeyReactNativeCapabilities,
24
+ PasskeyReactNativeUnavailable
25
+ >;
26
+ /** Registers a platform passkey; requested algorithms must include ES256 (-7). */
27
+ readonly register: (
28
+ started: typeof PasskeyRegistrationStarted.Type,
29
+ ) => Effect.Effect<PasskeyReactNativeRegistration, PasskeyReactNativeFailure>;
30
+ /** Conditional mediation is accepted for port compatibility but fails Unsupported. */
31
+ readonly authenticate: (input: {
32
+ readonly started: typeof PasskeyAuthenticationStarted.Type;
33
+ readonly mediation: "required" | "conditional";
34
+ }) => Effect.Effect<PasskeyReactNativeAuthentication, PasskeyReactNativeFailure>;
35
+ }
36
+ >()("effect-auth/PasskeyReactNative") {}
@@ -0,0 +1,81 @@
1
+ import {
2
+ PasskeyAssertion,
3
+ PasskeyAttestation,
4
+ PasskeyAuthenticationStarted,
5
+ } from "@yielded/auth/Passkey";
6
+ import { Schema } from "effect";
7
+
8
+ /** OS availability only; does not attest to entitlements, linking, or a saved key. */
9
+ export const PasskeyReactNativeCapabilities = Schema.Struct({
10
+ supported: Schema.Boolean,
11
+ conditionalGet: Schema.Literal("unsupported"),
12
+ cancellation: Schema.Literal("unsupported"),
13
+ });
14
+
15
+ export type PasskeyReactNativeCapabilities = typeof PasskeyReactNativeCapabilities.Type;
16
+
17
+ export const PasskeyReactNativeRegistration = Schema.Struct({
18
+ flowId: PasskeyAuthenticationStarted.fields.flowId,
19
+ response: PasskeyAttestation,
20
+ });
21
+
22
+ export type PasskeyReactNativeRegistration = typeof PasskeyReactNativeRegistration.Type;
23
+
24
+ export const PasskeyReactNativeAuthentication = Schema.Struct({
25
+ flowId: PasskeyAuthenticationStarted.fields.flowId,
26
+ response: PasskeyAssertion,
27
+ });
28
+
29
+ export type PasskeyReactNativeAuthentication = typeof PasskeyReactNativeAuthentication.Type;
30
+
31
+ export class PasskeyReactNativeInputRejected extends Schema.TaggedError<PasskeyReactNativeInputRejected>()(
32
+ "PasskeyReactNativeInputRejected",
33
+ {},
34
+ ) {}
35
+
36
+ export class PasskeyReactNativeUnsupported extends Schema.TaggedError<PasskeyReactNativeUnsupported>()(
37
+ "PasskeyReactNativeUnsupported",
38
+ {},
39
+ ) {}
40
+
41
+ export class PasskeyReactNativeBusy extends Schema.TaggedError<PasskeyReactNativeBusy>()(
42
+ "PasskeyReactNativeBusy",
43
+ {},
44
+ ) {}
45
+
46
+ /** A local outcome, never evidence that a credential was not created. */
47
+ export class PasskeyReactNativeNotCompleted extends Schema.TaggedError<PasskeyReactNativeNotCompleted>()(
48
+ "PasskeyReactNativeNotCompleted",
49
+ {
50
+ reason: Schema.Literals([
51
+ "cancelled",
52
+ "no-credentials",
53
+ "credential-exists",
54
+ "interrupted",
55
+ "timed-out",
56
+ "request-failed",
57
+ ]),
58
+ },
59
+ ) {}
60
+
61
+ export class PasskeyReactNativeInvalidResponse extends Schema.TaggedError<PasskeyReactNativeInvalidResponse>()(
62
+ "PasskeyReactNativeInvalidResponse",
63
+ {},
64
+ ) {}
65
+
66
+ /** Configuration failures, native defects, and unrecognized failures; no native message/cause. */
67
+ export class PasskeyReactNativeUnavailable extends Schema.TaggedError<PasskeyReactNativeUnavailable>()(
68
+ "PasskeyReactNativeUnavailable",
69
+ {},
70
+ ) {}
71
+
72
+ export const PasskeyReactNativeFailure = Schema.Union([
73
+ PasskeyReactNativeInputRejected,
74
+ PasskeyReactNativeUnsupported,
75
+ PasskeyReactNativeBusy,
76
+ PasskeyReactNativeNotCompleted,
77
+ PasskeyReactNativeInvalidResponse,
78
+ PasskeyReactNativeUnavailable,
79
+ ]);
80
+
81
+ export type PasskeyReactNativeFailure = typeof PasskeyReactNativeFailure.Type;
@@ -0,0 +1,339 @@
1
+ import {
2
+ PasskeyAuthenticationStarted,
3
+ PasskeyRegistrationStarted,
4
+ snapshotPasskey,
5
+ } from "@yielded/auth/Passkey";
6
+ import { reportAuthFailure } from "@yielded/auth/Persistence";
7
+ import { Cause, DateTime, Effect, Layer, Predicate, Redacted, Schema } from "effect";
8
+ import { Platform } from "react-native";
9
+ import { Passkey, type PasskeyCreateRequest, type PasskeyGetRequest } from "react-native-passkey";
10
+
11
+ import {
12
+ PasskeyReactNativeAuthentication,
13
+ PasskeyReactNativeBusy,
14
+ type PasskeyReactNativeFailure,
15
+ PasskeyReactNativeInputRejected,
16
+ PasskeyReactNativeInvalidResponse,
17
+ PasskeyReactNativeNotCompleted,
18
+ PasskeyReactNativeRegistration,
19
+ PasskeyReactNativeUnavailable,
20
+ PasskeyReactNativeUnsupported,
21
+ } from "./models";
22
+ import { PasskeyReactNative } from "./PasskeyReactNative";
23
+ import {
24
+ authenticationWire,
25
+ nativeAuthentication,
26
+ nativeRegistration,
27
+ registrationWire,
28
+ } from "./wire";
29
+
30
+ type Native = PasskeyReactNative["Service"];
31
+ const unavailable = () => new PasskeyReactNativeUnavailable({});
32
+ const rejected = () => new PasskeyReactNativeInputRejected({});
33
+ const invalid = () => new PasskeyReactNativeInvalidResponse({});
34
+ const timedOut = () => new PasskeyReactNativeNotCompleted({ reason: "timed-out" });
35
+
36
+ const redactDefects =
37
+ <Failure>(failure: () => Failure) =>
38
+ <A, E, R>(effect: Effect.Effect<A, E, R>) =>
39
+ effect.pipe(
40
+ Effect.catchCause((cause): Effect.Effect<never, E | Failure> =>
41
+ Cause.hasDies(cause)
42
+ ? reportAuthFailure("passkey-react-native", cause).pipe(
43
+ Effect.andThen(Effect.fail(failure())),
44
+ )
45
+ : Effect.failCause(cause),
46
+ ),
47
+ );
48
+
49
+ const unexpected = redactDefects(unavailable);
50
+
51
+ const timeout = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 300000 }));
52
+
53
+ const registrationInput = Schema.Struct({
54
+ ...PasskeyRegistrationStarted.fields,
55
+ options: Schema.Struct({ ...PasskeyRegistrationStarted.fields.options.fields, timeout }),
56
+ });
57
+
58
+ const authenticationInput = Schema.Struct({
59
+ started: Schema.Struct({
60
+ ...PasskeyAuthenticationStarted.fields,
61
+ options: Schema.Struct({ ...PasskeyAuthenticationStarted.fields.options.fields, timeout }),
62
+ }),
63
+ mediation: Schema.Literals(["required", "conditional"]),
64
+ });
65
+
66
+ const environment = Effect.sync(() => {
67
+ if (Platform.OS !== "ios") return { supported: false, exclusions: false };
68
+
69
+ // The maintained helper only checks OS version (and includes iOS 15).
70
+ // Decode platform metadata before applying this adapter's iOS 16 minimum.
71
+ // eslint-disable-next-line no-restricted-properties
72
+ const version = Schema.decodeUnknownSync(
73
+ Schema.String.check(Schema.isPattern(/^\d+(?:\.\d+){0,2}$/)),
74
+ )(Platform.Version);
75
+
76
+ const [major = 0, minor = 0] = version.split(".").map(Number);
77
+ // eslint-disable-next-line no-restricted-properties
78
+ const supported = Schema.decodeUnknownSync(Schema.Boolean)(Passkey.isSupported());
79
+
80
+ return {
81
+ supported: major >= 16 && supported,
82
+ exclusions: major > 17 || (major === 17 && minor >= 4),
83
+ };
84
+ }).pipe(unexpected);
85
+
86
+ interface Lease {
87
+ phase: "pre-native" | "native" | "settled";
88
+ listening: boolean;
89
+ }
90
+ // The peer has one native resolver. Coordinate every instance of this installed
91
+ // module; callers must not also use the peer directly or load duplicate copies.
92
+ let activeLease: Lease | undefined;
93
+
94
+ const settle = (lease: Lease): void => {
95
+ lease.phase = "settled";
96
+ if (activeLease === lease) activeLease = undefined;
97
+ };
98
+
99
+ const acquire = Effect.gen(function* () {
100
+ if (activeLease !== undefined) return yield* new PasskeyReactNativeBusy({});
101
+ const lease: Lease = { phase: "pre-native", listening: true };
102
+
103
+ activeLease = lease;
104
+
105
+ return lease;
106
+ });
107
+
108
+ const release = (lease: Lease) =>
109
+ Effect.sync(() => {
110
+ lease.listening = false;
111
+ // There is no cancel API. Free only a request that has not reached native.
112
+ if (lease.phase === "pre-native") settle(lease);
113
+ });
114
+
115
+ const nativeFailure = (value: unknown): Effect.Effect<never, PasskeyReactNativeFailure> =>
116
+ Effect.suspend((): Effect.Effect<never, PasskeyReactNativeFailure> => {
117
+ if (!Predicate.isObject(value)) return Effect.die(value);
118
+ // Inspect only the normalized code; never copy message, native stack, or cause.
119
+ // eslint-disable-next-line no-restricted-properties
120
+ const code = Schema.decodeUnknownOption(Schema.String)(value.error);
121
+
122
+ if (code._tag === "None") return Effect.die(value);
123
+ switch (code.value) {
124
+ case "NotSupported":
125
+ return Effect.fail(new PasskeyReactNativeUnsupported({}));
126
+ case "InvalidChallenge":
127
+ case "InvalidUserId":
128
+ return Effect.fail(rejected());
129
+ case "UserCancelled":
130
+ return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "cancelled" }));
131
+ case "NoCredentials":
132
+ return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "no-credentials" }));
133
+ case "CredentialAlreadyExists":
134
+ return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "credential-exists" }));
135
+ case "Interrupted":
136
+ return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "interrupted" }));
137
+ case "TimedOut":
138
+ return Effect.fail(timedOut());
139
+ case "RequestFailed":
140
+ return Effect.fail(new PasskeyReactNativeNotCompleted({ reason: "request-failed" }));
141
+ default:
142
+ return Effect.die(value);
143
+ }
144
+ });
145
+
146
+ const native = (lease: Lease, call: () => Promise<unknown>) =>
147
+ Effect.callback<unknown, PasskeyReactNativeFailure>((resume, signal) => {
148
+ if (
149
+ signal.aborted ||
150
+ !lease.listening ||
151
+ lease.phase !== "pre-native" ||
152
+ activeLease !== lease
153
+ ) {
154
+ resume(Effect.die(new Error("Invalid passkey native lease")));
155
+
156
+ return;
157
+ }
158
+ lease.phase = "native";
159
+ try {
160
+ // The caller owns classification and diagnostic reporting. Callbacks only
161
+ // settle the guard and resume a live caller. After interruption they discard
162
+ // the outcome without inspecting it or retaining/running a scoped logger.
163
+ void Promise.resolve(call()).then(
164
+ (value) => {
165
+ try {
166
+ settle(lease);
167
+ if (!signal.aborted && lease.listening) resume(Effect.succeed(value));
168
+ } catch {
169
+ /* A detached handler must not reject. */
170
+ }
171
+ },
172
+ (error) => {
173
+ try {
174
+ settle(lease);
175
+ if (!signal.aborted && lease.listening) resume(nativeFailure(error));
176
+ } catch {
177
+ /* A detached handler must not reject. */
178
+ }
179
+ },
180
+ );
181
+ } catch (error) {
182
+ settle(lease);
183
+ resume(Effect.die(error));
184
+ }
185
+ });
186
+
187
+ const remaining = Effect.fnUntraced(function* (deadline: number) {
188
+ const duration = deadline - DateTime.toEpochMillis(yield* DateTime.now);
189
+
190
+ if (duration <= 0) return yield* timedOut();
191
+
192
+ return Math.max(1, Math.floor(duration));
193
+ });
194
+
195
+ const runCeremony = Effect.fnUntraced(function* <A>(
196
+ started: { readonly expiresAtMillis: number; readonly options: { readonly timeout: number } },
197
+ use: (lease: Lease, deadline: number) => Effect.Effect<A, PasskeyReactNativeFailure>,
198
+ ) {
199
+ const now = DateTime.toEpochMillis(yield* DateTime.now);
200
+
201
+ if (started.expiresAtMillis <= now) return yield* rejected();
202
+ const deadline = Math.min(started.expiresAtMillis, now + started.options.timeout);
203
+
204
+ return yield* Effect.scoped(
205
+ Effect.gen(function* () {
206
+ const lease = yield* Effect.acquireRelease(acquire, release);
207
+ const duration = yield* remaining(deadline);
208
+
209
+ return yield* use(lease, deadline).pipe(
210
+ Effect.timeoutOrElse({ duration, orElse: () => Effect.fail(timedOut()) }),
211
+ );
212
+ }),
213
+ );
214
+ });
215
+
216
+ const decodeResponse = <S extends Schema.Codec<unknown, unknown, never, never>>(
217
+ schema: S,
218
+ value: unknown,
219
+ ) =>
220
+ // Peer declarations are not validation. Parse bounded base64url fields and
221
+ // discard unknown fields before constructing the server's JSON envelope.
222
+ // eslint-disable-next-line no-restricted-properties
223
+ Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(invalid), redactDefects(invalid));
224
+
225
+ /** Creates the iOS adapter. Ceremonies own their scopes; construction performs no native I/O. */
226
+ export const make = (): Effect.Effect<Native> =>
227
+ Effect.sync(() => {
228
+ const register = Effect.fnUntraced(function* (input: Parameters<Native["register"]>[0]) {
229
+ const started = yield* snapshotPasskey(registrationInput, input).pipe(
230
+ Effect.mapError(rejected),
231
+ );
232
+
233
+ const env = yield* environment;
234
+
235
+ if (
236
+ !env.supported ||
237
+ !started.options.pubKeyCredParams.some(({ alg }) => alg === -7) ||
238
+ (started.options.excludeCredentials.length > 0 && !env.exclusions)
239
+ )
240
+ return yield* new PasskeyReactNativeUnsupported({});
241
+
242
+ return yield* runCeremony(started, (lease, deadline) =>
243
+ Effect.gen(function* () {
244
+ const options = started.options;
245
+
246
+ const request: PasskeyCreateRequest = {
247
+ ...options,
248
+ rp: { ...options.rp },
249
+ user: { ...options.user },
250
+ authenticatorSelection: { ...options.authenticatorSelection },
251
+ pubKeyCredParams: options.pubKeyCredParams.map((value) => ({ ...value })),
252
+ excludeCredentials: options.excludeCredentials.map((value) => ({ ...value })),
253
+ timeout: yield* remaining(deadline),
254
+ };
255
+
256
+ // The platform path preserves the required attestation: "none" policy.
257
+ const credential = yield* decodeResponse(
258
+ nativeRegistration,
259
+ yield* native(lease, () => Passkey.createPlatformKey(request)),
260
+ );
261
+
262
+ const response = yield* Schema.encodeEffect(registrationWire)({
263
+ id: credential.id,
264
+ rawId: credential.rawId,
265
+ type: "public-key",
266
+ response: credential.response,
267
+ clientExtensionResults: {},
268
+ }).pipe(Effect.mapError(invalid));
269
+
270
+ yield* remaining(deadline);
271
+
272
+ return yield* snapshotPasskey(PasskeyReactNativeRegistration, {
273
+ flowId: started.flowId,
274
+ response: Redacted.make(response),
275
+ }).pipe(Effect.mapError(invalid));
276
+ }),
277
+ );
278
+ }, unexpected);
279
+
280
+ const authenticate = Effect.fnUntraced(function* (
281
+ input: Parameters<Native["authenticate"]>[0],
282
+ ) {
283
+ const { started, mediation } = yield* snapshotPasskey(authenticationInput, input).pipe(
284
+ Effect.mapError(rejected),
285
+ );
286
+
287
+ if (mediation === "conditional" || !(yield* environment).supported)
288
+ return yield* new PasskeyReactNativeUnsupported({});
289
+
290
+ return yield* runCeremony(started, (lease, deadline) =>
291
+ Effect.gen(function* () {
292
+ const request: PasskeyGetRequest = {
293
+ ...started.options,
294
+ allowCredentials: started.options.allowCredentials.map((value) => ({ ...value })),
295
+ timeout: yield* remaining(deadline),
296
+ };
297
+
298
+ const credential = yield* decodeResponse(
299
+ nativeAuthentication,
300
+ yield* native(lease, () => Passkey.get(request)),
301
+ );
302
+
303
+ const { userHandle, ...responseFields } = credential.response;
304
+
305
+ const response = yield* Schema.encodeEffect(authenticationWire)({
306
+ id: credential.id,
307
+ rawId: credential.rawId ?? credential.id,
308
+ type: "public-key",
309
+ response: {
310
+ ...responseFields,
311
+ ...(userHandle === null || userHandle === undefined ? {} : { userHandle }),
312
+ },
313
+ clientExtensionResults: {},
314
+ }).pipe(Effect.mapError(invalid));
315
+
316
+ yield* remaining(deadline);
317
+
318
+ return yield* snapshotPasskey(PasskeyReactNativeAuthentication, {
319
+ flowId: started.flowId,
320
+ response: Redacted.make(response),
321
+ }).pipe(Effect.mapError(invalid));
322
+ }),
323
+ );
324
+ }, unexpected);
325
+
326
+ return PasskeyReactNative.of({
327
+ register,
328
+ authenticate,
329
+ capabilities: environment.pipe(
330
+ Effect.map(({ supported }) => ({
331
+ supported,
332
+ conditionalGet: "unsupported" as const,
333
+ cancellation: "unsupported" as const,
334
+ })),
335
+ ),
336
+ });
337
+ });
338
+
339
+ export const layer: Layer.Layer<PasskeyReactNative> = Layer.effect(PasskeyReactNative, make());
@@ -0,0 +1,73 @@
1
+ import { PasskeyProtocolCredentialId, PasskeyUserHandle } from "@yielded/auth/Passkey";
2
+ import { Schema } from "effect";
3
+ import { Base64Url } from "effect/encoding";
4
+
5
+ const bytes = (minimum: number, maximum: number) =>
6
+ Schema.String.check(
7
+ Schema.isMaxLength(Math.ceil((maximum * 4) / 3)),
8
+ Schema.makeFilter((value) => {
9
+ const decoded = Base64Url.decode(value);
10
+
11
+ return (
12
+ decoded._tag === "Success" &&
13
+ decoded.success.length >= minimum &&
14
+ decoded.success.length <= maximum &&
15
+ Base64Url.encode(decoded.success) === value
16
+ );
17
+ }),
18
+ );
19
+
20
+ // The peer allows omitted type/rawId in assertions. Normalize only missing fields;
21
+ // a supplied mismatch must fail. Never forward unsolicited extensions or metadata.
22
+ const envelope = {
23
+ id: PasskeyProtocolCredentialId,
24
+ rawId: Schema.optionalKey(PasskeyProtocolCredentialId),
25
+ type: Schema.optionalKey(Schema.Literal("public-key")),
26
+ };
27
+
28
+ const matchingId = Schema.makeFilter<{ readonly id: string; readonly rawId?: string }>(
29
+ (value) => value.rawId === undefined || value.rawId === value.id,
30
+ );
31
+
32
+ export const nativeRegistration = Schema.Struct({
33
+ ...envelope,
34
+ rawId: PasskeyProtocolCredentialId,
35
+ response: Schema.Struct({
36
+ clientDataJSON: bytes(1, 8192),
37
+ attestationObject: bytes(1, 65536),
38
+ }),
39
+ }).check(matchingId);
40
+
41
+ export const nativeAuthentication = Schema.Struct({
42
+ ...envelope,
43
+ response: Schema.Struct({
44
+ clientDataJSON: bytes(1, 8192),
45
+ authenticatorData: bytes(37, 16384),
46
+ signature: bytes(1, 1024),
47
+ userHandle: Schema.optionalKey(Schema.NullOr(PasskeyUserHandle)),
48
+ }),
49
+ }).check(matchingId);
50
+
51
+ const wireEnvelope = {
52
+ id: PasskeyProtocolCredentialId,
53
+ rawId: PasskeyProtocolCredentialId,
54
+ type: Schema.Literal("public-key"),
55
+ clientExtensionResults: Schema.Struct({}),
56
+ };
57
+
58
+ export const registrationWire = Schema.fromJsonString(
59
+ Schema.Struct({
60
+ ...wireEnvelope,
61
+ response: nativeRegistration.fields.response,
62
+ }),
63
+ );
64
+
65
+ export const authenticationWire = Schema.fromJsonString(
66
+ Schema.Struct({
67
+ ...wireEnvelope,
68
+ response: Schema.Struct({
69
+ ...nativeAuthentication.fields.response.fields,
70
+ userHandle: Schema.optionalKey(PasskeyUserHandle),
71
+ }),
72
+ }),
73
+ );