@elvix.is/sdk 0.5.5 → 0.5.6
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/react.d.ts +2 -0
- package/dist/react.js +187 -1
- package/package.json +1 -1
package/dist/react.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ type ElvixCopy = {
|
|
|
58
58
|
subtitle?: string;
|
|
59
59
|
/** Google factor button. */
|
|
60
60
|
googleButton?: string;
|
|
61
|
+
/** Passkey factor button. */
|
|
62
|
+
passkeyButton?: string;
|
|
61
63
|
/** Email field placeholder. */
|
|
62
64
|
emailPlaceholder?: string;
|
|
63
65
|
/** Email submit button (identify step). */
|
package/dist/react.js
CHANGED
|
@@ -204,6 +204,7 @@ import { useState as useState2 } from "react";
|
|
|
204
204
|
var DEFAULT_COPY = {
|
|
205
205
|
subtitle: "Pick how you want to continue.",
|
|
206
206
|
googleButton: "Continue with Google",
|
|
207
|
+
passkeyButton: "Continue with passkey",
|
|
207
208
|
emailPlaceholder: "you@example.com",
|
|
208
209
|
sendCodeButton: "Send code",
|
|
209
210
|
sendingLabel: "Sending\u2026",
|
|
@@ -272,6 +273,97 @@ function isSameOrigin(baseUrl) {
|
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
275
|
|
|
276
|
+
// src/react/passkey.ts
|
|
277
|
+
function b64urlToBuf(b64url) {
|
|
278
|
+
const b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
279
|
+
const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
|
|
280
|
+
const bin = atob(b64 + pad);
|
|
281
|
+
const bytes = new Uint8Array(bin.length);
|
|
282
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
283
|
+
return bytes.buffer;
|
|
284
|
+
}
|
|
285
|
+
function bufToB64url(buf) {
|
|
286
|
+
const bytes = new Uint8Array(buf);
|
|
287
|
+
let bin = "";
|
|
288
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
289
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
290
|
+
}
|
|
291
|
+
async function runPasskeySignIn(baseUrl, clientId) {
|
|
292
|
+
if (!clientId) return { ok: false, error: "missing_client_id", message: "ElvixProvider needs a clientId." };
|
|
293
|
+
if (typeof window === "undefined" || !window.PublicKeyCredential || !navigator.credentials?.get) {
|
|
294
|
+
return { ok: false, error: "passkey_unsupported", message: "This browser can't use passkeys." };
|
|
295
|
+
}
|
|
296
|
+
const credentials = isSameOrigin(baseUrl) ? "include" : "omit";
|
|
297
|
+
let options;
|
|
298
|
+
try {
|
|
299
|
+
const res = await fetch(`${baseUrl}/api/auth/passkey/sign-in/start`, {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: { "content-type": "application/json" },
|
|
302
|
+
credentials,
|
|
303
|
+
body: JSON.stringify({ intent: "app", clientId })
|
|
304
|
+
});
|
|
305
|
+
const body = await res.json();
|
|
306
|
+
if (!res.ok || !body.success || !body.data?.options) {
|
|
307
|
+
return { ok: false, error: body.errorMessage ?? "passkey_start_failed" };
|
|
308
|
+
}
|
|
309
|
+
options = body.data.options;
|
|
310
|
+
} catch (e) {
|
|
311
|
+
return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
|
|
312
|
+
}
|
|
313
|
+
let assertion;
|
|
314
|
+
try {
|
|
315
|
+
const publicKey = {
|
|
316
|
+
challenge: b64urlToBuf(options.challenge),
|
|
317
|
+
timeout: options.timeout,
|
|
318
|
+
rpId: options.rpId,
|
|
319
|
+
userVerification: options.userVerification,
|
|
320
|
+
allowCredentials: options.allowCredentials?.map((c) => ({
|
|
321
|
+
id: b64urlToBuf(c.id),
|
|
322
|
+
type: c.type,
|
|
323
|
+
transports: c.transports
|
|
324
|
+
}))
|
|
325
|
+
};
|
|
326
|
+
const cred = await navigator.credentials.get({ publicKey });
|
|
327
|
+
if (!cred) return { ok: false, error: "passkey_cancelled" };
|
|
328
|
+
const resp = cred.response;
|
|
329
|
+
assertion = {
|
|
330
|
+
id: cred.id,
|
|
331
|
+
rawId: bufToB64url(cred.rawId),
|
|
332
|
+
type: "public-key",
|
|
333
|
+
clientExtensionResults: cred.getClientExtensionResults(),
|
|
334
|
+
authenticatorAttachment: cred.authenticatorAttachment ?? void 0,
|
|
335
|
+
response: {
|
|
336
|
+
clientDataJSON: bufToB64url(resp.clientDataJSON),
|
|
337
|
+
authenticatorData: bufToB64url(resp.authenticatorData),
|
|
338
|
+
signature: bufToB64url(resp.signature),
|
|
339
|
+
userHandle: resp.userHandle ? bufToB64url(resp.userHandle) : void 0
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
} catch (e) {
|
|
343
|
+
const name = e?.name;
|
|
344
|
+
if (name === "NotAllowedError" || name === "AbortError") {
|
|
345
|
+
return { ok: false, error: "passkey_cancelled" };
|
|
346
|
+
}
|
|
347
|
+
return { ok: false, error: "passkey_failed", message: e instanceof Error ? e.message : void 0 };
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const res = await fetch(`${baseUrl}/api/auth/passkey/sign-in/finish`, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
headers: { "content-type": "application/json" },
|
|
353
|
+
credentials,
|
|
354
|
+
body: JSON.stringify({ intent: "app", clientId, ...assertion })
|
|
355
|
+
});
|
|
356
|
+
const body = await res.json();
|
|
357
|
+
if (!res.ok || !body.success) {
|
|
358
|
+
return { ok: false, error: body.errorMessage ?? "passkey_verify_failed" };
|
|
359
|
+
}
|
|
360
|
+
if (body.data?.token) setElvixToken(body.data.token);
|
|
361
|
+
return { ok: true, redirect: body.data?.redirect, token: body.data?.token };
|
|
362
|
+
} catch (e) {
|
|
363
|
+
return { ok: false, error: "network", message: e instanceof Error ? e.message : void 0 };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
275
367
|
// src/react/elvix-sign-in.tsx
|
|
276
368
|
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
277
369
|
function ElvixSignIn({
|
|
@@ -310,6 +402,29 @@ function ElvixSignIn({
|
|
|
310
402
|
`${ctx.baseUrl}/api/auth/google/start?intent=app&clientId=${encodeURIComponent(ctx.clientId)}`
|
|
311
403
|
);
|
|
312
404
|
}
|
|
405
|
+
async function startPasskey() {
|
|
406
|
+
setBusy(true);
|
|
407
|
+
setError(null);
|
|
408
|
+
try {
|
|
409
|
+
const result = await runPasskeySignIn(ctx.baseUrl, ctx.clientId);
|
|
410
|
+
if (!result.ok) {
|
|
411
|
+
if (result.error === "passkey_cancelled") {
|
|
412
|
+
onResult?.({ ok: false, error: result.error });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
return fail(result.error, result.message);
|
|
416
|
+
}
|
|
417
|
+
setStep("done");
|
|
418
|
+
onResult?.({
|
|
419
|
+
ok: true,
|
|
420
|
+
method: "passkey",
|
|
421
|
+
redirect: result.redirect ?? redirectAfterSignIn,
|
|
422
|
+
token: result.token
|
|
423
|
+
});
|
|
424
|
+
} finally {
|
|
425
|
+
setBusy(false);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
313
428
|
async function startOtp(e) {
|
|
314
429
|
e.preventDefault();
|
|
315
430
|
if (!email.trim()) return fail("invalid_input", copy.errorEnterEmail);
|
|
@@ -388,6 +503,17 @@ function ElvixSignIn({
|
|
|
388
503
|
children: copy.googleButton
|
|
389
504
|
}
|
|
390
505
|
),
|
|
506
|
+
app?.methodPasskey && /* @__PURE__ */ jsx3(
|
|
507
|
+
"button",
|
|
508
|
+
{
|
|
509
|
+
type: "button",
|
|
510
|
+
onClick: startPasskey,
|
|
511
|
+
disabled: busy,
|
|
512
|
+
className: "elvix-btn elvix-btn-passkey",
|
|
513
|
+
"data-elvix-method": "passkey",
|
|
514
|
+
children: copy.passkeyButton
|
|
515
|
+
}
|
|
516
|
+
),
|
|
391
517
|
app?.methodEmailOtp && /* @__PURE__ */ jsxs2("form", { onSubmit: startOtp, "data-elvix-method": "email_otp", className: "elvix-otp-form", children: [
|
|
392
518
|
/* @__PURE__ */ jsx3(
|
|
393
519
|
"input",
|
|
@@ -529,6 +655,28 @@ function ElvixSecuredBadge({
|
|
|
529
655
|
|
|
530
656
|
// src/react/elvix-sign-in-form.tsx
|
|
531
657
|
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
658
|
+
function PasskeyIcon({ size = 18 }) {
|
|
659
|
+
return /* @__PURE__ */ jsxs5(
|
|
660
|
+
"svg",
|
|
661
|
+
{
|
|
662
|
+
width: size,
|
|
663
|
+
height: size,
|
|
664
|
+
viewBox: "0 0 24 24",
|
|
665
|
+
fill: "none",
|
|
666
|
+
stroke: "currentColor",
|
|
667
|
+
strokeWidth: 1.8,
|
|
668
|
+
strokeLinecap: "round",
|
|
669
|
+
strokeLinejoin: "round",
|
|
670
|
+
"aria-hidden": true,
|
|
671
|
+
style: { display: "block" },
|
|
672
|
+
children: [
|
|
673
|
+
/* @__PURE__ */ jsx6("circle", { cx: "9", cy: "8", r: "4" }),
|
|
674
|
+
/* @__PURE__ */ jsx6("path", { d: "M4 20c0-3 2.5-5 5-5 1 0 1.9.3 2.7.8" }),
|
|
675
|
+
/* @__PURE__ */ jsx6("path", { d: "M17 12.5a2.5 2.5 0 1 0-2.5 2.5v5l1.2-1.2 1.3 1.2v-5a2.5 2.5 0 0 0 0-2.5Z" })
|
|
676
|
+
]
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
}
|
|
532
680
|
function GoogleG({ size = 18 }) {
|
|
533
681
|
return /* @__PURE__ */ jsxs5("svg", { width: size, height: size, viewBox: "0 0 18 18", "aria-hidden": true, style: { display: "block" }, children: [
|
|
534
682
|
/* @__PURE__ */ jsx6(
|
|
@@ -590,8 +738,9 @@ function ElvixSignInForm({
|
|
|
590
738
|
const title = copy.title ? fillCopy(copy.title, { app: app?.appName ?? "" }) : defaultTitle;
|
|
591
739
|
const submitLabel = copy.submitButton ?? verb;
|
|
592
740
|
const showGoogle = Boolean(app?.methodGoogle);
|
|
741
|
+
const showPasskey = Boolean(app?.methodPasskey);
|
|
593
742
|
const showEmail = Boolean(app?.methodEmailOtp);
|
|
594
|
-
const showDivider = showGoogle && showEmail;
|
|
743
|
+
const showDivider = (showGoogle || showPasskey) && showEmail;
|
|
595
744
|
const logoSrc = app?.iconUrl || app?.logoUrl || null;
|
|
596
745
|
const privacyUrl = app?.privacyPolicyUrl || null;
|
|
597
746
|
const termsUrl = app?.termsOfServiceUrl || null;
|
|
@@ -629,6 +778,29 @@ function ElvixSignInForm({
|
|
|
629
778
|
`${ctx.baseUrl}/api/auth/google/start?intent=app&clientId=${encodeURIComponent(ctx.clientId)}`
|
|
630
779
|
);
|
|
631
780
|
}
|
|
781
|
+
async function startPasskey() {
|
|
782
|
+
setBusy(true);
|
|
783
|
+
setError(null);
|
|
784
|
+
try {
|
|
785
|
+
const result = await runPasskeySignIn(ctx.baseUrl, ctx.clientId);
|
|
786
|
+
if (!result.ok) {
|
|
787
|
+
if (result.error === "passkey_cancelled") {
|
|
788
|
+
onResult?.({ ok: false, error: result.error });
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
return fail(result.error, result.message);
|
|
792
|
+
}
|
|
793
|
+
setStep("done");
|
|
794
|
+
onResult?.({
|
|
795
|
+
ok: true,
|
|
796
|
+
method: "passkey",
|
|
797
|
+
redirect: result.redirect ?? redirectAfterSignIn,
|
|
798
|
+
token: result.token
|
|
799
|
+
});
|
|
800
|
+
} finally {
|
|
801
|
+
setBusy(false);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
632
804
|
async function startOtp(e) {
|
|
633
805
|
e.preventDefault();
|
|
634
806
|
if (!email.trim()) return fail("invalid_input", copy.errorEnterEmail);
|
|
@@ -792,6 +964,20 @@ function ElvixSignInForm({
|
|
|
792
964
|
]
|
|
793
965
|
}
|
|
794
966
|
),
|
|
967
|
+
showPasskey && /* @__PURE__ */ jsxs5(
|
|
968
|
+
"button",
|
|
969
|
+
{
|
|
970
|
+
type: "button",
|
|
971
|
+
onClick: startPasskey,
|
|
972
|
+
disabled: busy,
|
|
973
|
+
style: googleBtnStyle,
|
|
974
|
+
"data-elvix-method": "passkey",
|
|
975
|
+
children: [
|
|
976
|
+
/* @__PURE__ */ jsx6(PasskeyIcon, {}),
|
|
977
|
+
/* @__PURE__ */ jsx6("span", { children: copy.passkeyButton })
|
|
978
|
+
]
|
|
979
|
+
}
|
|
980
|
+
),
|
|
795
981
|
showDivider && /* @__PURE__ */ jsxs5(
|
|
796
982
|
"div",
|
|
797
983
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elvix.is/sdk",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Official elvix SDK. Drop-in React components, server helpers, and an MCP server so AI coding agents integrate elvix on the first try.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://elvix.is",
|