@guettonenrique/passwordless-auth 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +387 -2
- package/dist/functions/index.cjs +391 -0
- package/dist/functions/index.cjs.map +1 -0
- package/dist/functions/index.d.cts +70 -0
- package/dist/functions/index.d.ts +70 -0
- package/dist/functions/index.js +348 -0
- package/dist/functions/index.js.map +1 -0
- package/dist/react-native/index.cjs +617 -0
- package/dist/react-native/index.cjs.map +1 -0
- package/dist/react-native/index.d.cts +64 -0
- package/dist/react-native/index.d.ts +64 -0
- package/dist/react-native/index.js +606 -0
- package/dist/react-native/index.js.map +1 -0
- package/dist/shared/index.cjs +151 -0
- package/dist/shared/index.cjs.map +1 -0
- package/dist/shared/index.d.cts +50 -0
- package/dist/shared/index.d.ts +50 -0
- package/dist/shared/index.js +111 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/types-BUi8ZPam.d.cts +110 -0
- package/dist/types-BUi8ZPam.d.ts +110 -0
- package/package.json +91 -4
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// src/functions/requestEmailOtp.ts
|
|
2
|
+
import { onCall, HttpsError } from "firebase-functions/https";
|
|
3
|
+
import { getFirestore, FieldValue } from "firebase-admin/firestore";
|
|
4
|
+
|
|
5
|
+
// src/shared/constants.ts
|
|
6
|
+
var CODE_LENGTH = 6;
|
|
7
|
+
var RESEND_COOLDOWN_MS = 60 * 1e3;
|
|
8
|
+
var OTP_EXPIRY_MS = 10 * 60 * 1e3;
|
|
9
|
+
var MAX_ATTEMPTS = 5;
|
|
10
|
+
var DEFAULT_COLLECTION = "auth_otps";
|
|
11
|
+
var DEFAULT_CLEANUP_SCHEDULE = "every 15 minutes";
|
|
12
|
+
var CLEANUP_BATCH_SIZE = 400;
|
|
13
|
+
|
|
14
|
+
// src/shared/types.ts
|
|
15
|
+
var OTP_ERROR_CODES = {
|
|
16
|
+
InvalidArgument: "invalid-argument",
|
|
17
|
+
NotFound: "not-found",
|
|
18
|
+
DeadlineExceeded: "deadline-exceeded",
|
|
19
|
+
ResourceExhausted: "resource-exhausted",
|
|
20
|
+
Internal: "internal"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/shared/utils.ts
|
|
24
|
+
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
25
|
+
function normalizeEmail(raw) {
|
|
26
|
+
return raw.trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
function isValidEmail(raw) {
|
|
29
|
+
return EMAIL_REGEX.test(normalizeEmail(raw));
|
|
30
|
+
}
|
|
31
|
+
function isOtpCode(value, length = CODE_LENGTH) {
|
|
32
|
+
return new RegExp(`^\\d{${length}}$`).test(value);
|
|
33
|
+
}
|
|
34
|
+
function cooldownMessage(remainingMs) {
|
|
35
|
+
const seconds = Math.ceil(remainingMs / 1e3);
|
|
36
|
+
return `Please wait ${seconds} second${seconds === 1 ? "" : "s"} before requesting a new code.`;
|
|
37
|
+
}
|
|
38
|
+
function attemptsMessage(remaining) {
|
|
39
|
+
return `Invalid verification code. You have ${remaining} attempt${remaining === 1 ? "" : "s"} remaining.`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/shared/email-template.ts
|
|
43
|
+
function buildOtpEmail(input) {
|
|
44
|
+
const { to, appName, code, expiresMinutes } = input;
|
|
45
|
+
const subject = `${appName} - Your verification code is ${code}`;
|
|
46
|
+
const text = `Your verification code for ${appName} is: ${code}
|
|
47
|
+
|
|
48
|
+
This code is valid for ${expiresMinutes} minutes.
|
|
49
|
+
If you did not request this, please ignore this email.`;
|
|
50
|
+
const html = `
|
|
51
|
+
<!DOCTYPE html>
|
|
52
|
+
<html>
|
|
53
|
+
<head>
|
|
54
|
+
<meta charset="utf-8">
|
|
55
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
56
|
+
<title>${escapeHtml(subject)}</title>
|
|
57
|
+
</head>
|
|
58
|
+
<body style="margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
|
|
59
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f8fafc; padding: 40px 16px;">
|
|
60
|
+
<tr>
|
|
61
|
+
<td align="center">
|
|
62
|
+
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 480px; background-color: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);">
|
|
63
|
+
<tr>
|
|
64
|
+
<td style="padding: 32px 32px 16px 32px; text-align: center;">
|
|
65
|
+
<h1 style="margin: 0 0 8px 0; font-size: 24px; font-weight: 700; color: #1e293b;">${escapeHtml(appName)}</h1>
|
|
66
|
+
<p style="margin: 0; font-size: 15px; color: #64748b;">Sign in to your account</p>
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
<tr>
|
|
70
|
+
<td style="padding: 16px 32px 24px 32px; text-align: center;">
|
|
71
|
+
<p style="margin: 0; margin-bottom: 16px; font-size: 14px; color: #475569;">Enter this 6-digit code in the app to continue:</p>
|
|
72
|
+
<div style="display: inline-block; background-color: #f1f5f9; border: 2px dashed #cbd5e1; border-radius: 10px; padding: 16px 32px; font-size: 34px; font-weight: 800; letter-spacing: 8px; color: #0f172a; font-family: monospace;">
|
|
73
|
+
${escapeHtml(code)}
|
|
74
|
+
</div>
|
|
75
|
+
<p style="margin: 16px 0 0 0; font-size: 13px; color: #94a3b8;">
|
|
76
|
+
This code will expire in <strong>${expiresMinutes} minutes</strong>.
|
|
77
|
+
</p>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
<tr>
|
|
81
|
+
<td style="padding: 20px 32px; background-color: #f8fafc; border-top: 1px solid #e2e8f0; text-align: center;">
|
|
82
|
+
<p style="margin: 0; font-size: 12px; color: #94a3b8;">
|
|
83
|
+
If you did not request this verification code, you can safely ignore this email.
|
|
84
|
+
</p>
|
|
85
|
+
</td>
|
|
86
|
+
</tr>
|
|
87
|
+
</table>
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
</table>
|
|
91
|
+
</body>
|
|
92
|
+
</html>
|
|
93
|
+
`;
|
|
94
|
+
return { to, subject, text, html };
|
|
95
|
+
}
|
|
96
|
+
function escapeHtml(value) {
|
|
97
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/functions/crypto.ts
|
|
101
|
+
import { randomInt, createHash, timingSafeEqual } from "crypto";
|
|
102
|
+
function generateOtpCode(length = CODE_LENGTH) {
|
|
103
|
+
return randomInt(10 ** (length - 1), 10 ** length).toString();
|
|
104
|
+
}
|
|
105
|
+
function hashOtpCode(email, code) {
|
|
106
|
+
return createHash("sha256").update(`${email}:${code}`).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
function isOtpHashMatch(expectedHex, actualHex) {
|
|
109
|
+
const expected = Buffer.from(expectedHex, "utf-8");
|
|
110
|
+
const actual = Buffer.from(actualHex, "utf-8");
|
|
111
|
+
return expected.length === actual.length && timingSafeEqual(expected, actual);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/functions/senders/sendgrid.ts
|
|
115
|
+
var resolveApiKey = (key) => {
|
|
116
|
+
if (typeof key === "string") return key;
|
|
117
|
+
if (typeof key === "function") return key();
|
|
118
|
+
return key.value();
|
|
119
|
+
};
|
|
120
|
+
function createSendgridSender(options) {
|
|
121
|
+
return async (email) => {
|
|
122
|
+
const sgMail = (await import("@sendgrid/mail")).default;
|
|
123
|
+
sgMail.setApiKey(resolveApiKey(options.apiKey));
|
|
124
|
+
await sgMail.send({
|
|
125
|
+
to: email.to,
|
|
126
|
+
from: { email: options.fromEmail, name: options.fromName },
|
|
127
|
+
subject: email.subject,
|
|
128
|
+
text: email.text,
|
|
129
|
+
html: email.html
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/functions/requestEmailOtp.ts
|
|
135
|
+
function createRequestEmailOtp(options) {
|
|
136
|
+
const collection = options.collection ?? DEFAULT_COLLECTION;
|
|
137
|
+
const cooldownMs = options.cooldownMs ?? RESEND_COOLDOWN_MS;
|
|
138
|
+
const expiryMs = options.expiryMs ?? OTP_EXPIRY_MS;
|
|
139
|
+
const sendEmail = options.sendEmail ?? (() => {
|
|
140
|
+
if (!options.sendgrid) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"createRequestEmailOtp: provide either `sendgrid` options or a custom `sendEmail`."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return createSendgridSender(options.sendgrid);
|
|
146
|
+
})();
|
|
147
|
+
return onCall(
|
|
148
|
+
{
|
|
149
|
+
enforceAppCheck: options.enforceAppCheck ?? false
|
|
150
|
+
// NOTE: if using the default SendGrid sender, register the project's
|
|
151
|
+
// secret here, e.g. `secrets: [mySecret]`. The factory cannot know it —
|
|
152
|
+
// see README.
|
|
153
|
+
},
|
|
154
|
+
async (request) => {
|
|
155
|
+
const rawEmail = request.data?.email;
|
|
156
|
+
if (!rawEmail || typeof rawEmail !== "string") {
|
|
157
|
+
throw new HttpsError(OTP_ERROR_CODES.InvalidArgument, "Email is required.");
|
|
158
|
+
}
|
|
159
|
+
const email = normalizeEmail(rawEmail);
|
|
160
|
+
if (!isValidEmail(email)) {
|
|
161
|
+
throw new HttpsError(
|
|
162
|
+
OTP_ERROR_CODES.InvalidArgument,
|
|
163
|
+
"Please enter a valid email address."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const db = getFirestore();
|
|
167
|
+
const otpDocRef = db.collection(collection).doc(email);
|
|
168
|
+
const existingSnap = await otpDocRef.get();
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
if (existingSnap.exists) {
|
|
171
|
+
const data = existingSnap.data();
|
|
172
|
+
const lastSentAt = data?.lastSentAt || 0;
|
|
173
|
+
const elapsed = now - lastSentAt;
|
|
174
|
+
if (elapsed < cooldownMs) {
|
|
175
|
+
throw new HttpsError(
|
|
176
|
+
OTP_ERROR_CODES.ResourceExhausted,
|
|
177
|
+
cooldownMessage(cooldownMs - elapsed)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const code = generateOtpCode(CODE_LENGTH);
|
|
182
|
+
const codeHash = hashOtpCode(email, code);
|
|
183
|
+
await otpDocRef.set({
|
|
184
|
+
hash: codeHash,
|
|
185
|
+
attempts: 0,
|
|
186
|
+
expiresAt: now + expiryMs,
|
|
187
|
+
lastSentAt: now,
|
|
188
|
+
createdAt: FieldValue.serverTimestamp()
|
|
189
|
+
});
|
|
190
|
+
const composed = buildOtpEmail({
|
|
191
|
+
to: email,
|
|
192
|
+
appName: options.appName,
|
|
193
|
+
code,
|
|
194
|
+
expiresMinutes: Math.round(expiryMs / 6e4)
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
await sendEmail(composed);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error("Failed to send OTP email:", err);
|
|
200
|
+
await otpDocRef.delete().catch(() => null);
|
|
201
|
+
throw new HttpsError(
|
|
202
|
+
OTP_ERROR_CODES.Internal,
|
|
203
|
+
"Failed to deliver verification code email. Please try again."
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
status: "success",
|
|
208
|
+
message: "Verification code sent."
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/functions/verifyEmailOtp.ts
|
|
215
|
+
import { onCall as onCall2, HttpsError as HttpsError2 } from "firebase-functions/https";
|
|
216
|
+
import { getAuth } from "firebase-admin/auth";
|
|
217
|
+
import { getFirestore as getFirestore2 } from "firebase-admin/firestore";
|
|
218
|
+
function createVerifyEmailOtp(options) {
|
|
219
|
+
const collection = options.collection ?? DEFAULT_COLLECTION;
|
|
220
|
+
const maxAttempts = options.maxAttempts ?? MAX_ATTEMPTS;
|
|
221
|
+
return onCall2(
|
|
222
|
+
{
|
|
223
|
+
enforceAppCheck: options.enforceAppCheck ?? false
|
|
224
|
+
},
|
|
225
|
+
async (request) => {
|
|
226
|
+
const rawEmail = request.data?.email;
|
|
227
|
+
const rawCode = request.data?.code;
|
|
228
|
+
if (!rawEmail || typeof rawEmail !== "string") {
|
|
229
|
+
throw new HttpsError2(OTP_ERROR_CODES.InvalidArgument, "Email is required.");
|
|
230
|
+
}
|
|
231
|
+
const email = normalizeEmail(rawEmail);
|
|
232
|
+
if (!isValidEmail(email)) {
|
|
233
|
+
throw new HttpsError2(
|
|
234
|
+
OTP_ERROR_CODES.InvalidArgument,
|
|
235
|
+
"Please enter a valid email address."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (!rawCode || typeof rawCode !== "string") {
|
|
239
|
+
throw new HttpsError2(
|
|
240
|
+
OTP_ERROR_CODES.InvalidArgument,
|
|
241
|
+
"Verification code is required."
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const code = rawCode.trim();
|
|
245
|
+
if (!isOtpCode(code, CODE_LENGTH)) {
|
|
246
|
+
throw new HttpsError2(
|
|
247
|
+
OTP_ERROR_CODES.InvalidArgument,
|
|
248
|
+
`Verification code must be exactly ${CODE_LENGTH} digits.`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const db = getFirestore2();
|
|
252
|
+
const otpDocRef = db.collection(collection).doc(email);
|
|
253
|
+
let verificationError = null;
|
|
254
|
+
await db.runTransaction(async (transaction) => {
|
|
255
|
+
const otpSnap = await transaction.get(otpDocRef);
|
|
256
|
+
if (!otpSnap.exists) {
|
|
257
|
+
verificationError = new HttpsError2(
|
|
258
|
+
OTP_ERROR_CODES.NotFound,
|
|
259
|
+
"No active verification code found for this email. Please request a new code."
|
|
260
|
+
);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const otpData = otpSnap.data();
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
if (otpData?.expiresAt && now > otpData.expiresAt) {
|
|
266
|
+
transaction.delete(otpDocRef);
|
|
267
|
+
verificationError = new HttpsError2(
|
|
268
|
+
OTP_ERROR_CODES.DeadlineExceeded,
|
|
269
|
+
"This verification code has expired. Please request a new code."
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const currentAttempts = otpData?.attempts || 0;
|
|
274
|
+
if (currentAttempts >= maxAttempts) {
|
|
275
|
+
transaction.delete(otpDocRef);
|
|
276
|
+
verificationError = new HttpsError2(
|
|
277
|
+
OTP_ERROR_CODES.ResourceExhausted,
|
|
278
|
+
"Too many incorrect attempts. Please request a new verification code."
|
|
279
|
+
);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const isMatch = isOtpHashMatch(otpData?.hash || "", hashOtpCode(email, code));
|
|
283
|
+
if (!isMatch) {
|
|
284
|
+
const newAttempts = currentAttempts + 1;
|
|
285
|
+
if (newAttempts >= maxAttempts) {
|
|
286
|
+
transaction.delete(otpDocRef);
|
|
287
|
+
verificationError = new HttpsError2(
|
|
288
|
+
OTP_ERROR_CODES.ResourceExhausted,
|
|
289
|
+
"Too many incorrect attempts. Please request a new verification code."
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
transaction.update(otpDocRef, { attempts: newAttempts });
|
|
293
|
+
verificationError = new HttpsError2(
|
|
294
|
+
OTP_ERROR_CODES.InvalidArgument,
|
|
295
|
+
attemptsMessage(maxAttempts - newAttempts)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
transaction.delete(otpDocRef);
|
|
301
|
+
});
|
|
302
|
+
if (verificationError) {
|
|
303
|
+
throw verificationError;
|
|
304
|
+
}
|
|
305
|
+
const uid = await options.provision(email);
|
|
306
|
+
const customToken = await getAuth().createCustomToken(uid);
|
|
307
|
+
return {
|
|
308
|
+
status: "success",
|
|
309
|
+
customToken
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/functions/cleanupExpiredOtps.ts
|
|
316
|
+
import { onSchedule } from "firebase-functions/scheduler";
|
|
317
|
+
import { getFirestore as getFirestore3 } from "firebase-admin/firestore";
|
|
318
|
+
function createCleanupExpiredOtps(options = {}) {
|
|
319
|
+
const schedule = options.schedule ?? DEFAULT_CLEANUP_SCHEDULE;
|
|
320
|
+
const collection = options.collection ?? DEFAULT_COLLECTION;
|
|
321
|
+
const batchSize = options.batchSize ?? CLEANUP_BATCH_SIZE;
|
|
322
|
+
return onSchedule(schedule, async () => {
|
|
323
|
+
const db = getFirestore3();
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
let deleted = 0;
|
|
326
|
+
for (; ; ) {
|
|
327
|
+
const snap = await db.collection(collection).where("expiresAt", "<", now).limit(batchSize).get();
|
|
328
|
+
if (snap.empty) break;
|
|
329
|
+
const batch = db.batch();
|
|
330
|
+
snap.docs.forEach((doc) => batch.delete(doc.ref));
|
|
331
|
+
await batch.commit();
|
|
332
|
+
deleted += snap.docs.length;
|
|
333
|
+
}
|
|
334
|
+
if (deleted > 0) {
|
|
335
|
+
console.log(`[passwordless-auth] deleted ${deleted} expired OTP doc(s)`);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
export {
|
|
340
|
+
createCleanupExpiredOtps,
|
|
341
|
+
createRequestEmailOtp,
|
|
342
|
+
createSendgridSender,
|
|
343
|
+
createVerifyEmailOtp,
|
|
344
|
+
generateOtpCode,
|
|
345
|
+
hashOtpCode,
|
|
346
|
+
isOtpHashMatch
|
|
347
|
+
};
|
|
348
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/functions/requestEmailOtp.ts","../../src/shared/constants.ts","../../src/shared/types.ts","../../src/shared/utils.ts","../../src/shared/email-template.ts","../../src/functions/crypto.ts","../../src/functions/senders/sendgrid.ts","../../src/functions/verifyEmailOtp.ts","../../src/functions/cleanupExpiredOtps.ts"],"sourcesContent":["import { onCall, HttpsError } from 'firebase-functions/https';\nimport { getFirestore, FieldValue } from 'firebase-admin/firestore';\nimport {\n CODE_LENGTH,\n DEFAULT_COLLECTION,\n OTP_EXPIRY_MS,\n RESEND_COOLDOWN_MS,\n cooldownMessage,\n buildOtpEmail,\n isValidEmail,\n normalizeEmail,\n OTP_ERROR_CODES,\n type RequestOtpOptions,\n type RequestOtpPayload,\n type RequestOtpResponse,\n} from '../shared';\nimport { generateOtpCode, hashOtpCode } from './crypto';\nimport { createSendgridSender } from './senders/sendgrid';\n\n/**\n * Factory for the `requestEmailOtp` callable.\n *\n * Returns a ready-to-register Firebase callable function:\n *\n * ```ts\n * exports.requestEmailOtp = createRequestEmailOtp({\n * appName: 'MyApp',\n * sendgrid: { apiKey: defineSecret('SENDGRID_API_KEY'), fromEmail, fromName },\n * });\n * ```\n */\nexport function createRequestEmailOtp(options: RequestOtpOptions) {\n const collection = options.collection ?? DEFAULT_COLLECTION;\n const cooldownMs = options.cooldownMs ?? RESEND_COOLDOWN_MS;\n const expiryMs = options.expiryMs ?? OTP_EXPIRY_MS;\n\n const sendEmail =\n options.sendEmail ??\n (() => {\n if (!options.sendgrid) {\n throw new Error(\n 'createRequestEmailOtp: provide either `sendgrid` options or a custom `sendEmail`.'\n );\n }\n return createSendgridSender(options.sendgrid);\n })();\n\n return onCall<RequestOtpPayload, Promise<RequestOtpResponse>>(\n {\n enforceAppCheck: options.enforceAppCheck ?? false,\n // NOTE: if using the default SendGrid sender, register the project's\n // secret here, e.g. `secrets: [mySecret]`. The factory cannot know it —\n // see README.\n },\n async (request) => {\n const rawEmail = request.data?.email;\n\n if (!rawEmail || typeof rawEmail !== 'string') {\n throw new HttpsError(OTP_ERROR_CODES.InvalidArgument, 'Email is required.');\n }\n\n const email = normalizeEmail(rawEmail);\n if (!isValidEmail(email)) {\n throw new HttpsError(\n OTP_ERROR_CODES.InvalidArgument,\n 'Please enter a valid email address.'\n );\n }\n\n const db = getFirestore();\n const otpDocRef = db.collection(collection).doc(email);\n const existingSnap = await otpDocRef.get();\n\n const now = Date.now();\n\n // Enforce the resend cooldown\n if (existingSnap.exists) {\n const data = existingSnap.data();\n const lastSentAt = data?.lastSentAt || 0;\n const elapsed = now - lastSentAt;\n if (elapsed < cooldownMs) {\n throw new HttpsError(\n OTP_ERROR_CODES.ResourceExhausted,\n cooldownMessage(cooldownMs - elapsed)\n );\n }\n }\n\n const code = generateOtpCode(CODE_LENGTH);\n const codeHash = hashOtpCode(email, code);\n\n await otpDocRef.set({\n hash: codeHash,\n attempts: 0,\n expiresAt: now + expiryMs,\n lastSentAt: now,\n createdAt: FieldValue.serverTimestamp(),\n });\n\n const composed = buildOtpEmail({\n to: email,\n appName: options.appName,\n code,\n expiresMinutes: Math.round(expiryMs / 60000),\n });\n\n try {\n await sendEmail(composed);\n } catch (err) {\n console.error('Failed to send OTP email:', err);\n // Clean up the created OTP document if delivery fails\n await otpDocRef.delete().catch(() => null);\n throw new HttpsError(\n OTP_ERROR_CODES.Internal,\n 'Failed to deliver verification code email. Please try again.'\n );\n }\n\n return {\n status: 'success' as const,\n message: 'Verification code sent.',\n };\n }\n );\n}\n","/**\n * Single source of truth for the OTP contract.\n *\n * Backend factories and mobile components both read these as their defaults,\n * so the UI cooldown and the backend rate limit cannot drift apart silently.\n * A project may override them via factory options — if it does, override the\n * SAME value on both sides (e.g. a custom cooldownMs in the backend factory\n * AND as the `cooldownMs` prop on the mobile modal).\n */\n\n/** Number of digits in an OTP code. */\nexport const CODE_LENGTH = 6;\n\n/** Minimum time between two code requests for the same email. */\nexport const RESEND_COOLDOWN_MS = 60 * 1000; // 60 seconds\n\n/** Lifetime of an issued code. */\nexport const OTP_EXPIRY_MS = 10 * 60 * 1000; // 10 minutes\n\n/** Failed verifications allowed per code before it is invalidated. */\nexport const MAX_ATTEMPTS = 5;\n\n/** Firestore collection that stores pending OTPs (doc id = email). */\nexport const DEFAULT_COLLECTION = 'auth_otps';\n\n/** Default schedule for the cleanup function that reaps expired docs. */\nexport const DEFAULT_CLEANUP_SCHEDULE = 'every 15 minutes';\n\n/** Writes per batch in the cleanup drain loop (Firestore limit is 500). */\nexport const CLEANUP_BATCH_SIZE = 400;\n","// ============================================================================\n// Payloads (App -> Backend)\n// ============================================================================\n\nexport interface RequestOtpPayload {\n email: string;\n}\n\nexport interface VerifyOtpPayload {\n email: string;\n code: string;\n}\n\n// ============================================================================\n// Responses (Backend -> App)\n// ============================================================================\n\nexport interface RequestOtpResponse {\n status: 'success';\n message: string;\n}\n\nexport interface VerifyOtpResponse {\n status: 'success';\n /** Firebase custom auth token — exchange with signInWithCustomToken. */\n customToken: string;\n}\n\n// ============================================================================\n// Backend option types\n// ============================================================================\n\n/** A fully composed OTP email, ready to dispatch. */\nexport interface OtpEmail {\n to: string;\n subject: string;\n text: string;\n html: string;\n}\n\n/**\n * Delivers the OTP email. The default implementation sends via SendGrid;\n * projects can replace it entirely (custom provider, or a console-logging\n * sender during development).\n */\nexport type OtpEmailSender = (email: OtpEmail) => Promise<void>;\n\n/** Inputs for {@link buildOtpEmail}. */\nexport interface OtpEmailInput {\n to: string;\n appName: string;\n /** The plaintext 6-digit code — only ever passed to the sender, never persisted. */\n code: string;\n /** Code lifetime in minutes, for the email copy. */\n expiresMinutes: number;\n}\n\nexport interface SendgridSenderOptions {\n /**\n * The API key, resolved per call. Accepts a plain string, a getter\n * function (`defineSecret('X').value`), or a Firebase params Secret\n * object (`defineSecret('X')`) directly.\n */\n apiKey: string | (() => string) | { value: () => string };\n fromEmail: string;\n fromName: string;\n}\n\n/** Base options shared by the request/verify factories. */\nexport interface BaseOtpOptions {\n /** Firestore collection holding pending OTPs. Default: `auth_otps`. */\n collection?: string;\n /** Resend cooldown in ms. Default: 60_000. */\n cooldownMs?: number;\n /** Code lifetime in ms. Default: 600_000 (10 minutes). */\n expiryMs?: number;\n /** Failed verifications before the code is invalidated. Default: 5. */\n maxAttempts?: number;\n /** Enforce App Check on the callable. Default: false (flip true in production). */\n enforceAppCheck?: boolean;\n}\n\nexport interface RequestOtpOptions extends BaseOtpOptions {\n /** Brand name used in the email subject and body. Required. */\n appName: string;\n /** Replace the default SendGrid delivery entirely. */\n sendEmail?: OtpEmailSender;\n /** SendGrid sender config; required unless `sendEmail` is provided. */\n sendgrid?: SendgridSenderOptions;\n}\n\n/**\n * Resolves (and, if needed, creates) the auth user for an email, returning\n * the uid the custom token will be minted for. This is the one required\n * project-specific hook — it owns the app's user schema.\n */\nexport type OtpProvisioner = (email: string) => Promise<string>;\n\nexport interface VerifyOtpOptions extends BaseOtpOptions {\n /** Required. Creates or resolves the user and returns their uid. */\n provision: OtpProvisioner;\n}\n\nexport interface CleanupOtpOptions {\n /** Cloud Scheduler cron. Default: `every 15 minutes`. */\n schedule?: string;\n /** Collection to clean. Default: `auth_otps`. */\n collection?: string;\n /** Writes per batch. Default: 400 (Firestore limit is 500). */\n batchSize?: number;\n}\n\n// ============================================================================\n// Stored document shape (Firestore `auth_otps/{email}`)\n// ============================================================================\n\nexport interface OtpDocumentData {\n /** SHA-256 hex of `email:code`. The plaintext code is never stored. */\n hash: string;\n /** Failed guess counter. */\n attempts: number;\n /** Expiry as epoch milliseconds. */\n expiresAt: number;\n /** Last dispatch as epoch milliseconds (drives the resend cooldown). */\n lastSentAt: number;\n createdAt?: unknown;\n}\n\n// ============================================================================\n// Error codes (HttpsError status codes used by the factories)\n// ============================================================================\n\nexport const OTP_ERROR_CODES = {\n InvalidArgument: 'invalid-argument',\n NotFound: 'not-found',\n DeadlineExceeded: 'deadline-exceeded',\n ResourceExhausted: 'resource-exhausted',\n Internal: 'internal',\n} as const;\n\nexport type OtpErrorCode = (typeof OTP_ERROR_CODES)[keyof typeof OTP_ERROR_CODES];\n","import { CODE_LENGTH } from './constants';\n\n/** Deliberately simple RFC-ish check — mirrors the client-side pre-check. */\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Trim + lowercase an email so doc keys and hash inputs always match. */\nexport function normalizeEmail(raw: string): string {\n return raw.trim().toLowerCase();\n}\n\nexport function isValidEmail(raw: string): boolean {\n return EMAIL_REGEX.test(normalizeEmail(raw));\n}\n\nexport function isOtpCode(value: string, length: number = CODE_LENGTH): boolean {\n return new RegExp(`^\\\\d{${length}}$`).test(value);\n}\n\n/**\n * Formats the resend-cooldown error message exactly as the backend emits it.\n * Exported so mobile copy (if ever shown manually) cannot drift from it.\n */\nexport function cooldownMessage(remainingMs: number): string {\n const seconds = Math.ceil(remainingMs / 1000);\n return `Please wait ${seconds} second${seconds === 1 ? '' : 's'} before requesting a new code.`;\n}\n\n/** Formats the remaining-attempts message for a wrong-code rejection. */\nexport function attemptsMessage(remaining: number): string {\n return `Invalid verification code. You have ${remaining} attempt${remaining === 1 ? '' : 's'} remaining.`;\n}\n","import type { OtpEmail, OtpEmailInput } from './types';\n\n/**\n * Builds a fully composed OTP email { to, subject, text, html }.\n *\n * Inline HTML (no SendGrid dynamic template) on purpose: zero template\n * management per project, and the look is good enough for a code-delivery\n * email. Projects with a branded template should pass their own `sendEmail`\n * instead of customizing this builder.\n */\nexport function buildOtpEmail(input: OtpEmailInput): OtpEmail {\n const { to, appName, code, expiresMinutes } = input;\n\n const subject = `${appName} - Your verification code is ${code}`;\n\n const text =\n `Your verification code for ${appName} is: ${code}\\n\\n` +\n `This code is valid for ${expiresMinutes} minutes.\\n` +\n `If you did not request this, please ignore this email.`;\n\n const html = `\n <!DOCTYPE html>\n <html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${escapeHtml(subject)}</title>\n </head>\n <body style=\"margin: 0; padding: 0; background-color: #f8fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background-color: #f8fafc; padding: 40px 16px;\">\n <tr>\n <td align=\"center\">\n <table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"max-width: 480px; background-color: #ffffff; border-radius: 12px; border: 1px solid #e2e8f0; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);\">\n <tr>\n <td style=\"padding: 32px 32px 16px 32px; text-align: center;\">\n <h1 style=\"margin: 0 0 8px 0; font-size: 24px; font-weight: 700; color: #1e293b;\">${escapeHtml(appName)}</h1>\n <p style=\"margin: 0; font-size: 15px; color: #64748b;\">Sign in to your account</p>\n </td>\n </tr>\n <tr>\n <td style=\"padding: 16px 32px 24px 32px; text-align: center;\">\n <p style=\"margin: 0; margin-bottom: 16px; font-size: 14px; color: #475569;\">Enter this 6-digit code in the app to continue:</p>\n <div style=\"display: inline-block; background-color: #f1f5f9; border: 2px dashed #cbd5e1; border-radius: 10px; padding: 16px 32px; font-size: 34px; font-weight: 800; letter-spacing: 8px; color: #0f172a; font-family: monospace;\">\n ${escapeHtml(code)}\n </div>\n <p style=\"margin: 16px 0 0 0; font-size: 13px; color: #94a3b8;\">\n This code will expire in <strong>${expiresMinutes} minutes</strong>.\n </p>\n </td>\n </tr>\n <tr>\n <td style=\"padding: 20px 32px; background-color: #f8fafc; border-top: 1px solid #e2e8f0; text-align: center;\">\n <p style=\"margin: 0; font-size: 12px; color: #94a3b8;\">\n If you did not request this verification code, you can safely ignore this email.\n </p>\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n </html>\n `;\n\n return { to, subject, text, html };\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n","import { randomInt, createHash, timingSafeEqual } from 'node:crypto';\nimport { CODE_LENGTH } from '../shared';\n\n/**\n * Node-only crypto helpers. These live in the `functions` entry (not `shared`)\n * because they import `node:crypto`, which must never be bundled into the\n * React Native entry.\n */\n\n/** Generates a cryptographically secure numeric code of `length` digits. */\nexport function generateOtpCode(length: number = CODE_LENGTH): string {\n return randomInt(10 ** (length - 1), 10 ** length).toString();\n}\n\n/** SHA-256 hex digest of `email:code` — the value stored in Firestore. */\nexport function hashOtpCode(email: string, code: string): string {\n return createHash('sha256').update(`${email}:${code}`).digest('hex');\n}\n\n/** Timing-safe comparison of two hex digests. */\nexport function isOtpHashMatch(expectedHex: string, actualHex: string): boolean {\n const expected = Buffer.from(expectedHex, 'utf-8');\n const actual = Buffer.from(actualHex, 'utf-8');\n return expected.length === actual.length && timingSafeEqual(expected, actual);\n}\n","import type { OtpEmailSender, SendgridSenderOptions } from '../../shared';\n\n/**\n * Default email delivery via SendGrid.\n *\n * `@sendgrid/mail` is imported lazily (dynamic import, not a static import)\n * because it is an *optional* peer dependency — projects that pass a custom\n * `sendEmail` must not need SendGrid installed at all.\n */\nconst resolveApiKey = (key: SendgridSenderOptions['apiKey']): string => {\n if (typeof key === 'string') return key;\n if (typeof key === 'function') return key();\n return key.value();\n};\n\nexport function createSendgridSender(options: SendgridSenderOptions): OtpEmailSender {\n return async (email) => {\n const sgMail = (await import('@sendgrid/mail')).default;\n sgMail.setApiKey(resolveApiKey(options.apiKey));\n\n await sgMail.send({\n to: email.to,\n from: { email: options.fromEmail, name: options.fromName },\n subject: email.subject,\n text: email.text,\n html: email.html,\n });\n };\n}\n","import { onCall, HttpsError } from 'firebase-functions/https';\nimport { getAuth } from 'firebase-admin/auth';\nimport { getFirestore } from 'firebase-admin/firestore';\nimport {\n CODE_LENGTH,\n DEFAULT_COLLECTION,\n MAX_ATTEMPTS,\n OTP_ERROR_CODES,\n attemptsMessage,\n isValidEmail,\n isOtpCode,\n normalizeEmail,\n type VerifyOtpOptions,\n type VerifyOtpPayload,\n type VerifyOtpResponse,\n} from '../shared';\nimport { hashOtpCode, isOtpHashMatch } from './crypto';\n\n/**\n * Factory for the `verifyEmailOtp` callable.\n *\n * Returns a ready-to-register Firebase callable function:\n *\n * ```ts\n * exports.verifyEmailOtp = createVerifyEmailOtp({\n * provision: async (email) => {\n * const { user } = await resolveOrCreateUser(email, { emailVerified: true });\n * return user.uid;\n * },\n * });\n * ```\n */\nexport function createVerifyEmailOtp(options: VerifyOtpOptions) {\n const collection = options.collection ?? DEFAULT_COLLECTION;\n const maxAttempts = options.maxAttempts ?? MAX_ATTEMPTS;\n\n return onCall<VerifyOtpPayload, Promise<VerifyOtpResponse>>(\n {\n enforceAppCheck: options.enforceAppCheck ?? false,\n },\n async (request) => {\n const rawEmail = request.data?.email;\n const rawCode = request.data?.code;\n\n if (!rawEmail || typeof rawEmail !== 'string') {\n throw new HttpsError(OTP_ERROR_CODES.InvalidArgument, 'Email is required.');\n }\n\n const email = normalizeEmail(rawEmail);\n if (!isValidEmail(email)) {\n throw new HttpsError(\n OTP_ERROR_CODES.InvalidArgument,\n 'Please enter a valid email address.'\n );\n }\n\n if (!rawCode || typeof rawCode !== 'string') {\n throw new HttpsError(\n OTP_ERROR_CODES.InvalidArgument,\n 'Verification code is required.'\n );\n }\n\n const code = rawCode.trim();\n if (!isOtpCode(code, CODE_LENGTH)) {\n throw new HttpsError(\n OTP_ERROR_CODES.InvalidArgument,\n `Verification code must be exactly ${CODE_LENGTH} digits.`\n );\n }\n\n const db = getFirestore();\n const otpDocRef = db.collection(collection).doc(email);\n\n let verificationError: HttpsError | null = null;\n\n await db.runTransaction(async (transaction) => {\n const otpSnap = await transaction.get(otpDocRef);\n\n if (!otpSnap.exists) {\n verificationError = new HttpsError(\n OTP_ERROR_CODES.NotFound,\n 'No active verification code found for this email. Please request a new code.'\n );\n return;\n }\n\n const otpData = otpSnap.data();\n const now = Date.now();\n\n // 1. Expiration check\n if (otpData?.expiresAt && now > otpData.expiresAt) {\n transaction.delete(otpDocRef);\n verificationError = new HttpsError(\n OTP_ERROR_CODES.DeadlineExceeded,\n 'This verification code has expired. Please request a new code.'\n );\n return;\n }\n\n // 2. Max attempts check\n const currentAttempts = otpData?.attempts || 0;\n if (currentAttempts >= maxAttempts) {\n transaction.delete(otpDocRef);\n verificationError = new HttpsError(\n OTP_ERROR_CODES.ResourceExhausted,\n 'Too many incorrect attempts. Please request a new verification code.'\n );\n return;\n }\n\n // 3. Timing-safe hash validation\n const isMatch = isOtpHashMatch(otpData?.hash || '', hashOtpCode(email, code));\n\n if (!isMatch) {\n const newAttempts = currentAttempts + 1;\n\n if (newAttempts >= maxAttempts) {\n transaction.delete(otpDocRef);\n verificationError = new HttpsError(\n OTP_ERROR_CODES.ResourceExhausted,\n 'Too many incorrect attempts. Please request a new verification code.'\n );\n } else {\n transaction.update(otpDocRef, { attempts: newAttempts });\n verificationError = new HttpsError(\n OTP_ERROR_CODES.InvalidArgument,\n attemptsMessage(maxAttempts - newAttempts)\n );\n }\n return;\n }\n\n // 4. Valid code → delete inside the transaction (single-use, atomic)\n transaction.delete(otpDocRef);\n });\n\n if (verificationError) {\n throw verificationError;\n }\n\n // 5. Project-specific user resolution & provisioning\n const uid = await options.provision(email);\n\n // 6. Mint a Firebase custom token for the client to sign in with\n const customToken = await getAuth().createCustomToken(uid);\n\n return {\n status: 'success' as const,\n customToken,\n };\n }\n );\n}\n","import { onSchedule } from 'firebase-functions/scheduler';\nimport { getFirestore } from 'firebase-admin/firestore';\nimport {\n CLEANUP_BATCH_SIZE,\n DEFAULT_CLEANUP_SCHEDULE,\n DEFAULT_COLLECTION,\n type CleanupOtpOptions,\n} from '../shared';\n\n/**\n * Factory for the scheduled function that reaps expired OTP docs.\n *\n * `requestEmailOtp` writes `auth_otps/{email}` before sending the email, and\n * the doc is only removed on a successful verify, the final failed attempt,\n * or a verify attempt made after expiry. A user who requests a code and never\n * enters it orphans the doc — dead data, but it accumulates one per email\n * seen. This function deletes every doc whose `expiresAt` has passed, on a\n * timer, so the collection stays bounded with no manual intervention.\n *\n * ```ts\n * exports.cleanupExpiredOtps = createCleanupExpiredOtps();\n * ```\n *\n * The drain loop re-runs the filtered query until it is empty (deleting in\n * batches) rather than cursor-paginating: the docs we delete are exactly the\n * ones the query matches, so the shrinking result set is trivially correct —\n * and the final empty query is the certificate that nothing expired remains.\n */\nexport function createCleanupExpiredOtps(options: CleanupOtpOptions = {}) {\n const schedule = options.schedule ?? DEFAULT_CLEANUP_SCHEDULE;\n const collection = options.collection ?? DEFAULT_COLLECTION;\n const batchSize = options.batchSize ?? CLEANUP_BATCH_SIZE;\n\n return onSchedule(schedule, async () => {\n const db = getFirestore();\n const now = Date.now();\n\n let deleted = 0;\n\n for (;;) {\n const snap = await db\n .collection(collection)\n .where('expiresAt', '<', now)\n .limit(batchSize)\n .get();\n\n if (snap.empty) break;\n\n const batch = db.batch();\n snap.docs.forEach((doc) => batch.delete(doc.ref));\n await batch.commit();\n\n deleted += snap.docs.length;\n }\n\n if (deleted > 0) {\n console.log(`[passwordless-auth] deleted ${deleted} expired OTP doc(s)`);\n }\n });\n}\n"],"mappings":"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","names":["onCall","HttpsError","getFirestore","onCall","HttpsError","getFirestore","getFirestore","getFirestore"]}
|