@rakomi/react-native 0.1.0 → 0.3.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/dist/index.cjs CHANGED
@@ -12,6 +12,24 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  throw Error('Dynamic require of "' + x + '" is not supported');
13
13
  });
14
14
 
15
+ // src/internal/ceremony-lock.ts
16
+ function createCeremonyLock() {
17
+ let held = false;
18
+ return {
19
+ tryAcquire() {
20
+ if (held) return false;
21
+ held = true;
22
+ return true;
23
+ },
24
+ release() {
25
+ held = false;
26
+ },
27
+ get held() {
28
+ return held;
29
+ }
30
+ };
31
+ }
32
+
15
33
  // src/internal/canonical-url.ts
16
34
  var CanonicalizeUrlError = class extends Error {
17
35
  constructor(code, message) {
@@ -117,6 +135,37 @@ function createDpopSession(options) {
117
135
  return new DpopSession(options);
118
136
  }
119
137
 
138
+ // src/internal/compose-signals.ts
139
+ function composeSignals(caller, internal) {
140
+ if (caller === void 0) {
141
+ return { signal: internal, dispose: () => void 0 };
142
+ }
143
+ const controller = new AbortController();
144
+ const abort = (reason) => {
145
+ if (!controller.signal.aborted) controller.abort(reason);
146
+ };
147
+ const onCaller = () => abort(caller.reason);
148
+ const onInternal = () => abort(internal.reason);
149
+ if (caller.aborted) {
150
+ abort(caller.reason);
151
+ } else if (internal.aborted) {
152
+ abort(internal.reason);
153
+ } else {
154
+ caller.addEventListener("abort", onCaller, { once: true });
155
+ internal.addEventListener("abort", onInternal, { once: true });
156
+ }
157
+ let disposed = false;
158
+ return {
159
+ signal: controller.signal,
160
+ dispose: () => {
161
+ if (disposed) return;
162
+ disposed = true;
163
+ caller.removeEventListener("abort", onCaller);
164
+ internal.removeEventListener("abort", onInternal);
165
+ }
166
+ };
167
+ }
168
+
120
169
  // src/internal/http-client.ts
121
170
  function createRnHttpClient(options = {}) {
122
171
  const baseUrl = options.baseUrl?.replace(/\/$/, "") ?? "";
@@ -125,18 +174,41 @@ function createRnHttpClient(options = {}) {
125
174
  fetch: async (url, init = {}) => {
126
175
  const fullUrl = url.startsWith("http://") || url.startsWith("https://") ? url : `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
127
176
  const controller = new AbortController();
128
- const timer = setTimeout(() => controller.abort(), timeout);
177
+ const timedOut = Object.assign(new Error("request timed out"), { name: "TimeoutError" });
178
+ const timer = setTimeout(() => controller.abort(timedOut), timeout);
179
+ const composed = composeSignals(init.signal, controller.signal);
180
+ let response;
129
181
  try {
130
- return await fetch(fullUrl, {
182
+ response = await fetch(fullUrl, {
131
183
  method: init.method ?? "GET",
132
184
  headers: init.headers,
133
185
  body: init.body,
134
186
  redirect: "error",
135
- signal: init.signal ?? controller.signal
187
+ signal: composed.signal
136
188
  });
137
- } finally {
189
+ } catch (err2) {
138
190
  clearTimeout(timer);
191
+ composed.dispose();
192
+ throw err2;
193
+ }
194
+ clearTimeout(timer);
195
+ const body = response.body;
196
+ const streaming = body !== null && body !== void 0 && typeof body.pipeThrough === "function" && typeof TransformStream !== "undefined";
197
+ if (!streaming) {
198
+ composed.dispose();
199
+ return response;
139
200
  }
201
+ const release = new TransformStream({
202
+ flush: () => composed.dispose()
203
+ });
204
+ const piped = body.pipeThrough(release);
205
+ const wrapped = new Response(piped, {
206
+ status: response.status,
207
+ statusText: response.statusText,
208
+ headers: response.headers
209
+ });
210
+ composed.signal.addEventListener("abort", () => composed.dispose(), { once: true });
211
+ return wrapped;
140
212
  }
141
213
  };
142
214
  }
@@ -225,8 +297,8 @@ function deriveDefaultJwksUri(tokenEndpoint) {
225
297
  return tokenEndpoint.replace(/\/[^/]*$/, "") + "/.well-known/jwks.json";
226
298
  }
227
299
  }
228
- var RAKOMI_PLATFORM_ISSUER_DEFAULT = "https://rakomi.com";
229
- var RAKOMI_PLATFORM_AUDIENCE_DEFAULT = "https://rakomi.com";
300
+ var RAKOMI_PLATFORM_ISSUER_DEFAULT = "https://api.rakomi.com";
301
+ var RAKOMI_PLATFORM_AUDIENCE_DEFAULT = "https://api.rakomi.com";
230
302
  function deriveDefaultIssuer(_tokenEndpoint) {
231
303
  return RAKOMI_PLATFORM_ISSUER_DEFAULT;
232
304
  }
@@ -941,8 +1013,24 @@ function RakomiProvider(props) {
941
1013
  return Object.freeze(built);
942
1014
  }, [props.nativeAdapter, props.tokenCache]);
943
1015
  const http = react.useMemo(() => createRnHttpClient({ baseUrl: props.baseUrl }), [props.baseUrl]);
1016
+ const authorizationEndpointCache = react.useMemo(
1017
+ () => sdkCore.createAuthorizationEndpointCache({
1018
+ fetchDiscoveryDocument: async () => {
1019
+ const response = await http.fetch("/.well-known/openid-configuration", {
1020
+ method: "GET",
1021
+ headers: { Accept: "application/json" }
1022
+ });
1023
+ if (!response.ok) {
1024
+ throw new Error(`discovery fetch failed: HTTP ${response.status}`);
1025
+ }
1026
+ return response.json();
1027
+ }
1028
+ }),
1029
+ [http]
1030
+ );
944
1031
  const events = react.useMemo(() => new sdkCore.EventLog(props.onEvent), [props.onEvent]);
945
1032
  const [snapshot, dispatch] = react.useReducer(sdkCore.reduce, sdkCore.INITIAL_SNAPSHOT);
1033
+ const passkeyCeremonyLock = react.useRef(createCeremonyLock()).current;
946
1034
  const translate = react.useMemo(
947
1035
  () => sdkCore.createTranslator(props.locale ?? "en", props.translations),
948
1036
  [props.locale, props.translations]
@@ -1075,6 +1163,7 @@ function RakomiProvider(props) {
1075
1163
  redirectUri: props.redirectUri,
1076
1164
  adapter,
1077
1165
  http,
1166
+ authorizationEndpointCache,
1078
1167
  snapshot,
1079
1168
  state: snapshot.state,
1080
1169
  isSignedIn: sdkCore.isSignedIn(snapshot),
@@ -1086,10 +1175,11 @@ function RakomiProvider(props) {
1086
1175
  signOut,
1087
1176
  getToken,
1088
1177
  submitOAuthTokens,
1178
+ passkeyCeremonyLock,
1089
1179
  beginAuthFlow,
1090
1180
  dpopSession
1091
1181
  }),
1092
- [adapter, http, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession]
1182
+ [adapter, http, authorizationEndpointCache, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession, passkeyCeremonyLock]
1093
1183
  );
1094
1184
  return /* @__PURE__ */ jsxRuntime.jsx(RakomiContext.Provider, { value, children: props.children });
1095
1185
  }
@@ -1203,19 +1293,20 @@ function useFlag(flag, options) {
1203
1293
  }
1204
1294
  function useOrganization() {
1205
1295
  const { user } = useRakomiContext();
1206
- const orgId = user?.rawClaims["org_id"];
1207
- const orgRole = user?.rawClaims["org_role"];
1208
- const orgMemberships = user?.rawClaims["org_memberships"] ?? [];
1296
+ const { org_id: orgId, org_role: orgRole, org_memberships: orgMemberships } = sdkCore.extractOrgClaims(
1297
+ user?.rawClaims ?? {}
1298
+ );
1209
1299
  return {
1210
1300
  isLoaded: !!user,
1211
- organization: orgId && orgRole ? { orgId, orgRole, orgMemberships } : null
1301
+ organization: orgId && orgRole ? { orgId, orgRole, orgMemberships: orgMemberships ?? [] } : null
1212
1302
  };
1213
1303
  }
1214
1304
  function useOrganizationList() {
1215
1305
  const { user } = useRakomiContext();
1306
+ const { org_memberships: organizations } = sdkCore.extractOrgClaims(user?.rawClaims ?? {});
1216
1307
  return {
1217
1308
  isLoaded: !!user,
1218
- organizations: user?.rawClaims["org_memberships"] ?? []
1309
+ organizations: organizations ?? []
1219
1310
  };
1220
1311
  }
1221
1312
  function useLinkedAccounts() {
@@ -1249,7 +1340,8 @@ function useAuthConfig() {
1249
1340
  headers: { Accept: "application/json", "X-API-Key": publishableKey }
1250
1341
  });
1251
1342
  if (!response.ok) return null;
1252
- return await response.json();
1343
+ const raw = await response.json();
1344
+ return sdkCore.parseAuthConfigResponse(raw, baseUrl);
1253
1345
  } catch {
1254
1346
  return null;
1255
1347
  }
@@ -1545,13 +1637,23 @@ function SignIn(props) {
1545
1637
  const [code, setCode] = react.useState("");
1546
1638
  const [busy, setBusy] = react.useState(false);
1547
1639
  const tokenEndpoint = props.tokenEndpoint ?? "/v1/auth/oauth/callback";
1548
- const authorizationEndpoint = props.authorizationEndpoint ?? "/oauth/authorize";
1549
1640
  const totpVerifyEndpoint = props.totpVerifyEndpoint ?? "/v1/auth/mfa/totp/verify";
1550
1641
  const providers = props.providers ?? ["google"];
1551
1642
  async function onProviderPress(provider) {
1552
1643
  if (busy) return;
1553
1644
  setBusy(true);
1554
1645
  ctx.events.push({ type: "sign_in_attempted", severity: "info", metadata: { provider } });
1646
+ let authorizationEndpoint = props.authorizationEndpoint;
1647
+ if (!authorizationEndpoint) {
1648
+ const resolved = await ctx.authorizationEndpointCache.resolve(ctx.baseUrl);
1649
+ if (!resolved.ok) {
1650
+ ctx.events.push({ type: "sign_in_failed", severity: "warning", error: resolved.error });
1651
+ setStep({ kind: "error", error: resolved.error });
1652
+ setBusy(false);
1653
+ return;
1654
+ }
1655
+ authorizationEndpoint = resolved.authorizationEndpoint;
1656
+ }
1555
1657
  const outcome = await startSocialSignIn({
1556
1658
  adapter: ctx.adapter,
1557
1659
  http: ctx.http,
@@ -1757,7 +1859,7 @@ function UserButton(props) {
1757
1859
  function UserProfile(props) {
1758
1860
  react.useEffect(() => {
1759
1861
  if (typeof __DEV__ !== "undefined" && __DEV__) {
1760
- console.warn("[@rakomi/react-native] <UserProfile /> is in preview until 0.2.0 \u2014 feedback welcome.");
1862
+ console.warn("[@rakomi/react-native] <UserProfile /> is in preview \u2014 feedback welcome.");
1761
1863
  }
1762
1864
  }, []);
1763
1865
  const ctx = useRakomiContext();
@@ -1805,6 +1907,264 @@ function createNativeDpopProver(options) {
1805
1907
  };
1806
1908
  }
1807
1909
 
1910
+ // src/native/passkey-envelope.ts
1911
+ var BASE64URL = /^[A-Za-z0-9_-]+$/;
1912
+ var RULES = {
1913
+ registration: {
1914
+ required: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
1915
+ base64url: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
1916
+ requiredStringArray: ["response.transports"],
1917
+ requiredObject: ["clientExtensionResults"],
1918
+ keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
1919
+ keepInResponse: [
1920
+ "clientDataJSON",
1921
+ "attestationObject",
1922
+ "authenticatorData",
1923
+ "publicKey",
1924
+ "publicKeyAlgorithm"
1925
+ ],
1926
+ keepStringArrayInResponse: ["transports"]
1927
+ },
1928
+ assertion: {
1929
+ required: [
1930
+ "id",
1931
+ "rawId",
1932
+ "response.clientDataJSON",
1933
+ "response.authenticatorData",
1934
+ "response.signature"
1935
+ ],
1936
+ base64url: [
1937
+ "id",
1938
+ "rawId",
1939
+ "response.clientDataJSON",
1940
+ "response.authenticatorData",
1941
+ "response.signature",
1942
+ "response.userHandle"
1943
+ ],
1944
+ requiredStringArray: [],
1945
+ requiredObject: ["clientExtensionResults"],
1946
+ keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
1947
+ keepInResponse: ["clientDataJSON", "authenticatorData", "signature", "userHandle"],
1948
+ keepStringArrayInResponse: []
1949
+ }
1950
+ };
1951
+ var CredentialEnvelopeError = class extends Error {
1952
+ constructor(message) {
1953
+ super(message);
1954
+ this.name = "CredentialEnvelopeError";
1955
+ }
1956
+ };
1957
+ function isRecord(value) {
1958
+ return typeof value === "object" && value !== null;
1959
+ }
1960
+ function readPath(envelope, path) {
1961
+ const dot = path.indexOf(".");
1962
+ if (dot === -1) return envelope[path];
1963
+ const parent = envelope[path.slice(0, dot)];
1964
+ return isRecord(parent) ? parent[path.slice(dot + 1)] : void 0;
1965
+ }
1966
+ function assertW3cCredentialEnvelope(kind, parsed) {
1967
+ if (!isRecord(parsed)) {
1968
+ throw new CredentialEnvelopeError(
1969
+ `the native module returned a ${parsed === null ? "null" : typeof parsed}; the contract is a JSON object`
1970
+ );
1971
+ }
1972
+ if (parsed["type"] !== "public-key") {
1973
+ throw new CredentialEnvelopeError(`'type' must be the string "public-key" (W3C WebAuthn L3)`);
1974
+ }
1975
+ if (!isRecord(parsed["response"])) {
1976
+ throw new CredentialEnvelopeError(`'response' is missing or is not an object`);
1977
+ }
1978
+ const rule = RULES[kind];
1979
+ for (const path of rule.required) {
1980
+ const value = readPath(parsed, path);
1981
+ if (typeof value !== "string" || value.length === 0) {
1982
+ throw new CredentialEnvelopeError(
1983
+ `'${path}' is required on a ${kind} response and must be a non-empty string (W3C WebAuthn L3 JSON serialization)`
1984
+ );
1985
+ }
1986
+ }
1987
+ for (const path of rule.requiredStringArray) {
1988
+ const value = readPath(parsed, path);
1989
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
1990
+ throw new CredentialEnvelopeError(
1991
+ `'${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.`
1992
+ );
1993
+ }
1994
+ }
1995
+ for (const path of rule.requiredObject) {
1996
+ const value = readPath(parsed, path);
1997
+ if (!isRecord(value)) {
1998
+ throw new CredentialEnvelopeError(
1999
+ `'${path}' is required on a ${kind} response and must be an object (an empty object is fine)`
2000
+ );
2001
+ }
2002
+ }
2003
+ for (const path of rule.base64url) {
2004
+ const value = readPath(parsed, path);
2005
+ if (value === void 0 || value === null) continue;
2006
+ if (typeof value !== "string" || !BASE64URL.test(value)) {
2007
+ throw new CredentialEnvelopeError(
2008
+ `'${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.`
2009
+ );
2010
+ }
2011
+ }
2012
+ }
2013
+ function sanitizeCredentialEnvelope(kind, parsed) {
2014
+ assertW3cCredentialEnvelope(kind, parsed);
2015
+ const envelope = parsed;
2016
+ const source = envelope["response"];
2017
+ const rule = RULES[kind];
2018
+ const response = /* @__PURE__ */ Object.create(null);
2019
+ for (const key of rule.keepInResponse) {
2020
+ const value = source[key];
2021
+ if (typeof value === "string" || typeof value === "number") response[key] = value;
2022
+ }
2023
+ for (const key of rule.keepStringArrayInResponse) {
2024
+ const value = source[key];
2025
+ if (Array.isArray(value)) response[key] = value.filter((entry) => typeof entry === "string");
2026
+ }
2027
+ const projected = /* @__PURE__ */ Object.create(null);
2028
+ for (const key of rule.keep) {
2029
+ const value = envelope[key];
2030
+ if (value !== void 0) projected[key] = value;
2031
+ }
2032
+ projected["response"] = response;
2033
+ return projected;
2034
+ }
2035
+
2036
+ // src/native/passkey-adapter.ts
2037
+ var NATIVE_CODE_TO_KIND = Object.assign(/* @__PURE__ */ Object.create(null), {
2038
+ PASSKEY_CANCELLED: "cancelled",
2039
+ PASSKEY_UNSUPPORTED: "unsupported",
2040
+ PASSKEY_NO_CREDENTIAL: "cancelled"
2041
+ });
2042
+ function isRecord2(value) {
2043
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2044
+ }
2045
+ function normalizeNativeRejection(err2) {
2046
+ const code = isRecord2(err2) ? err2["code"] : void 0;
2047
+ if (typeof code === "string") {
2048
+ const kind = NATIVE_CODE_TO_KIND[code];
2049
+ if (kind !== void 0) {
2050
+ throw new sdkCore.PasskeyCeremonyError(kind);
2051
+ }
2052
+ }
2053
+ throw err2;
2054
+ }
2055
+ function assertNever(value) {
2056
+ throw new sdkCore.PasskeyCeremonyError("failed", `unhandled ceremony-error kind: ${String(value)}`);
2057
+ }
2058
+ function describeKind(kind) {
2059
+ switch (kind) {
2060
+ case "cancelled":
2061
+ return "the user dismissed the passkey sheet";
2062
+ case "unsupported":
2063
+ return "this platform cannot run a passkey ceremony";
2064
+ case "failed":
2065
+ return "the passkey ceremony failed";
2066
+ default:
2067
+ return assertNever(kind);
2068
+ }
2069
+ }
2070
+ function createNativePasskeyAdapter(options) {
2071
+ const mod = options.module;
2072
+ let inFlight = null;
2073
+ const candidate = mod;
2074
+ const missing = ["isPasskeySupported", "createPasskey", "getPasskey"].filter(
2075
+ (method) => typeof candidate?.[method] !== "function"
2076
+ );
2077
+ if (missing.length > 0) {
2078
+ throw new Error(
2079
+ `native passkey module is missing: ${missing.join(", ")} \u2014 it does not satisfy NativePasskeyModuleSpec`
2080
+ );
2081
+ }
2082
+ async function runCeremony(kind, requestJson, ceremonyOptions) {
2083
+ const signal = ceremonyOptions?.signal;
2084
+ if (signal?.aborted === true) {
2085
+ throw new sdkCore.PasskeyCeremonyError("cancelled", describeKind("cancelled"));
2086
+ }
2087
+ const ticket = /* @__PURE__ */ Symbol("passkey-ceremony");
2088
+ inFlight = ticket;
2089
+ const onAbort = () => {
2090
+ if (inFlight !== ticket) return;
2091
+ try {
2092
+ void Promise.resolve(mod.cancelPasskeyRequest?.()).catch(() => void 0);
2093
+ } catch {
2094
+ }
2095
+ };
2096
+ signal?.addEventListener("abort", onAbort, { once: true });
2097
+ let raw;
2098
+ try {
2099
+ raw = await (kind === "registration" ? mod.createPasskey(requestJson) : mod.getPasskey(requestJson));
2100
+ } catch (err2) {
2101
+ normalizeNativeRejection(err2);
2102
+ } finally {
2103
+ signal?.removeEventListener("abort", onAbort);
2104
+ if (inFlight === ticket) inFlight = null;
2105
+ }
2106
+ if (typeof raw !== "string") {
2107
+ throw new CredentialEnvelopeError(
2108
+ `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`
2109
+ );
2110
+ }
2111
+ let parsed;
2112
+ try {
2113
+ parsed = JSON.parse(raw);
2114
+ } catch {
2115
+ throw new CredentialEnvelopeError(
2116
+ "the native module returned a string that is not valid JSON (a double-encoded response does this)"
2117
+ );
2118
+ }
2119
+ return sanitizeCredentialEnvelope(kind, parsed);
2120
+ }
2121
+ async function ceremonyOrMalformed(kind, requestJson, ceremonyOptions) {
2122
+ try {
2123
+ return await runCeremony(kind, requestJson, ceremonyOptions);
2124
+ } catch (err2) {
2125
+ if (err2 instanceof CredentialEnvelopeError) {
2126
+ return { rakomiEnvelopeViolation: err2.message };
2127
+ }
2128
+ throw err2;
2129
+ }
2130
+ }
2131
+ return {
2132
+ async isSupported() {
2133
+ try {
2134
+ return await mod.isPasskeySupported() === true;
2135
+ } catch {
2136
+ return false;
2137
+ }
2138
+ },
2139
+ hasPlatformAuthenticator() {
2140
+ return probePlatformAuthenticator(mod);
2141
+ },
2142
+ createCredential(creationOptions, ceremonyOptions) {
2143
+ return ceremonyOrMalformed(
2144
+ "registration",
2145
+ JSON.stringify(creationOptions),
2146
+ ceremonyOptions
2147
+ );
2148
+ },
2149
+ getCredential(requestOptions, ceremonyOptions) {
2150
+ return ceremonyOrMalformed(
2151
+ "assertion",
2152
+ JSON.stringify(requestOptions),
2153
+ ceremonyOptions
2154
+ );
2155
+ }
2156
+ };
2157
+ }
2158
+ async function probePlatformAuthenticator(mod) {
2159
+ if (typeof mod.isPlatformAuthenticatorAvailable !== "function") return null;
2160
+ try {
2161
+ const available = await mod.isPlatformAuthenticatorAvailable();
2162
+ return typeof available === "boolean" ? available : null;
2163
+ } catch {
2164
+ return null;
2165
+ }
2166
+ }
2167
+
1808
2168
  // src/internal/webhook-verify.ts
1809
2169
  var RKSEC_PREFIX = "rksec_";
1810
2170
  var EXPECTED_KEY_LENGTH = 32;
@@ -1920,6 +2280,349 @@ async function verifyPublisherWebhook(body, headers, secret, tolerance = DEFAULT
1920
2280
  const clamped = Math.min(Math.max(0, tolerance), MAX_WEBHOOK_TOLERANCE);
1921
2281
  return verifyWebhook(body, headers, secret, clamped);
1922
2282
  }
2283
+ var CEREMONY_ACTIONS = /* @__PURE__ */ new Set(["signIn", "register", "stepUp"]);
2284
+ function platformProbe(adapter) {
2285
+ const candidate = adapter.hasPlatformAuthenticator;
2286
+ return typeof candidate === "function" ? candidate.bind(adapter) : void 0;
2287
+ }
2288
+ function sessionIdentity(token) {
2289
+ const payload = sdkCore.decodeJwtPayload(token);
2290
+ if (payload === null) return null;
2291
+ const subject = payload["sub"];
2292
+ const session = payload["sid"];
2293
+ if (typeof subject !== "string" || !subject) return null;
2294
+ if (typeof session !== "string" || !session) return null;
2295
+ return { subject, session };
2296
+ }
2297
+ var notSupported = () => sdkCore.passkeyError(
2298
+ "PASSKEY_NOT_SUPPORTED",
2299
+ "no passkey ceremony adapter is wired: pass one on `nativeAdapter.passkeys` (see createNativePasskeyAdapter)"
2300
+ );
2301
+ function usePasskeys(options = {}) {
2302
+ const ctx = useRakomiContext();
2303
+ const {
2304
+ adapter: nativeAdapter,
2305
+ http,
2306
+ baseUrl,
2307
+ publishableKey,
2308
+ getToken,
2309
+ beginAuthFlow,
2310
+ submitOAuthTokens,
2311
+ passkeyCeremonyLock
2312
+ } = ctx;
2313
+ const [isSupported, setIsSupported] = react.useState(null);
2314
+ const [hasPlatformAuthenticator, setHasPlatformAuthenticator] = react.useState(null);
2315
+ const [passkeys, setPasskeys] = react.useState(void 0);
2316
+ const [error, setError] = react.useState(null);
2317
+ const [isSigningIn, setIsSigningIn] = react.useState(false);
2318
+ const [isRegistering, setIsRegistering] = react.useState(false);
2319
+ const [isSteppingUp, setIsSteppingUp] = react.useState(false);
2320
+ const [isLoading, setIsLoading] = react.useState(false);
2321
+ const [isMutating, setIsMutating] = react.useState(false);
2322
+ const adapter = options.adapter ?? nativeAdapter.passkeys;
2323
+ const mountedRef = react.useRef(true);
2324
+ const busyRef = react.useRef({
2325
+ signIn: false,
2326
+ register: false,
2327
+ stepUp: false,
2328
+ list: false,
2329
+ mutate: false
2330
+ });
2331
+ const inFlightRef = react.useRef(/* @__PURE__ */ new Set());
2332
+ react.useEffect(() => {
2333
+ mountedRef.current = true;
2334
+ const inFlight = inFlightRef.current;
2335
+ return () => {
2336
+ mountedRef.current = false;
2337
+ for (const controller of inFlight) controller.abort();
2338
+ inFlight.clear();
2339
+ };
2340
+ }, []);
2341
+ react.useEffect(() => {
2342
+ if (adapter === void 0) {
2343
+ setIsSupported(false);
2344
+ setHasPlatformAuthenticator(null);
2345
+ return;
2346
+ }
2347
+ let cancelled = false;
2348
+ void (async () => {
2349
+ const [supported, platform] = await Promise.all([
2350
+ Promise.resolve(adapter.isSupported()).catch(() => false),
2351
+ Promise.resolve(platformProbe(adapter)?.() ?? null).catch(() => null)
2352
+ ]);
2353
+ if (cancelled || !mountedRef.current) return;
2354
+ setIsSupported(supported === true);
2355
+ setHasPlatformAuthenticator(typeof platform === "boolean" ? platform : null);
2356
+ })();
2357
+ return () => {
2358
+ cancelled = true;
2359
+ };
2360
+ }, [adapter]);
2361
+ const setBusy = react.useCallback((key, value) => {
2362
+ busyRef.current[key] = value;
2363
+ if (key === "signIn") setIsSigningIn(value);
2364
+ else if (key === "register") setIsRegistering(value);
2365
+ else if (key === "stepUp") setIsSteppingUp(value);
2366
+ else if (key === "list") setIsLoading(value);
2367
+ else setIsMutating(value);
2368
+ }, []);
2369
+ const requireSession = react.useCallback(async () => {
2370
+ const expired = () => sdkCore.passkeyError("PASSKEY_SESSION_EXPIRED", "you must be signed in to manage passkeys");
2371
+ const token = await getToken();
2372
+ if (!token.ok) {
2373
+ const err2 = token.error;
2374
+ if (err2.code === "NETWORK_ERROR" || err2.code === "REFRESH_FAILED" && err2.reason === "network") {
2375
+ return sdkCore.passkeyError("PASSKEY_NETWORK_ERROR", err2.message);
2376
+ }
2377
+ return expired();
2378
+ }
2379
+ const user = sdkCore.decodeUser(token.token);
2380
+ if (user === null) return expired();
2381
+ return { accessToken: token.token, userId: user.id };
2382
+ }, [getToken]);
2383
+ const runAction = react.useCallback(
2384
+ async (key, guarded, run, onSuccess) => {
2385
+ const refuse = (result) => {
2386
+ if (!result.ok && mountedRef.current) setError(result.error);
2387
+ return result;
2388
+ };
2389
+ if (busyRef.current[key]) return refuse(guarded);
2390
+ const isCeremony = CEREMONY_ACTIONS.has(key);
2391
+ if (isCeremony && !passkeyCeremonyLock.tryAcquire()) return refuse(guarded);
2392
+ setBusy(key, true);
2393
+ setError(null);
2394
+ const controller = new AbortController();
2395
+ inFlightRef.current.add(controller);
2396
+ try {
2397
+ const result = await run(controller.signal);
2398
+ if (!result.ok && mountedRef.current) setError(result.error);
2399
+ if (result.ok && mountedRef.current) onSuccess?.(result);
2400
+ return result;
2401
+ } finally {
2402
+ inFlightRef.current.delete(controller);
2403
+ busyRef.current[key] = false;
2404
+ if (isCeremony) passkeyCeremonyLock.release();
2405
+ if (mountedRef.current) setBusy(key, false);
2406
+ }
2407
+ },
2408
+ [passkeyCeremonyLock, setBusy]
2409
+ );
2410
+ const busyResult = react.useCallback(
2411
+ (action) => ({
2412
+ ok: false,
2413
+ error: sdkCore.passkeyError("PASSKEY_INVALID_INPUT", `a passkey ${action} is already in progress`)
2414
+ }),
2415
+ []
2416
+ );
2417
+ const unsupported = react.useCallback(() => {
2418
+ const result = { ok: false, error: notSupported() };
2419
+ if (mountedRef.current) setError(result.error);
2420
+ return Promise.resolve(result);
2421
+ }, []);
2422
+ const sessionFailure = (session) => !("accessToken" in session);
2423
+ const signInWithPasskey = react.useCallback(
2424
+ (input = {}) => {
2425
+ if (adapter === void 0) return unsupported();
2426
+ return runAction("signIn", busyResult("sign-in"), async (signal) => {
2427
+ const result = await sdkCore.assertPasskey({
2428
+ http,
2429
+ baseUrl,
2430
+ clientId: publishableKey,
2431
+ adapter,
2432
+ ...input.userHandle === void 0 ? {} : { userHandle: input.userHandle },
2433
+ signal
2434
+ });
2435
+ if (!result.ok) return result;
2436
+ const failed = (message) => ({
2437
+ ok: false,
2438
+ error: sdkCore.passkeyError("PASSKEY_REQUEST_FAILED", message)
2439
+ });
2440
+ let nonce;
2441
+ try {
2442
+ nonce = await beginAuthFlow();
2443
+ } catch (err2) {
2444
+ return failed(
2445
+ `the passkey was accepted but the session could not be started: ${err2 instanceof Error ? err2.message : "unknown error"}`
2446
+ );
2447
+ }
2448
+ const tokens = result.tokens;
2449
+ if (nativeAdapter.dpopProver !== void 0 && tokens.token_type !== "DPoP") {
2450
+ return failed(
2451
+ "the passkey was accepted but the session was refused: this app is configured for sender-constrained (DPoP) sessions, and the passkey sign-in returned an unbound bearer session. Storing it would silently downgrade the app\u2019s security posture."
2452
+ );
2453
+ }
2454
+ try {
2455
+ await submitOAuthTokens(tokens, nonce);
2456
+ } catch (err2) {
2457
+ return failed(
2458
+ `the passkey was accepted but the session could not be stored: ${err2 instanceof Error ? err2.message : "unknown error"}`
2459
+ );
2460
+ }
2461
+ const confirmed = await getToken();
2462
+ if (!confirmed.ok) {
2463
+ return failed(
2464
+ "the passkey was accepted but the session was not established (the token submission was rejected)"
2465
+ );
2466
+ }
2467
+ const submitted = sessionIdentity(tokens.access_token);
2468
+ const live = sessionIdentity(confirmed.token);
2469
+ if (submitted === null || live === null) {
2470
+ if (confirmed.token !== tokens.access_token) {
2471
+ return failed("the passkey was accepted but the resulting session token could not be read");
2472
+ }
2473
+ return result;
2474
+ }
2475
+ if (live.subject !== submitted.subject || live.session !== submitted.session) {
2476
+ return failed(
2477
+ "the passkey was accepted but the session was not established (the token submission was rejected)"
2478
+ );
2479
+ }
2480
+ return result;
2481
+ });
2482
+ },
2483
+ [
2484
+ adapter,
2485
+ baseUrl,
2486
+ beginAuthFlow,
2487
+ busyResult,
2488
+ getToken,
2489
+ http,
2490
+ publishableKey,
2491
+ runAction,
2492
+ submitOAuthTokens,
2493
+ unsupported
2494
+ ]
2495
+ );
2496
+ const stepUpWithPasskey = react.useCallback(() => {
2497
+ if (adapter === void 0) return unsupported();
2498
+ return runAction("stepUp", busyResult("step-up"), async (signal) => {
2499
+ const session = await requireSession();
2500
+ if (sessionFailure(session)) return { ok: false, error: session };
2501
+ return sdkCore.stepUpWithPasskey({
2502
+ http,
2503
+ baseUrl,
2504
+ clientId: publishableKey,
2505
+ accessToken: session.accessToken,
2506
+ adapter,
2507
+ signal
2508
+ });
2509
+ });
2510
+ }, [adapter, baseUrl, busyResult, http, publishableKey, requireSession, runAction, unsupported]);
2511
+ const registerPasskey = react.useCallback(
2512
+ (input) => {
2513
+ if (adapter === void 0) return unsupported();
2514
+ return runAction("register", busyResult("registration"), async (signal) => {
2515
+ const session = await requireSession();
2516
+ if (sessionFailure(session)) return { ok: false, error: session };
2517
+ return sdkCore.registerPasskey({
2518
+ http,
2519
+ baseUrl,
2520
+ clientId: publishableKey,
2521
+ accessToken: session.accessToken,
2522
+ stepUpToken: input.stepUpToken,
2523
+ adapter,
2524
+ ...input.nickname === void 0 ? {} : { nickname: input.nickname },
2525
+ signal
2526
+ });
2527
+ });
2528
+ },
2529
+ [adapter, baseUrl, busyResult, http, publishableKey, requireSession, runAction, unsupported]
2530
+ );
2531
+ const listPasskeys = react.useCallback(
2532
+ (input) => runAction(
2533
+ "list",
2534
+ busyResult("list"),
2535
+ async (signal) => {
2536
+ const session = await requireSession();
2537
+ if (sessionFailure(session)) return { ok: false, error: session };
2538
+ return sdkCore.listPasskeys({
2539
+ http,
2540
+ baseUrl,
2541
+ clientId: publishableKey,
2542
+ accessToken: session.accessToken,
2543
+ stepUpToken: input.stepUpToken,
2544
+ userId: session.userId,
2545
+ signal
2546
+ });
2547
+ },
2548
+ (result) => {
2549
+ if (result.ok) setPasskeys(result.data);
2550
+ }
2551
+ ),
2552
+ [baseUrl, busyResult, http, publishableKey, requireSession, runAction]
2553
+ );
2554
+ const renamePasskey = react.useCallback(
2555
+ (input) => runAction(
2556
+ "mutate",
2557
+ busyResult("mutation"),
2558
+ async (signal) => {
2559
+ const session = await requireSession();
2560
+ if (sessionFailure(session)) return { ok: false, error: session };
2561
+ return sdkCore.renamePasskey({
2562
+ http,
2563
+ baseUrl,
2564
+ clientId: publishableKey,
2565
+ accessToken: session.accessToken,
2566
+ stepUpToken: input.stepUpToken,
2567
+ userId: session.userId,
2568
+ passkeyId: input.passkeyId,
2569
+ nickname: input.nickname,
2570
+ signal
2571
+ });
2572
+ },
2573
+ (result) => {
2574
+ if (!result.ok) return;
2575
+ setPasskeys(
2576
+ (current) => current?.map((passkey) => passkey.id === result.passkey.id ? result.passkey : passkey)
2577
+ );
2578
+ }
2579
+ ),
2580
+ [baseUrl, busyResult, http, publishableKey, requireSession, runAction]
2581
+ );
2582
+ const deletePasskey = react.useCallback(
2583
+ (input) => runAction(
2584
+ "mutate",
2585
+ busyResult("mutation"),
2586
+ async (signal) => {
2587
+ const session = await requireSession();
2588
+ if (sessionFailure(session)) return { ok: false, error: session };
2589
+ return sdkCore.deletePasskey({
2590
+ http,
2591
+ baseUrl,
2592
+ clientId: publishableKey,
2593
+ accessToken: session.accessToken,
2594
+ stepUpToken: input.stepUpToken,
2595
+ userId: session.userId,
2596
+ passkeyId: input.passkeyId,
2597
+ signal
2598
+ });
2599
+ },
2600
+ (result) => {
2601
+ if (result.ok) {
2602
+ setPasskeys((current) => current?.filter((passkey) => passkey.id !== input.passkeyId));
2603
+ }
2604
+ }
2605
+ ),
2606
+ [baseUrl, busyResult, http, publishableKey, requireSession, runAction]
2607
+ );
2608
+ return {
2609
+ isSupported,
2610
+ hasPlatformAuthenticator,
2611
+ passkeys,
2612
+ error,
2613
+ isSigningIn,
2614
+ isRegistering,
2615
+ isSteppingUp,
2616
+ isLoading,
2617
+ isMutating,
2618
+ signInWithPasskey,
2619
+ stepUpWithPasskey,
2620
+ registerPasskey,
2621
+ listPasskeys,
2622
+ renamePasskey,
2623
+ deletePasskey
2624
+ };
2625
+ }
1923
2626
 
1924
2627
  // src/hooks/use-submit-oauth-tokens.ts
1925
2628
  function useSubmitOAuthTokens() {
@@ -2014,6 +2717,10 @@ Object.defineProperty(exports, "base64url", {
2014
2717
  enumerable: true,
2015
2718
  get: function () { return sdkCore.base64url; }
2016
2719
  });
2720
+ Object.defineProperty(exports, "createAuthorizationEndpointCache", {
2721
+ enumerable: true,
2722
+ get: function () { return sdkCore.createAuthorizationEndpointCache; }
2723
+ });
2017
2724
  Object.defineProperty(exports, "createTranslator", {
2018
2725
  enumerable: true,
2019
2726
  get: function () { return sdkCore.createTranslator; }
@@ -2030,6 +2737,10 @@ Object.defineProperty(exports, "decodeUser", {
2030
2737
  enumerable: true,
2031
2738
  get: function () { return sdkCore.decodeUser; }
2032
2739
  });
2740
+ Object.defineProperty(exports, "deriveAuthorizationEndpointFallback", {
2741
+ enumerable: true,
2742
+ get: function () { return sdkCore.deriveAuthorizationEndpointFallback; }
2743
+ });
2033
2744
  Object.defineProperty(exports, "generatePkce", {
2034
2745
  enumerable: true,
2035
2746
  get: function () { return sdkCore.generatePkce; }
@@ -2085,6 +2796,7 @@ exports.UserProfile = UserProfile;
2085
2796
  exports.createDefaultExpoAdapter = createDefaultExpoAdapter;
2086
2797
  exports.createDpopSession = createDpopSession;
2087
2798
  exports.createNativeDpopProver = createNativeDpopProver;
2799
+ exports.createNativePasskeyAdapter = createNativePasskeyAdapter;
2088
2800
  exports.createRnHttpClient = createRnHttpClient;
2089
2801
  exports.startSocialSignIn = startSocialSignIn;
2090
2802
  exports.useAnonymousSignin = useAnonymousSignin;
@@ -2100,6 +2812,7 @@ exports.useMagicLink = useMagicLink;
2100
2812
  exports.useMfa = useMfa;
2101
2813
  exports.useOrganization = useOrganization;
2102
2814
  exports.useOrganizationList = useOrganizationList;
2815
+ exports.usePasskeys = usePasskeys;
2103
2816
  exports.useRegister = useRegister;
2104
2817
  exports.useSession = useSession;
2105
2818
  exports.useSubmitOAuthTokens = useSubmitOAuthTokens;