@open-mercato/onboarding 0.6.7-develop.6631.1.9d64b49ddc → 0.6.7-develop.6646.1.be515f3d49

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.
@@ -22,8 +22,7 @@ const feedbackSchema = z.object({
22
22
  email: z.string().email(),
23
23
  message: z.string().max(5e3).optional().default(""),
24
24
  termsAccepted: z.literal(true),
25
- marketingConsent: z.boolean().optional().default(false),
26
- sendCopy: z.boolean().optional().default(true)
25
+ marketingConsent: z.boolean().optional().default(false)
27
26
  });
28
27
  async function POST(req) {
29
28
  const { error: rateLimitError } = await checkAuthRateLimit({
@@ -41,7 +40,7 @@ async function POST(req) {
41
40
  if (!parsed.success) {
42
41
  return NextResponse.json({ ok: false, error: "Please check the form and try again." }, { status: 400 });
43
42
  }
44
- const { email, message, marketingConsent, sendCopy } = parsed.data;
43
+ const { email, message, marketingConsent } = parsed.data;
45
44
  const adminEmail = process.env.ADMIN_EMAIL || "piotr@catchthetornado.com";
46
45
  const marketingText = marketingConsent ? "Marketing consent: Yes" : "Marketing consent: No";
47
46
  const adminCopy = {
@@ -65,33 +64,13 @@ async function POST(req) {
65
64
  logger.error("Admin email failed", { err });
66
65
  return NextResponse.json({ ok: false, error: "Failed to send feedback. Please try again." }, { status: 502 });
67
66
  }
68
- if (sendCopy) {
69
- const userCopy = {
70
- preview: "Your feedback to Open Mercato",
71
- heading: "Thank you for reaching out!",
72
- body: "We received your message and will get back to you shortly. Below is a copy of what you submitted.",
73
- messageLabel: "Your message:",
74
- message: message || "(no message provided)",
75
- marketingConsent: marketingText,
76
- footer: "Open Mercato \xB7 demo.openmercato.com"
77
- };
78
- try {
79
- await sendEmail({
80
- to: email,
81
- subject: "Your feedback to Open Mercato",
82
- react: FeedbackEmail({ copy: userCopy })
83
- });
84
- } catch (err) {
85
- logger.error("User copy email failed", { err });
86
- }
87
- }
88
67
  return NextResponse.json({ ok: true });
89
68
  }
90
69
  var route_default = POST;
91
70
  const feedbackTag = "Demo";
92
71
  const feedbackPostDoc = {
93
72
  summary: "Submit demo feedback",
94
- description: "Sends a feedback/contact request from the demo environment to the admin and optionally to the user.",
73
+ description: "Sends a feedback/contact request from the demo environment to the configured admin.",
95
74
  tags: [feedbackTag],
96
75
  requestBody: {
97
76
  contentType: "application/json",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/onboarding/api/demo-feedback/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport FeedbackEmail from '@open-mercato/onboarding/modules/onboarding/emails/FeedbackEmail'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('onboarding').child({ component: 'demo-feedback' })\n\nconst demoFeedbackIpRateLimitConfig = readEndpointRateLimitConfig('DEMO_FEEDBACK_IP', {\n points: 5, duration: 300, blockDuration: 300, keyPrefix: 'demo-feedback-ip',\n})\n\nexport const metadata = {\n path: '/onboarding/demo-feedback',\n POST: {\n requireAuth: false,\n },\n}\n\nconst feedbackSchema = z.object({\n email: z.string().email(),\n message: z.string().max(5000).optional().default(''),\n termsAccepted: z.literal(true),\n marketingConsent: z.boolean().optional().default(false),\n sendCopy: z.boolean().optional().default(true),\n})\n\nexport async function POST(req: Request) {\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: demoFeedbackIpRateLimitConfig,\n })\n if (rateLimitError) return rateLimitError\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid payload' }, { status: 400 })\n }\n\n const parsed = feedbackSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Please check the form and try again.' }, { status: 400 })\n }\n\n const { email, message, marketingConsent, sendCopy } = parsed.data\n const adminEmail = process.env.ADMIN_EMAIL || 'piotr@catchthetornado.com'\n\n const marketingText = marketingConsent ? 'Marketing consent: Yes' : 'Marketing consent: No'\n\n const adminCopy = {\n preview: `Demo feedback from ${email}`,\n heading: 'New demo feedback',\n body: `${email} submitted a feedback/contact request from the demo environment.`,\n senderEmailLabel: 'From email:',\n senderEmail: email,\n messageLabel: 'Message:',\n message: message || '(no message provided)',\n marketingConsent: marketingText,\n footer: 'Open Mercato \\u00b7 Demo feedback',\n }\n\n try {\n await sendEmail({\n to: adminEmail,\n subject: `Demo feedback from ${email}`,\n react: FeedbackEmail({ copy: adminCopy }),\n })\n } catch (err) {\n logger.error('Admin email failed', { err })\n return NextResponse.json({ ok: false, error: 'Failed to send feedback. Please try again.' }, { status: 502 })\n }\n\n if (sendCopy) {\n const userCopy = {\n preview: 'Your feedback to Open Mercato',\n heading: 'Thank you for reaching out!',\n body: 'We received your message and will get back to you shortly. Below is a copy of what you submitted.',\n messageLabel: 'Your message:',\n message: message || '(no message provided)',\n marketingConsent: marketingText,\n footer: 'Open Mercato \\u00b7 demo.openmercato.com',\n }\n try {\n await sendEmail({\n to: email,\n subject: 'Your feedback to Open Mercato',\n react: FeedbackEmail({ copy: userCopy }),\n })\n } catch (err) {\n logger.error('User copy email failed', { err })\n }\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport default POST\n\nconst feedbackTag = 'Demo'\n\nconst feedbackPostDoc: OpenApiMethodDoc = {\n summary: 'Submit demo feedback',\n description: 'Sends a feedback/contact request from the demo environment to the admin and optionally to the user.',\n tags: [feedbackTag],\n requestBody: {\n contentType: 'application/json',\n schema: feedbackSchema,\n description: 'Feedback form payload.',\n },\n responses: [\n { status: 200, description: 'Feedback sent successfully.' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: feedbackTag,\n summary: 'Demo feedback submission',\n methods: {\n POST: feedbackPostDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport FeedbackEmail from '@open-mercato/onboarding/modules/onboarding/emails/FeedbackEmail'\nimport { checkAuthRateLimit } from '@open-mercato/core/modules/auth/lib/rateLimitCheck'\nimport { readEndpointRateLimitConfig } from '@open-mercato/shared/lib/ratelimit/config'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('onboarding').child({ component: 'demo-feedback' })\n\nconst demoFeedbackIpRateLimitConfig = readEndpointRateLimitConfig('DEMO_FEEDBACK_IP', {\n points: 5, duration: 300, blockDuration: 300, keyPrefix: 'demo-feedback-ip',\n})\n\nexport const metadata = {\n path: '/onboarding/demo-feedback',\n POST: {\n requireAuth: false,\n },\n}\n\nconst feedbackSchema = z.object({\n email: z.string().email(),\n message: z.string().max(5000).optional().default(''),\n termsAccepted: z.literal(true),\n marketingConsent: z.boolean().optional().default(false),\n})\n\nexport async function POST(req: Request) {\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: demoFeedbackIpRateLimitConfig,\n })\n if (rateLimitError) return rateLimitError\n\n let payload: unknown\n try {\n payload = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid payload' }, { status: 400 })\n }\n\n const parsed = feedbackSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Please check the form and try again.' }, { status: 400 })\n }\n\n const { email, message, marketingConsent } = parsed.data\n const adminEmail = process.env.ADMIN_EMAIL || 'piotr@catchthetornado.com'\n\n const marketingText = marketingConsent ? 'Marketing consent: Yes' : 'Marketing consent: No'\n\n const adminCopy = {\n preview: `Demo feedback from ${email}`,\n heading: 'New demo feedback',\n body: `${email} submitted a feedback/contact request from the demo environment.`,\n senderEmailLabel: 'From email:',\n senderEmail: email,\n messageLabel: 'Message:',\n message: message || '(no message provided)',\n marketingConsent: marketingText,\n footer: 'Open Mercato \\u00b7 Demo feedback',\n }\n\n try {\n await sendEmail({\n to: adminEmail,\n subject: `Demo feedback from ${email}`,\n react: FeedbackEmail({ copy: adminCopy }),\n })\n } catch (err) {\n logger.error('Admin email failed', { err })\n return NextResponse.json({ ok: false, error: 'Failed to send feedback. Please try again.' }, { status: 502 })\n }\n\n return NextResponse.json({ ok: true })\n}\n\nexport default POST\n\nconst feedbackTag = 'Demo'\n\nconst feedbackPostDoc: OpenApiMethodDoc = {\n summary: 'Submit demo feedback',\n description: 'Sends a feedback/contact request from the demo environment to the configured admin.',\n tags: [feedbackTag],\n requestBody: {\n contentType: 'application/json',\n schema: feedbackSchema,\n description: 'Feedback form payload.',\n },\n responses: [\n { status: 200, description: 'Feedback sent successfully.' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: feedbackTag,\n summary: 'Demo feedback submission',\n methods: {\n POST: feedbackPostDoc,\n },\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAClB,SAAS,iBAAiB;AAC1B,OAAO,mBAAmB;AAC1B,SAAS,0BAA0B;AACnC,SAAS,mCAAmC;AAE5C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,WAAW,gBAAgB,CAAC;AAE9E,MAAM,gCAAgC,4BAA4B,oBAAoB;AAAA,EACpF,QAAQ;AAAA,EAAG,UAAU;AAAA,EAAK,eAAe;AAAA,EAAK,WAAW;AAC3D,CAAC;AAEM,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,aAAa;AAAA,EACf;AACF;AAEA,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,EACxB,SAAS,EAAE,OAAO,EAAE,IAAI,GAAI,EAAE,SAAS,EAAE,QAAQ,EAAE;AAAA,EACnD,eAAe,EAAE,QAAQ,IAAI;AAAA,EAC7B,kBAAkB,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK;AACxD,CAAC;AAED,eAAsB,KAAK,KAAc;AACvC,QAAM,EAAE,OAAO,eAAe,IAAI,MAAM,mBAAmB;AAAA,IACzD;AAAA,IACA,UAAU;AAAA,EACZ,CAAC;AACD,MAAI,eAAgB,QAAO;AAE3B,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,IAAI,KAAK;AAAA,EAC3B,QAAQ;AACN,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,kBAAkB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACnF;AAEA,QAAM,SAAS,eAAe,UAAU,OAAO;AAC/C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,uCAAuC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxG;AAEA,QAAM,EAAE,OAAO,SAAS,iBAAiB,IAAI,OAAO;AACpD,QAAM,aAAa,QAAQ,IAAI,eAAe;AAE9C,QAAM,gBAAgB,mBAAmB,2BAA2B;AAEpE,QAAM,YAAY;AAAA,IAChB,SAAS,sBAAsB,KAAK;AAAA,IACpC,SAAS;AAAA,IACT,MAAM,GAAG,KAAK;AAAA,IACd,kBAAkB;AAAA,IAClB,aAAa;AAAA,IACb,cAAc;AAAA,IACd,SAAS,WAAW;AAAA,IACpB,kBAAkB;AAAA,IAClB,QAAQ;AAAA,EACV;AAEA,MAAI;AACF,UAAM,UAAU;AAAA,MACd,IAAI;AAAA,MACJ,SAAS,sBAAsB,KAAK;AAAA,MACpC,OAAO,cAAc,EAAE,MAAM,UAAU,CAAC;AAAA,IAC1C,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,WAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC;AAC1C,WAAO,aAAa,KAAK,EAAE,IAAI,OAAO,OAAO,6CAA6C,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9G;AAEA,SAAO,aAAa,KAAK,EAAE,IAAI,KAAK,CAAC;AACvC;AAEA,IAAO,gBAAQ;AAEf,MAAM,cAAc;AAEpB,MAAM,kBAAoC;AAAA,EACxC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,WAAW;AAAA,EAClB,aAAa;AAAA,IACX,aAAa;AAAA,IACb,QAAQ;AAAA,IACR,aAAa;AAAA,EACf;AAAA,EACA,WAAW;AAAA,IACT,EAAE,QAAQ,KAAK,aAAa,8BAA8B;AAAA,EAC5D;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,EACR;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
1
  import { after, NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
3
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
5
  import { createLogger } from "@open-mercato/shared/lib/logger";
5
6
  import { assertAllowedAppOrigin, mapSecurityEmailUrlError } from "@open-mercato/shared/lib/url";
@@ -39,6 +40,9 @@ function readCookie(req, name) {
39
40
  return null;
40
41
  }
41
42
  async function GET(req) {
43
+ if (parseBooleanToken(process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? "") !== true) {
44
+ return NextResponse.json({ ok: false, error: "Self-service onboarding is disabled." }, { status: 404 });
45
+ }
42
46
  const url = new URL(req.url);
43
47
  const tenantId = url.searchParams.get("tenantId") || url.searchParams.get("tenant") || "";
44
48
  const parsed = onboardingStatusQuerySchema.safeParse({ tenantId });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/onboarding/api/get/onboarding/status.ts"],
4
- "sourcesContent": ["import { after, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { assertAllowedAppOrigin, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\nimport {\n resolveProvisioningIds,\n runDeferredProvisioning,\n} from '@open-mercato/onboarding/modules/onboarding/lib/deferred-provisioning'\nimport { isPreparationClaimActive } from '@open-mercato/onboarding/modules/onboarding/lib/preparation-claim'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst logger = createLogger('onboarding').child({ component: 'status' })\n\nexport const metadata = {\n path: '/onboarding/onboarding/status',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ONBOARDING_LOGIN_TENANT_COOKIE = 'om_login_tenant'\n\nconst onboardingStatusQuerySchema = z.object({\n tenantId: z.string().uuid(),\n})\n\nfunction readCookie(req: Request, name: string): string | null {\n const header = req.headers.get('cookie')\n if (!header) return null\n for (const part of header.split(';')) {\n const separatorIndex = part.indexOf('=')\n if (separatorIndex === -1) continue\n const key = part.slice(0, separatorIndex).trim()\n if (key !== name) continue\n const rawValue = part.slice(separatorIndex + 1).trim()\n try {\n return decodeURIComponent(rawValue)\n } catch {\n return rawValue\n }\n }\n return null\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''\n const parsed = onboardingStatusQuerySchema.safeParse({ tenantId })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })\n }\n\n const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE)\n if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {\n return NextResponse.json({ ok: false, error: 'Not authorized for this tenant.' }, { status: 403 })\n }\n\n try {\n assertAllowedAppOrigin(req)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'onboarding.status',\n configMessage: 'Onboarding status is not configured.',\n })\n if (mapped) return NextResponse.json({ ok: false, error: mapped.body.error }, { status: mapped.status })\n throw error\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = new OnboardingService(em)\n const request = await service.findLatestByTenantId(parsed.data.tenantId)\n if (!request) {\n return NextResponse.json({ ok: false, error: 'Onboarding request not found.' }, { status: 404 })\n }\n\n const provisioningIds = resolveProvisioningIds(request)\n if (provisioningIds && request.status === 'processing') {\n await service.markCompleted(request, provisioningIds)\n }\n // Schedule deferred provisioning only while no runner holds a fresh claim \u2014\n // otherwise every ~1s poll piles another full seed + reindex chain onto the\n // connection pool. The atomic claim inside runDeferredProvisioning remains\n // the authoritative gate; this check just keeps polls cheap.\n if (\n provisioningIds &&\n request.status === 'completed' &&\n !request.preparationCompletedAt &&\n !isPreparationClaimActive(request.preparationStartedAt)\n ) {\n after(async () => {\n await runDeferredProvisioning({\n requestId: request.id,\n tenantId: provisioningIds.tenantId,\n organizationId: provisioningIds.organizationId,\n })\n })\n }\n\n const emailSent = Boolean(request.readyEmailSentAt)\n const ready = request.status === 'completed' && Boolean(request.preparationCompletedAt)\n const loginUrl = ready && request.tenantId ? `/login?tenant=${encodeURIComponent(request.tenantId)}` : null\n\n if (ready && request.tenantId && !request.readyEmailSentAt) {\n const readyTenantId = request.tenantId\n after(async () => {\n await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: readyTenantId,\n }).catch((error) => {\n logger.error('Ready email retry failed', {\n requestId: request.id,\n tenantId: readyTenantId,\n organizationId: request.organizationId,\n err: error,\n })\n })\n })\n }\n\n return NextResponse.json({\n ok: true,\n status: request.status,\n ready,\n emailSent,\n tenantId: request.tenantId ?? parsed.data.tenantId,\n loginUrl,\n })\n}\n\nconst onboardingTag = 'Onboarding'\n\nconst onboardingStatusSuccessSchema = z.object({\n ok: z.literal(true),\n status: z.enum(['pending', 'processing', 'completed', 'expired']),\n ready: z.boolean(),\n emailSent: z.boolean(),\n tenantId: z.string().uuid(),\n loginUrl: z.string().nullable(),\n})\n\nconst onboardingStatusErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst onboardingStatusDoc: OpenApiMethodDoc = {\n summary: 'Get onboarding preparation status',\n description: 'Resolves whether a tenant workspace finished deferred onboarding preparation and can be opened.',\n tags: [onboardingTag],\n query: onboardingStatusQuerySchema,\n responses: [\n { status: 200, description: 'Onboarding status resolved.', schema: onboardingStatusSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid tenant id or request origin.', schema: onboardingStatusErrorSchema },\n { status: 403, description: 'Caller is not authorized for this tenant.', schema: onboardingStatusErrorSchema },\n { status: 404, description: 'Onboarding request not found.', schema: onboardingStatusErrorSchema },\n { status: 500, description: 'Onboarding status is not configured.', schema: onboardingStatusErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: onboardingTag,\n summary: 'Onboarding preparation status',\n methods: {\n GET: onboardingStatusDoc,\n },\n}\n\nexport default GET\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { after, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { assertAllowedAppOrigin, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\nimport {\n resolveProvisioningIds,\n runDeferredProvisioning,\n} from '@open-mercato/onboarding/modules/onboarding/lib/deferred-provisioning'\nimport { isPreparationClaimActive } from '@open-mercato/onboarding/modules/onboarding/lib/preparation-claim'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nconst logger = createLogger('onboarding').child({ component: 'status' })\n\nexport const metadata = {\n path: '/onboarding/onboarding/status',\n GET: {\n requireAuth: false,\n },\n}\n\nconst ONBOARDING_LOGIN_TENANT_COOKIE = 'om_login_tenant'\n\nconst onboardingStatusQuerySchema = z.object({\n tenantId: z.string().uuid(),\n})\n\nfunction readCookie(req: Request, name: string): string | null {\n const header = req.headers.get('cookie')\n if (!header) return null\n for (const part of header.split(';')) {\n const separatorIndex = part.indexOf('=')\n if (separatorIndex === -1) continue\n const key = part.slice(0, separatorIndex).trim()\n if (key !== name) continue\n const rawValue = part.slice(separatorIndex + 1).trim()\n try {\n return decodeURIComponent(rawValue)\n } catch {\n return rawValue\n }\n }\n return null\n}\n\nexport async function GET(req: Request) {\n if (parseBooleanToken(process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? '') !== true) {\n return NextResponse.json({ ok: false, error: 'Self-service onboarding is disabled.' }, { status: 404 })\n }\n const url = new URL(req.url)\n const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''\n const parsed = onboardingStatusQuerySchema.safeParse({ tenantId })\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })\n }\n\n const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE)\n if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {\n return NextResponse.json({ ok: false, error: 'Not authorized for this tenant.' }, { status: 403 })\n }\n\n try {\n assertAllowedAppOrigin(req)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'onboarding.status',\n configMessage: 'Onboarding status is not configured.',\n })\n if (mapped) return NextResponse.json({ ok: false, error: mapped.body.error }, { status: mapped.status })\n throw error\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = new OnboardingService(em)\n const request = await service.findLatestByTenantId(parsed.data.tenantId)\n if (!request) {\n return NextResponse.json({ ok: false, error: 'Onboarding request not found.' }, { status: 404 })\n }\n\n const provisioningIds = resolveProvisioningIds(request)\n if (provisioningIds && request.status === 'processing') {\n await service.markCompleted(request, provisioningIds)\n }\n // Schedule deferred provisioning only while no runner holds a fresh claim \u2014\n // otherwise every ~1s poll piles another full seed + reindex chain onto the\n // connection pool. The atomic claim inside runDeferredProvisioning remains\n // the authoritative gate; this check just keeps polls cheap.\n if (\n provisioningIds &&\n request.status === 'completed' &&\n !request.preparationCompletedAt &&\n !isPreparationClaimActive(request.preparationStartedAt)\n ) {\n after(async () => {\n await runDeferredProvisioning({\n requestId: request.id,\n tenantId: provisioningIds.tenantId,\n organizationId: provisioningIds.organizationId,\n })\n })\n }\n\n const emailSent = Boolean(request.readyEmailSentAt)\n const ready = request.status === 'completed' && Boolean(request.preparationCompletedAt)\n const loginUrl = ready && request.tenantId ? `/login?tenant=${encodeURIComponent(request.tenantId)}` : null\n\n if (ready && request.tenantId && !request.readyEmailSentAt) {\n const readyTenantId = request.tenantId\n after(async () => {\n await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: readyTenantId,\n }).catch((error) => {\n logger.error('Ready email retry failed', {\n requestId: request.id,\n tenantId: readyTenantId,\n organizationId: request.organizationId,\n err: error,\n })\n })\n })\n }\n\n return NextResponse.json({\n ok: true,\n status: request.status,\n ready,\n emailSent,\n tenantId: request.tenantId ?? parsed.data.tenantId,\n loginUrl,\n })\n}\n\nconst onboardingTag = 'Onboarding'\n\nconst onboardingStatusSuccessSchema = z.object({\n ok: z.literal(true),\n status: z.enum(['pending', 'processing', 'completed', 'expired']),\n ready: z.boolean(),\n emailSent: z.boolean(),\n tenantId: z.string().uuid(),\n loginUrl: z.string().nullable(),\n})\n\nconst onboardingStatusErrorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst onboardingStatusDoc: OpenApiMethodDoc = {\n summary: 'Get onboarding preparation status',\n description: 'Resolves whether a tenant workspace finished deferred onboarding preparation and can be opened.',\n tags: [onboardingTag],\n query: onboardingStatusQuerySchema,\n responses: [\n { status: 200, description: 'Onboarding status resolved.', schema: onboardingStatusSuccessSchema },\n ],\n errors: [\n { status: 400, description: 'Invalid tenant id or request origin.', schema: onboardingStatusErrorSchema },\n { status: 403, description: 'Caller is not authorized for this tenant.', schema: onboardingStatusErrorSchema },\n { status: 404, description: 'Onboarding request not found.', schema: onboardingStatusErrorSchema },\n { status: 500, description: 'Onboarding status is not configured.', schema: onboardingStatusErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: onboardingTag,\n summary: 'Onboarding preparation status',\n methods: {\n GET: onboardingStatusDoc,\n },\n}\n\nexport default GET\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,6 @@
1
1
  import { after, NextResponse } from "next/server";
2
2
  import { z } from "zod";
3
+ import { parseBooleanToken } from "@open-mercato/shared/lib/boolean";
3
4
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
5
  import { onboardingVerifySchema } from "@open-mercato/onboarding/modules/onboarding/data/validators";
5
6
  import { OnboardingService } from "@open-mercato/onboarding/modules/onboarding/lib/service";
@@ -85,6 +86,9 @@ async function completeProvisionedRequest(args) {
85
86
  return ids;
86
87
  }
87
88
  async function GET(req) {
89
+ if (parseBooleanToken(process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? "") !== true) {
90
+ return NextResponse.json({ ok: false, error: "Self-service onboarding is disabled." }, { status: 404 });
91
+ }
88
92
  const url = new URL(req.url);
89
93
  const baseUrlResult = resolveVerifyRedirectBaseUrl(req);
90
94
  if (!baseUrlResult.ok) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/onboarding/api/get/onboarding/verify.ts"],
4
- "sourcesContent": ["import { after, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { onboardingVerifySchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\nimport type { OnboardingRequest } from '@open-mercato/onboarding/modules/onboarding/data/entities'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\nimport {\n redirectToLogin,\n redirectToPreparing,\n redirectWithStatus,\n} from '@open-mercato/onboarding/modules/onboarding/lib/verify-redirects'\nimport { resolveVerifyRedirectBaseUrl } from '@open-mercato/onboarding/modules/onboarding/lib/verify-base-url'\nimport {\n resolveProvisioningIds,\n runDeferredProvisioning,\n} from '@open-mercato/onboarding/modules/onboarding/lib/deferred-provisioning'\nimport { setupInitialTenant } from '@open-mercato/core/modules/auth/lib/setup-app'\nimport { UserConsent } from '@open-mercato/core/modules/auth/data/entities'\nimport { computeConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'\nimport { resolveConsentClientIp } from '@open-mercato/onboarding/modules/onboarding/lib/consentClientIp'\nimport { runBestEffortProvisioningStep } from '@open-mercato/onboarding/modules/onboarding/lib/provisioning'\nimport { getModules } from '@open-mercato/shared/lib/modules/registry'\nimport { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('onboarding').child({ component: 'verify' })\n\nexport const metadata = {\n path: '/onboarding/onboarding/verify',\n GET: {\n requireAuth: false,\n },\n}\n\nconst SEED_DEFAULTS_TIMEOUT_MS = 15_000\n\nfunction createTimeoutPromise(label: string, timeoutMs: number): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)\n })\n}\n\nasync function runModuleSetupHook(args: {\n moduleId: string\n phase: 'seedDefaults' | 'seedExamples'\n timeoutMs: number\n run: () => Promise<void>\n}) {\n const startedAt = Date.now()\n logger.info('Module hook started', {\n moduleId: args.moduleId,\n phase: args.phase,\n timeoutMs: args.timeoutMs,\n })\n try {\n await Promise.race([\n args.run(),\n createTimeoutPromise(`module ${args.moduleId} ${args.phase}`, args.timeoutMs),\n ])\n logger.info('Module hook completed', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n })\n } catch (error) {\n if (isUniqueViolation(error)) {\n // A concurrent verify (or a re-verify after the request was already\n // provisioned) re-runs seedDefaults against rows that exist. seed hooks\n // are not fully idempotent, so the collision is expected and harmless \u2014\n // the workspace already exists. Log at info so real failures stand out.\n logger.info('Module hook skipped (already seeded)', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n })\n } else {\n logger.error('Module hook failed', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n timeoutMs: args.timeoutMs,\n err: error,\n })\n }\n throw error\n }\n}\n\nasync function completeProvisionedRequest(args: {\n service: OnboardingService\n request: OnboardingRequest\n}) {\n const ids = resolveProvisioningIds(args.request)\n if (!ids) return null\n await args.service.markCompleted(args.request, ids)\n after(async () => {\n await runDeferredProvisioning({\n requestId: args.request.id,\n tenantId: ids.tenantId,\n organizationId: ids.organizationId,\n })\n })\n return ids\n}\n\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrlResult = resolveVerifyRedirectBaseUrl(req)\n if (!baseUrlResult.ok) {\n if (baseUrlResult.redirectOrigin) {\n return redirectWithStatus(baseUrlResult.redirectOrigin, baseUrlResult.status)\n }\n return NextResponse.json(\n { ok: false, error: baseUrlResult.message },\n { status: baseUrlResult.httpStatus },\n )\n }\n const baseUrl = baseUrlResult.baseUrl\n const token = url.searchParams.get('token') ?? ''\n const parsed = onboardingVerifySchema.safeParse({ token })\n if (!parsed.success) {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const service = new OnboardingService(em)\n const request = await service.findByToken(parsed.data.token)\n if (!request) {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n if (request.expiresAt <= new Date() && request.status !== 'completed') {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n if (request.status === 'completed' && request.tenantId) {\n if (!request.preparationCompletedAt) {\n return redirectToPreparing(baseUrl, request.tenantId)\n }\n if (!request.readyEmailSentAt) {\n after(async () => {\n await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: request.tenantId!,\n }).catch((error) => {\n logger.error('Retry ready email failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n err: error,\n })\n })\n })\n }\n return redirectToLogin(baseUrl, request.tenantId)\n }\n const lockWindowMs = 15 * 60 * 1000\n const processingStartedAt = request.processingStartedAt?.getTime() ?? 0\n const processingFresh = request.status === 'processing' && processingStartedAt > Date.now() - lockWindowMs\n if (processingFresh) {\n const recovered = await completeProvisionedRequest({ service, request })\n if (recovered) return redirectToPreparing(baseUrl, recovered.tenantId)\n return redirectToPreparing(baseUrl, request.tenantId ?? null)\n }\n if (request.status === 'processing' && !processingFresh) {\n const recovered = await completeProvisionedRequest({ service, request })\n if (recovered) return redirectToPreparing(baseUrl, recovered.tenantId)\n await service.resetProcessing(request)\n }\n if (request.status !== 'pending') {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n const claimed = await service.startProcessing(request, new Date())\n if (!claimed) {\n // A concurrent verify request already claimed this token (pending \u2192 processing).\n // Re-read on a fresh fork \u2014 the request EM's identity map still holds the stale\n // pre-claim copy \u2014 and route off the winner's committed state instead of re-running\n // provisioning, which would throw USER_EXISTS and could strand the request (#2742).\n const currentService = new OnboardingService(em.fork())\n const current = await currentService.findById(request.id)\n if (current?.status === 'completed' && current.tenantId) {\n return current.preparationCompletedAt\n ? redirectToLogin(baseUrl, current.tenantId)\n : redirectToPreparing(baseUrl, current.tenantId)\n }\n if (current?.status === 'processing') {\n const recovered = await completeProvisionedRequest({ service: currentService, request: current })\n if (recovered) return redirectToPreparing(baseUrl, recovered.tenantId)\n }\n return redirectToPreparing(baseUrl, current?.tenantId ?? request.tenantId ?? null)\n }\n if (!request.passwordHash) {\n logger.error('Missing password hash for request', { requestId: request.id })\n await service.resetProcessing(request)\n return redirectWithStatus(baseUrl, 'error')\n }\n\n let tenantId: string | null = null\n let organizationId: string | null = null\n let userId: string | null = null\n\n try {\n const setupResult = await setupInitialTenant(em, {\n orgName: request.organizationName,\n includeDerivedUsers: false,\n failIfUserExists: true,\n primaryUserRoles: ['admin'],\n includeSuperadminRole: false,\n primaryUser: {\n email: request.email,\n firstName: request.firstName,\n lastName: request.lastName,\n displayName: `${request.firstName} ${request.lastName}`.trim(),\n hashedPassword: request.passwordHash,\n confirm: true,\n },\n modules: getModules(),\n })\n\n const resolvedTenantId = String(setupResult.tenantId)\n const resolvedOrganizationId = String(setupResult.organizationId)\n tenantId = resolvedTenantId\n organizationId = resolvedOrganizationId\n\n const mainUserSnapshot = setupResult.users.find((entry) => entry.user.email === request.email)\n if (!mainUserSnapshot) throw new Error('USER_NOT_CREATED')\n const user = mainUserSnapshot.user\n const resolvedUserId = String(user.id)\n userId = resolvedUserId\n await service.updateProvisioningIds(request, {\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n userId: resolvedUserId,\n })\n\n if (request.marketingConsent) {\n const now = new Date()\n const clientIp = resolveConsentClientIp(req)\n const integrityHash = computeConsentIntegrityHash({\n userId: resolvedUserId,\n consentType: 'marketing_email',\n isGranted: true,\n grantedAt: now,\n ipAddress: clientIp,\n source: 'onboarding',\n })\n // Persist the marketing consent on an isolated EM fork wrapped in a\n // transaction so it commits all-or-nothing AND a failure here can neither\n // abort provisioning nor poison the request EM's unit of work before\n // markCompleted runs. Recording the consent is best-effort: the workspace\n // is already provisioned, and a lost consent record fails safe (treated as\n // not granted) and is logged for follow-up rather than stranding the user.\n await runBestEffortProvisioningStep('marketing-consent', () =>\n em.fork().transactional(async (txEm) => {\n txEm.create(UserConsent, {\n userId: resolvedUserId,\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n consentType: 'marketing_email',\n isGranted: true,\n grantedAt: now,\n source: 'onboarding',\n ipAddress: clientIp,\n integrityHash,\n createdAt: now,\n })\n }),\n )\n }\n\n // Call module seedDefaults hooks. Each hook is best-effort and runs on its\n // own isolated EM fork: a single module's throw or 15s timeout must not\n // strand the freshly provisioned workspace \u2014 the tenant/org/user already\n // exist and the request must still reach markCompleted so the user can sign\n // in. A per-module fork also keeps a failed module's unflushed unit of work\n // from leaking into (or aborting) the next module's flush. Failures are\n // logged for follow-up (deferred seedExamples is already non-fatal in the\n // same way).\n const modules = getModules()\n for (const mod of modules) {\n if (!mod.setup?.seedDefaults) continue\n const seedEm = em.fork()\n await runBestEffortProvisioningStep(`seedDefaults:${mod.id}`, () =>\n runModuleSetupHook({\n moduleId: mod.id,\n phase: 'seedDefaults',\n timeoutMs: SEED_DEFAULTS_TIMEOUT_MS,\n run: () => mod.setup!.seedDefaults!({\n em: seedEm,\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n container,\n }),\n }),\n )\n }\n await service.markCompleted(request, {\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n userId: resolvedUserId,\n })\n // TODO: Move deferred provisioning into a durable job keyed by request id so process restarts can resume\n // seedExamples/index rebuild/email dispatch instead of leaving completed requests stuck on preparing.\n after(async () => {\n await runDeferredProvisioning({\n requestId: request.id,\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n })\n })\n return redirectToPreparing(baseUrl, resolvedTenantId)\n } catch (error) {\n if (error instanceof Error && error.message === 'USER_EXISTS') {\n await service.resetProcessing(request)\n return redirectWithStatus(baseUrl, 'already_exists')\n }\n logger.error('Verification failed', { err: error })\n await service.resetProcessing(request)\n return redirectWithStatus(baseUrl, 'error')\n }\n}\n\nexport default GET\n\nconst onboardingTag = 'Onboarding'\n\nconst onboardingVerifyQuerySchema = z.object({\n token: onboardingVerifySchema.shape.token,\n})\n\nconst onboardingVerifyDoc: OpenApiMethodDoc = {\n summary: 'Verify onboarding token',\n description: 'Validates the onboarding token, provisions the tenant, seeds demo data, and redirects the user to the login screen.',\n tags: [onboardingTag],\n query: onboardingVerifyQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to onboarding UI or login' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: onboardingTag,\n summary: 'Onboarding verification redirect',\n methods: {\n GET: onboardingVerifyDoc,\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { after, NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { onboardingVerifySchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\nimport type { OnboardingRequest } from '@open-mercato/onboarding/modules/onboarding/data/entities'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\nimport {\n redirectToLogin,\n redirectToPreparing,\n redirectWithStatus,\n} from '@open-mercato/onboarding/modules/onboarding/lib/verify-redirects'\nimport { resolveVerifyRedirectBaseUrl } from '@open-mercato/onboarding/modules/onboarding/lib/verify-base-url'\nimport {\n resolveProvisioningIds,\n runDeferredProvisioning,\n} from '@open-mercato/onboarding/modules/onboarding/lib/deferred-provisioning'\nimport { setupInitialTenant } from '@open-mercato/core/modules/auth/lib/setup-app'\nimport { UserConsent } from '@open-mercato/core/modules/auth/data/entities'\nimport { computeConsentIntegrityHash } from '@open-mercato/core/modules/auth/lib/consentIntegrity'\nimport { resolveConsentClientIp } from '@open-mercato/onboarding/modules/onboarding/lib/consentClientIp'\nimport { runBestEffortProvisioningStep } from '@open-mercato/onboarding/modules/onboarding/lib/provisioning'\nimport { getModules } from '@open-mercato/shared/lib/modules/registry'\nimport { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('onboarding').child({ component: 'verify' })\n\nexport const metadata = {\n path: '/onboarding/onboarding/verify',\n GET: {\n requireAuth: false,\n },\n}\n\nconst SEED_DEFAULTS_TIMEOUT_MS = 15_000\n\nfunction createTimeoutPromise(label: string, timeoutMs: number): Promise<never> {\n return new Promise((_, reject) => {\n setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)\n })\n}\n\nasync function runModuleSetupHook(args: {\n moduleId: string\n phase: 'seedDefaults' | 'seedExamples'\n timeoutMs: number\n run: () => Promise<void>\n}) {\n const startedAt = Date.now()\n logger.info('Module hook started', {\n moduleId: args.moduleId,\n phase: args.phase,\n timeoutMs: args.timeoutMs,\n })\n try {\n await Promise.race([\n args.run(),\n createTimeoutPromise(`module ${args.moduleId} ${args.phase}`, args.timeoutMs),\n ])\n logger.info('Module hook completed', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n })\n } catch (error) {\n if (isUniqueViolation(error)) {\n // A concurrent verify (or a re-verify after the request was already\n // provisioned) re-runs seedDefaults against rows that exist. seed hooks\n // are not fully idempotent, so the collision is expected and harmless \u2014\n // the workspace already exists. Log at info so real failures stand out.\n logger.info('Module hook skipped (already seeded)', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n })\n } else {\n logger.error('Module hook failed', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n timeoutMs: args.timeoutMs,\n err: error,\n })\n }\n throw error\n }\n}\n\nasync function completeProvisionedRequest(args: {\n service: OnboardingService\n request: OnboardingRequest\n}) {\n const ids = resolveProvisioningIds(args.request)\n if (!ids) return null\n await args.service.markCompleted(args.request, ids)\n after(async () => {\n await runDeferredProvisioning({\n requestId: args.request.id,\n tenantId: ids.tenantId,\n organizationId: ids.organizationId,\n })\n })\n return ids\n}\n\nexport async function GET(req: Request) {\n if (parseBooleanToken(process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? '') !== true) {\n return NextResponse.json({ ok: false, error: 'Self-service onboarding is disabled.' }, { status: 404 })\n }\n const url = new URL(req.url)\n const baseUrlResult = resolveVerifyRedirectBaseUrl(req)\n if (!baseUrlResult.ok) {\n if (baseUrlResult.redirectOrigin) {\n return redirectWithStatus(baseUrlResult.redirectOrigin, baseUrlResult.status)\n }\n return NextResponse.json(\n { ok: false, error: baseUrlResult.message },\n { status: baseUrlResult.httpStatus },\n )\n }\n const baseUrl = baseUrlResult.baseUrl\n const token = url.searchParams.get('token') ?? ''\n const parsed = onboardingVerifySchema.safeParse({ token })\n if (!parsed.success) {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const service = new OnboardingService(em)\n const request = await service.findByToken(parsed.data.token)\n if (!request) {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n if (request.expiresAt <= new Date() && request.status !== 'completed') {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n if (request.status === 'completed' && request.tenantId) {\n if (!request.preparationCompletedAt) {\n return redirectToPreparing(baseUrl, request.tenantId)\n }\n if (!request.readyEmailSentAt) {\n after(async () => {\n await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: request.tenantId!,\n }).catch((error) => {\n logger.error('Retry ready email failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n err: error,\n })\n })\n })\n }\n return redirectToLogin(baseUrl, request.tenantId)\n }\n const lockWindowMs = 15 * 60 * 1000\n const processingStartedAt = request.processingStartedAt?.getTime() ?? 0\n const processingFresh = request.status === 'processing' && processingStartedAt > Date.now() - lockWindowMs\n if (processingFresh) {\n const recovered = await completeProvisionedRequest({ service, request })\n if (recovered) return redirectToPreparing(baseUrl, recovered.tenantId)\n return redirectToPreparing(baseUrl, request.tenantId ?? null)\n }\n if (request.status === 'processing' && !processingFresh) {\n const recovered = await completeProvisionedRequest({ service, request })\n if (recovered) return redirectToPreparing(baseUrl, recovered.tenantId)\n await service.resetProcessing(request)\n }\n if (request.status !== 'pending') {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n const claimed = await service.startProcessing(request, new Date())\n if (!claimed) {\n // A concurrent verify request already claimed this token (pending \u2192 processing).\n // Re-read on a fresh fork \u2014 the request EM's identity map still holds the stale\n // pre-claim copy \u2014 and route off the winner's committed state instead of re-running\n // provisioning, which would throw USER_EXISTS and could strand the request (#2742).\n const currentService = new OnboardingService(em.fork())\n const current = await currentService.findById(request.id)\n if (current?.status === 'completed' && current.tenantId) {\n return current.preparationCompletedAt\n ? redirectToLogin(baseUrl, current.tenantId)\n : redirectToPreparing(baseUrl, current.tenantId)\n }\n if (current?.status === 'processing') {\n const recovered = await completeProvisionedRequest({ service: currentService, request: current })\n if (recovered) return redirectToPreparing(baseUrl, recovered.tenantId)\n }\n return redirectToPreparing(baseUrl, current?.tenantId ?? request.tenantId ?? null)\n }\n if (!request.passwordHash) {\n logger.error('Missing password hash for request', { requestId: request.id })\n await service.resetProcessing(request)\n return redirectWithStatus(baseUrl, 'error')\n }\n\n let tenantId: string | null = null\n let organizationId: string | null = null\n let userId: string | null = null\n\n try {\n const setupResult = await setupInitialTenant(em, {\n orgName: request.organizationName,\n includeDerivedUsers: false,\n failIfUserExists: true,\n primaryUserRoles: ['admin'],\n includeSuperadminRole: false,\n primaryUser: {\n email: request.email,\n firstName: request.firstName,\n lastName: request.lastName,\n displayName: `${request.firstName} ${request.lastName}`.trim(),\n hashedPassword: request.passwordHash,\n confirm: true,\n },\n modules: getModules(),\n })\n\n const resolvedTenantId = String(setupResult.tenantId)\n const resolvedOrganizationId = String(setupResult.organizationId)\n tenantId = resolvedTenantId\n organizationId = resolvedOrganizationId\n\n const mainUserSnapshot = setupResult.users.find((entry) => entry.user.email === request.email)\n if (!mainUserSnapshot) throw new Error('USER_NOT_CREATED')\n const user = mainUserSnapshot.user\n const resolvedUserId = String(user.id)\n userId = resolvedUserId\n await service.updateProvisioningIds(request, {\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n userId: resolvedUserId,\n })\n\n if (request.marketingConsent) {\n const now = new Date()\n const clientIp = resolveConsentClientIp(req)\n const integrityHash = computeConsentIntegrityHash({\n userId: resolvedUserId,\n consentType: 'marketing_email',\n isGranted: true,\n grantedAt: now,\n ipAddress: clientIp,\n source: 'onboarding',\n })\n // Persist the marketing consent on an isolated EM fork wrapped in a\n // transaction so it commits all-or-nothing AND a failure here can neither\n // abort provisioning nor poison the request EM's unit of work before\n // markCompleted runs. Recording the consent is best-effort: the workspace\n // is already provisioned, and a lost consent record fails safe (treated as\n // not granted) and is logged for follow-up rather than stranding the user.\n await runBestEffortProvisioningStep('marketing-consent', () =>\n em.fork().transactional(async (txEm) => {\n txEm.create(UserConsent, {\n userId: resolvedUserId,\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n consentType: 'marketing_email',\n isGranted: true,\n grantedAt: now,\n source: 'onboarding',\n ipAddress: clientIp,\n integrityHash,\n createdAt: now,\n })\n }),\n )\n }\n\n // Call module seedDefaults hooks. Each hook is best-effort and runs on its\n // own isolated EM fork: a single module's throw or 15s timeout must not\n // strand the freshly provisioned workspace \u2014 the tenant/org/user already\n // exist and the request must still reach markCompleted so the user can sign\n // in. A per-module fork also keeps a failed module's unflushed unit of work\n // from leaking into (or aborting) the next module's flush. Failures are\n // logged for follow-up (deferred seedExamples is already non-fatal in the\n // same way).\n const modules = getModules()\n for (const mod of modules) {\n if (!mod.setup?.seedDefaults) continue\n const seedEm = em.fork()\n await runBestEffortProvisioningStep(`seedDefaults:${mod.id}`, () =>\n runModuleSetupHook({\n moduleId: mod.id,\n phase: 'seedDefaults',\n timeoutMs: SEED_DEFAULTS_TIMEOUT_MS,\n run: () => mod.setup!.seedDefaults!({\n em: seedEm,\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n container,\n }),\n }),\n )\n }\n await service.markCompleted(request, {\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n userId: resolvedUserId,\n })\n // TODO: Move deferred provisioning into a durable job keyed by request id so process restarts can resume\n // seedExamples/index rebuild/email dispatch instead of leaving completed requests stuck on preparing.\n after(async () => {\n await runDeferredProvisioning({\n requestId: request.id,\n tenantId: resolvedTenantId,\n organizationId: resolvedOrganizationId,\n })\n })\n return redirectToPreparing(baseUrl, resolvedTenantId)\n } catch (error) {\n if (error instanceof Error && error.message === 'USER_EXISTS') {\n await service.resetProcessing(request)\n return redirectWithStatus(baseUrl, 'already_exists')\n }\n logger.error('Verification failed', { err: error })\n await service.resetProcessing(request)\n return redirectWithStatus(baseUrl, 'error')\n }\n}\n\nexport default GET\n\nconst onboardingTag = 'Onboarding'\n\nconst onboardingVerifyQuerySchema = z.object({\n token: onboardingVerifySchema.shape.token,\n})\n\nconst onboardingVerifyDoc: OpenApiMethodDoc = {\n summary: 'Verify onboarding token',\n description: 'Validates the onboarding token, provisions the tenant, seeds demo data, and redirects the user to the login screen.',\n tags: [onboardingTag],\n query: onboardingVerifyQuerySchema,\n responses: [\n { status: 302, description: 'Redirect to onboarding UI or login' },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: onboardingTag,\n summary: 'Onboarding verification redirect',\n methods: {\n GET: onboardingVerifyDoc,\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/onboarding",
3
- "version": "0.6.7-develop.6631.1.9d64b49ddc",
3
+ "version": "0.6.7-develop.6646.1.be515f3d49",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -70,10 +70,10 @@
70
70
  }
71
71
  },
72
72
  "peerDependencies": {
73
- "@open-mercato/shared": "0.6.7-develop.6631.1.9d64b49ddc"
73
+ "@open-mercato/shared": "0.6.7-develop.6646.1.be515f3d49"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.6.7-develop.6631.1.9d64b49ddc",
76
+ "@open-mercato/shared": "0.6.7-develop.6646.1.be515f3d49",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.4.2",
79
79
  "ts-jest": "^29.4.11",
@@ -0,0 +1,64 @@
1
+ const sendEmailMock = jest.fn()
2
+ const checkAuthRateLimitMock = jest.fn()
3
+
4
+ jest.mock('@open-mercato/shared/lib/email/send', () => ({
5
+ sendEmail: jest.fn((args: unknown) => sendEmailMock(args)),
6
+ }))
7
+
8
+ jest.mock('@open-mercato/core/modules/auth/lib/rateLimitCheck', () => ({
9
+ checkAuthRateLimit: jest.fn((args: unknown) => checkAuthRateLimitMock(args)),
10
+ }))
11
+
12
+ jest.mock('@open-mercato/shared/lib/ratelimit/config', () => ({
13
+ readEndpointRateLimitConfig: jest.fn(() => ({})),
14
+ }))
15
+
16
+ jest.mock('@open-mercato/onboarding/modules/onboarding/emails/FeedbackEmail', () => ({
17
+ __esModule: true,
18
+ default: jest.fn(() => null),
19
+ }))
20
+
21
+ import { POST } from '../modules/onboarding/api/demo-feedback/route'
22
+
23
+ function makeRequest(sendCopy?: boolean) {
24
+ return new Request('http://localhost/api/onboarding/demo-feedback', {
25
+ method: 'POST',
26
+ headers: { 'content-type': 'application/json' },
27
+ body: JSON.stringify({
28
+ email: 'unverified@example.com',
29
+ message: 'Please contact me.',
30
+ termsAccepted: true,
31
+ marketingConsent: false,
32
+ ...(sendCopy === undefined ? {} : { sendCopy }),
33
+ }),
34
+ })
35
+ }
36
+
37
+ describe('POST /api/onboarding/demo-feedback', () => {
38
+ const originalEnv = process.env
39
+
40
+ beforeEach(() => {
41
+ jest.clearAllMocks()
42
+ process.env = { ...originalEnv, ADMIN_EMAIL: 'admin@example.com' }
43
+ checkAuthRateLimitMock.mockResolvedValue({ error: null })
44
+ sendEmailMock.mockResolvedValue(undefined)
45
+ })
46
+
47
+ afterAll(() => {
48
+ process.env = originalEnv
49
+ })
50
+
51
+ it.each([
52
+ ['when sendCopy is omitted', undefined],
53
+ ['when sendCopy is explicitly requested', true],
54
+ ])('sends feedback only to the configured admin %s', async (_label, sendCopy) => {
55
+ const response = await POST(makeRequest(sendCopy))
56
+
57
+ expect(response.status).toBe(200)
58
+ expect(sendEmailMock).toHaveBeenCalledTimes(1)
59
+ expect(sendEmailMock).toHaveBeenCalledWith(expect.objectContaining({ to: 'admin@example.com' }))
60
+ expect(sendEmailMock).not.toHaveBeenCalledWith(
61
+ expect.objectContaining({ to: 'unverified@example.com' }),
62
+ )
63
+ })
64
+ })
@@ -0,0 +1,35 @@
1
+ import { GET as getOnboardingStatus } from '../modules/onboarding/api/get/onboarding/status'
2
+ import { GET as verifyOnboardingRequest } from '../modules/onboarding/api/get/onboarding/verify'
3
+
4
+ jest.mock('@open-mercato/onboarding/modules/onboarding/lib/consentClientIp', () => ({
5
+ resolveConsentClientIp: jest.fn(),
6
+ }))
7
+
8
+ const originalSelfServiceOnboardingEnabled = process.env.SELF_SERVICE_ONBOARDING_ENABLED
9
+
10
+ describe('self-service onboarding route gate', () => {
11
+ beforeEach(() => {
12
+ process.env.SELF_SERVICE_ONBOARDING_ENABLED = 'false'
13
+ })
14
+
15
+ afterAll(() => {
16
+ if (originalSelfServiceOnboardingEnabled === undefined) {
17
+ delete process.env.SELF_SERVICE_ONBOARDING_ENABLED
18
+ } else {
19
+ process.env.SELF_SERVICE_ONBOARDING_ENABLED = originalSelfServiceOnboardingEnabled
20
+ }
21
+ })
22
+
23
+ it.each([
24
+ ['verify', verifyOnboardingRequest, 'https://app.example.com/api/onboarding/onboarding/verify'],
25
+ ['status', getOnboardingStatus, 'https://app.example.com/api/onboarding/onboarding/status'],
26
+ ])('returns 404 from the %s route when self-service onboarding is disabled', async (_name, handler, url) => {
27
+ const response = await handler(new Request(url))
28
+
29
+ expect(response.status).toBe(404)
30
+ await expect(response.json()).resolves.toEqual({
31
+ ok: false,
32
+ error: 'Self-service onboarding is disabled.',
33
+ })
34
+ })
35
+ })
@@ -5,6 +5,7 @@ const sendWorkspaceReadyEmail = jest.fn()
5
5
  const assertAllowedAppOrigin = jest.fn()
6
6
  const markCompleted = jest.fn()
7
7
  const runDeferredProvisioning = jest.fn()
8
+ const originalSelfServiceOnboardingEnabled = process.env.SELF_SERVICE_ONBOARDING_ENABLED
8
9
 
9
10
  jest.mock('next/server', () => {
10
11
  const actual = jest.requireActual('next/server')
@@ -85,6 +86,7 @@ function buildRequest(args: { tenantId: string; cookie?: string }) {
85
86
 
86
87
  describe('onboarding status endpoint authorization', () => {
87
88
  beforeEach(() => {
89
+ process.env.SELF_SERVICE_ONBOARDING_ENABLED = 'true'
88
90
  findLatestByTenantId.mockReset()
89
91
  sendWorkspaceReadyEmail.mockReset()
90
92
  assertAllowedAppOrigin.mockReset()
@@ -105,6 +107,14 @@ describe('onboarding status endpoint authorization', () => {
105
107
  findLatestByTenantId.mockResolvedValue(makeRequest())
106
108
  })
107
109
 
110
+ afterAll(() => {
111
+ if (originalSelfServiceOnboardingEnabled === undefined) {
112
+ delete process.env.SELF_SERVICE_ONBOARDING_ENABLED
113
+ } else {
114
+ process.env.SELF_SERVICE_ONBOARDING_ENABLED = originalSelfServiceOnboardingEnabled
115
+ }
116
+ })
117
+
108
118
  it('returns 403 and does not look up tenant state when no om_login_tenant cookie is present', async () => {
109
119
  const res = await GET(buildRequest({ tenantId: TENANT_ID }))
110
120
  expect(res.status).toBe(403)
@@ -25,7 +25,6 @@ const feedbackSchema = z.object({
25
25
  message: z.string().max(5000).optional().default(''),
26
26
  termsAccepted: z.literal(true),
27
27
  marketingConsent: z.boolean().optional().default(false),
28
- sendCopy: z.boolean().optional().default(true),
29
28
  })
30
29
 
31
30
  export async function POST(req: Request) {
@@ -47,7 +46,7 @@ export async function POST(req: Request) {
47
46
  return NextResponse.json({ ok: false, error: 'Please check the form and try again.' }, { status: 400 })
48
47
  }
49
48
 
50
- const { email, message, marketingConsent, sendCopy } = parsed.data
49
+ const { email, message, marketingConsent } = parsed.data
51
50
  const adminEmail = process.env.ADMIN_EMAIL || 'piotr@catchthetornado.com'
52
51
 
53
52
  const marketingText = marketingConsent ? 'Marketing consent: Yes' : 'Marketing consent: No'
@@ -75,27 +74,6 @@ export async function POST(req: Request) {
75
74
  return NextResponse.json({ ok: false, error: 'Failed to send feedback. Please try again.' }, { status: 502 })
76
75
  }
77
76
 
78
- if (sendCopy) {
79
- const userCopy = {
80
- preview: 'Your feedback to Open Mercato',
81
- heading: 'Thank you for reaching out!',
82
- body: 'We received your message and will get back to you shortly. Below is a copy of what you submitted.',
83
- messageLabel: 'Your message:',
84
- message: message || '(no message provided)',
85
- marketingConsent: marketingText,
86
- footer: 'Open Mercato \u00b7 demo.openmercato.com',
87
- }
88
- try {
89
- await sendEmail({
90
- to: email,
91
- subject: 'Your feedback to Open Mercato',
92
- react: FeedbackEmail({ copy: userCopy }),
93
- })
94
- } catch (err) {
95
- logger.error('User copy email failed', { err })
96
- }
97
- }
98
-
99
77
  return NextResponse.json({ ok: true })
100
78
  }
101
79
 
@@ -105,7 +83,7 @@ const feedbackTag = 'Demo'
105
83
 
106
84
  const feedbackPostDoc: OpenApiMethodDoc = {
107
85
  summary: 'Submit demo feedback',
108
- description: 'Sends a feedback/contact request from the demo environment to the admin and optionally to the user.',
86
+ description: 'Sends a feedback/contact request from the demo environment to the configured admin.',
109
87
  tags: [feedbackTag],
110
88
  requestBody: {
111
89
  contentType: 'application/json',
@@ -1,6 +1,7 @@
1
1
  import { after, NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
4
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
6
  import { createLogger } from '@open-mercato/shared/lib/logger'
6
7
  import { assertAllowedAppOrigin, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'
@@ -47,6 +48,9 @@ function readCookie(req: Request, name: string): string | null {
47
48
  }
48
49
 
49
50
  export async function GET(req: Request) {
51
+ if (parseBooleanToken(process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? '') !== true) {
52
+ return NextResponse.json({ ok: false, error: 'Self-service onboarding is disabled.' }, { status: 404 })
53
+ }
50
54
  const url = new URL(req.url)
51
55
  const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''
52
56
  const parsed = onboardingStatusQuerySchema.safeParse({ tenantId })
@@ -1,6 +1,7 @@
1
1
  import { after, NextResponse } from 'next/server'
2
2
  import { z } from 'zod'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
4
5
  import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
5
6
  import { onboardingVerifySchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'
6
7
  import type { OnboardingRequest } from '@open-mercato/onboarding/modules/onboarding/data/entities'
@@ -107,6 +108,9 @@ async function completeProvisionedRequest(args: {
107
108
  }
108
109
 
109
110
  export async function GET(req: Request) {
111
+ if (parseBooleanToken(process.env.SELF_SERVICE_ONBOARDING_ENABLED ?? '') !== true) {
112
+ return NextResponse.json({ ok: false, error: 'Self-service onboarding is disabled.' }, { status: 404 })
113
+ }
110
114
  const url = new URL(req.url)
111
115
  const baseUrlResult = resolveVerifyRedirectBaseUrl(req)
112
116
  if (!baseUrlResult.ok) {
@@ -14,7 +14,6 @@
14
14
  "demoFeedback.form.marketingLabel": "Ich stimme dem Erhalt von Direktmarketing von CT Tornado per E-Mail zu. Ich kann meine Einwilligung jederzeit widerrufen. Siehe {termsLink} und {privacyLink}.",
15
15
  "demoFeedback.form.message": "Ihre Nachricht (optional)",
16
16
  "demoFeedback.form.privacyLink": "Datenschutzerklärung",
17
- "demoFeedback.form.sendCopy": "Kopie dieser Nachricht an mich senden",
18
17
  "demoFeedback.form.submit": "Kontaktieren Sie mich",
19
18
  "demoFeedback.form.suppressPopup": "Dieses Popup nicht automatisch anzeigen",
20
19
  "demoFeedback.form.termsAnd": " und ",
@@ -14,7 +14,6 @@
14
14
  "demoFeedback.form.marketingLabel": "I consent to receiving direct marketing from CT Tornado by email. I can withdraw my consent at any time. See our {termsLink} and {privacyLink}.",
15
15
  "demoFeedback.form.message": "Your message (optional)",
16
16
  "demoFeedback.form.privacyLink": "Privacy Policy",
17
- "demoFeedback.form.sendCopy": "Send me a copy of this message",
18
17
  "demoFeedback.form.submit": "Contact me",
19
18
  "demoFeedback.form.suppressPopup": "Do not show this popup automatically",
20
19
  "demoFeedback.form.termsAnd": " and ",
@@ -14,7 +14,6 @@
14
14
  "demoFeedback.form.marketingLabel": "Doy mi consentimiento para recibir marketing directo de CT Tornado por correo electrónico. Puedo retirar mi consentimiento en cualquier momento. Consulte los {termsLink} y la {privacyLink}.",
15
15
  "demoFeedback.form.message": "Su mensaje (opcional)",
16
16
  "demoFeedback.form.privacyLink": "Política de privacidad",
17
- "demoFeedback.form.sendCopy": "Enviarme una copia de este mensaje",
18
17
  "demoFeedback.form.submit": "Contácteme",
19
18
  "demoFeedback.form.suppressPopup": "No mostrar esta ventana automáticamente",
20
19
  "demoFeedback.form.termsAnd": " y la ",
@@ -14,7 +14,6 @@
14
14
  "demoFeedback.form.marketingLabel": "Wyrażam zgodę na otrzymywanie marketingu bezpośredniego od CT Tornado drogą elektroniczną. Mogę wycofać zgodę w dowolnym momencie. Zobacz {termsLink} i {privacyLink}.",
15
15
  "demoFeedback.form.message": "Twoja wiadomość (opcjonalnie)",
16
16
  "demoFeedback.form.privacyLink": "Politykę prywatności",
17
- "demoFeedback.form.sendCopy": "Wyślij mi kopię tej wiadomości",
18
17
  "demoFeedback.form.submit": "Skontaktuj się ze mną",
19
18
  "demoFeedback.form.suppressPopup": "Nie pokazuj tego okna automatycznie",
20
19
  "demoFeedback.form.termsAnd": " oraz ",