cauthy-auth 0.9.0 → 0.11.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/client.d.mts +61 -5
- package/dist/client.d.ts +61 -5
- package/dist/client.js +118 -12
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +117 -12
- package/dist/client.mjs.map +1 -1
- package/dist/index.js +121 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -6
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +68 -5
- package/dist/react.d.ts +68 -5
- package/dist/react.js +203 -18
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +202 -18
- package/dist/react.mjs.map +1 -1
- package/dist/svelte.d.mts +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.js +116 -12
- package/dist/svelte.js.map +1 -1
- package/dist/svelte.mjs +116 -12
- package/dist/svelte.mjs.map +1 -1
- package/dist/vue.d.mts +1 -1
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +116 -12
- package/dist/vue.js.map +1 -1
- package/dist/vue.mjs +116 -12
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
package/dist/react.d.mts
CHANGED
|
@@ -210,6 +210,9 @@ interface PublicSession {
|
|
|
210
210
|
|
|
211
211
|
type AuthField = 'email' | 'password' | 'newPassword' | 'name' | 'code';
|
|
212
212
|
|
|
213
|
+
/** Whether this browser can use a passkey at all. */
|
|
214
|
+
declare function canUsePasskeys(): boolean;
|
|
215
|
+
|
|
213
216
|
/** What went wrong, in a form the screen can show. */
|
|
214
217
|
interface AuthError {
|
|
215
218
|
/** Stable, for translating: `invalid_credentials`, `invalid_email`, `too_short`, `email_taken`, `unavailable`… */
|
|
@@ -235,8 +238,29 @@ interface AuthMethods {
|
|
|
235
238
|
/** Whether people may create an account themselves. */
|
|
236
239
|
signUp: boolean;
|
|
237
240
|
magicLink: boolean;
|
|
238
|
-
/** Whether to offer a passkey.
|
|
241
|
+
/** Whether to offer a passkey. */
|
|
239
242
|
passkey: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Whether the passkey is used on this page itself. False: `signIn.passkey`
|
|
245
|
+
* sends the person to Cauthy's page for that step alone, and they come back
|
|
246
|
+
* signed in. That happens only when this page is outside the domain the
|
|
247
|
+
* workspace's passkeys belong to.
|
|
248
|
+
*/
|
|
249
|
+
passkeyHere: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Whether Cauthy's page can use the passkey, for that step alone: where a
|
|
252
|
+
* page that cannot, or a browser that refuses to here, sends the person.
|
|
253
|
+
*/
|
|
254
|
+
passkeyOnCauthy: boolean;
|
|
255
|
+
}
|
|
256
|
+
/** A passkey of the signed-in account. Moments are UTC. */
|
|
257
|
+
interface Passkey {
|
|
258
|
+
id: string;
|
|
259
|
+
/** What the person called it ("My laptop"), or "". */
|
|
260
|
+
name: string;
|
|
261
|
+
createdAt: string;
|
|
262
|
+
/** When it last signed the person in, or null. */
|
|
263
|
+
lastUsedAt: string | null;
|
|
240
264
|
}
|
|
241
265
|
/** A person's own account, as their account screen shows it. */
|
|
242
266
|
interface Account$1 {
|
|
@@ -340,6 +364,22 @@ interface AccountClient {
|
|
|
340
364
|
sessions(): Promise<AccountSession[]>;
|
|
341
365
|
/** Signs the account out everywhere, here too. */
|
|
342
366
|
signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
|
|
367
|
+
/** The account's passkeys. */
|
|
368
|
+
passkeys(): Promise<Passkey[]>;
|
|
369
|
+
/**
|
|
370
|
+
* Makes a passkey on this device, on this page: the browser asks the person
|
|
371
|
+
* for their fingerprint, face or screen lock. An account with a password
|
|
372
|
+
* proves it again first. `cancelled`: the person closed the browser's prompt.
|
|
373
|
+
*/
|
|
374
|
+
addPasskey(form?: {
|
|
375
|
+
password?: string;
|
|
376
|
+
name?: string;
|
|
377
|
+
}): Promise<AuthResult<'added', {
|
|
378
|
+
passkey: Passkey;
|
|
379
|
+
}>>;
|
|
380
|
+
removePasskey(id: string): Promise<AuthResult<'removed', {
|
|
381
|
+
id: string;
|
|
382
|
+
}>>;
|
|
343
383
|
}
|
|
344
384
|
interface AuthClient {
|
|
345
385
|
signIn: {
|
|
@@ -372,12 +412,21 @@ interface AuthClient {
|
|
|
372
412
|
}): Promise<AuthResult<'signed_in', {
|
|
373
413
|
session: PublicSession;
|
|
374
414
|
}>>;
|
|
375
|
-
/**
|
|
415
|
+
/**
|
|
416
|
+
* Signs in with a passkey, on this page: the browser asks the person for
|
|
417
|
+
* their fingerprint, face or screen lock. When this page cannot use the
|
|
418
|
+
* workspace's passkeys (`methods().passkeyHere` is false), the browser goes
|
|
419
|
+
* to Cauthy's page for that step alone and comes back signed in, at
|
|
420
|
+
* `returnTo`: then the answer is `redirected`. `cancelled`: the person
|
|
421
|
+
* closed the browser's prompt.
|
|
422
|
+
*/
|
|
376
423
|
passkey(options?: {
|
|
377
424
|
returnTo?: string;
|
|
378
425
|
locale?: string;
|
|
379
|
-
}):
|
|
380
|
-
|
|
426
|
+
}): Promise<AuthResult<'signed_in', {
|
|
427
|
+
session: PublicSession;
|
|
428
|
+
}> | AuthResult<'redirected'>>;
|
|
429
|
+
/** Where `passkey` sends the browser when this page cannot use the passkey itself. */
|
|
381
430
|
passkeyUrl(options?: {
|
|
382
431
|
returnTo?: string;
|
|
383
432
|
locale?: string;
|
|
@@ -432,6 +481,13 @@ interface AuthClientOptions {
|
|
|
432
481
|
/** Where the server mounted the routes. Default: `/api/auth`. */
|
|
433
482
|
basePath?: string;
|
|
434
483
|
fetch?: typeof fetch;
|
|
484
|
+
/**
|
|
485
|
+
* The language the person reads, for what Cauthy mails them (a sign-in
|
|
486
|
+
* link, a new password, a confirmation): a tag like `es` or `es-CO`, or a
|
|
487
|
+
* function that answers it when the person can change it. Default: the
|
|
488
|
+
* page's own (`<html lang>`), else the browser's.
|
|
489
|
+
*/
|
|
490
|
+
locale?: string | (() => string | undefined);
|
|
435
491
|
}
|
|
436
492
|
declare function createAuthClient(options?: AuthClientOptions): AuthClient;
|
|
437
493
|
|
|
@@ -482,6 +538,13 @@ interface AuthTexts {
|
|
|
482
538
|
keepCodes: string;
|
|
483
539
|
passkeys: string;
|
|
484
540
|
managePasskeys: string;
|
|
541
|
+
passkeysHelp: string;
|
|
542
|
+
noPasskeys: string;
|
|
543
|
+
passkeyName: string;
|
|
544
|
+
addPasskey: string;
|
|
545
|
+
removePasskey: string;
|
|
546
|
+
passkeyAdded: string;
|
|
547
|
+
passkeyUnnamed: string;
|
|
485
548
|
sessions: string;
|
|
486
549
|
signOutEverywhere: string;
|
|
487
550
|
unknownDevice: string;
|
|
@@ -622,4 +685,4 @@ declare function useSession(): SessionContext;
|
|
|
622
685
|
*/
|
|
623
686
|
declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
|
|
624
687
|
|
|
625
|
-
export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type LinkedIdentity, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
|
|
688
|
+
export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type LinkedIdentity, type Passkey, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
|
package/dist/react.d.ts
CHANGED
|
@@ -210,6 +210,9 @@ interface PublicSession {
|
|
|
210
210
|
|
|
211
211
|
type AuthField = 'email' | 'password' | 'newPassword' | 'name' | 'code';
|
|
212
212
|
|
|
213
|
+
/** Whether this browser can use a passkey at all. */
|
|
214
|
+
declare function canUsePasskeys(): boolean;
|
|
215
|
+
|
|
213
216
|
/** What went wrong, in a form the screen can show. */
|
|
214
217
|
interface AuthError {
|
|
215
218
|
/** Stable, for translating: `invalid_credentials`, `invalid_email`, `too_short`, `email_taken`, `unavailable`… */
|
|
@@ -235,8 +238,29 @@ interface AuthMethods {
|
|
|
235
238
|
/** Whether people may create an account themselves. */
|
|
236
239
|
signUp: boolean;
|
|
237
240
|
magicLink: boolean;
|
|
238
|
-
/** Whether to offer a passkey.
|
|
241
|
+
/** Whether to offer a passkey. */
|
|
239
242
|
passkey: boolean;
|
|
243
|
+
/**
|
|
244
|
+
* Whether the passkey is used on this page itself. False: `signIn.passkey`
|
|
245
|
+
* sends the person to Cauthy's page for that step alone, and they come back
|
|
246
|
+
* signed in. That happens only when this page is outside the domain the
|
|
247
|
+
* workspace's passkeys belong to.
|
|
248
|
+
*/
|
|
249
|
+
passkeyHere: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Whether Cauthy's page can use the passkey, for that step alone: where a
|
|
252
|
+
* page that cannot, or a browser that refuses to here, sends the person.
|
|
253
|
+
*/
|
|
254
|
+
passkeyOnCauthy: boolean;
|
|
255
|
+
}
|
|
256
|
+
/** A passkey of the signed-in account. Moments are UTC. */
|
|
257
|
+
interface Passkey {
|
|
258
|
+
id: string;
|
|
259
|
+
/** What the person called it ("My laptop"), or "". */
|
|
260
|
+
name: string;
|
|
261
|
+
createdAt: string;
|
|
262
|
+
/** When it last signed the person in, or null. */
|
|
263
|
+
lastUsedAt: string | null;
|
|
240
264
|
}
|
|
241
265
|
/** A person's own account, as their account screen shows it. */
|
|
242
266
|
interface Account$1 {
|
|
@@ -340,6 +364,22 @@ interface AccountClient {
|
|
|
340
364
|
sessions(): Promise<AccountSession[]>;
|
|
341
365
|
/** Signs the account out everywhere, here too. */
|
|
342
366
|
signOutEverywhere(): Promise<AuthResult<'signed_out'>>;
|
|
367
|
+
/** The account's passkeys. */
|
|
368
|
+
passkeys(): Promise<Passkey[]>;
|
|
369
|
+
/**
|
|
370
|
+
* Makes a passkey on this device, on this page: the browser asks the person
|
|
371
|
+
* for their fingerprint, face or screen lock. An account with a password
|
|
372
|
+
* proves it again first. `cancelled`: the person closed the browser's prompt.
|
|
373
|
+
*/
|
|
374
|
+
addPasskey(form?: {
|
|
375
|
+
password?: string;
|
|
376
|
+
name?: string;
|
|
377
|
+
}): Promise<AuthResult<'added', {
|
|
378
|
+
passkey: Passkey;
|
|
379
|
+
}>>;
|
|
380
|
+
removePasskey(id: string): Promise<AuthResult<'removed', {
|
|
381
|
+
id: string;
|
|
382
|
+
}>>;
|
|
343
383
|
}
|
|
344
384
|
interface AuthClient {
|
|
345
385
|
signIn: {
|
|
@@ -372,12 +412,21 @@ interface AuthClient {
|
|
|
372
412
|
}): Promise<AuthResult<'signed_in', {
|
|
373
413
|
session: PublicSession;
|
|
374
414
|
}>>;
|
|
375
|
-
/**
|
|
415
|
+
/**
|
|
416
|
+
* Signs in with a passkey, on this page: the browser asks the person for
|
|
417
|
+
* their fingerprint, face or screen lock. When this page cannot use the
|
|
418
|
+
* workspace's passkeys (`methods().passkeyHere` is false), the browser goes
|
|
419
|
+
* to Cauthy's page for that step alone and comes back signed in, at
|
|
420
|
+
* `returnTo`: then the answer is `redirected`. `cancelled`: the person
|
|
421
|
+
* closed the browser's prompt.
|
|
422
|
+
*/
|
|
376
423
|
passkey(options?: {
|
|
377
424
|
returnTo?: string;
|
|
378
425
|
locale?: string;
|
|
379
|
-
}):
|
|
380
|
-
|
|
426
|
+
}): Promise<AuthResult<'signed_in', {
|
|
427
|
+
session: PublicSession;
|
|
428
|
+
}> | AuthResult<'redirected'>>;
|
|
429
|
+
/** Where `passkey` sends the browser when this page cannot use the passkey itself. */
|
|
381
430
|
passkeyUrl(options?: {
|
|
382
431
|
returnTo?: string;
|
|
383
432
|
locale?: string;
|
|
@@ -432,6 +481,13 @@ interface AuthClientOptions {
|
|
|
432
481
|
/** Where the server mounted the routes. Default: `/api/auth`. */
|
|
433
482
|
basePath?: string;
|
|
434
483
|
fetch?: typeof fetch;
|
|
484
|
+
/**
|
|
485
|
+
* The language the person reads, for what Cauthy mails them (a sign-in
|
|
486
|
+
* link, a new password, a confirmation): a tag like `es` or `es-CO`, or a
|
|
487
|
+
* function that answers it when the person can change it. Default: the
|
|
488
|
+
* page's own (`<html lang>`), else the browser's.
|
|
489
|
+
*/
|
|
490
|
+
locale?: string | (() => string | undefined);
|
|
435
491
|
}
|
|
436
492
|
declare function createAuthClient(options?: AuthClientOptions): AuthClient;
|
|
437
493
|
|
|
@@ -482,6 +538,13 @@ interface AuthTexts {
|
|
|
482
538
|
keepCodes: string;
|
|
483
539
|
passkeys: string;
|
|
484
540
|
managePasskeys: string;
|
|
541
|
+
passkeysHelp: string;
|
|
542
|
+
noPasskeys: string;
|
|
543
|
+
passkeyName: string;
|
|
544
|
+
addPasskey: string;
|
|
545
|
+
removePasskey: string;
|
|
546
|
+
passkeyAdded: string;
|
|
547
|
+
passkeyUnnamed: string;
|
|
485
548
|
sessions: string;
|
|
486
549
|
signOutEverywhere: string;
|
|
487
550
|
unknownDevice: string;
|
|
@@ -622,4 +685,4 @@ declare function useSession(): SessionContext;
|
|
|
622
685
|
*/
|
|
623
686
|
declare function useAuth(): AuthClient & Pick<SessionContext, 'session' | 'status' | 'refresh'>;
|
|
624
687
|
|
|
625
|
-
export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type LinkedIdentity, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, createAuthClient, definePolicy, useAuth, useSession };
|
|
688
|
+
export { Account, type AccountClient, type Account$1 as AccountDetails, type AccountProps, type AccountSession, type AuthClient, type AuthError, type AuthFormProps, type AuthMethods, type AuthResult, type AuthTexts, CauthyProvider, type CauthyProviderProps, ForgotPassword, type JsonValue, type LinkedIdentity, type Passkey, type PermissionOf, type Policy, type PublicSession, ResetPassword, type RoleOf, type SessionContext, type SessionOrganization, type SessionUser, SignIn, SignUp, VerifyEmail, can, canUsePasskeys, createAuthClient, definePolicy, useAuth, useSession };
|
package/dist/react.js
CHANGED
|
@@ -29,6 +29,7 @@ __export(react_exports, {
|
|
|
29
29
|
SignUp: () => SignUp,
|
|
30
30
|
VerifyEmail: () => VerifyEmail,
|
|
31
31
|
can: () => can,
|
|
32
|
+
canUsePasskeys: () => canUsePasskeys,
|
|
32
33
|
createAuthClient: () => createAuthClient,
|
|
33
34
|
definePolicy: () => definePolicy,
|
|
34
35
|
useAuth: () => useAuth,
|
|
@@ -37,6 +38,73 @@ __export(react_exports, {
|
|
|
37
38
|
module.exports = __toCommonJS(react_exports);
|
|
38
39
|
var import_react3 = require("react");
|
|
39
40
|
|
|
41
|
+
// src/passkey.ts
|
|
42
|
+
function canUsePasskeys() {
|
|
43
|
+
return typeof window !== "undefined" && typeof window.PublicKeyCredential === "function" && typeof navigator !== "undefined" && typeof navigator.credentials?.get === "function";
|
|
44
|
+
}
|
|
45
|
+
function bytes(text2) {
|
|
46
|
+
const b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
|
|
47
|
+
const bin = atob(b64);
|
|
48
|
+
const out = new Uint8Array(bin.length);
|
|
49
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
50
|
+
return out.buffer;
|
|
51
|
+
}
|
|
52
|
+
function text(buf) {
|
|
53
|
+
if (!buf) return null;
|
|
54
|
+
const view = new Uint8Array(buf);
|
|
55
|
+
let bin = "";
|
|
56
|
+
for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]);
|
|
57
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
58
|
+
}
|
|
59
|
+
var withId = (list) => (list ?? []).map((c) => ({ ...c, id: bytes(c.id) }));
|
|
60
|
+
async function getPasskey(options) {
|
|
61
|
+
const publicKey = { ...options, challenge: bytes(options.challenge), allowCredentials: withId(options.allowCredentials) };
|
|
62
|
+
const cred = await navigator.credentials.get({ publicKey });
|
|
63
|
+
if (cred === null) throw new DOMException("No passkey was chosen.", "NotAllowedError");
|
|
64
|
+
const r = cred.response;
|
|
65
|
+
return {
|
|
66
|
+
id: cred.id,
|
|
67
|
+
rawId: text(cred.rawId),
|
|
68
|
+
type: cred.type,
|
|
69
|
+
response: {
|
|
70
|
+
authenticatorData: text(r.authenticatorData),
|
|
71
|
+
clientDataJSON: text(r.clientDataJSON),
|
|
72
|
+
signature: text(r.signature),
|
|
73
|
+
userHandle: text(r.userHandle)
|
|
74
|
+
},
|
|
75
|
+
clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function makePasskey(options) {
|
|
79
|
+
const publicKey = {
|
|
80
|
+
...options,
|
|
81
|
+
challenge: bytes(options.challenge),
|
|
82
|
+
user: { ...options.user, id: bytes(options.user.id) },
|
|
83
|
+
excludeCredentials: withId(options.excludeCredentials)
|
|
84
|
+
};
|
|
85
|
+
const cred = await navigator.credentials.create({ publicKey });
|
|
86
|
+
if (cred === null) throw new DOMException("No passkey was made.", "NotAllowedError");
|
|
87
|
+
const r = cred.response;
|
|
88
|
+
return {
|
|
89
|
+
id: cred.id,
|
|
90
|
+
rawId: text(cred.rawId),
|
|
91
|
+
type: cred.type,
|
|
92
|
+
response: {
|
|
93
|
+
clientDataJSON: text(r.clientDataJSON),
|
|
94
|
+
attestationObject: text(r.attestationObject),
|
|
95
|
+
transports: typeof r.getTransports === "function" ? r.getTransports() : []
|
|
96
|
+
},
|
|
97
|
+
clientExtensionResults: cred.getClientExtensionResults?.() ?? {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function ceremonyError(e) {
|
|
101
|
+
const name = e instanceof Error || e instanceof DOMException ? e.name : "";
|
|
102
|
+
if (name === "NotAllowedError" || name === "AbortError") return { code: "cancelled", message: "The passkey was not used. Try again when you are ready." };
|
|
103
|
+
if (name === "InvalidStateError") return { code: "passkey_exists", message: "This device already has a passkey for this account." };
|
|
104
|
+
if (name === "SecurityError") return { code: "passkey_unavailable", message: "Passkeys cannot be used on this page." };
|
|
105
|
+
return { code: "passkey_failed", message: "The passkey could not be used. Try again, or sign in another way." };
|
|
106
|
+
}
|
|
107
|
+
|
|
40
108
|
// src/theme.ts
|
|
41
109
|
function themeVariables(theme) {
|
|
42
110
|
const vars = {};
|
|
@@ -139,13 +207,17 @@ var UNAVAILABLE = { code: "unavailable", message: "Sign-in is unavailable right
|
|
|
139
207
|
function createAuthClient(options = {}) {
|
|
140
208
|
const base = (options.basePath ?? "/api/auth").replace(/\/+$/, "");
|
|
141
209
|
const doFetch = (...args) => (options.fetch ?? fetch)(...args);
|
|
210
|
+
const locale = () => {
|
|
211
|
+
const said = typeof options.locale === "function" ? options.locale() : options.locale;
|
|
212
|
+
return (said ?? (typeof document === "undefined" ? "" : document.documentElement?.lang) ?? "").trim();
|
|
213
|
+
};
|
|
142
214
|
async function post(path, form, bad) {
|
|
143
215
|
if (bad) return { ok: false, error: { code: bad.code, message: bad.message, field: bad.field } };
|
|
144
216
|
try {
|
|
145
217
|
const res = await doFetch(`${base}${path}`, {
|
|
146
218
|
method: "POST",
|
|
147
219
|
credentials: "same-origin",
|
|
148
|
-
headers: { "content-type": "application/json", accept: "application/json" },
|
|
220
|
+
headers: { "content-type": "application/json", accept: "application/json", ...locale() ? { "x-cauthy-locale": locale() } : {} },
|
|
149
221
|
body: JSON.stringify(form)
|
|
150
222
|
});
|
|
151
223
|
const body = await res.json().catch(() => null);
|
|
@@ -173,6 +245,28 @@ function createAuthClient(options = {}) {
|
|
|
173
245
|
if (o.locale) q.set("locale", o.locale);
|
|
174
246
|
return `${base}/login?${q}`;
|
|
175
247
|
};
|
|
248
|
+
async function methods() {
|
|
249
|
+
try {
|
|
250
|
+
const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
251
|
+
if (res.ok) {
|
|
252
|
+
const m = await res.json();
|
|
253
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false, ...m, passkeyHere: m.passkeyHere === true, passkeyOnCauthy: m.passkeyOnCauthy ?? m.passkey === true };
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
return { providers: [], signUp: true, magicLink: false, passkey: false, passkeyHere: false, passkeyOnCauthy: false };
|
|
258
|
+
}
|
|
259
|
+
async function ceremony(begin, finish, form, run, more = {}) {
|
|
260
|
+
const begun = await post(begin, form, null);
|
|
261
|
+
if (!begun.ok) return begun;
|
|
262
|
+
let credential;
|
|
263
|
+
try {
|
|
264
|
+
credential = await run(begun.options);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
return { ok: false, error: ceremonyError(e) };
|
|
267
|
+
}
|
|
268
|
+
return post(finish, { credential, ...more }, null);
|
|
269
|
+
}
|
|
176
270
|
return {
|
|
177
271
|
signIn: {
|
|
178
272
|
email: (f) => post("/sign-in/email", { email: f.email, password: f.password }, firstProblem(checkEmail(f.email), checkPassword(f.password))),
|
|
@@ -181,8 +275,16 @@ function createAuthClient(options = {}) {
|
|
|
181
275
|
magicLinkVerify: (f) => post("/sign-in/magic-link/verify", { token: f.token }, null),
|
|
182
276
|
emailCode: (f) => post("/sign-in/email-code", { code: f.code }, checkCode(f.code)),
|
|
183
277
|
passkeyUrl,
|
|
184
|
-
passkey(o) {
|
|
278
|
+
async passkey(o) {
|
|
279
|
+
const m = await methods();
|
|
280
|
+
if (m.passkeyHere && canUsePasskeys()) {
|
|
281
|
+
const r = await ceremony("/sign-in/passkey/begin", "/sign-in/passkey/finish", {}, getPasskey);
|
|
282
|
+
if (r.ok || r.error.code !== "passkey_unavailable" || !m.passkeyOnCauthy) return r;
|
|
283
|
+
} else if (!m.passkeyOnCauthy) {
|
|
284
|
+
return { ok: false, error: { code: "passkey_unavailable", message: "Passkeys cannot be used here." } };
|
|
285
|
+
}
|
|
185
286
|
window.location.assign(passkeyUrl(o));
|
|
287
|
+
return { ok: true, status: "redirected" };
|
|
186
288
|
},
|
|
187
289
|
socialUrl,
|
|
188
290
|
social(provider, o) {
|
|
@@ -226,7 +328,16 @@ function createAuthClient(options = {}) {
|
|
|
226
328
|
const body = await read("/me/sessions");
|
|
227
329
|
return body && Array.isArray(body.sessions) ? body.sessions : [];
|
|
228
330
|
},
|
|
229
|
-
signOutEverywhere: () => post("/me/sessions/end", {}, null)
|
|
331
|
+
signOutEverywhere: () => post("/me/sessions/end", {}, null),
|
|
332
|
+
async passkeys() {
|
|
333
|
+
const body = await read("/me/passkeys");
|
|
334
|
+
return body && Array.isArray(body.passkeys) ? body.passkeys : [];
|
|
335
|
+
},
|
|
336
|
+
addPasskey(f = {}) {
|
|
337
|
+
if (!canUsePasskeys()) return Promise.resolve({ ok: false, error: { code: "passkey_unavailable", message: "This browser cannot make passkeys." } });
|
|
338
|
+
return ceremony("/me/passkeys/add/begin", "/me/passkeys/add/finish", { password: f.password ?? "" }, makePasskey, { name: f.name ?? "" });
|
|
339
|
+
},
|
|
340
|
+
removePasskey: (id) => post("/me/passkeys/remove", { id }, null)
|
|
230
341
|
},
|
|
231
342
|
async getSession() {
|
|
232
343
|
try {
|
|
@@ -241,14 +352,7 @@ function createAuthClient(options = {}) {
|
|
|
241
352
|
looked ??= read("/look").then((wire) => wire && typeof wire === "object" ? fromWire(wire) : null);
|
|
242
353
|
return looked;
|
|
243
354
|
},
|
|
244
|
-
|
|
245
|
-
try {
|
|
246
|
-
const res = await doFetch(`${base}/methods`, { credentials: "same-origin", headers: { accept: "application/json" } });
|
|
247
|
-
if (res.ok) return await res.json();
|
|
248
|
-
} catch {
|
|
249
|
-
}
|
|
250
|
-
return { providers: [], signUp: true, magicLink: false, passkey: false };
|
|
251
|
-
}
|
|
355
|
+
methods
|
|
252
356
|
};
|
|
253
357
|
}
|
|
254
358
|
|
|
@@ -355,6 +459,13 @@ var TEXTS = {
|
|
|
355
459
|
keepCodes: "Keep these codes somewhere safe. Each works once, and they are not shown again.",
|
|
356
460
|
passkeys: "Passkeys",
|
|
357
461
|
managePasskeys: "Add or remove a passkey",
|
|
462
|
+
passkeysHelp: "Sign in with your fingerprint, your face or your device's lock, without a password.",
|
|
463
|
+
noPasskeys: "No passkey yet.",
|
|
464
|
+
passkeyName: "A name for this device (optional)",
|
|
465
|
+
addPasskey: "Add a passkey on this device",
|
|
466
|
+
removePasskey: "Remove",
|
|
467
|
+
passkeyAdded: "The passkey was added. Next time, sign in with it.",
|
|
468
|
+
passkeyUnnamed: "Passkey",
|
|
358
469
|
sessions: "Where you are signed in",
|
|
359
470
|
signOutEverywhere: "Sign out everywhere",
|
|
360
471
|
unknownDevice: "Unknown device",
|
|
@@ -385,7 +496,10 @@ var TEXTS = {
|
|
|
385
496
|
wrong_password: "The current password is not right.",
|
|
386
497
|
step_up_required: "Enter your current password.",
|
|
387
498
|
weak_password: "Choose a stronger password.",
|
|
388
|
-
unauthenticated: "Sign in first."
|
|
499
|
+
unauthenticated: "Sign in first.",
|
|
500
|
+
passkey_failed: "The passkey could not be used. Try again, or sign in another way.",
|
|
501
|
+
passkey_exists: "This device already has a passkey for your account.",
|
|
502
|
+
passkey_unavailable: "Passkeys cannot be used here."
|
|
389
503
|
}
|
|
390
504
|
},
|
|
391
505
|
es: {
|
|
@@ -434,6 +548,13 @@ var TEXTS = {
|
|
|
434
548
|
keepCodes: "Guarda estos c\xF3digos en un lugar seguro. Cada uno sirve una vez y no se vuelven a mostrar.",
|
|
435
549
|
passkeys: "Llaves de acceso",
|
|
436
550
|
managePasskeys: "Agregar o quitar una llave de acceso",
|
|
551
|
+
passkeysHelp: "Entra con tu huella, tu rostro o el bloqueo de tu dispositivo, sin contrase\xF1a.",
|
|
552
|
+
noPasskeys: "Todav\xEDa no tienes llaves de acceso.",
|
|
553
|
+
passkeyName: "Un nombre para este dispositivo (opcional)",
|
|
554
|
+
addPasskey: "Agregar una llave en este dispositivo",
|
|
555
|
+
removePasskey: "Quitar",
|
|
556
|
+
passkeyAdded: "La llave de acceso qued\xF3 agregada. La pr\xF3xima vez, entra con ella.",
|
|
557
|
+
passkeyUnnamed: "Llave de acceso",
|
|
437
558
|
sessions: "D\xF3nde tienes la sesi\xF3n abierta",
|
|
438
559
|
signOutEverywhere: "Cerrar sesi\xF3n en todas partes",
|
|
439
560
|
unknownDevice: "Dispositivo desconocido",
|
|
@@ -464,7 +585,10 @@ var TEXTS = {
|
|
|
464
585
|
wrong_password: "La contrase\xF1a actual no es correcta.",
|
|
465
586
|
step_up_required: "Escribe tu contrase\xF1a actual.",
|
|
466
587
|
weak_password: "Elige una contrase\xF1a m\xE1s fuerte.",
|
|
467
|
-
unauthenticated: "Inicia sesi\xF3n primero."
|
|
588
|
+
unauthenticated: "Inicia sesi\xF3n primero.",
|
|
589
|
+
passkey_failed: "No se pudo usar la llave de acceso. Intenta de nuevo o entra de otra forma.",
|
|
590
|
+
passkey_exists: "Este dispositivo ya tiene una llave de acceso de tu cuenta.",
|
|
591
|
+
passkey_unavailable: "Aqu\xED no se pueden usar llaves de acceso."
|
|
468
592
|
}
|
|
469
593
|
}
|
|
470
594
|
};
|
|
@@ -602,6 +726,15 @@ function SignIn(props) {
|
|
|
602
726
|
if (r.status === "mfa_required") return setStep("code");
|
|
603
727
|
f.done();
|
|
604
728
|
};
|
|
729
|
+
const usePasskey = async () => {
|
|
730
|
+
f.setBusy(true);
|
|
731
|
+
f.setError(null);
|
|
732
|
+
const r = await auth.signIn.passkey({ returnTo: props.returnTo, locale: props.locale });
|
|
733
|
+
if (r.ok && r.status === "redirected") return;
|
|
734
|
+
f.setBusy(false);
|
|
735
|
+
if (!r.ok) return r.error.code === "cancelled" ? void 0 : f.setError(r.error);
|
|
736
|
+
f.done();
|
|
737
|
+
};
|
|
605
738
|
const sendLink = async () => {
|
|
606
739
|
f.setBusy(true);
|
|
607
740
|
f.setError(null);
|
|
@@ -636,7 +769,7 @@ function SignIn(props) {
|
|
|
636
769
|
" ",
|
|
637
770
|
p.name
|
|
638
771
|
] }, p.id)),
|
|
639
|
-
methods?.passkey && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", className: f.cx("providerButton"), onClick: () =>
|
|
772
|
+
methods?.passkey && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void usePasskey(), children: f.t.usePasskey }),
|
|
640
773
|
((methods?.providers.length ?? 0) > 0 || methods?.passkey) && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("separator"), children: f.t.orWithEmail }),
|
|
641
774
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "email", type: "email", label: f.t.email, value: email, autoComplete: "username", required: true, onChange: setEmail }),
|
|
642
775
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -822,10 +955,7 @@ function Account(props) {
|
|
|
822
955
|
account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordPart, { props }),
|
|
823
956
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(LinkedPart, { props }),
|
|
824
957
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecondStepPart, { props, account, onChanged: (mfaEnabled) => setAccount({ ...account, mfaEnabled }) }),
|
|
825
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
826
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.passkeys }),
|
|
827
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: cx("link"), href: accountUrl(), children: t.managePasskeys })
|
|
828
|
-
] }),
|
|
958
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasskeysPart, { props, account, accountUrl: accountUrl(), when }),
|
|
829
959
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: cx("section"), children: [
|
|
830
960
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: cx("heading"), children: t.sessions }),
|
|
831
961
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: cx("sessions"), children: sessions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: cx("session"), children: [
|
|
@@ -839,6 +969,60 @@ function Account(props) {
|
|
|
839
969
|
] })
|
|
840
970
|
] });
|
|
841
971
|
}
|
|
972
|
+
function PasskeysPart({ props, account, accountUrl, when }) {
|
|
973
|
+
const auth = useAuth();
|
|
974
|
+
const { f, done, setDone, run, general } = useSection(props);
|
|
975
|
+
const [here, setHere] = (0, import_react.useState)(null);
|
|
976
|
+
const [list, setList] = (0, import_react.useState)([]);
|
|
977
|
+
const [name, setName] = (0, import_react.useState)("");
|
|
978
|
+
const [password, setPassword] = (0, import_react.useState)("");
|
|
979
|
+
(0, import_react.useEffect)(() => {
|
|
980
|
+
void auth.methods().then((m) => {
|
|
981
|
+
const can2 = m.passkeyHere && canUsePasskeys();
|
|
982
|
+
setHere(can2);
|
|
983
|
+
if (can2) void auth.account.passkeys().then(setList);
|
|
984
|
+
});
|
|
985
|
+
}, []);
|
|
986
|
+
if (here === null) return null;
|
|
987
|
+
if (!here) {
|
|
988
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: f.cx("section"), children: [
|
|
989
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.passkeys }),
|
|
990
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { className: f.cx("link"), href: accountUrl, children: f.t.managePasskeys })
|
|
991
|
+
] });
|
|
992
|
+
}
|
|
993
|
+
const add = async (e) => {
|
|
994
|
+
e.preventDefault();
|
|
995
|
+
const r = await run(() => auth.account.addPasskey({ name, password }));
|
|
996
|
+
if (!r.ok) {
|
|
997
|
+
if (r.error.code === "cancelled") f.setError(null);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
setList([...list, r.passkey]);
|
|
1001
|
+
setName("");
|
|
1002
|
+
setPassword("");
|
|
1003
|
+
setDone(f.t.passkeyAdded);
|
|
1004
|
+
};
|
|
1005
|
+
const remove = async (id) => {
|
|
1006
|
+
const r = await run(() => auth.account.removePasskey(id));
|
|
1007
|
+
if (r.ok) setList(list.filter((p) => p.id !== id));
|
|
1008
|
+
};
|
|
1009
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: f.cx("section"), onSubmit: add, noValidate: true, children: [
|
|
1010
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: f.cx("heading"), children: f.t.passkeys }),
|
|
1011
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.passkeysHelp }),
|
|
1012
|
+
general && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "alert", className: f.cx("error"), children: f.say(general) }),
|
|
1013
|
+
list.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: f.cx("notice"), children: f.t.noPasskeys }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: f.cx("sessions"), children: list.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { className: f.cx("session"), children: [
|
|
1014
|
+
p.name || f.t.passkeyUnnamed,
|
|
1015
|
+
" \xB7 ",
|
|
1016
|
+
when(p.createdAt),
|
|
1017
|
+
" ",
|
|
1018
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "button", disabled: f.busy, className: f.cx("providerButton"), onClick: () => void remove(p.id), children: f.t.removePasskey })
|
|
1019
|
+
] }, p.id)) }),
|
|
1020
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "name", type: "text", label: f.t.passkeyName, value: name, autoComplete: "off", onChange: setName }),
|
|
1021
|
+
account.hasPassword && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, { f, name: "password", type: "password", label: f.t.currentPassword, value: password, autoComplete: "current-password", required: true, onChange: setPassword }),
|
|
1022
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(f.Button, { type: "submit", disabled: f.busy, className: f.cx("button"), children: f.t.addPasskey }),
|
|
1023
|
+
done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { role: "status", className: f.cx("notice"), children: done })
|
|
1024
|
+
] });
|
|
1025
|
+
}
|
|
842
1026
|
function ProfilePart({ props, account, onSaved }) {
|
|
843
1027
|
const auth = useAuth();
|
|
844
1028
|
const { f, done, setDone, run, general } = useSection(props);
|
|
@@ -1128,6 +1312,7 @@ function useAuth() {
|
|
|
1128
1312
|
SignUp,
|
|
1129
1313
|
VerifyEmail,
|
|
1130
1314
|
can,
|
|
1315
|
+
canUsePasskeys,
|
|
1131
1316
|
createAuthClient,
|
|
1132
1317
|
definePolicy,
|
|
1133
1318
|
useAuth,
|