@rakomi/react-native 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPLIANCE.md +36 -6
- package/README.md +247 -0
- package/SECURITY.md +1 -1
- package/THIRD-PARTY-NOTICES +1 -1
- package/dist/index.cjs +692 -13
- package/dist/index.d.cts +70 -6
- package/dist/index.d.ts +70 -6
- package/dist/index.js +693 -16
- package/dist/native/index.cjs +265 -0
- package/dist/native/index.d.cts +33 -3
- package/dist/native/index.d.ts +33 -3
- package/dist/native/index.js +261 -1
- package/dist/{expo-adapter-CtPCEgue.d.cts → passkey-adapter-D_Z3Z7lV.d.cts} +172 -2
- package/dist/{expo-adapter-CtPCEgue.d.ts → passkey-adapter-D_Z3Z7lV.d.ts} +172 -2
- package/package.json +3 -3
- package/sbom.cdx.json +17 -11
package/dist/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
|
|
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
|
-
|
|
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:
|
|
187
|
+
signal: composed.signal
|
|
136
188
|
});
|
|
137
|
-
}
|
|
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
|
}
|
|
@@ -943,6 +1015,7 @@ function RakomiProvider(props) {
|
|
|
943
1015
|
const http = react.useMemo(() => createRnHttpClient({ baseUrl: props.baseUrl }), [props.baseUrl]);
|
|
944
1016
|
const events = react.useMemo(() => new sdkCore.EventLog(props.onEvent), [props.onEvent]);
|
|
945
1017
|
const [snapshot, dispatch] = react.useReducer(sdkCore.reduce, sdkCore.INITIAL_SNAPSHOT);
|
|
1018
|
+
const passkeyCeremonyLock = react.useRef(createCeremonyLock()).current;
|
|
946
1019
|
const translate = react.useMemo(
|
|
947
1020
|
() => sdkCore.createTranslator(props.locale ?? "en", props.translations),
|
|
948
1021
|
[props.locale, props.translations]
|
|
@@ -1086,10 +1159,11 @@ function RakomiProvider(props) {
|
|
|
1086
1159
|
signOut,
|
|
1087
1160
|
getToken,
|
|
1088
1161
|
submitOAuthTokens,
|
|
1162
|
+
passkeyCeremonyLock,
|
|
1089
1163
|
beginAuthFlow,
|
|
1090
1164
|
dpopSession
|
|
1091
1165
|
}),
|
|
1092
|
-
[adapter, http, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession]
|
|
1166
|
+
[adapter, http, snapshot, events, props.publishableKey, props.baseUrl, props.redirectUri, props.locale, translate, signOut, getToken, submitOAuthTokens, beginAuthFlow, dpopSession, passkeyCeremonyLock]
|
|
1093
1167
|
);
|
|
1094
1168
|
return /* @__PURE__ */ jsxRuntime.jsx(RakomiContext.Provider, { value, children: props.children });
|
|
1095
1169
|
}
|
|
@@ -1203,19 +1277,20 @@ function useFlag(flag, options) {
|
|
|
1203
1277
|
}
|
|
1204
1278
|
function useOrganization() {
|
|
1205
1279
|
const { user } = useRakomiContext();
|
|
1206
|
-
const orgId =
|
|
1207
|
-
|
|
1208
|
-
|
|
1280
|
+
const { org_id: orgId, org_role: orgRole, org_memberships: orgMemberships } = sdkCore.extractOrgClaims(
|
|
1281
|
+
user?.rawClaims ?? {}
|
|
1282
|
+
);
|
|
1209
1283
|
return {
|
|
1210
1284
|
isLoaded: !!user,
|
|
1211
|
-
organization: orgId && orgRole ? { orgId, orgRole, orgMemberships } : null
|
|
1285
|
+
organization: orgId && orgRole ? { orgId, orgRole, orgMemberships: orgMemberships ?? [] } : null
|
|
1212
1286
|
};
|
|
1213
1287
|
}
|
|
1214
1288
|
function useOrganizationList() {
|
|
1215
1289
|
const { user } = useRakomiContext();
|
|
1290
|
+
const { org_memberships: organizations } = sdkCore.extractOrgClaims(user?.rawClaims ?? {});
|
|
1216
1291
|
return {
|
|
1217
1292
|
isLoaded: !!user,
|
|
1218
|
-
organizations:
|
|
1293
|
+
organizations: organizations ?? []
|
|
1219
1294
|
};
|
|
1220
1295
|
}
|
|
1221
1296
|
function useLinkedAccounts() {
|
|
@@ -1249,7 +1324,8 @@ function useAuthConfig() {
|
|
|
1249
1324
|
headers: { Accept: "application/json", "X-API-Key": publishableKey }
|
|
1250
1325
|
});
|
|
1251
1326
|
if (!response.ok) return null;
|
|
1252
|
-
|
|
1327
|
+
const raw = await response.json();
|
|
1328
|
+
return sdkCore.parseAuthConfigResponse(raw, baseUrl);
|
|
1253
1329
|
} catch {
|
|
1254
1330
|
return null;
|
|
1255
1331
|
}
|
|
@@ -1805,6 +1881,264 @@ function createNativeDpopProver(options) {
|
|
|
1805
1881
|
};
|
|
1806
1882
|
}
|
|
1807
1883
|
|
|
1884
|
+
// src/native/passkey-envelope.ts
|
|
1885
|
+
var BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
1886
|
+
var RULES = {
|
|
1887
|
+
registration: {
|
|
1888
|
+
required: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
|
|
1889
|
+
base64url: ["id", "rawId", "response.clientDataJSON", "response.attestationObject"],
|
|
1890
|
+
requiredStringArray: ["response.transports"],
|
|
1891
|
+
requiredObject: ["clientExtensionResults"],
|
|
1892
|
+
keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
|
|
1893
|
+
keepInResponse: [
|
|
1894
|
+
"clientDataJSON",
|
|
1895
|
+
"attestationObject",
|
|
1896
|
+
"authenticatorData",
|
|
1897
|
+
"publicKey",
|
|
1898
|
+
"publicKeyAlgorithm"
|
|
1899
|
+
],
|
|
1900
|
+
keepStringArrayInResponse: ["transports"]
|
|
1901
|
+
},
|
|
1902
|
+
assertion: {
|
|
1903
|
+
required: [
|
|
1904
|
+
"id",
|
|
1905
|
+
"rawId",
|
|
1906
|
+
"response.clientDataJSON",
|
|
1907
|
+
"response.authenticatorData",
|
|
1908
|
+
"response.signature"
|
|
1909
|
+
],
|
|
1910
|
+
base64url: [
|
|
1911
|
+
"id",
|
|
1912
|
+
"rawId",
|
|
1913
|
+
"response.clientDataJSON",
|
|
1914
|
+
"response.authenticatorData",
|
|
1915
|
+
"response.signature",
|
|
1916
|
+
"response.userHandle"
|
|
1917
|
+
],
|
|
1918
|
+
requiredStringArray: [],
|
|
1919
|
+
requiredObject: ["clientExtensionResults"],
|
|
1920
|
+
keep: ["id", "rawId", "type", "authenticatorAttachment", "clientExtensionResults"],
|
|
1921
|
+
keepInResponse: ["clientDataJSON", "authenticatorData", "signature", "userHandle"],
|
|
1922
|
+
keepStringArrayInResponse: []
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
var CredentialEnvelopeError = class extends Error {
|
|
1926
|
+
constructor(message) {
|
|
1927
|
+
super(message);
|
|
1928
|
+
this.name = "CredentialEnvelopeError";
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
function isRecord(value) {
|
|
1932
|
+
return typeof value === "object" && value !== null;
|
|
1933
|
+
}
|
|
1934
|
+
function readPath(envelope, path) {
|
|
1935
|
+
const dot = path.indexOf(".");
|
|
1936
|
+
if (dot === -1) return envelope[path];
|
|
1937
|
+
const parent = envelope[path.slice(0, dot)];
|
|
1938
|
+
return isRecord(parent) ? parent[path.slice(dot + 1)] : void 0;
|
|
1939
|
+
}
|
|
1940
|
+
function assertW3cCredentialEnvelope(kind, parsed) {
|
|
1941
|
+
if (!isRecord(parsed)) {
|
|
1942
|
+
throw new CredentialEnvelopeError(
|
|
1943
|
+
`the native module returned a ${parsed === null ? "null" : typeof parsed}; the contract is a JSON object`
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
if (parsed["type"] !== "public-key") {
|
|
1947
|
+
throw new CredentialEnvelopeError(`'type' must be the string "public-key" (W3C WebAuthn L3)`);
|
|
1948
|
+
}
|
|
1949
|
+
if (!isRecord(parsed["response"])) {
|
|
1950
|
+
throw new CredentialEnvelopeError(`'response' is missing or is not an object`);
|
|
1951
|
+
}
|
|
1952
|
+
const rule = RULES[kind];
|
|
1953
|
+
for (const path of rule.required) {
|
|
1954
|
+
const value = readPath(parsed, path);
|
|
1955
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1956
|
+
throw new CredentialEnvelopeError(
|
|
1957
|
+
`'${path}' is required on a ${kind} response and must be a non-empty string (W3C WebAuthn L3 JSON serialization)`
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
for (const path of rule.requiredStringArray) {
|
|
1962
|
+
const value = readPath(parsed, path);
|
|
1963
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
1964
|
+
throw new CredentialEnvelopeError(
|
|
1965
|
+
`'${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.`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
for (const path of rule.requiredObject) {
|
|
1970
|
+
const value = readPath(parsed, path);
|
|
1971
|
+
if (!isRecord(value)) {
|
|
1972
|
+
throw new CredentialEnvelopeError(
|
|
1973
|
+
`'${path}' is required on a ${kind} response and must be an object (an empty object is fine)`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
for (const path of rule.base64url) {
|
|
1978
|
+
const value = readPath(parsed, path);
|
|
1979
|
+
if (value === void 0 || value === null) continue;
|
|
1980
|
+
if (typeof value !== "string" || !BASE64URL.test(value)) {
|
|
1981
|
+
throw new CredentialEnvelopeError(
|
|
1982
|
+
`'${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.`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
function sanitizeCredentialEnvelope(kind, parsed) {
|
|
1988
|
+
assertW3cCredentialEnvelope(kind, parsed);
|
|
1989
|
+
const envelope = parsed;
|
|
1990
|
+
const source = envelope["response"];
|
|
1991
|
+
const rule = RULES[kind];
|
|
1992
|
+
const response = /* @__PURE__ */ Object.create(null);
|
|
1993
|
+
for (const key of rule.keepInResponse) {
|
|
1994
|
+
const value = source[key];
|
|
1995
|
+
if (typeof value === "string" || typeof value === "number") response[key] = value;
|
|
1996
|
+
}
|
|
1997
|
+
for (const key of rule.keepStringArrayInResponse) {
|
|
1998
|
+
const value = source[key];
|
|
1999
|
+
if (Array.isArray(value)) response[key] = value.filter((entry) => typeof entry === "string");
|
|
2000
|
+
}
|
|
2001
|
+
const projected = /* @__PURE__ */ Object.create(null);
|
|
2002
|
+
for (const key of rule.keep) {
|
|
2003
|
+
const value = envelope[key];
|
|
2004
|
+
if (value !== void 0) projected[key] = value;
|
|
2005
|
+
}
|
|
2006
|
+
projected["response"] = response;
|
|
2007
|
+
return projected;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/native/passkey-adapter.ts
|
|
2011
|
+
var NATIVE_CODE_TO_KIND = Object.assign(/* @__PURE__ */ Object.create(null), {
|
|
2012
|
+
PASSKEY_CANCELLED: "cancelled",
|
|
2013
|
+
PASSKEY_UNSUPPORTED: "unsupported",
|
|
2014
|
+
PASSKEY_NO_CREDENTIAL: "cancelled"
|
|
2015
|
+
});
|
|
2016
|
+
function isRecord2(value) {
|
|
2017
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2018
|
+
}
|
|
2019
|
+
function normalizeNativeRejection(err2) {
|
|
2020
|
+
const code = isRecord2(err2) ? err2["code"] : void 0;
|
|
2021
|
+
if (typeof code === "string") {
|
|
2022
|
+
const kind = NATIVE_CODE_TO_KIND[code];
|
|
2023
|
+
if (kind !== void 0) {
|
|
2024
|
+
throw new sdkCore.PasskeyCeremonyError(kind);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
throw err2;
|
|
2028
|
+
}
|
|
2029
|
+
function assertNever(value) {
|
|
2030
|
+
throw new sdkCore.PasskeyCeremonyError("failed", `unhandled ceremony-error kind: ${String(value)}`);
|
|
2031
|
+
}
|
|
2032
|
+
function describeKind(kind) {
|
|
2033
|
+
switch (kind) {
|
|
2034
|
+
case "cancelled":
|
|
2035
|
+
return "the user dismissed the passkey sheet";
|
|
2036
|
+
case "unsupported":
|
|
2037
|
+
return "this platform cannot run a passkey ceremony";
|
|
2038
|
+
case "failed":
|
|
2039
|
+
return "the passkey ceremony failed";
|
|
2040
|
+
default:
|
|
2041
|
+
return assertNever(kind);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
function createNativePasskeyAdapter(options) {
|
|
2045
|
+
const mod = options.module;
|
|
2046
|
+
let inFlight = null;
|
|
2047
|
+
const candidate = mod;
|
|
2048
|
+
const missing = ["isPasskeySupported", "createPasskey", "getPasskey"].filter(
|
|
2049
|
+
(method) => typeof candidate?.[method] !== "function"
|
|
2050
|
+
);
|
|
2051
|
+
if (missing.length > 0) {
|
|
2052
|
+
throw new Error(
|
|
2053
|
+
`native passkey module is missing: ${missing.join(", ")} \u2014 it does not satisfy NativePasskeyModuleSpec`
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
async function runCeremony(kind, requestJson, ceremonyOptions) {
|
|
2057
|
+
const signal = ceremonyOptions?.signal;
|
|
2058
|
+
if (signal?.aborted === true) {
|
|
2059
|
+
throw new sdkCore.PasskeyCeremonyError("cancelled", describeKind("cancelled"));
|
|
2060
|
+
}
|
|
2061
|
+
const ticket = /* @__PURE__ */ Symbol("passkey-ceremony");
|
|
2062
|
+
inFlight = ticket;
|
|
2063
|
+
const onAbort = () => {
|
|
2064
|
+
if (inFlight !== ticket) return;
|
|
2065
|
+
try {
|
|
2066
|
+
void Promise.resolve(mod.cancelPasskeyRequest?.()).catch(() => void 0);
|
|
2067
|
+
} catch {
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2071
|
+
let raw;
|
|
2072
|
+
try {
|
|
2073
|
+
raw = await (kind === "registration" ? mod.createPasskey(requestJson) : mod.getPasskey(requestJson));
|
|
2074
|
+
} catch (err2) {
|
|
2075
|
+
normalizeNativeRejection(err2);
|
|
2076
|
+
} finally {
|
|
2077
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2078
|
+
if (inFlight === ticket) inFlight = null;
|
|
2079
|
+
}
|
|
2080
|
+
if (typeof raw !== "string") {
|
|
2081
|
+
throw new CredentialEnvelopeError(
|
|
2082
|
+
`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`
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
2085
|
+
let parsed;
|
|
2086
|
+
try {
|
|
2087
|
+
parsed = JSON.parse(raw);
|
|
2088
|
+
} catch {
|
|
2089
|
+
throw new CredentialEnvelopeError(
|
|
2090
|
+
"the native module returned a string that is not valid JSON (a double-encoded response does this)"
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
return sanitizeCredentialEnvelope(kind, parsed);
|
|
2094
|
+
}
|
|
2095
|
+
async function ceremonyOrMalformed(kind, requestJson, ceremonyOptions) {
|
|
2096
|
+
try {
|
|
2097
|
+
return await runCeremony(kind, requestJson, ceremonyOptions);
|
|
2098
|
+
} catch (err2) {
|
|
2099
|
+
if (err2 instanceof CredentialEnvelopeError) {
|
|
2100
|
+
return { rakomiEnvelopeViolation: err2.message };
|
|
2101
|
+
}
|
|
2102
|
+
throw err2;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
return {
|
|
2106
|
+
async isSupported() {
|
|
2107
|
+
try {
|
|
2108
|
+
return await mod.isPasskeySupported() === true;
|
|
2109
|
+
} catch {
|
|
2110
|
+
return false;
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
hasPlatformAuthenticator() {
|
|
2114
|
+
return probePlatformAuthenticator(mod);
|
|
2115
|
+
},
|
|
2116
|
+
createCredential(creationOptions, ceremonyOptions) {
|
|
2117
|
+
return ceremonyOrMalformed(
|
|
2118
|
+
"registration",
|
|
2119
|
+
JSON.stringify(creationOptions),
|
|
2120
|
+
ceremonyOptions
|
|
2121
|
+
);
|
|
2122
|
+
},
|
|
2123
|
+
getCredential(requestOptions, ceremonyOptions) {
|
|
2124
|
+
return ceremonyOrMalformed(
|
|
2125
|
+
"assertion",
|
|
2126
|
+
JSON.stringify(requestOptions),
|
|
2127
|
+
ceremonyOptions
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
async function probePlatformAuthenticator(mod) {
|
|
2133
|
+
if (typeof mod.isPlatformAuthenticatorAvailable !== "function") return null;
|
|
2134
|
+
try {
|
|
2135
|
+
const available = await mod.isPlatformAuthenticatorAvailable();
|
|
2136
|
+
return typeof available === "boolean" ? available : null;
|
|
2137
|
+
} catch {
|
|
2138
|
+
return null;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1808
2142
|
// src/internal/webhook-verify.ts
|
|
1809
2143
|
var RKSEC_PREFIX = "rksec_";
|
|
1810
2144
|
var EXPECTED_KEY_LENGTH = 32;
|
|
@@ -1920,6 +2254,349 @@ async function verifyPublisherWebhook(body, headers, secret, tolerance = DEFAULT
|
|
|
1920
2254
|
const clamped = Math.min(Math.max(0, tolerance), MAX_WEBHOOK_TOLERANCE);
|
|
1921
2255
|
return verifyWebhook(body, headers, secret, clamped);
|
|
1922
2256
|
}
|
|
2257
|
+
var CEREMONY_ACTIONS = /* @__PURE__ */ new Set(["signIn", "register", "stepUp"]);
|
|
2258
|
+
function platformProbe(adapter) {
|
|
2259
|
+
const candidate = adapter.hasPlatformAuthenticator;
|
|
2260
|
+
return typeof candidate === "function" ? candidate.bind(adapter) : void 0;
|
|
2261
|
+
}
|
|
2262
|
+
function sessionIdentity(token) {
|
|
2263
|
+
const payload = sdkCore.decodeJwtPayload(token);
|
|
2264
|
+
if (payload === null) return null;
|
|
2265
|
+
const subject = payload["sub"];
|
|
2266
|
+
const session = payload["sid"];
|
|
2267
|
+
if (typeof subject !== "string" || !subject) return null;
|
|
2268
|
+
if (typeof session !== "string" || !session) return null;
|
|
2269
|
+
return { subject, session };
|
|
2270
|
+
}
|
|
2271
|
+
var notSupported = () => sdkCore.passkeyError(
|
|
2272
|
+
"PASSKEY_NOT_SUPPORTED",
|
|
2273
|
+
"no passkey ceremony adapter is wired: pass one on `nativeAdapter.passkeys` (see createNativePasskeyAdapter)"
|
|
2274
|
+
);
|
|
2275
|
+
function usePasskeys(options = {}) {
|
|
2276
|
+
const ctx = useRakomiContext();
|
|
2277
|
+
const {
|
|
2278
|
+
adapter: nativeAdapter,
|
|
2279
|
+
http,
|
|
2280
|
+
baseUrl,
|
|
2281
|
+
publishableKey,
|
|
2282
|
+
getToken,
|
|
2283
|
+
beginAuthFlow,
|
|
2284
|
+
submitOAuthTokens,
|
|
2285
|
+
passkeyCeremonyLock
|
|
2286
|
+
} = ctx;
|
|
2287
|
+
const [isSupported, setIsSupported] = react.useState(null);
|
|
2288
|
+
const [hasPlatformAuthenticator, setHasPlatformAuthenticator] = react.useState(null);
|
|
2289
|
+
const [passkeys, setPasskeys] = react.useState(void 0);
|
|
2290
|
+
const [error, setError] = react.useState(null);
|
|
2291
|
+
const [isSigningIn, setIsSigningIn] = react.useState(false);
|
|
2292
|
+
const [isRegistering, setIsRegistering] = react.useState(false);
|
|
2293
|
+
const [isSteppingUp, setIsSteppingUp] = react.useState(false);
|
|
2294
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
2295
|
+
const [isMutating, setIsMutating] = react.useState(false);
|
|
2296
|
+
const adapter = options.adapter ?? nativeAdapter.passkeys;
|
|
2297
|
+
const mountedRef = react.useRef(true);
|
|
2298
|
+
const busyRef = react.useRef({
|
|
2299
|
+
signIn: false,
|
|
2300
|
+
register: false,
|
|
2301
|
+
stepUp: false,
|
|
2302
|
+
list: false,
|
|
2303
|
+
mutate: false
|
|
2304
|
+
});
|
|
2305
|
+
const inFlightRef = react.useRef(/* @__PURE__ */ new Set());
|
|
2306
|
+
react.useEffect(() => {
|
|
2307
|
+
mountedRef.current = true;
|
|
2308
|
+
const inFlight = inFlightRef.current;
|
|
2309
|
+
return () => {
|
|
2310
|
+
mountedRef.current = false;
|
|
2311
|
+
for (const controller of inFlight) controller.abort();
|
|
2312
|
+
inFlight.clear();
|
|
2313
|
+
};
|
|
2314
|
+
}, []);
|
|
2315
|
+
react.useEffect(() => {
|
|
2316
|
+
if (adapter === void 0) {
|
|
2317
|
+
setIsSupported(false);
|
|
2318
|
+
setHasPlatformAuthenticator(null);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
let cancelled = false;
|
|
2322
|
+
void (async () => {
|
|
2323
|
+
const [supported, platform] = await Promise.all([
|
|
2324
|
+
Promise.resolve(adapter.isSupported()).catch(() => false),
|
|
2325
|
+
Promise.resolve(platformProbe(adapter)?.() ?? null).catch(() => null)
|
|
2326
|
+
]);
|
|
2327
|
+
if (cancelled || !mountedRef.current) return;
|
|
2328
|
+
setIsSupported(supported === true);
|
|
2329
|
+
setHasPlatformAuthenticator(typeof platform === "boolean" ? platform : null);
|
|
2330
|
+
})();
|
|
2331
|
+
return () => {
|
|
2332
|
+
cancelled = true;
|
|
2333
|
+
};
|
|
2334
|
+
}, [adapter]);
|
|
2335
|
+
const setBusy = react.useCallback((key, value) => {
|
|
2336
|
+
busyRef.current[key] = value;
|
|
2337
|
+
if (key === "signIn") setIsSigningIn(value);
|
|
2338
|
+
else if (key === "register") setIsRegistering(value);
|
|
2339
|
+
else if (key === "stepUp") setIsSteppingUp(value);
|
|
2340
|
+
else if (key === "list") setIsLoading(value);
|
|
2341
|
+
else setIsMutating(value);
|
|
2342
|
+
}, []);
|
|
2343
|
+
const requireSession = react.useCallback(async () => {
|
|
2344
|
+
const expired = () => sdkCore.passkeyError("PASSKEY_SESSION_EXPIRED", "you must be signed in to manage passkeys");
|
|
2345
|
+
const token = await getToken();
|
|
2346
|
+
if (!token.ok) {
|
|
2347
|
+
const err2 = token.error;
|
|
2348
|
+
if (err2.code === "NETWORK_ERROR" || err2.code === "REFRESH_FAILED" && err2.reason === "network") {
|
|
2349
|
+
return sdkCore.passkeyError("PASSKEY_NETWORK_ERROR", err2.message);
|
|
2350
|
+
}
|
|
2351
|
+
return expired();
|
|
2352
|
+
}
|
|
2353
|
+
const user = sdkCore.decodeUser(token.token);
|
|
2354
|
+
if (user === null) return expired();
|
|
2355
|
+
return { accessToken: token.token, userId: user.id };
|
|
2356
|
+
}, [getToken]);
|
|
2357
|
+
const runAction = react.useCallback(
|
|
2358
|
+
async (key, guarded, run, onSuccess) => {
|
|
2359
|
+
const refuse = (result) => {
|
|
2360
|
+
if (!result.ok && mountedRef.current) setError(result.error);
|
|
2361
|
+
return result;
|
|
2362
|
+
};
|
|
2363
|
+
if (busyRef.current[key]) return refuse(guarded);
|
|
2364
|
+
const isCeremony = CEREMONY_ACTIONS.has(key);
|
|
2365
|
+
if (isCeremony && !passkeyCeremonyLock.tryAcquire()) return refuse(guarded);
|
|
2366
|
+
setBusy(key, true);
|
|
2367
|
+
setError(null);
|
|
2368
|
+
const controller = new AbortController();
|
|
2369
|
+
inFlightRef.current.add(controller);
|
|
2370
|
+
try {
|
|
2371
|
+
const result = await run(controller.signal);
|
|
2372
|
+
if (!result.ok && mountedRef.current) setError(result.error);
|
|
2373
|
+
if (result.ok && mountedRef.current) onSuccess?.(result);
|
|
2374
|
+
return result;
|
|
2375
|
+
} finally {
|
|
2376
|
+
inFlightRef.current.delete(controller);
|
|
2377
|
+
busyRef.current[key] = false;
|
|
2378
|
+
if (isCeremony) passkeyCeremonyLock.release();
|
|
2379
|
+
if (mountedRef.current) setBusy(key, false);
|
|
2380
|
+
}
|
|
2381
|
+
},
|
|
2382
|
+
[passkeyCeremonyLock, setBusy]
|
|
2383
|
+
);
|
|
2384
|
+
const busyResult = react.useCallback(
|
|
2385
|
+
(action) => ({
|
|
2386
|
+
ok: false,
|
|
2387
|
+
error: sdkCore.passkeyError("PASSKEY_INVALID_INPUT", `a passkey ${action} is already in progress`)
|
|
2388
|
+
}),
|
|
2389
|
+
[]
|
|
2390
|
+
);
|
|
2391
|
+
const unsupported = react.useCallback(() => {
|
|
2392
|
+
const result = { ok: false, error: notSupported() };
|
|
2393
|
+
if (mountedRef.current) setError(result.error);
|
|
2394
|
+
return Promise.resolve(result);
|
|
2395
|
+
}, []);
|
|
2396
|
+
const sessionFailure = (session) => !("accessToken" in session);
|
|
2397
|
+
const signInWithPasskey = react.useCallback(
|
|
2398
|
+
(input = {}) => {
|
|
2399
|
+
if (adapter === void 0) return unsupported();
|
|
2400
|
+
return runAction("signIn", busyResult("sign-in"), async (signal) => {
|
|
2401
|
+
const result = await sdkCore.assertPasskey({
|
|
2402
|
+
http,
|
|
2403
|
+
baseUrl,
|
|
2404
|
+
clientId: publishableKey,
|
|
2405
|
+
adapter,
|
|
2406
|
+
...input.userHandle === void 0 ? {} : { userHandle: input.userHandle },
|
|
2407
|
+
signal
|
|
2408
|
+
});
|
|
2409
|
+
if (!result.ok) return result;
|
|
2410
|
+
const failed = (message) => ({
|
|
2411
|
+
ok: false,
|
|
2412
|
+
error: sdkCore.passkeyError("PASSKEY_REQUEST_FAILED", message)
|
|
2413
|
+
});
|
|
2414
|
+
let nonce;
|
|
2415
|
+
try {
|
|
2416
|
+
nonce = await beginAuthFlow();
|
|
2417
|
+
} catch (err2) {
|
|
2418
|
+
return failed(
|
|
2419
|
+
`the passkey was accepted but the session could not be started: ${err2 instanceof Error ? err2.message : "unknown error"}`
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
const tokens = result.tokens;
|
|
2423
|
+
if (nativeAdapter.dpopProver !== void 0 && tokens.token_type !== "DPoP") {
|
|
2424
|
+
return failed(
|
|
2425
|
+
"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."
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
try {
|
|
2429
|
+
await submitOAuthTokens(tokens, nonce);
|
|
2430
|
+
} catch (err2) {
|
|
2431
|
+
return failed(
|
|
2432
|
+
`the passkey was accepted but the session could not be stored: ${err2 instanceof Error ? err2.message : "unknown error"}`
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
const confirmed = await getToken();
|
|
2436
|
+
if (!confirmed.ok) {
|
|
2437
|
+
return failed(
|
|
2438
|
+
"the passkey was accepted but the session was not established (the token submission was rejected)"
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
const submitted = sessionIdentity(tokens.access_token);
|
|
2442
|
+
const live = sessionIdentity(confirmed.token);
|
|
2443
|
+
if (submitted === null || live === null) {
|
|
2444
|
+
if (confirmed.token !== tokens.access_token) {
|
|
2445
|
+
return failed("the passkey was accepted but the resulting session token could not be read");
|
|
2446
|
+
}
|
|
2447
|
+
return result;
|
|
2448
|
+
}
|
|
2449
|
+
if (live.subject !== submitted.subject || live.session !== submitted.session) {
|
|
2450
|
+
return failed(
|
|
2451
|
+
"the passkey was accepted but the session was not established (the token submission was rejected)"
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
2454
|
+
return result;
|
|
2455
|
+
});
|
|
2456
|
+
},
|
|
2457
|
+
[
|
|
2458
|
+
adapter,
|
|
2459
|
+
baseUrl,
|
|
2460
|
+
beginAuthFlow,
|
|
2461
|
+
busyResult,
|
|
2462
|
+
getToken,
|
|
2463
|
+
http,
|
|
2464
|
+
publishableKey,
|
|
2465
|
+
runAction,
|
|
2466
|
+
submitOAuthTokens,
|
|
2467
|
+
unsupported
|
|
2468
|
+
]
|
|
2469
|
+
);
|
|
2470
|
+
const stepUpWithPasskey = react.useCallback(() => {
|
|
2471
|
+
if (adapter === void 0) return unsupported();
|
|
2472
|
+
return runAction("stepUp", busyResult("step-up"), async (signal) => {
|
|
2473
|
+
const session = await requireSession();
|
|
2474
|
+
if (sessionFailure(session)) return { ok: false, error: session };
|
|
2475
|
+
return sdkCore.stepUpWithPasskey({
|
|
2476
|
+
http,
|
|
2477
|
+
baseUrl,
|
|
2478
|
+
clientId: publishableKey,
|
|
2479
|
+
accessToken: session.accessToken,
|
|
2480
|
+
adapter,
|
|
2481
|
+
signal
|
|
2482
|
+
});
|
|
2483
|
+
});
|
|
2484
|
+
}, [adapter, baseUrl, busyResult, http, publishableKey, requireSession, runAction, unsupported]);
|
|
2485
|
+
const registerPasskey = react.useCallback(
|
|
2486
|
+
(input) => {
|
|
2487
|
+
if (adapter === void 0) return unsupported();
|
|
2488
|
+
return runAction("register", busyResult("registration"), async (signal) => {
|
|
2489
|
+
const session = await requireSession();
|
|
2490
|
+
if (sessionFailure(session)) return { ok: false, error: session };
|
|
2491
|
+
return sdkCore.registerPasskey({
|
|
2492
|
+
http,
|
|
2493
|
+
baseUrl,
|
|
2494
|
+
clientId: publishableKey,
|
|
2495
|
+
accessToken: session.accessToken,
|
|
2496
|
+
stepUpToken: input.stepUpToken,
|
|
2497
|
+
adapter,
|
|
2498
|
+
...input.nickname === void 0 ? {} : { nickname: input.nickname },
|
|
2499
|
+
signal
|
|
2500
|
+
});
|
|
2501
|
+
});
|
|
2502
|
+
},
|
|
2503
|
+
[adapter, baseUrl, busyResult, http, publishableKey, requireSession, runAction, unsupported]
|
|
2504
|
+
);
|
|
2505
|
+
const listPasskeys = react.useCallback(
|
|
2506
|
+
(input) => runAction(
|
|
2507
|
+
"list",
|
|
2508
|
+
busyResult("list"),
|
|
2509
|
+
async (signal) => {
|
|
2510
|
+
const session = await requireSession();
|
|
2511
|
+
if (sessionFailure(session)) return { ok: false, error: session };
|
|
2512
|
+
return sdkCore.listPasskeys({
|
|
2513
|
+
http,
|
|
2514
|
+
baseUrl,
|
|
2515
|
+
clientId: publishableKey,
|
|
2516
|
+
accessToken: session.accessToken,
|
|
2517
|
+
stepUpToken: input.stepUpToken,
|
|
2518
|
+
userId: session.userId,
|
|
2519
|
+
signal
|
|
2520
|
+
});
|
|
2521
|
+
},
|
|
2522
|
+
(result) => {
|
|
2523
|
+
if (result.ok) setPasskeys(result.data);
|
|
2524
|
+
}
|
|
2525
|
+
),
|
|
2526
|
+
[baseUrl, busyResult, http, publishableKey, requireSession, runAction]
|
|
2527
|
+
);
|
|
2528
|
+
const renamePasskey = react.useCallback(
|
|
2529
|
+
(input) => runAction(
|
|
2530
|
+
"mutate",
|
|
2531
|
+
busyResult("mutation"),
|
|
2532
|
+
async (signal) => {
|
|
2533
|
+
const session = await requireSession();
|
|
2534
|
+
if (sessionFailure(session)) return { ok: false, error: session };
|
|
2535
|
+
return sdkCore.renamePasskey({
|
|
2536
|
+
http,
|
|
2537
|
+
baseUrl,
|
|
2538
|
+
clientId: publishableKey,
|
|
2539
|
+
accessToken: session.accessToken,
|
|
2540
|
+
stepUpToken: input.stepUpToken,
|
|
2541
|
+
userId: session.userId,
|
|
2542
|
+
passkeyId: input.passkeyId,
|
|
2543
|
+
nickname: input.nickname,
|
|
2544
|
+
signal
|
|
2545
|
+
});
|
|
2546
|
+
},
|
|
2547
|
+
(result) => {
|
|
2548
|
+
if (!result.ok) return;
|
|
2549
|
+
setPasskeys(
|
|
2550
|
+
(current) => current?.map((passkey) => passkey.id === result.passkey.id ? result.passkey : passkey)
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
),
|
|
2554
|
+
[baseUrl, busyResult, http, publishableKey, requireSession, runAction]
|
|
2555
|
+
);
|
|
2556
|
+
const deletePasskey = react.useCallback(
|
|
2557
|
+
(input) => runAction(
|
|
2558
|
+
"mutate",
|
|
2559
|
+
busyResult("mutation"),
|
|
2560
|
+
async (signal) => {
|
|
2561
|
+
const session = await requireSession();
|
|
2562
|
+
if (sessionFailure(session)) return { ok: false, error: session };
|
|
2563
|
+
return sdkCore.deletePasskey({
|
|
2564
|
+
http,
|
|
2565
|
+
baseUrl,
|
|
2566
|
+
clientId: publishableKey,
|
|
2567
|
+
accessToken: session.accessToken,
|
|
2568
|
+
stepUpToken: input.stepUpToken,
|
|
2569
|
+
userId: session.userId,
|
|
2570
|
+
passkeyId: input.passkeyId,
|
|
2571
|
+
signal
|
|
2572
|
+
});
|
|
2573
|
+
},
|
|
2574
|
+
(result) => {
|
|
2575
|
+
if (result.ok) {
|
|
2576
|
+
setPasskeys((current) => current?.filter((passkey) => passkey.id !== input.passkeyId));
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
),
|
|
2580
|
+
[baseUrl, busyResult, http, publishableKey, requireSession, runAction]
|
|
2581
|
+
);
|
|
2582
|
+
return {
|
|
2583
|
+
isSupported,
|
|
2584
|
+
hasPlatformAuthenticator,
|
|
2585
|
+
passkeys,
|
|
2586
|
+
error,
|
|
2587
|
+
isSigningIn,
|
|
2588
|
+
isRegistering,
|
|
2589
|
+
isSteppingUp,
|
|
2590
|
+
isLoading,
|
|
2591
|
+
isMutating,
|
|
2592
|
+
signInWithPasskey,
|
|
2593
|
+
stepUpWithPasskey,
|
|
2594
|
+
registerPasskey,
|
|
2595
|
+
listPasskeys,
|
|
2596
|
+
renamePasskey,
|
|
2597
|
+
deletePasskey
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
1923
2600
|
|
|
1924
2601
|
// src/hooks/use-submit-oauth-tokens.ts
|
|
1925
2602
|
function useSubmitOAuthTokens() {
|
|
@@ -2085,6 +2762,7 @@ exports.UserProfile = UserProfile;
|
|
|
2085
2762
|
exports.createDefaultExpoAdapter = createDefaultExpoAdapter;
|
|
2086
2763
|
exports.createDpopSession = createDpopSession;
|
|
2087
2764
|
exports.createNativeDpopProver = createNativeDpopProver;
|
|
2765
|
+
exports.createNativePasskeyAdapter = createNativePasskeyAdapter;
|
|
2088
2766
|
exports.createRnHttpClient = createRnHttpClient;
|
|
2089
2767
|
exports.startSocialSignIn = startSocialSignIn;
|
|
2090
2768
|
exports.useAnonymousSignin = useAnonymousSignin;
|
|
@@ -2100,6 +2778,7 @@ exports.useMagicLink = useMagicLink;
|
|
|
2100
2778
|
exports.useMfa = useMfa;
|
|
2101
2779
|
exports.useOrganization = useOrganization;
|
|
2102
2780
|
exports.useOrganizationList = useOrganizationList;
|
|
2781
|
+
exports.usePasskeys = usePasskeys;
|
|
2103
2782
|
exports.useRegister = useRegister;
|
|
2104
2783
|
exports.useSession = useSession;
|
|
2105
2784
|
exports.useSubmitOAuthTokens = useSubmitOAuthTokens;
|