@xk2800/nextjs-template 0.8.0-beta.4 → 0.8.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/dashboard/admin/feedbackTestForm.tsx +55 -0
- package/components/dashboard/admin/systemSettingsForm.tsx +28 -0
- package/components/dashboard/errorFeedback.tsx +65 -0
- package/components/dashboard/feedbackDialog.tsx +53 -11
- package/dist/activity/logger.js +12 -0
- package/dist/activity/logger.js.map +1 -1
- package/dist/activity/logger.mjs +3 -3
- package/dist/activity/queries.d.mts +1 -1
- package/dist/activity/queries.d.ts +1 -1
- package/dist/activity/queries.js +12 -0
- package/dist/activity/queries.js.map +1 -1
- package/dist/activity/queries.mjs +2 -2
- package/dist/admin/queries.js +12 -0
- package/dist/admin/queries.js.map +1 -1
- package/dist/admin/queries.mjs +2 -2
- package/dist/api/feedback.d.mts +15 -1
- package/dist/api/feedback.d.ts +15 -1
- package/dist/api/feedback.js +90 -3
- package/dist/api/feedback.js.map +1 -1
- package/dist/api/feedback.mjs +38 -9
- package/dist/api/feedback.mjs.map +1 -1
- package/dist/auth/helpers.js +13 -0
- package/dist/auth/helpers.js.map +1 -1
- package/dist/auth/helpers.mjs +5 -5
- package/dist/auth/index.js +13 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/index.mjs +5 -5
- package/dist/{chunk-IFZGEDMN.mjs → chunk-3MGEAGHU.mjs} +5 -4
- package/dist/chunk-3MGEAGHU.mjs.map +1 -0
- package/dist/chunk-KAEA35OC.mjs +76 -0
- package/dist/chunk-KAEA35OC.mjs.map +1 -0
- package/dist/{chunk-G3B7NC5B.mjs → chunk-VQ5B4MA7.mjs} +4 -4
- package/dist/{chunk-G3B7NC5B.mjs.map → chunk-VQ5B4MA7.mjs.map} +1 -1
- package/dist/{chunk-LKPF3JCV.mjs → chunk-WZDAMCUX.mjs} +3 -3
- package/dist/{chunk-LKPF3JCV.mjs.map → chunk-WZDAMCUX.mjs.map} +1 -1
- package/dist/{chunk-GS33BWOL.mjs → chunk-XWVAJXAG.mjs} +6 -6
- package/dist/{chunk-GS33BWOL.mjs.map → chunk-XWVAJXAG.mjs.map} +1 -1
- package/dist/{chunk-YPKOQOTA.mjs → chunk-ZQCALTKB.mjs} +16 -4
- package/dist/chunk-ZQCALTKB.mjs.map +1 -0
- package/dist/db/index.d.mts +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +12 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/index.mjs +2 -2
- package/dist/db/schema.d.mts +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +12 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/schema.mjs +1 -1
- package/dist/errors.d.mts +29 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +365 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +7 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/{schema-CZnQXmhF.d.mts → schema-BNSBWNjK.d.mts} +95 -2
- package/dist/{schema-CZnQXmhF.d.ts → schema-BNSBWNjK.d.ts} +95 -2
- package/dist/sessions/queries.js +12 -0
- package/dist/sessions/queries.js.map +1 -1
- package/dist/sessions/queries.mjs +2 -2
- package/dist/settings/queries.d.mts +2 -2
- package/dist/settings/queries.d.ts +2 -2
- package/dist/settings/queries.js +13 -0
- package/dist/settings/queries.js.map +1 -1
- package/dist/settings/queries.mjs +3 -3
- package/dist/users/queries.js +12 -0
- package/dist/users/queries.js.map +1 -1
- package/dist/users/queries.mjs +2 -2
- package/package.json +9 -1
- package/dist/chunk-IFZGEDMN.mjs.map +0 -1
- package/dist/chunk-YPKOQOTA.mjs.map +0 -1
package/dist/api/feedback.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import '../chunk-
|
|
1
|
+
import { lookupErrorDetail, unexpectedErrorMessage } from '../chunk-KAEA35OC.mjs';
|
|
2
|
+
import { auth, getResend, EMAIL_FROM } from '../chunk-XWVAJXAG.mjs';
|
|
3
|
+
import '../chunk-VQ5B4MA7.mjs';
|
|
3
4
|
import '../chunk-N6ECRCHY.mjs';
|
|
4
|
-
import '../chunk-
|
|
5
|
-
import '../chunk-
|
|
6
|
-
import '../chunk-
|
|
5
|
+
import { getSystemSettings } from '../chunk-3MGEAGHU.mjs';
|
|
6
|
+
import '../chunk-WZDAMCUX.mjs';
|
|
7
|
+
import '../chunk-ZQCALTKB.mjs';
|
|
7
8
|
import { config } from '../chunk-TMJYAOC2.mjs';
|
|
8
9
|
import '../chunk-3465SI5N.mjs';
|
|
9
10
|
import 'server-only';
|
|
@@ -15,9 +16,11 @@ var feedbackSchema = z.object({
|
|
|
15
16
|
type: z.enum(["bug", "feedback"]),
|
|
16
17
|
// Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.
|
|
17
18
|
message: z.string().trim().min(1).max(2e3),
|
|
18
|
-
page: z.string().max(300).optional()
|
|
19
|
+
page: z.string().max(300).optional(),
|
|
20
|
+
// The error the user reported from (FeedbackDialog's errorDetails).
|
|
21
|
+
error: z.string().max(2e3).optional()
|
|
19
22
|
});
|
|
20
|
-
var COOLDOWN_MS =
|
|
23
|
+
var COOLDOWN_MS = 0;
|
|
21
24
|
var lastSentAt = /* @__PURE__ */ new Map();
|
|
22
25
|
async function POST(request) {
|
|
23
26
|
var _a;
|
|
@@ -43,6 +46,8 @@ async function POST(request) {
|
|
|
43
46
|
}
|
|
44
47
|
lastSentAt.set(session.user.id, now);
|
|
45
48
|
const { type, message, page } = parsed.data;
|
|
49
|
+
const errorDetails = parsed.data.error && (await getSystemSettings()).feedbackAttachErrors ? parsed.data.error : void 0;
|
|
50
|
+
const technicalDetail = errorDetails ? await lookupErrorDetail(errorDetails) : null;
|
|
46
51
|
const { user } = session;
|
|
47
52
|
const title = type === "bug" ? "\u{1F41E} Bug report" : "\u{1F4AC} Feedback";
|
|
48
53
|
const userAgent = request.headers.get("user-agent") || "unknown";
|
|
@@ -63,6 +68,8 @@ async function POST(request) {
|
|
|
63
68
|
fields: [
|
|
64
69
|
{ name: "User", value: `${user.name} (${user.email})`.slice(0, 1024) },
|
|
65
70
|
{ name: "Page", value: page || "unknown", inline: true },
|
|
71
|
+
...errorDetails ? [{ name: "Error", value: errorDetails.slice(0, 1024) }] : [],
|
|
72
|
+
...technicalDetail ? [{ name: "Technical details", value: "```\n" + technicalDetail.slice(0, 1e3) + "\n```" }] : [],
|
|
66
73
|
{ name: "User agent", value: userAgent.slice(0, 1024) }
|
|
67
74
|
],
|
|
68
75
|
timestamp: new Date(now).toISOString()
|
|
@@ -84,7 +91,11 @@ async function POST(request) {
|
|
|
84
91
|
|
|
85
92
|
---
|
|
86
93
|
User: ${user.name} <${user.email}>
|
|
87
|
-
Page: ${page || "unknown"}
|
|
94
|
+
Page: ${page || "unknown"}${errorDetails ? `
|
|
95
|
+
Error: ${errorDetails}` : ""}${technicalDetail ? `
|
|
96
|
+
|
|
97
|
+
Technical details:
|
|
98
|
+
${technicalDetail}` : ""}
|
|
88
99
|
User agent: ${userAgent}`
|
|
89
100
|
})).then(({ error }) => {
|
|
90
101
|
if (error) throw new Error(error.message);
|
|
@@ -101,7 +112,25 @@ User agent: ${userAgent}`
|
|
|
101
112
|
}
|
|
102
113
|
return NextResponse.json({ success: true });
|
|
103
114
|
}
|
|
115
|
+
async function testErrorPOST(request) {
|
|
116
|
+
if (process.env.NODE_ENV !== "development") {
|
|
117
|
+
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
118
|
+
}
|
|
119
|
+
const session = await auth.api.getSession({ headers: await headers() });
|
|
120
|
+
if (!(session == null ? void 0 : session.user)) {
|
|
121
|
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
122
|
+
}
|
|
123
|
+
const body = await request.json().catch(() => null);
|
|
124
|
+
if (typeof (body == null ? void 0 : body.text) === "string" && body.text.trim() === "!error") {
|
|
125
|
+
return NextResponse.json({ error: "Test error from the feedback test form" }, { status: 400 });
|
|
126
|
+
}
|
|
127
|
+
if (typeof (body == null ? void 0 : body.text) === "string" && body.text.trim() === "!crash") {
|
|
128
|
+
const crash = new Error("Test crash: DB connection to postgres://admin:hunter2@db.internal failed");
|
|
129
|
+
return NextResponse.json({ error: unexpectedErrorMessage(crash) }, { status: 500 });
|
|
130
|
+
}
|
|
131
|
+
return NextResponse.json({ success: true });
|
|
132
|
+
}
|
|
104
133
|
|
|
105
|
-
export { POST };
|
|
134
|
+
export { POST, testErrorPOST };
|
|
106
135
|
//# sourceMappingURL=feedback.mjs.map
|
|
107
136
|
//# sourceMappingURL=feedback.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../lib/feedback-route.ts"],"names":[],"mappings":"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","file":"feedback.mjs","sourcesContent":["import \"server-only\"\n// POST handler behind components/dashboard/feedbackDialog.tsx. Shipped as\n// `@xk2800/nextjs-template/api/feedback` so apps mount it with a one-line\n// app/api/feedback/route.ts: export { POST } from \"@xk2800/nextjs-template/api/feedback\"\nimport { NextRequest, NextResponse } from \"next/server\"\nimport { headers } from \"next/headers\"\nimport { z } from \"zod\"\nimport { auth } from \"../server/auth\"\nimport { config } from \"../config/env\"\nimport { getResend, EMAIL_FROM } from \"./resend\"\n\nconst feedbackSchema = z.object({\n type: z.enum([\"bug\", \"feedback\"]),\n // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.\n message: z.string().trim().min(1).max(2000),\n page: z.string().max(300).optional(),\n})\n\nconst COOLDOWN_MS = 30_000\n// const COOLDOWN_MS = 0\n// ponytail: in-memory, per instance; move to a DB/Redis counter if one user\n// spamming across instances ever matters.\nconst lastSentAt = new Map<string, number>()\n\nexport async function POST(request: NextRequest) {\n const discordUrl = config.FEEDBACK_DISCORD_WEBHOOK_URL\n const emailTo = config.FEEDBACK_EMAIL\n if (!discordUrl && !emailTo) {\n return NextResponse.json({ error: \"Feedback is not configured\" }, { status: 503 })\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const parsed = feedbackSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return NextResponse.json({ error: \"Message must be 1-2000 characters\" }, { status: 400 })\n }\n\n const now = Date.now()\n if (now - (lastSentAt.get(session.user.id) ?? 0) < COOLDOWN_MS) {\n return NextResponse.json(\n { error: \"Please wait a moment before sending more feedback\" },\n { status: 429 }\n )\n }\n lastSentAt.set(session.user.id, now)\n\n const { type, message, page } = parsed.data\n const { user } = session\n const title = type === \"bug\" ? \"🐞 Bug report\" : \"💬 Feedback\"\n const userAgent = request.headers.get(\"user-agent\") || \"unknown\"\n\n const sends: Promise<unknown>[] = []\n\n if (discordUrl) {\n sends.push(\n fetch(discordUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n username: config.APP_NAME,\n // User text must never ping @everyone/@here/roles in the channel.\n allowed_mentions: { parse: [] },\n embeds: [{\n title,\n description: message,\n color: type === \"bug\" ? 0xef4444 : 0x3b82f6,\n fields: [\n { name: \"User\", value: `${user.name} (${user.email})`.slice(0, 1024) },\n { name: \"Page\", value: page || \"unknown\", inline: true },\n { name: \"User agent\", value: userAgent.slice(0, 1024) },\n ],\n timestamp: new Date(now).toISOString(),\n }],\n }),\n }).then((res) => {\n if (!res.ok) throw new Error(`Discord webhook responded ${res.status}`)\n })\n )\n }\n\n if (emailTo) {\n sends.push(\n getResend()\n .then((resend) => resend.emails.send({\n from: EMAIL_FROM,\n to: emailTo.split(\",\").map((s) => s.trim()),\n replyTo: user.email,\n subject: `[${config.APP_NAME}] ${title} from ${user.name}`,\n text: `${message}\\n\\n---\\nUser: ${user.name} <${user.email}>\\nPage: ${page || \"unknown\"}\\nUser agent: ${userAgent}`,\n }))\n .then(({ error }) => {\n if (error) throw new Error(error.message)\n })\n )\n }\n\n const results = await Promise.allSettled(sends)\n results.forEach((r) => {\n if (r.status === \"rejected\") console.error(\"Feedback delivery failed:\", r.reason)\n })\n\n if (!results.some((r) => r.status === \"fulfilled\")) {\n lastSentAt.delete(user.id)\n return NextResponse.json({ error: \"Failed to send feedback\" }, { status: 502 })\n }\n return NextResponse.json({ success: true })\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../lib/feedback-route.ts"],"names":[],"mappings":"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","file":"feedback.mjs","sourcesContent":["import \"server-only\"\n// POST handler behind components/dashboard/feedbackDialog.tsx. Shipped as\n// `@xk2800/nextjs-template/api/feedback` so apps mount it with a one-line\n// app/api/feedback/route.ts: export { POST } from \"@xk2800/nextjs-template/api/feedback\"\nimport { NextRequest, NextResponse } from \"next/server\"\nimport { headers } from \"next/headers\"\nimport { z } from \"zod\"\nimport { auth } from \"../server/auth\"\nimport { config } from \"../config/env\"\nimport { getResend, EMAIL_FROM } from \"./resend\"\nimport { getSystemSettings } from \"./settings-queries\"\nimport { lookupErrorDetail, unexpectedErrorMessage } from \"./errors\"\n\nconst feedbackSchema = z.object({\n type: z.enum([\"bug\", \"feedback\"]),\n // Keep in sync with FEEDBACK_MAX_LENGTH in components/dashboard/feedbackDialog.tsx.\n message: z.string().trim().min(1).max(2000),\n page: z.string().max(300).optional(),\n // The error the user reported from (FeedbackDialog's errorDetails).\n error: z.string().max(2000).optional(),\n})\n\n// const COOLDOWN_MS = 30_000\nconst COOLDOWN_MS = 0\n// ponytail: in-memory, per instance; move to a DB/Redis counter if one user\n// spamming across instances ever matters.\nconst lastSentAt = new Map<string, number>()\n\nexport async function POST(request: NextRequest) {\n const discordUrl = config.FEEDBACK_DISCORD_WEBHOOK_URL\n const emailTo = config.FEEDBACK_EMAIL\n if (!discordUrl && !emailTo) {\n return NextResponse.json({ error: \"Feedback is not configured\" }, { status: 503 })\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n\n const parsed = feedbackSchema.safeParse(await request.json().catch(() => null))\n if (!parsed.success) {\n return NextResponse.json({ error: \"Message must be 1-2000 characters\" }, { status: 400 })\n }\n\n const now = Date.now()\n if (now - (lastSentAt.get(session.user.id) ?? 0) < COOLDOWN_MS) {\n return NextResponse.json(\n { error: \"Please wait a moment before sending more feedback\" },\n { status: 429 }\n )\n }\n lastSentAt.set(session.user.id, now)\n\n const { type, message, page } = parsed.data\n // Admin-controlled (System Settings → Feedback); off drops it entirely.\n const errorDetails =\n parsed.data.error && (await getSystemSettings()).feedbackAttachErrors\n ? parsed.data.error\n : undefined\n // Technical detail stored under the error's ref (lib/errors.ts). Goes\n // server → Discord/email only; the browser never sees it.\n const technicalDetail = errorDetails ? await lookupErrorDetail(errorDetails) : null\n const { user } = session\n const title = type === \"bug\" ? \"🐞 Bug report\" : \"💬 Feedback\"\n const userAgent = request.headers.get(\"user-agent\") || \"unknown\"\n\n const sends: Promise<unknown>[] = []\n\n if (discordUrl) {\n sends.push(\n fetch(discordUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n username: config.APP_NAME,\n // User text must never ping @everyone/@here/roles in the channel.\n allowed_mentions: { parse: [] },\n embeds: [{\n title,\n description: message,\n color: type === \"bug\" ? 0xef4444 : 0x3b82f6,\n fields: [\n { name: \"User\", value: `${user.name} (${user.email})`.slice(0, 1024) },\n { name: \"Page\", value: page || \"unknown\", inline: true },\n ...(errorDetails ? [{ name: \"Error\", value: errorDetails.slice(0, 1024) }] : []),\n ...(technicalDetail\n ? [{ name: \"Technical details\", value: \"```\\n\" + technicalDetail.slice(0, 1000) + \"\\n```\" }]\n : []),\n { name: \"User agent\", value: userAgent.slice(0, 1024) },\n ],\n timestamp: new Date(now).toISOString(),\n }],\n }),\n }).then((res) => {\n if (!res.ok) throw new Error(`Discord webhook responded ${res.status}`)\n })\n )\n }\n\n if (emailTo) {\n sends.push(\n getResend()\n .then((resend) => resend.emails.send({\n from: EMAIL_FROM,\n to: emailTo.split(\",\").map((s) => s.trim()),\n replyTo: user.email,\n subject: `[${config.APP_NAME}] ${title} from ${user.name}`,\n text: `${message}\\n\\n---\\nUser: ${user.name} <${user.email}>\\nPage: ${page || \"unknown\"}${errorDetails ? `\\nError: ${errorDetails}` : \"\"}${technicalDetail ? `\\n\\nTechnical details:\\n${technicalDetail}` : \"\"}\\nUser agent: ${userAgent}`,\n }))\n .then(({ error }) => {\n if (error) throw new Error(error.message)\n })\n )\n }\n\n const results = await Promise.allSettled(sends)\n results.forEach((r) => {\n if (r.status === \"rejected\") console.error(\"Feedback delivery failed:\", r.reason)\n })\n\n if (!results.some((r) => r.status === \"fulfilled\")) {\n lastSentAt.delete(user.id)\n return NextResponse.json({ error: \"Failed to send feedback\" }, { status: 502 })\n }\n return NextResponse.json({ success: true })\n}\n\n/**\n * Development-only endpoint behind FeedbackTestForm, for testing the\n * report-from-error flow. \"!error\" answers with a safe, user-facing error;\n * \"!crash\" simulates an unexpected failure, so the user gets a generic message\n * with an error ref and the technical detail only reaches the server log.\n * 404 outside development.\n * Mount with app/api/feedback/test/route.ts:\n * export { testErrorPOST as POST } from \"@xk2800/nextjs-template/api/feedback\"\n */\nexport async function testErrorPOST(request: NextRequest) {\n if (process.env.NODE_ENV !== \"development\") {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 })\n }\n const session = await auth.api.getSession({ headers: await headers() })\n if (!session?.user) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 })\n }\n const body = await request.json().catch(() => null)\n if (typeof body?.text === \"string\" && body.text.trim() === \"!error\") {\n return NextResponse.json({ error: \"Test error from the feedback test form\" }, { status: 400 })\n }\n if (typeof body?.text === \"string\" && body.text.trim() === \"!crash\") {\n const crash = new Error(\"Test crash: DB connection to postgres://admin:hunter2@db.internal failed\")\n return NextResponse.json({ error: unexpectedErrorMessage(crash) }, { status: 500 })\n }\n return NextResponse.json({ success: true })\n}\n"]}
|
package/dist/auth/helpers.js
CHANGED
|
@@ -211,6 +211,7 @@ __export(schema_exports, {
|
|
|
211
211
|
activityLogs: () => activityLogs,
|
|
212
212
|
authThrottle: () => authThrottle,
|
|
213
213
|
deviceFingerprints: () => deviceFingerprints,
|
|
214
|
+
errorRefs: () => errorRefs,
|
|
214
215
|
passkeys: () => passkeys,
|
|
215
216
|
sessions: () => sessions,
|
|
216
217
|
systemSettings: () => systemSettings,
|
|
@@ -393,6 +394,9 @@ var systemSettings = pgCore.pgTable("system_settings", {
|
|
|
393
394
|
authEnableEmailPassword: pgCore.boolean("authEnableEmailPassword").notNull().default(true),
|
|
394
395
|
authEnableOneTap: pgCore.boolean("authEnableOneTap").notNull().default(false),
|
|
395
396
|
enableSessionRevocation: pgCore.boolean("enableSessionRevocation").notNull().default(true),
|
|
397
|
+
// Attach the error a user hit to feedback they send from it (see
|
|
398
|
+
// lib/feedback-route.ts). Off = only what the user typed is sent.
|
|
399
|
+
feedbackAttachErrors: pgCore.boolean("feedbackAttachErrors").notNull().default(true),
|
|
396
400
|
updatedAt: pgCore.timestamp("updatedAt").notNull().defaultNow(),
|
|
397
401
|
// Who last changed a setting. This row doesn't "belong" to that admin the
|
|
398
402
|
// way sessions/activityLogs belong to their user, so deleting that admin's
|
|
@@ -401,6 +405,14 @@ var systemSettings = pgCore.pgTable("system_settings", {
|
|
|
401
405
|
}, (table) => [
|
|
402
406
|
pgCore.check("system_settings_singleton", drizzleOrm.sql`${table.id} = 'default'`)
|
|
403
407
|
]);
|
|
408
|
+
var errorRefs = pgCore.pgTable("error_refs", {
|
|
409
|
+
ref: pgCore.text("ref").primaryKey(),
|
|
410
|
+
message: pgCore.text("message").notNull(),
|
|
411
|
+
detail: pgCore.text("detail").notNull(),
|
|
412
|
+
createdAt: pgCore.timestamp("createdAt").notNull().defaultNow()
|
|
413
|
+
}, (table) => [
|
|
414
|
+
pgCore.index("error_refs_created_at_idx").on(table.createdAt)
|
|
415
|
+
]);
|
|
404
416
|
var envSchema = zod.z.object({
|
|
405
417
|
NODE_ENV: zod.z.enum(["development", "production", "test"]).default("development"),
|
|
406
418
|
PORT: zod.z.string().default("3000").transform(Number),
|
|
@@ -603,6 +615,7 @@ function fallbackSettings() {
|
|
|
603
615
|
authEnableEmailPassword: config.AUTH_ENABLE_EMAIL_PASSWORD,
|
|
604
616
|
authEnableOneTap: config.AUTH_ENABLE_ONE_TAP,
|
|
605
617
|
enableSessionRevocation: config.ENABLE_SESSION_REVOCATION,
|
|
618
|
+
feedbackAttachErrors: true,
|
|
606
619
|
updatedAt: /* @__PURE__ */ new Date(0),
|
|
607
620
|
updatedBy: null
|
|
608
621
|
};
|