@corgelabs/corge 0.0.1 → 1.0.1
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/ai/index.d.ts +3 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +3 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/schema.d.ts +83 -0
- package/dist/ai/schema.d.ts.map +1 -0
- package/dist/ai/schema.js +19 -0
- package/dist/ai/schema.js.map +1 -0
- package/dist/ai/service.d.ts +14 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/service.js +266 -0
- package/dist/ai/service.js.map +1 -0
- package/dist/ai/utils.d.ts +10 -0
- package/dist/ai/utils.d.ts.map +1 -0
- package/dist/ai/utils.js +27 -0
- package/dist/ai/utils.js.map +1 -0
- package/dist/attachments/index.d.ts +4 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +4 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/routes.d.ts +75 -0
- package/dist/attachments/routes.d.ts.map +1 -0
- package/dist/attachments/routes.js +18 -0
- package/dist/attachments/routes.js.map +1 -0
- package/dist/attachments/schema.d.ts +61 -0
- package/dist/attachments/schema.d.ts.map +1 -0
- package/dist/attachments/schema.js +30 -0
- package/dist/attachments/schema.js.map +1 -0
- package/dist/attachments/service.d.ts +27 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +224 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/utils.d.ts +9 -0
- package/dist/attachments/utils.d.ts.map +1 -0
- package/dist/attachments/utils.js +80 -0
- package/dist/attachments/utils.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oidc.d.ts +25 -0
- package/dist/auth/oidc.d.ts.map +1 -0
- package/dist/auth/oidc.js +298 -0
- package/dist/auth/oidc.js.map +1 -0
- package/dist/auth/recovery.d.ts +41 -0
- package/dist/auth/recovery.d.ts.map +1 -0
- package/dist/auth/recovery.js +250 -0
- package/dist/auth/recovery.js.map +1 -0
- package/dist/auth/routes.d.ts +226 -0
- package/dist/auth/routes.d.ts.map +1 -0
- package/dist/auth/routes.js +96 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/schema.d.ts +158 -0
- package/dist/auth/schema.d.ts.map +1 -0
- package/dist/auth/schema.js +82 -0
- package/dist/auth/schema.js.map +1 -0
- package/dist/auth/service.d.ts +96 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +129 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/session.d.ts +51 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +344 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/utils.d.ts +17 -0
- package/dist/auth/utils.d.ts.map +1 -0
- package/dist/auth/utils.js +96 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/bin/corge.js +542 -0
- package/dist/chat/context.d.ts +10 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +221 -0
- package/dist/chat/context.js.map +1 -0
- package/dist/chat/index.d.ts +4 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +4 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/routes.d.ts +160 -0
- package/dist/chat/routes.d.ts.map +1 -0
- package/dist/chat/routes.js +210 -0
- package/dist/chat/routes.js.map +1 -0
- package/dist/chat/schema.d.ts +285 -0
- package/dist/chat/schema.d.ts.map +1 -0
- package/dist/chat/schema.js +74 -0
- package/dist/chat/schema.js.map +1 -0
- package/dist/chat/service.d.ts +64 -0
- package/dist/chat/service.d.ts.map +1 -0
- package/dist/chat/service.js +279 -0
- package/dist/chat/service.js.map +1 -0
- package/dist/chat/stream.d.ts +53 -0
- package/dist/chat/stream.d.ts.map +1 -0
- package/dist/chat/stream.js +339 -0
- package/dist/chat/stream.js.map +1 -0
- package/dist/chat/utils.d.ts +61 -0
- package/dist/chat/utils.d.ts.map +1 -0
- package/dist/chat/utils.js +285 -0
- package/dist/chat/utils.js.map +1 -0
- package/dist/cli/builder.d.ts +6 -0
- package/dist/cli/builder.d.ts.map +1 -0
- package/dist/cli/builder.js +91 -0
- package/dist/cli/builder.js.map +1 -0
- package/dist/cli/deployer.d.ts +2 -0
- package/dist/cli/deployer.d.ts.map +1 -0
- package/dist/cli/deployer.js +152 -0
- package/dist/cli/deployer.js.map +1 -0
- package/dist/cli/dev-server.d.ts +2 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +70 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/env.d.ts +4 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +97 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config.d.ts +262 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +212 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +410 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +294 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/service.d.ts +42 -0
- package/dist/db/service.d.ts.map +1 -0
- package/dist/db/service.js +372 -0
- package/dist/db/service.js.map +1 -0
- package/dist/db/utils.d.ts +25 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +188 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/emails/index.d.ts +4 -0
- package/dist/emails/index.d.ts.map +1 -0
- package/dist/emails/index.js +4 -0
- package/dist/emails/index.js.map +1 -0
- package/dist/emails/routes.d.ts +191 -0
- package/dist/emails/routes.d.ts.map +1 -0
- package/dist/emails/routes.js +39 -0
- package/dist/emails/routes.js.map +1 -0
- package/dist/emails/schema.d.ts +237 -0
- package/dist/emails/schema.d.ts.map +1 -0
- package/dist/emails/schema.js +98 -0
- package/dist/emails/schema.js.map +1 -0
- package/dist/emails/service.d.ts +22 -0
- package/dist/emails/service.d.ts.map +1 -0
- package/dist/emails/service.js +337 -0
- package/dist/emails/service.js.map +1 -0
- package/dist/emails/utils.d.ts +49 -0
- package/dist/emails/utils.d.ts.map +1 -0
- package/dist/emails/utils.js +341 -0
- package/dist/emails/utils.js.map +1 -0
- package/dist/files/index.d.ts +4 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +4 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/routes.d.ts +72 -0
- package/dist/files/routes.d.ts.map +1 -0
- package/dist/files/routes.js +38 -0
- package/dist/files/routes.js.map +1 -0
- package/dist/files/schema.d.ts +57 -0
- package/dist/files/schema.d.ts.map +1 -0
- package/dist/files/schema.js +39 -0
- package/dist/files/schema.js.map +1 -0
- package/dist/files/service.d.ts +27 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +175 -0
- package/dist/files/service.js.map +1 -0
- package/dist/files/utils.d.ts +3 -0
- package/dist/files/utils.d.ts.map +1 -0
- package/dist/files/utils.js +63 -0
- package/dist/files/utils.js.map +1 -0
- package/dist/index.d.ts +875 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9919 -0
- package/dist/index.js.map +7 -0
- package/dist/payments/billing.d.ts +29 -0
- package/dist/payments/billing.d.ts.map +1 -0
- package/dist/payments/billing.js +378 -0
- package/dist/payments/billing.js.map +1 -0
- package/dist/payments/index.d.ts +4 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +4 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/routes.d.ts +340 -0
- package/dist/payments/routes.d.ts.map +1 -0
- package/dist/payments/routes.js +56 -0
- package/dist/payments/routes.js.map +1 -0
- package/dist/payments/schema.d.ts +412 -0
- package/dist/payments/schema.d.ts.map +1 -0
- package/dist/payments/schema.js +260 -0
- package/dist/payments/schema.js.map +1 -0
- package/dist/payments/service.d.ts +31 -0
- package/dist/payments/service.d.ts.map +1 -0
- package/dist/payments/service.js +322 -0
- package/dist/payments/service.js.map +1 -0
- package/dist/payments/utils.d.ts +61 -0
- package/dist/payments/utils.d.ts.map +1 -0
- package/dist/payments/utils.js +369 -0
- package/dist/payments/utils.js.map +1 -0
- package/dist/router/guard.d.ts +19 -0
- package/dist/router/guard.d.ts.map +1 -0
- package/dist/router/guard.js +243 -0
- package/dist/router/guard.js.map +1 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/schema.d.ts +103 -0
- package/dist/router/schema.d.ts.map +1 -0
- package/dist/router/schema.js +282 -0
- package/dist/router/schema.js.map +1 -0
- package/dist/router/service.d.ts +25 -0
- package/dist/router/service.d.ts.map +1 -0
- package/dist/router/service.js +339 -0
- package/dist/router/service.js.map +1 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/routes.d.ts +54 -0
- package/dist/tasks/routes.d.ts.map +1 -0
- package/dist/tasks/routes.js +195 -0
- package/dist/tasks/routes.js.map +1 -0
- package/dist/tasks/schema.d.ts +261 -0
- package/dist/tasks/schema.d.ts.map +1 -0
- package/dist/tasks/schema.js +35 -0
- package/dist/tasks/schema.js.map +1 -0
- package/dist/tasks/service.d.ts +32 -0
- package/dist/tasks/service.d.ts.map +1 -0
- package/dist/tasks/service.js +201 -0
- package/dist/tasks/service.js.map +1 -0
- package/dist/tasks/steps.d.ts +18 -0
- package/dist/tasks/steps.d.ts.map +1 -0
- package/dist/tasks/steps.js +365 -0
- package/dist/tasks/steps.js.map +1 -0
- package/dist/tasks/utils.d.ts +75 -0
- package/dist/tasks/utils.d.ts.map +1 -0
- package/dist/tasks/utils.js +311 -0
- package/dist/tasks/utils.js.map +1 -0
- package/dist/tasks/watcher.d.ts +13 -0
- package/dist/tasks/watcher.d.ts.map +1 -0
- package/dist/tasks/watcher.js +255 -0
- package/dist/tasks/watcher.js.map +1 -0
- package/dist/tasks/worker.d.ts +28 -0
- package/dist/tasks/worker.d.ts.map +1 -0
- package/dist/tasks/worker.js +326 -0
- package/dist/tasks/worker.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/schema.d.ts +79 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +41 -0
- package/dist/utils/schema.js.map +1 -0
- package/dist/utils/service.d.ts +32 -0
- package/dist/utils/service.d.ts.map +1 -0
- package/dist/utils/service.js +384 -0
- package/dist/utils/service.js.map +1 -0
- package/docs/SDK_SPEC.md +1645 -0
- package/infra/firestore.base.indexes.json +138 -0
- package/infra/firestore.base.rules +152 -0
- package/infra/storage.base.rules +8 -0
- package/package.json +58 -13
- package/src/ai/index.ts +2 -0
- package/src/ai/schema.ts +64 -0
- package/src/ai/service.ts +317 -0
- package/src/ai/utils.ts +38 -0
- package/src/attachments/index.ts +3 -0
- package/src/attachments/routes.ts +28 -0
- package/src/attachments/schema.ts +45 -0
- package/src/attachments/service.ts +315 -0
- package/src/attachments/utils.ts +115 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oidc.ts +419 -0
- package/src/auth/recovery.ts +365 -0
- package/src/auth/routes.ts +142 -0
- package/src/auth/schema.ts +181 -0
- package/src/auth/service.ts +183 -0
- package/src/auth/session.ts +414 -0
- package/src/auth/utils.ts +106 -0
- package/src/chat/context.ts +310 -0
- package/src/chat/index.ts +3 -0
- package/src/chat/routes.ts +264 -0
- package/src/chat/schema.ts +295 -0
- package/src/chat/service.ts +360 -0
- package/src/chat/stream.ts +481 -0
- package/src/chat/utils.ts +370 -0
- package/src/cli/builder.ts +93 -0
- package/src/cli/deployer.ts +184 -0
- package/src/cli/dev-server.ts +77 -0
- package/src/cli/env.ts +89 -0
- package/src/cli/init.ts +121 -0
- package/src/config.ts +447 -0
- package/src/db/index.ts +2 -0
- package/src/db/schema.ts +409 -0
- package/src/db/service.ts +477 -0
- package/src/db/utils.ts +250 -0
- package/src/emails/index.ts +3 -0
- package/src/emails/routes.ts +64 -0
- package/src/emails/schema.ts +239 -0
- package/src/emails/service.ts +488 -0
- package/src/emails/utils.ts +449 -0
- package/src/files/index.ts +3 -0
- package/src/files/routes.ts +44 -0
- package/src/files/schema.ts +76 -0
- package/src/files/service.ts +238 -0
- package/src/files/utils.ts +77 -0
- package/src/index.ts +425 -0
- package/src/payments/billing.ts +488 -0
- package/src/payments/index.ts +3 -0
- package/src/payments/routes.ts +92 -0
- package/src/payments/schema.ts +401 -0
- package/src/payments/service.ts +445 -0
- package/src/payments/utils.ts +472 -0
- package/src/router/guard.ts +319 -0
- package/src/router/index.ts +2 -0
- package/src/router/schema.ts +458 -0
- package/src/router/service.ts +453 -0
- package/src/tasks/index.ts +11 -0
- package/src/tasks/routes.ts +230 -0
- package/src/tasks/schema.ts +310 -0
- package/src/tasks/service.ts +254 -0
- package/src/tasks/steps.ts +472 -0
- package/src/tasks/utils.ts +445 -0
- package/src/tasks/watcher.ts +307 -0
- package/src/tasks/worker.ts +461 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/schema.ts +69 -0
- package/src/utils/service.ts +461 -0
- package/tests/e2e.test.ts +2531 -0
- package/tsconfig.base.json +12 -0
- package/README.md +0 -2
- package/index.d.ts +0 -1
- package/index.js +0 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { randomBytes, randomInt } from 'node:crypto'
|
|
2
|
+
import type { UserRecord } from 'firebase-admin/auth'
|
|
3
|
+
import type { EntryId } from '../db'
|
|
4
|
+
import type { Core } from '../index'
|
|
5
|
+
import {
|
|
6
|
+
completeAccountRecoveryBody,
|
|
7
|
+
forgotPasswordBody,
|
|
8
|
+
initiateAccountRecoveryBody,
|
|
9
|
+
resetPasswordBody,
|
|
10
|
+
setRecoveryEmailBody,
|
|
11
|
+
verifyEmailBody,
|
|
12
|
+
verifyRecoveryEmailBody,
|
|
13
|
+
type AuthEmailPurpose,
|
|
14
|
+
type AuthTokenPurpose,
|
|
15
|
+
type AuthUser,
|
|
16
|
+
type CompleteAccountRecoveryInput,
|
|
17
|
+
type ForgotPasswordInput,
|
|
18
|
+
type InitiateAccountRecoveryInput,
|
|
19
|
+
type ResetPasswordInput,
|
|
20
|
+
type SetRecoveryEmailInput,
|
|
21
|
+
type TransactionalEmailPayload,
|
|
22
|
+
type VerifyEmailInput,
|
|
23
|
+
type VerifyRecoveryEmailInput
|
|
24
|
+
} from './schema'
|
|
25
|
+
import type { AuthSessionEngine } from './session'
|
|
26
|
+
import { hashToken, normalizeDate, safeEqualHex } from './utils'
|
|
27
|
+
|
|
28
|
+
export class AuthRecoveryEngine {
|
|
29
|
+
readonly #core: Core
|
|
30
|
+
readonly #session: AuthSessionEngine
|
|
31
|
+
|
|
32
|
+
constructor(coreClient: Core, session: AuthSessionEngine) {
|
|
33
|
+
this.#core = coreClient
|
|
34
|
+
this.#session = session
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getLastDevEmail = (to: string, purpose: AuthEmailPurpose): TransactionalEmailPayload | undefined =>
|
|
38
|
+
this.#core.internals.emails.getLastDevEmail(to, purpose)
|
|
39
|
+
|
|
40
|
+
#issueAuthToken = async (
|
|
41
|
+
userId: EntryId,
|
|
42
|
+
purpose: AuthTokenPurpose,
|
|
43
|
+
targetEmail: string,
|
|
44
|
+
expiresInMinutes = this.#core.config.emails.otpTtlMinutes
|
|
45
|
+
): Promise<TransactionalEmailPayload> => {
|
|
46
|
+
const normalizedTarget = targetEmail.trim().toLowerCase()
|
|
47
|
+
const code = String(randomInt(100_000, 1_000_000))
|
|
48
|
+
const token = randomBytes(32).toString('base64url')
|
|
49
|
+
const tokenHash = hashToken(code)
|
|
50
|
+
const urlTokenHash = hashToken(token)
|
|
51
|
+
const now = new Date()
|
|
52
|
+
const expiresAt = new Date(now.getTime() + expiresInMinutes * 60_000)
|
|
53
|
+
const docId = `${userId}_${purpose}`
|
|
54
|
+
|
|
55
|
+
await this.#core.db
|
|
56
|
+
.firestore()
|
|
57
|
+
.collection('auth_tokens')
|
|
58
|
+
.doc(docId)
|
|
59
|
+
.set({
|
|
60
|
+
id: docId,
|
|
61
|
+
userId,
|
|
62
|
+
purpose,
|
|
63
|
+
targetEmail: normalizedTarget,
|
|
64
|
+
tokenHash,
|
|
65
|
+
urlTokenHash,
|
|
66
|
+
attempts: 0,
|
|
67
|
+
expiresAt,
|
|
68
|
+
consumedAt: null,
|
|
69
|
+
deleted: false,
|
|
70
|
+
createdAt: now,
|
|
71
|
+
updatedAt: now
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const payload: TransactionalEmailPayload = {
|
|
75
|
+
to: normalizedTarget,
|
|
76
|
+
purpose,
|
|
77
|
+
code,
|
|
78
|
+
token,
|
|
79
|
+
expiresInMinutes,
|
|
80
|
+
userId
|
|
81
|
+
}
|
|
82
|
+
await this.#core.internals.emails.dispatchAuthEmail(payload)
|
|
83
|
+
return payload
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#consumeAuthToken = async (
|
|
87
|
+
userId: EntryId,
|
|
88
|
+
purpose: AuthTokenPurpose,
|
|
89
|
+
expectedTargetEmail: string,
|
|
90
|
+
codeOrToken: string
|
|
91
|
+
): Promise<void> => {
|
|
92
|
+
const db = this.#core.db.firestore()
|
|
93
|
+
const docRef = db.collection('auth_tokens').doc(`${userId}_${purpose}`)
|
|
94
|
+
const maxAttempts = this.#core.config.emails.maxVerifyAttempts
|
|
95
|
+
const normalizedExpected = expectedTargetEmail.trim().toLowerCase()
|
|
96
|
+
const candidateHash = hashToken(codeOrToken)
|
|
97
|
+
|
|
98
|
+
type ConsumeOutcome = 'valid' | 'missing_or_consumed' | 'expired' | 'max_attempts' | 'mismatch'
|
|
99
|
+
|
|
100
|
+
const outcome = await db.runTransaction<ConsumeOutcome>(async (tx) => {
|
|
101
|
+
const snap = await tx.get(docRef)
|
|
102
|
+
if (!snap.exists) return 'missing_or_consumed'
|
|
103
|
+
const data = snap.data() as Record<string, unknown> | undefined
|
|
104
|
+
if (!data || data.deleted === true || data.consumedAt != null) return 'missing_or_consumed'
|
|
105
|
+
|
|
106
|
+
const attempts = typeof data.attempts === 'number' ? data.attempts : 0
|
|
107
|
+
if (attempts >= maxAttempts) return 'max_attempts'
|
|
108
|
+
|
|
109
|
+
const now = new Date()
|
|
110
|
+
if (normalizeDate(data.expiresAt, new Date(0)).getTime() <= now.getTime()) {
|
|
111
|
+
tx.update(docRef, { deleted: true, updatedAt: now })
|
|
112
|
+
return 'expired'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const targetEmail = String(data.targetEmail ?? '').trim().toLowerCase()
|
|
116
|
+
const tokenHash = String(data.tokenHash ?? '')
|
|
117
|
+
const urlTokenHash = typeof data.urlTokenHash === 'string' ? data.urlTokenHash : ''
|
|
118
|
+
const matches =
|
|
119
|
+
targetEmail === normalizedExpected &&
|
|
120
|
+
(safeEqualHex(candidateHash, tokenHash) ||
|
|
121
|
+
(Boolean(urlTokenHash) && safeEqualHex(candidateHash, urlTokenHash)))
|
|
122
|
+
|
|
123
|
+
if (!matches) {
|
|
124
|
+
const nextAttempts = attempts + 1
|
|
125
|
+
const exhausted = nextAttempts >= maxAttempts
|
|
126
|
+
tx.update(docRef, {
|
|
127
|
+
attempts: nextAttempts,
|
|
128
|
+
...(exhausted ? { deleted: true, consumedAt: now } : {}),
|
|
129
|
+
updatedAt: now
|
|
130
|
+
})
|
|
131
|
+
return exhausted ? 'max_attempts' : 'mismatch'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
tx.update(docRef, { consumedAt: now, deleted: true, updatedAt: now })
|
|
135
|
+
return 'valid'
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (outcome === 'valid') return
|
|
139
|
+
if (outcome === 'expired') this.#core.errors.httpError(400, 'Verification token has expired', 'TOKEN_EXPIRED')
|
|
140
|
+
if (outcome === 'max_attempts') {
|
|
141
|
+
this.#core.errors.httpError(
|
|
142
|
+
400,
|
|
143
|
+
'Maximum verification attempts exceeded; token invalidated',
|
|
144
|
+
'TOKEN_MAX_ATTEMPTS_EXCEEDED'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
this.#core.errors.httpError(400, 'Invalid or expired verification code', 'INVALID_TOKEN')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#requireUserByEmail = async (
|
|
151
|
+
email: string,
|
|
152
|
+
errMessage: string,
|
|
153
|
+
errCode: string,
|
|
154
|
+
rejectDisabled = false
|
|
155
|
+
): Promise<{ user: UserRecord; normalizedEmail: string }> => {
|
|
156
|
+
const normalizedEmail = email.trim().toLowerCase()
|
|
157
|
+
const user = await this.#session
|
|
158
|
+
.getAuth()
|
|
159
|
+
.getUserByEmail(normalizedEmail)
|
|
160
|
+
.catch(() => undefined)
|
|
161
|
+
if (!user || !user.email || (rejectDisabled && user.disabled)) {
|
|
162
|
+
return this.#core.errors.httpError(400, errMessage, errCode)
|
|
163
|
+
}
|
|
164
|
+
return { user, normalizedEmail }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#revokeAllUserSessions = async (uid: EntryId): Promise<void> => {
|
|
168
|
+
this.#session.evictUserTokens(uid)
|
|
169
|
+
await Promise.all([
|
|
170
|
+
this.#session.recordRevocation(uid, this.#core.env.isDev),
|
|
171
|
+
this.#session
|
|
172
|
+
.getAuth()
|
|
173
|
+
.revokeRefreshTokens(uid)
|
|
174
|
+
.catch(() => {})
|
|
175
|
+
])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
sendVerificationEmail = async (
|
|
179
|
+
userId: EntryId = this.#core.userId
|
|
180
|
+
): Promise<{ sent: true; expiresInMinutes: number }> => {
|
|
181
|
+
if (!userId) return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
|
|
182
|
+
const user = await this.#session
|
|
183
|
+
.getAuth()
|
|
184
|
+
.getUser(userId)
|
|
185
|
+
.catch(() => undefined)
|
|
186
|
+
if (!user || !user.email) {
|
|
187
|
+
return this.#core.errors.httpError(400, 'User does not have a primary email address', 'MISSING_EMAIL')
|
|
188
|
+
}
|
|
189
|
+
const expiresInMinutes = this.#core.config.emails.otpTtlMinutes
|
|
190
|
+
await this.#issueAuthToken(user.uid, 'verify_email', user.email, expiresInMinutes)
|
|
191
|
+
return { sent: true, expiresInMinutes }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#consumeAndUpsertMeta = async (
|
|
195
|
+
uid: EntryId,
|
|
196
|
+
purpose: AuthTokenPurpose,
|
|
197
|
+
targetEmail: string,
|
|
198
|
+
codeOrToken: string,
|
|
199
|
+
metaPatch: Parameters<AuthSessionEngine['upsertUserAuthMeta']>[1]
|
|
200
|
+
): Promise<void> => {
|
|
201
|
+
await this.#consumeAuthToken(uid, purpose, targetEmail, codeOrToken)
|
|
202
|
+
await this.#session.upsertUserAuthMeta(uid, metaPatch, this.#core.env.isDev)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
verifyEmail = async (raw: VerifyEmailInput): Promise<{ verified: true; userId: string }> => {
|
|
206
|
+
const { email, codeOrToken } = verifyEmailBody.parse(raw)
|
|
207
|
+
const { user, normalizedEmail } = await this.#requireUserByEmail(
|
|
208
|
+
email,
|
|
209
|
+
'Invalid email or verification token',
|
|
210
|
+
'INVALID_TOKEN'
|
|
211
|
+
)
|
|
212
|
+
await this.#consumeAndUpsertMeta(user.uid, 'verify_email', user.email!, codeOrToken, {
|
|
213
|
+
email: normalizedEmail,
|
|
214
|
+
emailVerified: true
|
|
215
|
+
})
|
|
216
|
+
await this.#session.getAuth().updateUser(user.uid, { emailVerified: true })
|
|
217
|
+
this.#session.evictUserTokens(user.uid)
|
|
218
|
+
return { verified: true, userId: user.uid }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
forgotPassword = async (raw: ForgotPasswordInput): Promise<{ sent: true }> => {
|
|
222
|
+
const { email } = forgotPasswordBody.parse(raw)
|
|
223
|
+
const normalizedEmail = email.trim().toLowerCase()
|
|
224
|
+
const user = await this.#session
|
|
225
|
+
.getAuth()
|
|
226
|
+
.getUserByEmail(normalizedEmail)
|
|
227
|
+
.catch(() => undefined)
|
|
228
|
+
if (user && user.email && !user.disabled) {
|
|
229
|
+
await this.#issueAuthToken(user.uid, 'reset_password', user.email, this.#core.config.emails.otpTtlMinutes)
|
|
230
|
+
}
|
|
231
|
+
return { sent: true }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
resetPassword = async (raw: ResetPasswordInput): Promise<{ reset: true }> => {
|
|
235
|
+
const { email, codeOrToken, newPassword } = resetPasswordBody.parse(raw)
|
|
236
|
+
const { user } = await this.#requireUserByEmail(email, 'Invalid email or reset token', 'INVALID_TOKEN')
|
|
237
|
+
await this.#consumeAuthToken(user.uid, 'reset_password', user.email!, codeOrToken)
|
|
238
|
+
await this.#session.getAuth().updateUser(user.uid, { password: newPassword })
|
|
239
|
+
await this.#revokeAllUserSessions(user.uid)
|
|
240
|
+
return { reset: true }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
setRecoveryEmail = async (
|
|
244
|
+
raw: SetRecoveryEmailInput,
|
|
245
|
+
userId: EntryId = this.#core.userId
|
|
246
|
+
): Promise<{ recoveryEmail: string; recoveryEmailVerified: false; sent: true }> => {
|
|
247
|
+
if (!userId) return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
|
|
248
|
+
const { recoveryEmail } = setRecoveryEmailBody.parse(raw)
|
|
249
|
+
const normalizedRecovery = recoveryEmail.trim().toLowerCase()
|
|
250
|
+
const user = await this.#session
|
|
251
|
+
.getAuth()
|
|
252
|
+
.getUser(userId)
|
|
253
|
+
.catch(() => undefined)
|
|
254
|
+
if (!user) return this.#core.errors.httpError(404, 'User not found', 'NOT_FOUND')
|
|
255
|
+
|
|
256
|
+
const primaryEmail = (user.email ?? '').trim().toLowerCase()
|
|
257
|
+
if (primaryEmail && normalizedRecovery === primaryEmail) {
|
|
258
|
+
return this.#core.errors.httpError(
|
|
259
|
+
400,
|
|
260
|
+
'Recovery email must be different from primary email',
|
|
261
|
+
'INVALID_RECOVERY_EMAIL'
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await this.#session.upsertUserAuthMeta(
|
|
266
|
+
user.uid,
|
|
267
|
+
{
|
|
268
|
+
...(primaryEmail ? { email: primaryEmail } : {}),
|
|
269
|
+
recoveryEmail: normalizedRecovery,
|
|
270
|
+
recoveryEmailVerified: false
|
|
271
|
+
},
|
|
272
|
+
this.#core.env.isDev
|
|
273
|
+
)
|
|
274
|
+
await this.#issueAuthToken(
|
|
275
|
+
user.uid,
|
|
276
|
+
'verify_recovery_email',
|
|
277
|
+
normalizedRecovery,
|
|
278
|
+
this.#core.config.emails.otpTtlMinutes
|
|
279
|
+
)
|
|
280
|
+
return { recoveryEmail: normalizedRecovery, recoveryEmailVerified: false, sent: true }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
verifyRecoveryEmail = async (
|
|
284
|
+
raw: VerifyRecoveryEmailInput,
|
|
285
|
+
userId: EntryId = this.#core.userId
|
|
286
|
+
): Promise<{ recoveryEmail: string; recoveryEmailVerified: true; verified: true }> => {
|
|
287
|
+
if (!userId) return this.#core.errors.httpError(401, 'Unauthorized', 'UNAUTHORIZED')
|
|
288
|
+
const { codeOrToken } = verifyRecoveryEmailBody.parse(raw)
|
|
289
|
+
const meta = await this.#session.getUserAuthMeta(userId, this.#core.env.isDev)
|
|
290
|
+
if (!meta?.recoveryEmail) {
|
|
291
|
+
return this.#core.errors.httpError(
|
|
292
|
+
400,
|
|
293
|
+
'No recovery email configured for this account',
|
|
294
|
+
'MISSING_RECOVERY_EMAIL'
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
await this.#consumeAndUpsertMeta(userId, 'verify_recovery_email', meta.recoveryEmail, codeOrToken, {
|
|
298
|
+
recoveryEmailVerified: true
|
|
299
|
+
})
|
|
300
|
+
return { recoveryEmail: meta.recoveryEmail, recoveryEmailVerified: true, verified: true }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
initiateAccountRecovery = async (raw: InitiateAccountRecoveryInput): Promise<{ initiated: true }> => {
|
|
304
|
+
const { primaryEmail, recoveryEmail } = initiateAccountRecoveryBody.parse(raw)
|
|
305
|
+
const normPrimary = primaryEmail.trim().toLowerCase()
|
|
306
|
+
const normRecovery = recoveryEmail.trim().toLowerCase()
|
|
307
|
+
const user = await this.#session
|
|
308
|
+
.getAuth()
|
|
309
|
+
.getUserByEmail(normPrimary)
|
|
310
|
+
.catch(() => undefined)
|
|
311
|
+
if (user && !user.disabled && (user.email ?? '').trim().toLowerCase() === normPrimary) {
|
|
312
|
+
const meta = await this.#session.getUserAuthMeta(user.uid, this.#core.env.isDev)
|
|
313
|
+
if (meta?.recoveryEmail === normRecovery && meta.recoveryEmailVerified === true) {
|
|
314
|
+
await this.#issueAuthToken(
|
|
315
|
+
user.uid,
|
|
316
|
+
'account_recovery',
|
|
317
|
+
normRecovery,
|
|
318
|
+
this.#core.config.emails.recoveryOtpTtlMinutes
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return { initiated: true }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
completeAccountRecovery = async (
|
|
326
|
+
raw: CompleteAccountRecoveryInput
|
|
327
|
+
): Promise<{ user: AuthUser; sessionCookie: string }> => {
|
|
328
|
+
const { primaryEmail, recoveryEmail, codeOrToken, newPrimaryEmail, newPassword } =
|
|
329
|
+
completeAccountRecoveryBody.parse(raw)
|
|
330
|
+
const normPrimary = primaryEmail.trim().toLowerCase()
|
|
331
|
+
const normRecovery = recoveryEmail.trim().toLowerCase()
|
|
332
|
+
const normNewPrimary = newPrimaryEmail.trim().toLowerCase()
|
|
333
|
+
|
|
334
|
+
const { user } = await this.#requireUserByEmail(
|
|
335
|
+
normPrimary,
|
|
336
|
+
'Invalid account recovery request or token',
|
|
337
|
+
'INVALID_RECOVERY_REQUEST',
|
|
338
|
+
true
|
|
339
|
+
)
|
|
340
|
+
if ((user.email ?? '').trim().toLowerCase() !== normPrimary) {
|
|
341
|
+
return this.#core.errors.httpError(400, 'Invalid account recovery request or token', 'INVALID_RECOVERY_REQUEST')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const meta = await this.#session.getUserAuthMeta(user.uid, this.#core.env.isDev)
|
|
345
|
+
if (!meta || meta.recoveryEmail !== normRecovery || meta.recoveryEmailVerified !== true) {
|
|
346
|
+
return this.#core.errors.httpError(400, 'Invalid account recovery request or token', 'INVALID_RECOVERY_REQUEST')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await this.#consumeAndUpsertMeta(user.uid, 'account_recovery', normRecovery, codeOrToken, {
|
|
350
|
+
email: normNewPrimary,
|
|
351
|
+
emailVerified: true
|
|
352
|
+
})
|
|
353
|
+
await this.#session.getAuth().updateUser(user.uid, {
|
|
354
|
+
email: normNewPrimary,
|
|
355
|
+
emailVerified: true,
|
|
356
|
+
...(newPassword ? { password: newPassword } : {})
|
|
357
|
+
})
|
|
358
|
+
await this.#revokeAllUserSessions(user.uid)
|
|
359
|
+
|
|
360
|
+
return this.#session.mintSessionForUid(user.uid, {
|
|
361
|
+
email: normNewPrimary,
|
|
362
|
+
emailVerified: true
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { deleteCookie, getCookie } from 'hono/cookie'
|
|
2
|
+
import type { Context } from 'hono'
|
|
3
|
+
import type { z } from 'zod'
|
|
4
|
+
import { CORE_CONFIG, type AppEnv, type DbRole } from '../config'
|
|
5
|
+
import type { EntryId } from '../db'
|
|
6
|
+
import { core } from '../index'
|
|
7
|
+
import type { StepRunner } from '../tasks'
|
|
8
|
+
import {
|
|
9
|
+
blockUserBody,
|
|
10
|
+
completeAccountRecoveryBody,
|
|
11
|
+
customTokenBody,
|
|
12
|
+
emailSignInBody,
|
|
13
|
+
emailSignUpBody,
|
|
14
|
+
eraseUserInput,
|
|
15
|
+
eraseUserOutput,
|
|
16
|
+
forgotPasswordBody,
|
|
17
|
+
initiateAccountRecoveryBody,
|
|
18
|
+
resetPasswordBody,
|
|
19
|
+
sendVerificationEmailBody,
|
|
20
|
+
sessionBody,
|
|
21
|
+
setRecoveryEmailBody,
|
|
22
|
+
signOutBody,
|
|
23
|
+
socialSignInInput,
|
|
24
|
+
verifyEmailBody,
|
|
25
|
+
verifyRecoveryEmailBody,
|
|
26
|
+
type AuthUser,
|
|
27
|
+
type EraseUserInput
|
|
28
|
+
} from './schema'
|
|
29
|
+
|
|
30
|
+
const respondWithSession = async (
|
|
31
|
+
c: Context<AppEnv>,
|
|
32
|
+
p: Promise<{ user: AuthUser; sessionCookie: string }>
|
|
33
|
+
): Promise<AuthUser> => {
|
|
34
|
+
const { user, sessionCookie } = await p
|
|
35
|
+
core.internals.auth.setSessionCookie(c, sessionCookie)
|
|
36
|
+
return user
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const coreAuthRoutes = {
|
|
40
|
+
'auth.custom-token': {
|
|
41
|
+
input: customTokenBody,
|
|
42
|
+
handler: async ({
|
|
43
|
+
input,
|
|
44
|
+
userId,
|
|
45
|
+
role
|
|
46
|
+
}: {
|
|
47
|
+
input: z.infer<typeof customTokenBody>
|
|
48
|
+
userId: EntryId
|
|
49
|
+
role: DbRole
|
|
50
|
+
}) => {
|
|
51
|
+
if (input.uid && role !== 'admin' && userId !== CORE_CONFIG.auth.adminUserId) {
|
|
52
|
+
return core.errors.httpError(403, 'Admin privileges required to mint token for specific UID', 'FORBIDDEN')
|
|
53
|
+
}
|
|
54
|
+
return core.internals.auth.mintCustomToken(input.uid)
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
'auth.session.create': {
|
|
58
|
+
input: sessionBody,
|
|
59
|
+
handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof sessionBody> }) =>
|
|
60
|
+
respondWithSession(c, core.internals.auth.createSession(input.idToken)).catch(() =>
|
|
61
|
+
core.errors.httpError(401, 'Invalid ID token', 'INVALID_ID_TOKEN')
|
|
62
|
+
)
|
|
63
|
+
},
|
|
64
|
+
'auth.session.get': {
|
|
65
|
+
handler: async ({ userId }: { userId: EntryId }) => ({ userId })
|
|
66
|
+
},
|
|
67
|
+
'auth.sign-up.email': {
|
|
68
|
+
input: emailSignUpBody,
|
|
69
|
+
handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof emailSignUpBody> }) =>
|
|
70
|
+
respondWithSession(c, core.internals.auth.signUpEmail(input.email, input.password, input.name))
|
|
71
|
+
},
|
|
72
|
+
'auth.sign-in.email': {
|
|
73
|
+
input: emailSignInBody,
|
|
74
|
+
handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof emailSignInBody> }) =>
|
|
75
|
+
respondWithSession(c, core.internals.auth.signInEmail(input.email, input.password))
|
|
76
|
+
},
|
|
77
|
+
'auth.sign-in.social': {
|
|
78
|
+
input: socialSignInInput,
|
|
79
|
+
handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof socialSignInInput> }) =>
|
|
80
|
+
respondWithSession(c, core.auth.signInSocial(input))
|
|
81
|
+
},
|
|
82
|
+
'auth.email.send-verification': {
|
|
83
|
+
input: sendVerificationEmailBody,
|
|
84
|
+
handler: async ({ userId }: { userId: EntryId }) => core.auth.sendVerificationEmail(userId)
|
|
85
|
+
},
|
|
86
|
+
'auth.email.verify': {
|
|
87
|
+
input: verifyEmailBody,
|
|
88
|
+
handler: async ({ input }: { input: z.infer<typeof verifyEmailBody> }) => core.auth.verifyEmail(input)
|
|
89
|
+
},
|
|
90
|
+
'auth.password.forgot': {
|
|
91
|
+
input: forgotPasswordBody,
|
|
92
|
+
handler: async ({ input }: { input: z.infer<typeof forgotPasswordBody> }) => core.auth.forgotPassword(input)
|
|
93
|
+
},
|
|
94
|
+
'auth.password.reset': {
|
|
95
|
+
input: resetPasswordBody,
|
|
96
|
+
handler: async ({ input }: { input: z.infer<typeof resetPasswordBody> }) => core.auth.resetPassword(input)
|
|
97
|
+
},
|
|
98
|
+
'auth.recovery-email.set': {
|
|
99
|
+
input: setRecoveryEmailBody,
|
|
100
|
+
handler: async ({ userId, input }: { userId: EntryId; input: z.infer<typeof setRecoveryEmailBody> }) =>
|
|
101
|
+
core.auth.setRecoveryEmail(input, userId)
|
|
102
|
+
},
|
|
103
|
+
'auth.recovery-email.verify': {
|
|
104
|
+
input: verifyRecoveryEmailBody,
|
|
105
|
+
handler: async ({ userId, input }: { userId: EntryId; input: z.infer<typeof verifyRecoveryEmailBody> }) =>
|
|
106
|
+
core.auth.verifyRecoveryEmail(input, userId)
|
|
107
|
+
},
|
|
108
|
+
'auth.account-recovery.initiate': {
|
|
109
|
+
input: initiateAccountRecoveryBody,
|
|
110
|
+
handler: async ({ input }: { input: z.infer<typeof initiateAccountRecoveryBody> }) =>
|
|
111
|
+
core.auth.initiateAccountRecovery(input)
|
|
112
|
+
},
|
|
113
|
+
'auth.account-recovery.complete': {
|
|
114
|
+
input: completeAccountRecoveryBody,
|
|
115
|
+
handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof completeAccountRecoveryBody> }) =>
|
|
116
|
+
respondWithSession(c, core.auth.completeAccountRecovery(input))
|
|
117
|
+
},
|
|
118
|
+
'auth.users.list': {
|
|
119
|
+
handler: async () => core.auth.listUsers()
|
|
120
|
+
},
|
|
121
|
+
'auth.users.block': {
|
|
122
|
+
input: blockUserBody,
|
|
123
|
+
handler: async ({ userId, input }: { userId: EntryId; input: z.infer<typeof blockUserBody> }) =>
|
|
124
|
+
core.auth.blockUser(input.userId ?? userId, input.blocked)
|
|
125
|
+
},
|
|
126
|
+
'auth.users.delete': {
|
|
127
|
+
mode: 'workflow' as const,
|
|
128
|
+
workflowName: 'erase-user',
|
|
129
|
+
input: eraseUserInput,
|
|
130
|
+
output: eraseUserOutput,
|
|
131
|
+
handler: async ({ input, step }: { input: EraseUserInput; step: StepRunner }) => core.auth.eraseUser(input, step)
|
|
132
|
+
},
|
|
133
|
+
'auth.sign-out': {
|
|
134
|
+
input: signOutBody,
|
|
135
|
+
handler: async ({ c, input }: { c: Context<AppEnv>; input: z.infer<typeof signOutBody> }) => {
|
|
136
|
+
const cookie = getCookie(c, CORE_CONFIG.auth.sessionCookie)
|
|
137
|
+
if (cookie) await core.internals.auth.revokeSession(cookie, Boolean(input.deleteUser)).catch(() => {})
|
|
138
|
+
deleteCookie(c, CORE_CONFIG.auth.sessionCookie, { path: '/' })
|
|
139
|
+
return { ok: true }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import {
|
|
3
|
+
CORE_CONFIG,
|
|
4
|
+
type AuthEmailPurpose,
|
|
5
|
+
type AuthTokenPurpose,
|
|
6
|
+
type DbRole,
|
|
7
|
+
type TransactionalEmailPayload
|
|
8
|
+
} from '../config'
|
|
9
|
+
import { entryId, type BaseEntryFields, type EntryId } from '../db'
|
|
10
|
+
|
|
11
|
+
export { type AuthEmailPurpose, type AuthTokenPurpose, type TransactionalEmailPayload }
|
|
12
|
+
|
|
13
|
+
export const SESSION_COOKIE = CORE_CONFIG.auth.sessionCookie
|
|
14
|
+
export const SESSION_EXPIRES_IN = CORE_CONFIG.auth.sessionExpiresInMs
|
|
15
|
+
export const ADMIN_USER_ID: EntryId = CORE_CONFIG.auth.adminUserId
|
|
16
|
+
|
|
17
|
+
export type OAuthProvider = 'google' | 'apple'
|
|
18
|
+
|
|
19
|
+
export interface LinkedProvider {
|
|
20
|
+
provider: OAuthProvider
|
|
21
|
+
providerUserId: string
|
|
22
|
+
email?: string | null
|
|
23
|
+
linkedAt: Date
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CachedTokenEntry {
|
|
27
|
+
uid: EntryId
|
|
28
|
+
role: DbRole
|
|
29
|
+
exp: number
|
|
30
|
+
cachedAt: number
|
|
31
|
+
strictVerified: boolean
|
|
32
|
+
emailVerified?: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UserAuthMetaRecord {
|
|
36
|
+
email?: string | null
|
|
37
|
+
emailVerified?: boolean
|
|
38
|
+
name?: string | null
|
|
39
|
+
avatarUrl?: string | null
|
|
40
|
+
recoveryEmail?: string | null
|
|
41
|
+
recoveryEmailVerified?: boolean
|
|
42
|
+
linkedProviders?: LinkedProvider[]
|
|
43
|
+
providerKeys?: string[]
|
|
44
|
+
createdAt?: Date
|
|
45
|
+
updatedAt?: Date
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface VerifiedOidcClaims {
|
|
49
|
+
sub: string
|
|
50
|
+
email: string | null
|
|
51
|
+
emailVerified: boolean
|
|
52
|
+
name?: string
|
|
53
|
+
picture?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface AuthUser {
|
|
57
|
+
id: EntryId
|
|
58
|
+
email: string | null
|
|
59
|
+
emailVerified: boolean
|
|
60
|
+
name: string | null
|
|
61
|
+
avatarUrl?: string | null
|
|
62
|
+
/** Secondary recovery email (stored normalized/lowercase) */
|
|
63
|
+
recoveryEmail?: string | null
|
|
64
|
+
/** True only after the user verifies ownership of `recoveryEmail` via OTP */
|
|
65
|
+
recoveryEmailVerified?: boolean
|
|
66
|
+
linkedProviders?: LinkedProvider[]
|
|
67
|
+
customClaims?: Record<string, unknown>
|
|
68
|
+
deleted: boolean
|
|
69
|
+
createdAt: Date
|
|
70
|
+
updatedAt: Date
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AuthTokenDoc extends BaseEntryFields {
|
|
74
|
+
userId: EntryId
|
|
75
|
+
purpose: AuthTokenPurpose
|
|
76
|
+
/** Target email address (primary email or recovery email) */
|
|
77
|
+
targetEmail: string
|
|
78
|
+
/** SHA-256 hex digest of the 6-digit OTP code or 32-byte URL token */
|
|
79
|
+
tokenHash: string
|
|
80
|
+
urlTokenHash?: string
|
|
81
|
+
/** Max 5 failed verification attempts before automatic invalidation */
|
|
82
|
+
attempts: number
|
|
83
|
+
expiresAt: Date
|
|
84
|
+
consumedAt?: Date | null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const eraseUserInput = z.object({
|
|
88
|
+
userId: entryId,
|
|
89
|
+
backupBeforeErase: z.boolean().default(false)
|
|
90
|
+
})
|
|
91
|
+
export const eraseUserOutput = z.object({
|
|
92
|
+
userId: entryId,
|
|
93
|
+
backupOperation: z.string().optional(),
|
|
94
|
+
erasedFiles: z.number(),
|
|
95
|
+
erasedChats: z.number(),
|
|
96
|
+
erasedCollections: z.record(z.string(), z.number()).optional(),
|
|
97
|
+
erasedUser: z.boolean()
|
|
98
|
+
})
|
|
99
|
+
export type EraseUserInput = z.input<typeof eraseUserInput>
|
|
100
|
+
export type EraseUserOutput = z.infer<typeof eraseUserOutput>
|
|
101
|
+
|
|
102
|
+
export const customTokenBody = z
|
|
103
|
+
.object({ uid: entryId.refine((id) => id !== CORE_CONFIG.auth.adminUserId, 'Reserved UID').optional() })
|
|
104
|
+
.default({})
|
|
105
|
+
export const sessionBody = z.object({ idToken: z.string().min(1) })
|
|
106
|
+
export const emailSignUpBody = z.object({
|
|
107
|
+
email: z.string().email(),
|
|
108
|
+
password: z.string().min(8).max(128),
|
|
109
|
+
name: z.string().min(1).max(120).optional()
|
|
110
|
+
})
|
|
111
|
+
export const emailSignInBody = z.object({ email: z.string().email(), password: z.string().min(1).max(128) })
|
|
112
|
+
|
|
113
|
+
export const socialSignInInput = z.object({
|
|
114
|
+
provider: z.enum(['google', 'apple']),
|
|
115
|
+
/** OIDC ID token returned by Apple Sign-In (iOS/Capacitor/Web) or Google Sign-In */
|
|
116
|
+
idToken: z.string().min(20),
|
|
117
|
+
/** Optional raw nonce for Apple Sign-In replay protection */
|
|
118
|
+
nonce: z.string().optional(),
|
|
119
|
+
/** Optional access token (Google) */
|
|
120
|
+
accessToken: z.string().optional(),
|
|
121
|
+
/**
|
|
122
|
+
* Apple iOS/Capacitor quirk: Apple ONLY sends the user's `name` on the VERY FIRST authorization.
|
|
123
|
+
* Accept optional first-time profile data from the client and persist it immediately.
|
|
124
|
+
*/
|
|
125
|
+
profile: z
|
|
126
|
+
.object({
|
|
127
|
+
name: z.string().min(1).max(128).optional(),
|
|
128
|
+
avatarUrl: z.string().url().optional()
|
|
129
|
+
})
|
|
130
|
+
.optional()
|
|
131
|
+
})
|
|
132
|
+
export type SocialSignInInput = z.infer<typeof socialSignInInput>
|
|
133
|
+
|
|
134
|
+
export const sendVerificationEmailBody = z.object({}).default({})
|
|
135
|
+
export const verifyEmailBody = z.object({
|
|
136
|
+
email: z.string().email(),
|
|
137
|
+
codeOrToken: z.string().min(4).max(512)
|
|
138
|
+
})
|
|
139
|
+
export type VerifyEmailInput = z.infer<typeof verifyEmailBody>
|
|
140
|
+
|
|
141
|
+
export const forgotPasswordBody = z.object({
|
|
142
|
+
email: z.string().email()
|
|
143
|
+
})
|
|
144
|
+
export type ForgotPasswordInput = z.infer<typeof forgotPasswordBody>
|
|
145
|
+
|
|
146
|
+
export const resetPasswordBody = z.object({
|
|
147
|
+
email: z.string().email(),
|
|
148
|
+
codeOrToken: z.string().min(4).max(512),
|
|
149
|
+
newPassword: z.string().min(8).max(128)
|
|
150
|
+
})
|
|
151
|
+
export type ResetPasswordInput = z.infer<typeof resetPasswordBody>
|
|
152
|
+
|
|
153
|
+
export const setRecoveryEmailBody = z.object({
|
|
154
|
+
recoveryEmail: z.string().email()
|
|
155
|
+
})
|
|
156
|
+
export type SetRecoveryEmailInput = z.infer<typeof setRecoveryEmailBody>
|
|
157
|
+
|
|
158
|
+
export const verifyRecoveryEmailBody = z.object({
|
|
159
|
+
codeOrToken: z.string().min(4).max(512)
|
|
160
|
+
})
|
|
161
|
+
export type VerifyRecoveryEmailInput = z.infer<typeof verifyRecoveryEmailBody>
|
|
162
|
+
|
|
163
|
+
export const initiateAccountRecoveryBody = z.object({
|
|
164
|
+
primaryEmail: z.string().email(),
|
|
165
|
+
recoveryEmail: z.string().email()
|
|
166
|
+
})
|
|
167
|
+
export type InitiateAccountRecoveryInput = z.infer<typeof initiateAccountRecoveryBody>
|
|
168
|
+
|
|
169
|
+
export const completeAccountRecoveryBody = z.object({
|
|
170
|
+
primaryEmail: z.string().email(),
|
|
171
|
+
recoveryEmail: z.string().email(),
|
|
172
|
+
codeOrToken: z.string().min(4).max(512),
|
|
173
|
+
newPrimaryEmail: z.string().email(),
|
|
174
|
+
newPassword: z.string().min(8).max(128).optional()
|
|
175
|
+
})
|
|
176
|
+
export type CompleteAccountRecoveryInput = z.infer<typeof completeAccountRecoveryBody>
|
|
177
|
+
|
|
178
|
+
export const blockUserBody = z
|
|
179
|
+
.object({ userId: entryId.optional(), blocked: z.boolean().default(true) })
|
|
180
|
+
.default({ blocked: true })
|
|
181
|
+
export const signOutBody = z.object({ deleteUser: z.boolean().optional() }).default({})
|