@robelest/convex-auth 0.0.2-preview.2 → 0.0.3-preview
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/bin.cjs +467 -64
- package/dist/client/index.d.ts +127 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +424 -1
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +56 -1
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +141 -3
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +2 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/index.d.ts +5 -4
- package/dist/component/index.d.ts.map +1 -1
- package/dist/component/index.js +4 -3
- package/dist/component/index.js.map +1 -1
- package/dist/component/portalBridge.d.ts +80 -0
- package/dist/component/portalBridge.d.ts.map +1 -0
- package/dist/component/portalBridge.js +102 -0
- package/dist/component/portalBridge.js.map +1 -0
- package/dist/component/public.d.ts +353 -9
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +328 -33
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +168 -9
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +113 -7
- package/dist/component/schema.js.map +1 -1
- package/dist/providers/passkey.d.ts +20 -0
- package/dist/providers/passkey.d.ts.map +1 -0
- package/dist/providers/passkey.js +32 -0
- package/dist/providers/passkey.js.map +1 -0
- package/dist/providers/totp.d.ts +14 -0
- package/dist/providers/totp.d.ts.map +1 -0
- package/dist/providers/totp.js +23 -0
- package/dist/providers/totp.js.map +1 -0
- package/dist/server/convex-auth.d.ts +296 -0
- package/dist/server/convex-auth.d.ts.map +1 -0
- package/dist/server/convex-auth.js +480 -0
- package/dist/server/convex-auth.js.map +1 -0
- package/dist/server/email-templates.d.ts +18 -0
- package/dist/server/email-templates.d.ts.map +1 -0
- package/dist/server/email-templates.js +74 -0
- package/dist/server/email-templates.js.map +1 -0
- package/dist/server/implementation/apiKey.d.ts +74 -0
- package/dist/server/implementation/apiKey.d.ts.map +1 -0
- package/dist/server/implementation/apiKey.js +140 -0
- package/dist/server/implementation/apiKey.js.map +1 -0
- package/dist/server/implementation/index.d.ts +169 -7
- package/dist/server/implementation/index.d.ts.map +1 -1
- package/dist/server/implementation/index.js +220 -5
- package/dist/server/implementation/index.js.map +1 -1
- package/dist/server/implementation/passkey.d.ts +33 -0
- package/dist/server/implementation/passkey.d.ts.map +1 -0
- package/dist/server/implementation/passkey.js +450 -0
- package/dist/server/implementation/passkey.js.map +1 -0
- package/dist/server/implementation/redirects.d.ts.map +1 -1
- package/dist/server/implementation/redirects.js +4 -9
- package/dist/server/implementation/redirects.js.map +1 -1
- package/dist/server/implementation/signIn.d.ts +13 -0
- package/dist/server/implementation/signIn.d.ts.map +1 -1
- package/dist/server/implementation/signIn.js +29 -15
- package/dist/server/implementation/signIn.js.map +1 -1
- package/dist/server/implementation/totp.d.ts +40 -0
- package/dist/server/implementation/totp.d.ts.map +1 -0
- package/dist/server/implementation/totp.js +211 -0
- package/dist/server/implementation/totp.js.map +1 -0
- package/dist/server/index.d.ts +26 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +63 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/portal-email.d.ts +19 -0
- package/dist/server/portal-email.d.ts.map +1 -0
- package/dist/server/portal-email.js +89 -0
- package/dist/server/portal-email.js.map +1 -0
- package/dist/server/provider_utils.d.ts +3 -1
- package/dist/server/provider_utils.d.ts.map +1 -1
- package/dist/server/provider_utils.js +39 -1
- package/dist/server/provider_utils.js.map +1 -1
- package/dist/server/types.d.ts +263 -4
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.d.ts.map +1 -0
- package/dist/server/version.js +3 -0
- package/dist/server/version.js.map +1 -0
- package/package.json +7 -3
- package/src/cli/index.ts +49 -7
- package/src/cli/portal-link.ts +112 -0
- package/src/cli/portal-upload.ts +411 -0
- package/src/cli/utils.ts +248 -0
- package/src/client/index.ts +489 -1
- package/src/component/_generated/api.ts +72 -1
- package/src/component/_generated/component.ts +241 -4
- package/src/component/convex.config.ts +3 -0
- package/src/component/index.ts +8 -3
- package/src/component/portalBridge.ts +116 -0
- package/src/component/public.ts +373 -37
- package/src/component/schema.ts +122 -7
- package/src/providers/passkey.ts +35 -0
- package/src/providers/totp.ts +26 -0
- package/src/server/convex-auth.ts +602 -0
- package/src/server/email-templates.ts +77 -0
- package/src/server/implementation/apiKey.ts +185 -0
- package/src/server/implementation/index.ts +301 -8
- package/src/server/implementation/passkey.ts +650 -0
- package/src/server/implementation/redirects.ts +4 -11
- package/src/server/implementation/signIn.ts +41 -13
- package/src/server/implementation/totp.ts +366 -0
- package/src/server/index.ts +98 -34
- package/src/server/portal-email.ts +95 -0
- package/src/server/provider_utils.ts +42 -1
- package/src/server/types.ts +285 -4
- package/src/server/version.ts +2 -0
package/src/client/index.ts
CHANGED
|
@@ -34,6 +34,8 @@ type AuthSession = {
|
|
|
34
34
|
type SignInResult = {
|
|
35
35
|
signingIn: boolean;
|
|
36
36
|
redirect?: URL;
|
|
37
|
+
totpRequired?: boolean;
|
|
38
|
+
verifier?: string;
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
/** Reactive auth state snapshot returned by `auth.state` and `auth.onChange`. */
|
|
@@ -241,6 +243,13 @@ export function client(options: ClientOptions) {
|
|
|
241
243
|
isLoading = false;
|
|
242
244
|
const changed = updateSnapshot();
|
|
243
245
|
if (hadPendingLoad || changed) {
|
|
246
|
+
// Re-sync the Convex client so it picks up the new token immediately.
|
|
247
|
+
// Without this, the initial convex.setAuth(fetchAccessToken) from
|
|
248
|
+
// initialization never re-polls and queries run unauthenticated after
|
|
249
|
+
// magic link code exchange.
|
|
250
|
+
if (!proxy) {
|
|
251
|
+
convex.setAuth(fetchAccessToken);
|
|
252
|
+
}
|
|
244
253
|
notify();
|
|
245
254
|
}
|
|
246
255
|
};
|
|
@@ -339,6 +348,9 @@ export function client(options: ClientOptions) {
|
|
|
339
348
|
}
|
|
340
349
|
return { signingIn: false, redirect: redirectUrl };
|
|
341
350
|
}
|
|
351
|
+
if (result.totpRequired) {
|
|
352
|
+
return { signingIn: false, totpRequired: true, verifier: result.verifier };
|
|
353
|
+
}
|
|
342
354
|
if (result.tokens !== undefined) {
|
|
343
355
|
// Proxy returns { token, refreshToken: "dummy" }.
|
|
344
356
|
// Store JWT in memory only — real refresh token is in httpOnly cookie.
|
|
@@ -368,6 +380,9 @@ export function client(options: ClientOptions) {
|
|
|
368
380
|
}
|
|
369
381
|
return { signingIn: false, redirect: redirectUrl };
|
|
370
382
|
}
|
|
383
|
+
if (result.totpRequired) {
|
|
384
|
+
return { signingIn: false, totpRequired: true, verifier: result.verifier };
|
|
385
|
+
}
|
|
371
386
|
if (result.tokens !== undefined) {
|
|
372
387
|
await setToken({
|
|
373
388
|
shouldStore: true,
|
|
@@ -547,10 +562,479 @@ export function client(options: ClientOptions) {
|
|
|
547
562
|
}
|
|
548
563
|
} else {
|
|
549
564
|
// SPA mode: hydrate from localStorage, then handle OAuth code flow.
|
|
550
|
-
void hydrateFromStorage().then(() =>
|
|
565
|
+
void hydrateFromStorage().then(() =>
|
|
566
|
+
handleCodeFlow().catch((error: unknown) => {
|
|
567
|
+
console.error("[convex-auth] Code exchange failed:", error);
|
|
568
|
+
}),
|
|
569
|
+
);
|
|
551
570
|
}
|
|
552
571
|
}
|
|
553
572
|
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Passkey helpers
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Base64url encode/decode helpers for the WebAuthn credential API.
|
|
579
|
+
* These run client-side only (browser context).
|
|
580
|
+
*/
|
|
581
|
+
const base64urlEncode = (buffer: ArrayBuffer): string => {
|
|
582
|
+
const bytes = new Uint8Array(buffer);
|
|
583
|
+
let binary = "";
|
|
584
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
585
|
+
binary += String.fromCharCode(bytes[i]!);
|
|
586
|
+
}
|
|
587
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const base64urlDecode = (str: string): Uint8Array => {
|
|
591
|
+
const padded = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
592
|
+
const binary = atob(padded);
|
|
593
|
+
const bytes = new Uint8Array(binary.length);
|
|
594
|
+
for (let i = 0; i < binary.length; i++) {
|
|
595
|
+
bytes[i] = binary.charCodeAt(i);
|
|
596
|
+
}
|
|
597
|
+
return bytes;
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const passkey = {
|
|
601
|
+
/**
|
|
602
|
+
* Check if WebAuthn passkeys are supported in the current environment.
|
|
603
|
+
*/
|
|
604
|
+
isSupported: (): boolean => {
|
|
605
|
+
return (
|
|
606
|
+
typeof window !== "undefined" &&
|
|
607
|
+
typeof window.PublicKeyCredential !== "undefined"
|
|
608
|
+
);
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Check if conditional UI (autofill-assisted passkey sign-in) is supported.
|
|
613
|
+
*
|
|
614
|
+
* ```ts
|
|
615
|
+
* if (await auth.passkey.isAutofillSupported()) {
|
|
616
|
+
* auth.passkey.authenticate({ autofill: true });
|
|
617
|
+
* }
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
620
|
+
isAutofillSupported: async (): Promise<boolean> => {
|
|
621
|
+
if (typeof window === "undefined") return false;
|
|
622
|
+
if (typeof window.PublicKeyCredential === "undefined") return false;
|
|
623
|
+
if (
|
|
624
|
+
typeof (
|
|
625
|
+
window.PublicKeyCredential as any
|
|
626
|
+
).isConditionalMediationAvailable !== "function"
|
|
627
|
+
) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
return (
|
|
631
|
+
window.PublicKeyCredential as any
|
|
632
|
+
).isConditionalMediationAvailable();
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Register a new passkey for the current or new user.
|
|
637
|
+
*
|
|
638
|
+
* Performs the full two-round-trip WebAuthn registration ceremony:
|
|
639
|
+
* 1. Requests creation options from the server (challenge, RP info)
|
|
640
|
+
* 2. Calls `navigator.credentials.create()` with the options
|
|
641
|
+
* 3. Sends the attestation back to the server for verification
|
|
642
|
+
* 4. Server creates user + account + passkey records and returns tokens
|
|
643
|
+
*
|
|
644
|
+
* Works in both SPA and proxy (SSR) modes.
|
|
645
|
+
*
|
|
646
|
+
* ```ts
|
|
647
|
+
* await auth.passkey.register({ name: "MacBook Touch ID" });
|
|
648
|
+
* ```
|
|
649
|
+
*
|
|
650
|
+
* @param opts.name - Friendly name for this passkey
|
|
651
|
+
* @param opts.email - Email to associate with the new account
|
|
652
|
+
* @param opts.userName - Username for the credential (defaults to email)
|
|
653
|
+
* @param opts.userDisplayName - Display name for the credential
|
|
654
|
+
* @returns `{ signingIn: true }` on success
|
|
655
|
+
*/
|
|
656
|
+
register: async (
|
|
657
|
+
opts?: {
|
|
658
|
+
name?: string;
|
|
659
|
+
email?: string;
|
|
660
|
+
userName?: string;
|
|
661
|
+
userDisplayName?: string;
|
|
662
|
+
},
|
|
663
|
+
): Promise<SignInResult> => {
|
|
664
|
+
const phase1Params = {
|
|
665
|
+
flow: "register-options",
|
|
666
|
+
email: opts?.email,
|
|
667
|
+
userName: opts?.userName,
|
|
668
|
+
userDisplayName: opts?.userDisplayName,
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// Phase 1: Get registration options from server
|
|
672
|
+
let phase1Result: any;
|
|
673
|
+
if (proxy) {
|
|
674
|
+
phase1Result = await proxyFetch({
|
|
675
|
+
action: "auth:signIn",
|
|
676
|
+
args: { provider: "passkey", params: phase1Params },
|
|
677
|
+
});
|
|
678
|
+
} else {
|
|
679
|
+
phase1Result = await convex.action("auth:signIn" as any, {
|
|
680
|
+
provider: "passkey",
|
|
681
|
+
params: phase1Params,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!phase1Result.options) {
|
|
686
|
+
throw new Error("Server did not return passkey registration options");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const options = phase1Result.options;
|
|
690
|
+
|
|
691
|
+
// Convert base64url strings to ArrayBuffers for the credential API
|
|
692
|
+
const createOptions: CredentialCreationOptions = {
|
|
693
|
+
publicKey: {
|
|
694
|
+
rp: options.rp,
|
|
695
|
+
user: {
|
|
696
|
+
id: base64urlDecode(options.user.id).buffer as ArrayBuffer,
|
|
697
|
+
name: options.user.name,
|
|
698
|
+
displayName: options.user.displayName,
|
|
699
|
+
},
|
|
700
|
+
challenge: base64urlDecode(options.challenge).buffer as ArrayBuffer,
|
|
701
|
+
pubKeyCredParams: options.pubKeyCredParams,
|
|
702
|
+
timeout: options.timeout,
|
|
703
|
+
attestation: options.attestation,
|
|
704
|
+
authenticatorSelection: options.authenticatorSelection,
|
|
705
|
+
excludeCredentials: (options.excludeCredentials ?? []).map(
|
|
706
|
+
(cred: any) => ({
|
|
707
|
+
type: cred.type ?? "public-key",
|
|
708
|
+
id: base64urlDecode(cred.id).buffer as ArrayBuffer,
|
|
709
|
+
transports: cred.transports,
|
|
710
|
+
}),
|
|
711
|
+
),
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// Phase 2: Create credential via browser API
|
|
716
|
+
const credential = (await navigator.credentials.create(
|
|
717
|
+
createOptions,
|
|
718
|
+
)) as PublicKeyCredential | null;
|
|
719
|
+
if (!credential) {
|
|
720
|
+
throw new Error("Passkey registration was cancelled");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const response =
|
|
724
|
+
credential.response as AuthenticatorAttestationResponse;
|
|
725
|
+
|
|
726
|
+
// Extract transports if available
|
|
727
|
+
const transports =
|
|
728
|
+
typeof response.getTransports === "function"
|
|
729
|
+
? response.getTransports()
|
|
730
|
+
: undefined;
|
|
731
|
+
|
|
732
|
+
const phase2Params = {
|
|
733
|
+
flow: "register-verify",
|
|
734
|
+
clientDataJSON: base64urlEncode(response.clientDataJSON),
|
|
735
|
+
attestationObject: base64urlEncode(response.attestationObject),
|
|
736
|
+
transports,
|
|
737
|
+
passkeyName: opts?.name,
|
|
738
|
+
email: opts?.email,
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
// Phase 3: Send attestation to server for verification
|
|
742
|
+
let phase2Result: any;
|
|
743
|
+
if (proxy) {
|
|
744
|
+
// In proxy mode the verifier is stored in an httpOnly cookie by the proxy.
|
|
745
|
+
// We pass it back explicitly so the proxy can forward it to Convex.
|
|
746
|
+
phase2Result = await proxyFetch({
|
|
747
|
+
action: "auth:signIn",
|
|
748
|
+
args: {
|
|
749
|
+
provider: "passkey",
|
|
750
|
+
params: phase2Params,
|
|
751
|
+
verifier: phase1Result.verifier,
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
} else {
|
|
755
|
+
phase2Result = await convex.action("auth:signIn" as any, {
|
|
756
|
+
provider: "passkey",
|
|
757
|
+
params: phase2Params,
|
|
758
|
+
verifier: phase1Result.verifier,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (phase2Result.tokens) {
|
|
763
|
+
if (proxy) {
|
|
764
|
+
await setToken({
|
|
765
|
+
shouldStore: false,
|
|
766
|
+
tokens:
|
|
767
|
+
phase2Result.tokens === null
|
|
768
|
+
? null
|
|
769
|
+
: { token: phase2Result.tokens.token },
|
|
770
|
+
});
|
|
771
|
+
} else {
|
|
772
|
+
await setToken({
|
|
773
|
+
shouldStore: true,
|
|
774
|
+
tokens: phase2Result.tokens as AuthSession,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
return { signingIn: true };
|
|
778
|
+
}
|
|
779
|
+
return { signingIn: false };
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Authenticate with an existing passkey.
|
|
784
|
+
*
|
|
785
|
+
* Performs the full two-round-trip WebAuthn authentication ceremony:
|
|
786
|
+
* 1. Requests assertion options from the server (challenge, allowed credentials)
|
|
787
|
+
* 2. Calls `navigator.credentials.get()` with the options
|
|
788
|
+
* 3. Sends the assertion back to the server for signature verification
|
|
789
|
+
* 4. Server verifies signature, updates counter, creates session, returns tokens
|
|
790
|
+
*
|
|
791
|
+
* Works in both SPA and proxy (SSR) modes.
|
|
792
|
+
*
|
|
793
|
+
* ```ts
|
|
794
|
+
* // Discoverable credential (no email needed)
|
|
795
|
+
* await auth.passkey.authenticate();
|
|
796
|
+
*
|
|
797
|
+
* // Scoped to a specific user's credentials
|
|
798
|
+
* await auth.passkey.authenticate({ email: "user@example.com" });
|
|
799
|
+
*
|
|
800
|
+
* // Autofill-assisted (conditional UI)
|
|
801
|
+
* await auth.passkey.authenticate({ autofill: true });
|
|
802
|
+
* ```
|
|
803
|
+
*
|
|
804
|
+
* @param opts.email - Scope to credentials for this email's user
|
|
805
|
+
* @param opts.autofill - Use conditional mediation (autofill UI)
|
|
806
|
+
* @returns `{ signingIn: true }` on success
|
|
807
|
+
*/
|
|
808
|
+
authenticate: async (
|
|
809
|
+
opts?: { email?: string; autofill?: boolean },
|
|
810
|
+
): Promise<SignInResult> => {
|
|
811
|
+
const phase1Params = {
|
|
812
|
+
flow: "auth-options",
|
|
813
|
+
email: opts?.email,
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
// Phase 1: Get assertion options from server
|
|
817
|
+
let phase1Result: any;
|
|
818
|
+
if (proxy) {
|
|
819
|
+
phase1Result = await proxyFetch({
|
|
820
|
+
action: "auth:signIn",
|
|
821
|
+
args: { provider: "passkey", params: phase1Params },
|
|
822
|
+
});
|
|
823
|
+
} else {
|
|
824
|
+
phase1Result = await convex.action("auth:signIn" as any, {
|
|
825
|
+
provider: "passkey",
|
|
826
|
+
params: phase1Params,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!phase1Result.options) {
|
|
831
|
+
throw new Error("Server did not return passkey authentication options");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const options = phase1Result.options;
|
|
835
|
+
|
|
836
|
+
// Convert base64url strings to ArrayBuffers for the credential API
|
|
837
|
+
const getOptions: CredentialRequestOptions = {
|
|
838
|
+
publicKey: {
|
|
839
|
+
challenge: base64urlDecode(options.challenge).buffer as ArrayBuffer,
|
|
840
|
+
timeout: options.timeout,
|
|
841
|
+
rpId: options.rpId,
|
|
842
|
+
userVerification: options.userVerification,
|
|
843
|
+
allowCredentials: (options.allowCredentials ?? []).map(
|
|
844
|
+
(cred: any) => ({
|
|
845
|
+
type: cred.type ?? "public-key",
|
|
846
|
+
id: base64urlDecode(cred.id).buffer as ArrayBuffer,
|
|
847
|
+
transports: cred.transports,
|
|
848
|
+
}),
|
|
849
|
+
),
|
|
850
|
+
},
|
|
851
|
+
...(opts?.autofill ? { mediation: "conditional" as any } : {}),
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// Phase 2: Get credential via browser API
|
|
855
|
+
const credential = (await navigator.credentials.get(
|
|
856
|
+
getOptions,
|
|
857
|
+
)) as PublicKeyCredential | null;
|
|
858
|
+
if (!credential) {
|
|
859
|
+
throw new Error("Passkey authentication was cancelled");
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const response =
|
|
863
|
+
credential.response as AuthenticatorAssertionResponse;
|
|
864
|
+
|
|
865
|
+
const phase2Params = {
|
|
866
|
+
flow: "auth-verify",
|
|
867
|
+
credentialId: base64urlEncode(credential.rawId),
|
|
868
|
+
clientDataJSON: base64urlEncode(response.clientDataJSON),
|
|
869
|
+
authenticatorData: base64urlEncode(response.authenticatorData),
|
|
870
|
+
signature: base64urlEncode(response.signature),
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
// Phase 3: Send assertion to server for verification
|
|
874
|
+
let phase2Result: any;
|
|
875
|
+
if (proxy) {
|
|
876
|
+
phase2Result = await proxyFetch({
|
|
877
|
+
action: "auth:signIn",
|
|
878
|
+
args: {
|
|
879
|
+
provider: "passkey",
|
|
880
|
+
params: phase2Params,
|
|
881
|
+
verifier: phase1Result.verifier,
|
|
882
|
+
},
|
|
883
|
+
});
|
|
884
|
+
} else {
|
|
885
|
+
phase2Result = await convex.action("auth:signIn" as any, {
|
|
886
|
+
provider: "passkey",
|
|
887
|
+
params: phase2Params,
|
|
888
|
+
verifier: phase1Result.verifier,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
if (phase2Result.tokens) {
|
|
893
|
+
if (proxy) {
|
|
894
|
+
await setToken({
|
|
895
|
+
shouldStore: false,
|
|
896
|
+
tokens:
|
|
897
|
+
phase2Result.tokens === null
|
|
898
|
+
? null
|
|
899
|
+
: { token: phase2Result.tokens.token },
|
|
900
|
+
});
|
|
901
|
+
} else {
|
|
902
|
+
await setToken({
|
|
903
|
+
shouldStore: true,
|
|
904
|
+
tokens: phase2Result.tokens as AuthSession,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
return { signingIn: true };
|
|
908
|
+
}
|
|
909
|
+
return { signingIn: false };
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const totp = {
|
|
914
|
+
/**
|
|
915
|
+
* Start TOTP enrollment. Must be authenticated.
|
|
916
|
+
*
|
|
917
|
+
* Returns a URI for QR code display and a base32 secret for manual entry.
|
|
918
|
+
*
|
|
919
|
+
* ```ts
|
|
920
|
+
* const setup = await auth.totp.setup();
|
|
921
|
+
* // Display QR code from setup.uri
|
|
922
|
+
* // Or show setup.secret for manual entry
|
|
923
|
+
* ```
|
|
924
|
+
*/
|
|
925
|
+
setup: async (
|
|
926
|
+
opts?: { name?: string; accountName?: string },
|
|
927
|
+
): Promise<{ uri: string; secret: string; verifier: string; totpId: string }> => {
|
|
928
|
+
const params: Record<string, any> = { flow: "setup" };
|
|
929
|
+
if (opts?.name) params.name = opts.name;
|
|
930
|
+
if (opts?.accountName) params.accountName = opts.accountName;
|
|
931
|
+
|
|
932
|
+
if (proxy) {
|
|
933
|
+
const result = await proxyFetch({
|
|
934
|
+
action: "auth:signIn",
|
|
935
|
+
args: { provider: "totp", params },
|
|
936
|
+
});
|
|
937
|
+
return { uri: result.totpSetup.uri, secret: result.totpSetup.secret, verifier: result.verifier, totpId: result.totpSetup.totpId };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const result = await convex.action("auth:signIn" as any, {
|
|
941
|
+
provider: "totp",
|
|
942
|
+
params,
|
|
943
|
+
});
|
|
944
|
+
return { uri: result.totpSetup.uri, secret: result.totpSetup.secret, verifier: result.verifier, totpId: result.totpSetup.totpId };
|
|
945
|
+
},
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Complete TOTP enrollment by verifying the first code from the authenticator app.
|
|
949
|
+
*
|
|
950
|
+
* ```ts
|
|
951
|
+
* await auth.totp.confirm({ code: "123456", verifier: setup.verifier, totpId: setup.totpId });
|
|
952
|
+
* ```
|
|
953
|
+
*/
|
|
954
|
+
confirm: async (opts: {
|
|
955
|
+
code: string;
|
|
956
|
+
verifier: string;
|
|
957
|
+
totpId: string;
|
|
958
|
+
}): Promise<void> => {
|
|
959
|
+
const params: Record<string, any> = {
|
|
960
|
+
flow: "confirm",
|
|
961
|
+
code: opts.code,
|
|
962
|
+
totpId: opts.totpId,
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
if (proxy) {
|
|
966
|
+
const result = await proxyFetch({
|
|
967
|
+
action: "auth:signIn",
|
|
968
|
+
args: { provider: "totp", params, verifier: opts.verifier },
|
|
969
|
+
});
|
|
970
|
+
if (result.tokens) {
|
|
971
|
+
await setToken({
|
|
972
|
+
shouldStore: false,
|
|
973
|
+
tokens: result.tokens === null ? null : { token: result.tokens.token },
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const result = await convex.action("auth:signIn" as any, {
|
|
980
|
+
provider: "totp",
|
|
981
|
+
params,
|
|
982
|
+
verifier: opts.verifier,
|
|
983
|
+
});
|
|
984
|
+
if (result.tokens) {
|
|
985
|
+
await setToken({
|
|
986
|
+
shouldStore: true,
|
|
987
|
+
tokens: (result.tokens as AuthSession | null) ?? null,
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Complete 2FA verification during sign-in.
|
|
994
|
+
*
|
|
995
|
+
* Called after a credentials sign-in returns `totpRequired: true`.
|
|
996
|
+
*
|
|
997
|
+
* ```ts
|
|
998
|
+
* const result = await auth.signIn("password", { email, password });
|
|
999
|
+
* if (result.totpRequired) {
|
|
1000
|
+
* await auth.totp.verify({ code: "123456", verifier: result.verifier! });
|
|
1001
|
+
* }
|
|
1002
|
+
* ```
|
|
1003
|
+
*/
|
|
1004
|
+
verify: async (opts: { code: string; verifier: string }): Promise<void> => {
|
|
1005
|
+
const params: Record<string, any> = {
|
|
1006
|
+
flow: "verify",
|
|
1007
|
+
code: opts.code,
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (proxy) {
|
|
1011
|
+
const result = await proxyFetch({
|
|
1012
|
+
action: "auth:signIn",
|
|
1013
|
+
args: { provider: "totp", params, verifier: opts.verifier },
|
|
1014
|
+
});
|
|
1015
|
+
if (result.tokens) {
|
|
1016
|
+
await setToken({
|
|
1017
|
+
shouldStore: false,
|
|
1018
|
+
tokens: result.tokens === null ? null : { token: result.tokens.token },
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const result = await convex.action("auth:signIn" as any, {
|
|
1025
|
+
provider: "totp",
|
|
1026
|
+
params,
|
|
1027
|
+
verifier: opts.verifier,
|
|
1028
|
+
});
|
|
1029
|
+
if (result.tokens) {
|
|
1030
|
+
await setToken({
|
|
1031
|
+
shouldStore: true,
|
|
1032
|
+
tokens: (result.tokens as AuthSession | null) ?? null,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
};
|
|
1037
|
+
|
|
554
1038
|
return {
|
|
555
1039
|
/** Current auth state snapshot. */
|
|
556
1040
|
get state(): AuthState {
|
|
@@ -559,6 +1043,10 @@ export function client(options: ClientOptions) {
|
|
|
559
1043
|
signIn,
|
|
560
1044
|
signOut,
|
|
561
1045
|
onChange,
|
|
1046
|
+
/** Passkey (WebAuthn) authentication helpers. */
|
|
1047
|
+
passkey,
|
|
1048
|
+
/** TOTP two-factor authentication helpers. */
|
|
1049
|
+
totp,
|
|
562
1050
|
};
|
|
563
1051
|
}
|
|
564
1052
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type * as index from "../index.js";
|
|
12
|
+
import type * as portalBridge from "../portalBridge.js";
|
|
12
13
|
import type * as public_ from "../public.js";
|
|
13
14
|
|
|
14
15
|
import type {
|
|
@@ -20,6 +21,7 @@ import { anyApi, componentsGeneric } from "convex/server";
|
|
|
20
21
|
|
|
21
22
|
const fullApi: ApiFromModules<{
|
|
22
23
|
index: typeof index;
|
|
24
|
+
portalBridge: typeof portalBridge;
|
|
23
25
|
public: typeof public_;
|
|
24
26
|
}> = anyApi as any;
|
|
25
27
|
|
|
@@ -49,4 +51,73 @@ export const internal: FilterApi<
|
|
|
49
51
|
FunctionReference<any, "internal">
|
|
50
52
|
> = anyApi as any;
|
|
51
53
|
|
|
52
|
-
export const components = componentsGeneric() as unknown as {
|
|
54
|
+
export const components = componentsGeneric() as unknown as {
|
|
55
|
+
selfHosting: {
|
|
56
|
+
lib: {
|
|
57
|
+
gcOldAssets: FunctionReference<
|
|
58
|
+
"mutation",
|
|
59
|
+
"internal",
|
|
60
|
+
{ currentDeploymentId: string },
|
|
61
|
+
{ blobIds: Array<string>; storageIds: Array<string> }
|
|
62
|
+
>;
|
|
63
|
+
generateUploadUrl: FunctionReference<"mutation", "internal", {}, string>;
|
|
64
|
+
getByPath: FunctionReference<
|
|
65
|
+
"query",
|
|
66
|
+
"internal",
|
|
67
|
+
{ path: string },
|
|
68
|
+
{
|
|
69
|
+
_creationTime: number;
|
|
70
|
+
_id: string;
|
|
71
|
+
blobId?: string;
|
|
72
|
+
contentType: string;
|
|
73
|
+
deploymentId: string;
|
|
74
|
+
path: string;
|
|
75
|
+
storageId?: string;
|
|
76
|
+
} | null
|
|
77
|
+
>;
|
|
78
|
+
getCurrentDeployment: FunctionReference<
|
|
79
|
+
"query",
|
|
80
|
+
"internal",
|
|
81
|
+
{},
|
|
82
|
+
{
|
|
83
|
+
_creationTime: number;
|
|
84
|
+
_id: string;
|
|
85
|
+
currentDeploymentId: string;
|
|
86
|
+
deployedAt: number;
|
|
87
|
+
} | null
|
|
88
|
+
>;
|
|
89
|
+
listAssets: FunctionReference<
|
|
90
|
+
"query",
|
|
91
|
+
"internal",
|
|
92
|
+
{ limit?: number },
|
|
93
|
+
Array<{
|
|
94
|
+
_creationTime: number;
|
|
95
|
+
_id: string;
|
|
96
|
+
blobId?: string;
|
|
97
|
+
contentType: string;
|
|
98
|
+
deploymentId: string;
|
|
99
|
+
path: string;
|
|
100
|
+
storageId?: string;
|
|
101
|
+
}>
|
|
102
|
+
>;
|
|
103
|
+
recordAsset: FunctionReference<
|
|
104
|
+
"mutation",
|
|
105
|
+
"internal",
|
|
106
|
+
{
|
|
107
|
+
blobId?: string;
|
|
108
|
+
contentType: string;
|
|
109
|
+
deploymentId: string;
|
|
110
|
+
path: string;
|
|
111
|
+
storageId?: string;
|
|
112
|
+
},
|
|
113
|
+
{ oldBlobId: string | null; oldStorageId: string | null }
|
|
114
|
+
>;
|
|
115
|
+
setCurrentDeployment: FunctionReference<
|
|
116
|
+
"mutation",
|
|
117
|
+
"internal",
|
|
118
|
+
{ deploymentId: string },
|
|
119
|
+
null
|
|
120
|
+
>;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|