@open-mercato/onboarding 0.6.6-develop.5431.1.384a97c7a2 → 0.6.6-develop.5483.1.a1129165ea

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.
Files changed (28) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/onboarding/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.js +158 -0
  3. package/dist/modules/onboarding/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.js.map +7 -0
  4. package/dist/modules/onboarding/api/get/onboarding/status.js +2 -1
  5. package/dist/modules/onboarding/api/get/onboarding/status.js.map +2 -2
  6. package/dist/modules/onboarding/data/entities.js +3 -0
  7. package/dist/modules/onboarding/data/entities.js.map +2 -2
  8. package/dist/modules/onboarding/lib/deferred-provisioning.js +29 -13
  9. package/dist/modules/onboarding/lib/deferred-provisioning.js.map +2 -2
  10. package/dist/modules/onboarding/lib/preparation-claim.js +10 -0
  11. package/dist/modules/onboarding/lib/preparation-claim.js.map +7 -0
  12. package/dist/modules/onboarding/lib/service.js +31 -0
  13. package/dist/modules/onboarding/lib/service.js.map +2 -2
  14. package/dist/modules/onboarding/migrations/Migration20260611120000.js +13 -0
  15. package/dist/modules/onboarding/migrations/Migration20260611120000.js.map +7 -0
  16. package/generated/entities/onboarding_request/index.ts +1 -0
  17. package/generated/entity-fields-registry.ts +1 -0
  18. package/package.json +3 -3
  19. package/src/__tests__/deferred-provisioning.test.ts +210 -0
  20. package/src/__tests__/status-endpoint-auth.test.ts +38 -0
  21. package/src/modules/onboarding/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.ts +198 -0
  22. package/src/modules/onboarding/api/get/onboarding/status.ts +11 -1
  23. package/src/modules/onboarding/data/entities.ts +3 -0
  24. package/src/modules/onboarding/lib/deferred-provisioning.ts +50 -13
  25. package/src/modules/onboarding/lib/preparation-claim.ts +6 -0
  26. package/src/modules/onboarding/lib/service.ts +33 -0
  27. package/src/modules/onboarding/migrations/.snapshot-open-mercato.json +10 -0
  28. package/src/modules/onboarding/migrations/Migration20260611120000.ts +13 -0
@@ -1,2 +1,2 @@
1
- [build:onboarding] found 30 entry points
1
+ [build:onboarding] found 33 entry points
2
2
  [build:onboarding] built successfully
@@ -0,0 +1,158 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { expect, test } from "@playwright/test";
3
+ import { withClient } from "@open-mercato/core/helpers/integration/dbFixtures";
4
+ const integrationMeta = {
5
+ dependsOnModules: ["onboarding"],
6
+ requiredEnvVars: ["SELF_SERVICE_ONBOARDING_ENABLED"],
7
+ requiredAnyEnvVars: ["CONSENT_INTEGRITY_SECRET", "AUTH_SECRET", "NEXTAUTH_SECRET", "JWT_SECRET"]
8
+ };
9
+ const BASE_URL = process.env.BASE_URL?.trim() || "http://localhost:3000";
10
+ const ONBOARDING_PASSWORD = "ParallelPass123!";
11
+ function hashToken(token) {
12
+ return createHash("sha256").update(token).digest("hex");
13
+ }
14
+ async function replaceVerificationToken(email, token) {
15
+ return withClient(async (client) => {
16
+ const result = await client.query(
17
+ `update onboarding_requests
18
+ set token_hash = $2,
19
+ expires_at = now() + interval '24 hours',
20
+ updated_at = now()
21
+ where email = $1
22
+ returning id`,
23
+ [email, hashToken(token)]
24
+ );
25
+ expect(result.rowCount, `onboarding request should exist for ${email}`).toBe(1);
26
+ return result.rows[0].id;
27
+ });
28
+ }
29
+ async function readPreparationState(requestId) {
30
+ return withClient(async (client) => {
31
+ const result = await client.query(
32
+ `select preparation_completed_at, preparation_started_at
33
+ from onboarding_requests
34
+ where id = $1`,
35
+ [requestId]
36
+ );
37
+ expect(result.rowCount, `onboarding request ${requestId} should remain queryable`).toBe(1);
38
+ return result.rows[0];
39
+ });
40
+ }
41
+ async function waitForPreparationComplete(requestId) {
42
+ const deadline = Date.now() + 6e4;
43
+ let lastState = null;
44
+ while (Date.now() < deadline) {
45
+ lastState = await readPreparationState(requestId);
46
+ if (lastState.preparation_completed_at && !lastState.preparation_started_at) return;
47
+ await new Promise((resolve) => setTimeout(resolve, 500));
48
+ }
49
+ expect(lastState?.preparation_started_at, "deferred preparation lease should be cleared after completion").toBeNull();
50
+ expect(lastState?.preparation_completed_at, "workspace preparation should complete").toBeTruthy();
51
+ }
52
+ async function submitOnboarding(request, tenant) {
53
+ const response = await request.post(`${BASE_URL}/api/onboarding/onboarding`, {
54
+ data: {
55
+ email: tenant.email,
56
+ firstName: "Parallel",
57
+ lastName: "Login",
58
+ organizationName: tenant.organizationName,
59
+ password: ONBOARDING_PASSWORD,
60
+ confirmPassword: ONBOARDING_PASSWORD,
61
+ termsAccepted: true,
62
+ marketingConsent: true,
63
+ locale: "en"
64
+ }
65
+ });
66
+ expect(response.status(), `onboarding start should succeed for ${tenant.email}`).toBe(200);
67
+ }
68
+ async function verifyTenantInBrowser(browser, token) {
69
+ const context = await browser.newContext({ baseURL: BASE_URL });
70
+ const page = await context.newPage();
71
+ const refreshRequests = [];
72
+ page.on("request", (browserRequest) => {
73
+ const url = browserRequest.url();
74
+ if (url.includes("/api/auth/session/refresh")) refreshRequests.push(url);
75
+ });
76
+ await page.goto(`/api/onboarding/onboarding/verify?token=${encodeURIComponent(token)}`);
77
+ await expect(page).toHaveURL(/\/onboarding\/preparing\?tenant=[0-9a-f-]+/);
78
+ await expect(page.getByText("We are preparing your workspace")).toBeVisible();
79
+ const tenantId = new URL(page.url()).searchParams.get("tenant");
80
+ expect(tenantId, "verify redirect should include tenant id").toMatch(
81
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
82
+ );
83
+ return { context, page, refreshRequests, tenantId };
84
+ }
85
+ async function pollOnboardingStatus(request, tenantId) {
86
+ return request.get(`${BASE_URL}/api/onboarding/onboarding/status?tenantId=${encodeURIComponent(tenantId)}`, {
87
+ headers: {
88
+ Cookie: `om_login_tenant=${encodeURIComponent(tenantId)}`
89
+ }
90
+ });
91
+ }
92
+ async function loginAndAssertBackend(session, tenant) {
93
+ expect(tenant.tenantId, `tenant id should exist for ${tenant.email}`).toBeTruthy();
94
+ await expect(session.page).toHaveURL(new RegExp(`/login\\?tenant=${tenant.tenantId}`));
95
+ await expect(session.page.locator('form[data-auth-ready="1"]')).toBeVisible();
96
+ await session.page.getByLabel("Email").fill(tenant.email);
97
+ await session.page.getByLabel("Password", { exact: true }).fill(ONBOARDING_PASSWORD);
98
+ await session.page.getByRole("button", { name: "Sign in" }).click();
99
+ await expect(session.page, `browser login should land ${tenant.email} in the backend`).toHaveURL(/\/backend(?:\/.*)?$/);
100
+ await expect(session.page.getByText(/Session expired/i)).toHaveCount(0);
101
+ const profileResult = await session.page.evaluate(async () => {
102
+ const response = await fetch("/api/auth/profile", { credentials: "include" });
103
+ const body = await response.json().catch(() => null);
104
+ return { status: response.status, body };
105
+ });
106
+ expect(profileResult.status, `browser page should keep ${tenant.email} authenticated`).toBe(200);
107
+ const profile = profileResult.body;
108
+ expect(profile.email).toBe(tenant.email);
109
+ expect(profile.roles).toContain("admin");
110
+ await session.page.goto("/backend");
111
+ await expect(session.page).toHaveURL(/\/backend(?:\/.*)?$/);
112
+ expect(session.refreshRequests, `backend should not bounce ${tenant.email} through session refresh`).toHaveLength(0);
113
+ }
114
+ test.describe("TC-ONB-002: multi-tenant onboarding parallel login", () => {
115
+ test("creates two self-service tenants, handles repeated status polling, and keeps both logins authenticated", async ({
116
+ browser,
117
+ request
118
+ }) => {
119
+ test.setTimeout(12e4);
120
+ const unique = randomUUID().slice(0, 8);
121
+ const tenants = [1, 2].map((index) => ({
122
+ email: `qa-onboarding-parallel-${unique}-${index}@example.test`,
123
+ organizationName: `QA Onboarding Parallel ${unique} ${index}`,
124
+ token: `integration-parallel-${index}-${randomUUID().replace(/-/g, "")}`
125
+ }));
126
+ await Promise.all(tenants.map((tenant) => submitOnboarding(request, tenant)));
127
+ for (const tenant of tenants) {
128
+ tenant.requestId = await replaceVerificationToken(tenant.email, tenant.token);
129
+ }
130
+ const browserSessions = await Promise.all(
131
+ tenants.map(async (tenant) => {
132
+ const session = await verifyTenantInBrowser(browser, tenant.token);
133
+ tenant.tenantId = session.tenantId;
134
+ return session;
135
+ })
136
+ );
137
+ try {
138
+ const statusResponses = await Promise.all(
139
+ tenants.flatMap(
140
+ (tenant) => Array.from({ length: 6 }, () => pollOnboardingStatus(request, tenant.tenantId))
141
+ )
142
+ );
143
+ for (const response of statusResponses) {
144
+ expect(response.status(), "status polling should not exhaust the app DB pool").toBe(200);
145
+ }
146
+ await Promise.all(tenants.map((tenant) => waitForPreparationComplete(tenant.requestId)));
147
+ await Promise.all(
148
+ tenants.map((tenant, index) => loginAndAssertBackend(browserSessions[index], tenant))
149
+ );
150
+ } finally {
151
+ await Promise.all(browserSessions.map((session) => session.context.close()));
152
+ }
153
+ });
154
+ });
155
+ export {
156
+ integrationMeta
157
+ };
158
+ //# sourceMappingURL=TC-ONB-002-multi-tenant-parallel-login.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/onboarding/__integration__/TC-ONB-002-multi-tenant-parallel-login.spec.ts"],
4
+ "sourcesContent": ["// Adapted from PR #3007 (pkarw) \u2014 multi-tenant parallel onboarding repro for the\n// 2026-06-11 demo pool-exhaustion outage, ported to the preparation_started_at\n// lease column introduced by the single-flight deferred-provisioning fix.\nimport { createHash, randomUUID } from 'node:crypto';\nimport { expect, test, type APIRequestContext, type Browser, type BrowserContext, type Page } from '@playwright/test';\nimport { withClient } from '@open-mercato/core/helpers/integration/dbFixtures';\n\nexport const integrationMeta = {\n dependsOnModules: ['onboarding'],\n requiredEnvVars: ['SELF_SERVICE_ONBOARDING_ENABLED'],\n requiredAnyEnvVars: ['CONSENT_INTEGRITY_SECRET', 'AUTH_SECRET', 'NEXTAUTH_SECRET', 'JWT_SECRET'],\n};\n\ntype OnboardingTenant = {\n email: string;\n organizationName: string;\n token: string;\n requestId?: string;\n tenantId?: string;\n};\n\ntype BrowserTenantSession = {\n context: BrowserContext;\n page: Page;\n refreshRequests: string[];\n};\n\nconst BASE_URL = process.env.BASE_URL?.trim() || 'http://localhost:3000';\nconst ONBOARDING_PASSWORD = 'ParallelPass123!';\n\nfunction hashToken(token: string): string {\n return createHash('sha256').update(token).digest('hex');\n}\n\nasync function replaceVerificationToken(email: string, token: string): Promise<string> {\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `update onboarding_requests\n set token_hash = $2,\n expires_at = now() + interval '24 hours',\n updated_at = now()\n where email = $1\n returning id`,\n [email, hashToken(token)],\n );\n expect(result.rowCount, `onboarding request should exist for ${email}`).toBe(1);\n return result.rows[0].id;\n });\n}\n\nasync function readPreparationState(requestId: string): Promise<{\n preparation_completed_at: Date | null;\n preparation_started_at: Date | null;\n}> {\n return withClient(async (client) => {\n const result = await client.query<{\n preparation_completed_at: Date | null;\n preparation_started_at: Date | null;\n }>(\n `select preparation_completed_at, preparation_started_at\n from onboarding_requests\n where id = $1`,\n [requestId],\n );\n expect(result.rowCount, `onboarding request ${requestId} should remain queryable`).toBe(1);\n return result.rows[0];\n });\n}\n\nasync function waitForPreparationComplete(requestId: string): Promise<void> {\n const deadline = Date.now() + 60_000;\n let lastState: Awaited<ReturnType<typeof readPreparationState>> | null = null;\n while (Date.now() < deadline) {\n lastState = await readPreparationState(requestId);\n if (lastState.preparation_completed_at && !lastState.preparation_started_at) return;\n await new Promise((resolve) => setTimeout(resolve, 500));\n }\n expect(lastState?.preparation_started_at, 'deferred preparation lease should be cleared after completion').toBeNull();\n expect(lastState?.preparation_completed_at, 'workspace preparation should complete').toBeTruthy();\n}\n\nasync function submitOnboarding(request: APIRequestContext, tenant: OnboardingTenant): Promise<void> {\n const response = await request.post(`${BASE_URL}/api/onboarding/onboarding`, {\n data: {\n email: tenant.email,\n firstName: 'Parallel',\n lastName: 'Login',\n organizationName: tenant.organizationName,\n password: ONBOARDING_PASSWORD,\n confirmPassword: ONBOARDING_PASSWORD,\n termsAccepted: true,\n marketingConsent: true,\n locale: 'en',\n },\n });\n expect(response.status(), `onboarding start should succeed for ${tenant.email}`).toBe(200);\n}\n\nasync function verifyTenantInBrowser(browser: Browser, token: string): Promise<BrowserTenantSession & { tenantId: string }> {\n const context = await browser.newContext({ baseURL: BASE_URL });\n const page = await context.newPage();\n const refreshRequests: string[] = [];\n page.on('request', (browserRequest) => {\n const url = browserRequest.url();\n if (url.includes('/api/auth/session/refresh')) refreshRequests.push(url);\n });\n\n await page.goto(`/api/onboarding/onboarding/verify?token=${encodeURIComponent(token)}`);\n await expect(page).toHaveURL(/\\/onboarding\\/preparing\\?tenant=[0-9a-f-]+/);\n await expect(page.getByText('We are preparing your workspace')).toBeVisible();\n const tenantId = new URL(page.url()).searchParams.get('tenant');\n expect(tenantId, 'verify redirect should include tenant id').toMatch(\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,\n );\n return { context, page, refreshRequests, tenantId: tenantId! };\n}\n\nasync function pollOnboardingStatus(request: APIRequestContext, tenantId: string) {\n return request.get(`${BASE_URL}/api/onboarding/onboarding/status?tenantId=${encodeURIComponent(tenantId)}`, {\n headers: {\n Cookie: `om_login_tenant=${encodeURIComponent(tenantId)}`,\n },\n });\n}\n\nasync function loginAndAssertBackend(session: BrowserTenantSession, tenant: OnboardingTenant): Promise<void> {\n expect(tenant.tenantId, `tenant id should exist for ${tenant.email}`).toBeTruthy();\n await expect(session.page).toHaveURL(new RegExp(`/login\\\\?tenant=${tenant.tenantId}`));\n await expect(session.page.locator('form[data-auth-ready=\"1\"]')).toBeVisible();\n await session.page.getByLabel('Email').fill(tenant.email);\n await session.page.getByLabel('Password', { exact: true }).fill(ONBOARDING_PASSWORD);\n await session.page.getByRole('button', { name: 'Sign in' }).click();\n await expect(session.page, `browser login should land ${tenant.email} in the backend`).toHaveURL(/\\/backend(?:\\/.*)?$/);\n await expect(session.page.getByText(/Session expired/i)).toHaveCount(0);\n\n const profileResult = await session.page.evaluate(async () => {\n const response = await fetch('/api/auth/profile', { credentials: 'include' });\n const body = await response.json().catch(() => null);\n return { status: response.status, body };\n });\n expect(profileResult.status, `browser page should keep ${tenant.email} authenticated`).toBe(200);\n const profile = profileResult.body;\n expect(profile.email).toBe(tenant.email);\n expect(profile.roles).toContain('admin');\n\n await session.page.goto('/backend');\n await expect(session.page).toHaveURL(/\\/backend(?:\\/.*)?$/);\n expect(session.refreshRequests, `backend should not bounce ${tenant.email} through session refresh`).toHaveLength(0);\n}\n\ntest.describe('TC-ONB-002: multi-tenant onboarding parallel login', () => {\n test('creates two self-service tenants, handles repeated status polling, and keeps both logins authenticated', async ({\n browser,\n request,\n }) => {\n // Two full onboardings + parallel browser logins + DB-polled preparation\n // far exceed the 20s suite default (house pattern: TC-AI-AGENT-SETTINGS-005).\n test.setTimeout(120_000);\n const unique = randomUUID().slice(0, 8);\n const tenants: OnboardingTenant[] = [1, 2].map((index) => ({\n email: `qa-onboarding-parallel-${unique}-${index}@example.test`,\n organizationName: `QA Onboarding Parallel ${unique} ${index}`,\n token: `integration-parallel-${index}-${randomUUID().replace(/-/g, '')}`,\n }));\n\n await Promise.all(tenants.map((tenant) => submitOnboarding(request, tenant)));\n\n for (const tenant of tenants) {\n tenant.requestId = await replaceVerificationToken(tenant.email, tenant.token);\n }\n\n const browserSessions = await Promise.all(\n tenants.map(async (tenant) => {\n const session = await verifyTenantInBrowser(browser, tenant.token);\n tenant.tenantId = session.tenantId;\n return session;\n }),\n );\n\n try {\n const statusResponses = await Promise.all(\n tenants.flatMap((tenant) =>\n Array.from({ length: 6 }, () => pollOnboardingStatus(request, tenant.tenantId!)),\n ),\n );\n for (const response of statusResponses) {\n expect(response.status(), 'status polling should not exhaust the app DB pool').toBe(200);\n }\n\n await Promise.all(tenants.map((tenant) => waitForPreparationComplete(tenant.requestId!)));\n await Promise.all(\n tenants.map((tenant, index) => loginAndAssertBackend(browserSessions[index], tenant)),\n );\n } finally {\n await Promise.all(browserSessions.map((session) => session.context.close()));\n }\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -8,6 +8,7 @@ import {
8
8
  resolveProvisioningIds,
9
9
  runDeferredProvisioning
10
10
  } from "@open-mercato/onboarding/modules/onboarding/lib/deferred-provisioning";
11
+ import { isPreparationClaimActive } from "@open-mercato/onboarding/modules/onboarding/lib/preparation-claim";
11
12
  const metadata = {
12
13
  path: "/onboarding/onboarding/status",
13
14
  GET: {
@@ -67,7 +68,7 @@ async function GET(req) {
67
68
  if (provisioningIds && request.status === "processing") {
68
69
  await service.markCompleted(request, provisioningIds);
69
70
  }
70
- if (provisioningIds && request.status === "completed" && !request.preparationCompletedAt) {
71
+ if (provisioningIds && request.status === "completed" && !request.preparationCompletedAt && !isPreparationClaimActive(request.preparationStartedAt)) {
71
72
  after(async () => {
72
73
  await runDeferredProvisioning({
73
74
  requestId: request.id,
@@ -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 { 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 type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\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 if (provisioningIds && request.status === 'completed' && !request.preparationCompletedAt) {\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 console.error('[onboarding.status] ready email retry failed', {\n requestId: request.id,\n tenantId: readyTenantId,\n organizationId: request.organizationId,\n 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 { createRequestContainer } from '@open-mercato/shared/lib/di/container'\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\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 console.error('[onboarding.status] ready email retry failed', {\n requestId: request.id,\n tenantId: readyTenantId,\n organizationId: request.organizationId,\n 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
  }
@@ -71,6 +71,9 @@ __decorateClass([
71
71
  __decorateClass([
72
72
  Property({ name: "last_email_sent_at", type: Date, nullable: true })
73
73
  ], OnboardingRequest.prototype, "lastEmailSentAt", 2);
74
+ __decorateClass([
75
+ Property({ name: "preparation_started_at", type: Date, nullable: true })
76
+ ], OnboardingRequest.prototype, "preparationStartedAt", 2);
74
77
  __decorateClass([
75
78
  Property({ name: "preparation_completed_at", type: Date, nullable: true })
76
79
  ], OnboardingRequest.prototype, "preparationCompletedAt", 2);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/onboarding/data/entities.ts"],
4
- "sourcesContent": ["import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\ntype OnboardingStatus = 'pending' | 'processing' | 'completed' | 'expired'\n\n@Entity({ tableName: 'onboarding_requests' })\n@Unique({ properties: ['email'] })\n@Unique({ properties: ['tokenHash'] })\nexport class OnboardingRequest {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n email!: string\n\n @Property({ name: 'token_hash', type: 'text' })\n tokenHash!: string\n\n @Property({ type: 'text', default: 'pending' })\n status: OnboardingStatus = 'pending'\n\n @Property({ name: 'first_name', type: 'text' })\n firstName!: string\n\n @Property({ name: 'last_name', type: 'text' })\n lastName!: string\n\n @Property({ name: 'organization_name', type: 'text' })\n organizationName!: string\n\n @Property({ type: 'text', nullable: true })\n locale?: string | null\n\n @Property({ name: 'terms_accepted', type: 'boolean', default: false })\n termsAccepted: boolean = false\n\n @Property({ name: 'marketing_consent', type: 'boolean', default: false, nullable: true })\n marketingConsent?: boolean | null = false\n\n @Property({ name: 'password_hash', type: 'text', nullable: true })\n passwordHash?: string | null\n\n @Property({ name: 'processing_started_at', type: Date, nullable: true })\n processingStartedAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date })\n expiresAt!: Date\n\n @Property({ name: 'completed_at', type: Date, nullable: true })\n completedAt?: Date | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'user_id', type: 'uuid', nullable: true })\n userId?: string | null\n\n @Property({ name: 'last_email_sent_at', type: Date, nullable: true })\n lastEmailSentAt?: Date | null\n\n @Property({ name: 'preparation_completed_at', type: Date, nullable: true })\n preparationCompletedAt?: Date | null\n\n @Property({ name: 'ready_email_sent_at', type: Date, nullable: true })\n readyEmailSentAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })\n updatedAt?: Date\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\nexport type { OnboardingStatus }\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { Entity, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\ntype OnboardingStatus = 'pending' | 'processing' | 'completed' | 'expired'\n\n@Entity({ tableName: 'onboarding_requests' })\n@Unique({ properties: ['email'] })\n@Unique({ properties: ['tokenHash'] })\nexport class OnboardingRequest {\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ type: 'text' })\n email!: string\n\n @Property({ name: 'token_hash', type: 'text' })\n tokenHash!: string\n\n @Property({ type: 'text', default: 'pending' })\n status: OnboardingStatus = 'pending'\n\n @Property({ name: 'first_name', type: 'text' })\n firstName!: string\n\n @Property({ name: 'last_name', type: 'text' })\n lastName!: string\n\n @Property({ name: 'organization_name', type: 'text' })\n organizationName!: string\n\n @Property({ type: 'text', nullable: true })\n locale?: string | null\n\n @Property({ name: 'terms_accepted', type: 'boolean', default: false })\n termsAccepted: boolean = false\n\n @Property({ name: 'marketing_consent', type: 'boolean', default: false, nullable: true })\n marketingConsent?: boolean | null = false\n\n @Property({ name: 'password_hash', type: 'text', nullable: true })\n passwordHash?: string | null\n\n @Property({ name: 'processing_started_at', type: Date, nullable: true })\n processingStartedAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date })\n expiresAt!: Date\n\n @Property({ name: 'completed_at', type: Date, nullable: true })\n completedAt?: Date | null\n\n @Property({ name: 'tenant_id', type: 'uuid', nullable: true })\n tenantId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid', nullable: true })\n organizationId?: string | null\n\n @Property({ name: 'user_id', type: 'uuid', nullable: true })\n userId?: string | null\n\n @Property({ name: 'last_email_sent_at', type: Date, nullable: true })\n lastEmailSentAt?: Date | null\n\n @Property({ name: 'preparation_started_at', type: Date, nullable: true })\n preparationStartedAt?: Date | null\n\n @Property({ name: 'preparation_completed_at', type: Date, nullable: true })\n preparationCompletedAt?: Date | null\n\n @Property({ name: 'ready_email_sent_at', type: Date, nullable: true })\n readyEmailSentAt?: Date | null\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date(), nullable: true })\n updatedAt?: Date\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\nexport type { OnboardingStatus }\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -8,6 +8,7 @@ import { reindexEntity } from "@open-mercato/core/modules/query_index/lib/reinde
8
8
  import { purgeIndexScope } from "@open-mercato/core/modules/query_index/lib/purge";
9
9
  import { refreshCoverageSnapshot } from "@open-mercato/core/modules/query_index/lib/coverage";
10
10
  import { isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
11
+ import { PREPARATION_CLAIM_STALE_MS } from "@open-mercato/onboarding/modules/onboarding/lib/preparation-claim";
11
12
  const VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5e3;
12
13
  const SEED_EXAMPLES_TIMEOUT_MS = 15e3;
13
14
  function resolveProvisioningIds(request) {
@@ -60,12 +61,9 @@ async function runModuleSetupHook(args) {
60
61
  }
61
62
  }
62
63
  async function markWorkspaceReady(args) {
63
- const container = await createRequestContainer();
64
- const em = container.resolve("em");
65
- const service = new OnboardingService(em);
66
- const request = await service.findById(args.requestId);
67
- if (!request || request.preparationCompletedAt) return;
68
- await service.markPreparationCompleted(request, /* @__PURE__ */ new Date());
64
+ const request = await args.service.findById(args.requestId);
65
+ if (!request || request.preparationCompletedAt || request.status !== "completed") return;
66
+ await args.service.markPreparationCompleted(request, /* @__PURE__ */ new Date());
69
67
  }
70
68
  async function enqueueVectorReindex(args) {
71
69
  let searchIndexer = null;
@@ -148,9 +146,25 @@ async function rebuildTenantQueryIndexes(args) {
148
146
  async function runDeferredProvisioning(args) {
149
147
  const container = await createRequestContainer();
150
148
  const em = container.resolve("em");
149
+ const service = new OnboardingService(em);
150
+ const claimedAt = /* @__PURE__ */ new Date();
151
+ const claimed = await service.claimPreparation(
152
+ args.requestId,
153
+ claimedAt,
154
+ new Date(claimedAt.getTime() - PREPARATION_CLAIM_STALE_MS)
155
+ );
156
+ if (!claimed) {
157
+ console.info("[onboarding.verify] deferred provisioning skipped (already claimed or completed)", {
158
+ requestId: args.requestId,
159
+ tenantId: args.tenantId
160
+ });
161
+ return;
162
+ }
151
163
  const modules = getModules();
152
164
  for (const mod of modules) {
153
165
  if (!mod.setup?.seedExamples) continue;
166
+ await service.renewPreparation(args.requestId, /* @__PURE__ */ new Date()).catch(() => {
167
+ });
154
168
  try {
155
169
  await runModuleSetupHook({
156
170
  moduleId: mod.id,
@@ -180,8 +194,16 @@ async function runDeferredProvisioning(args) {
180
194
  }
181
195
  }
182
196
  }
197
+ await service.renewPreparation(args.requestId, /* @__PURE__ */ new Date()).catch(() => {
198
+ });
199
+ await rebuildTenantQueryIndexes({
200
+ em,
201
+ tenantId: args.tenantId,
202
+ organizationId: args.organizationId
203
+ });
183
204
  await markWorkspaceReady({
184
- requestId: args.requestId
205
+ requestId: args.requestId,
206
+ service
185
207
  });
186
208
  await sendWorkspaceReadyEmail({
187
209
  requestId: args.requestId,
@@ -193,12 +215,6 @@ async function runDeferredProvisioning(args) {
193
215
  organizationId: args.organizationId,
194
216
  error
195
217
  });
196
- throw error;
197
- });
198
- await rebuildTenantQueryIndexes({
199
- em,
200
- tenantId: args.tenantId,
201
- organizationId: args.organizationId
202
218
  });
203
219
  await enqueueVectorReindex({
204
220
  container,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/onboarding/lib/deferred-provisioning.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SearchIndexer } from '@open-mercato/search'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { getModules } from '@open-mercato/shared/lib/modules/registry'\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 { reindexEntity } from '@open-mercato/core/modules/query_index/lib/reindexer'\nimport { purgeIndexScope } from '@open-mercato/core/modules/query_index/lib/purge'\nimport { refreshCoverageSnapshot } from '@open-mercato/core/modules/query_index/lib/coverage'\nimport { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\n\nconst VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5_000\nconst SEED_EXAMPLES_TIMEOUT_MS = 15_000\n\nexport function resolveProvisioningIds(request: OnboardingRequest) {\n if (!request.tenantId || !request.organizationId || !request.userId) return null\n return {\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n userId: request.userId,\n }\n}\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: 'seedExamples'\n timeoutMs: number\n run: () => Promise<void>\n}) {\n const startedAt = Date.now()\n console.info('[onboarding.verify] 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 console.info('[onboarding.verify] 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 // Deferred provisioning is re-triggered on every preparing-page status\n // poll until preparationCompletedAt is set. seedExamples is not fully\n // idempotent (e.g. catalog product handles are unique-scoped), so a\n // re-run that lands before completion collides on an already-seeded row.\n // The workspace is already provisioned and the collision is expected and\n // harmless \u2014 log at info so genuine failures still stand out.\n console.info('[onboarding.verify] 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 console.error('[onboarding.verify] module hook failed', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n timeoutMs: args.timeoutMs,\n error,\n })\n }\n throw error\n }\n}\n\nasync function markWorkspaceReady(args: {\n requestId: string\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.findById(args.requestId)\n if (!request || request.preparationCompletedAt) return\n await service.markPreparationCompleted(request, new Date())\n}\n\nasync function enqueueVectorReindex(args: {\n container: { resolve: <T = unknown>(name: string) => T }\n tenantId: string\n organizationId: string\n}) {\n let searchIndexer: SearchIndexer | null = null\n try {\n searchIndexer = args.container.resolve<SearchIndexer>('searchIndexer')\n } catch {\n searchIndexer = null\n }\n if (!searchIndexer) return\n\n await Promise.race([\n searchIndexer.reindexAllToVector({\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n purgeFirst: true,\n useQueue: true,\n }),\n createTimeoutPromise('vector reindex enqueue', VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS),\n ])\n}\n\nasync function rebuildTenantQueryIndexes(args: {\n em: EntityManager\n tenantId: string\n organizationId: string\n}) {\n const coverageRefreshKeys = new Set<string>()\n try {\n const allEntities = getEntityIds()\n const entityIds = flattenSystemEntityIds(allEntities)\n for (const entityType of entityIds) {\n try {\n await purgeIndexScope(args.em, { entityType, tenantId: args.tenantId })\n } catch (error) {\n console.error('[onboarding.verify] failed to purge query index scope', {\n entityType,\n tenantId: args.tenantId,\n error,\n })\n }\n try {\n await reindexEntity(args.em, {\n entityType,\n tenantId: args.tenantId,\n force: true,\n emitVectorizeEvents: false,\n vectorService: null,\n })\n } catch (error) {\n console.error('[onboarding.verify] failed to reindex entity', {\n entityType,\n tenantId: args.tenantId,\n error,\n })\n }\n coverageRefreshKeys.add(`${entityType}|${args.tenantId}|__null__`)\n coverageRefreshKeys.add(`${entityType}|${args.tenantId}|${args.organizationId}`)\n }\n } catch (error) {\n console.error('[onboarding.verify] failed to rebuild query indexes', { tenantId: args.tenantId, error })\n }\n\n if (!coverageRefreshKeys.size) return\n\n for (const entry of coverageRefreshKeys) {\n const [entityType, tenantKey, orgKey] = entry.split('|')\n const orgScope = orgKey === '__null__' ? null : orgKey\n try {\n await refreshCoverageSnapshot(\n args.em,\n {\n entityType,\n tenantId: tenantKey,\n organizationId: orgScope,\n withDeleted: false,\n },\n )\n } catch (error) {\n console.error('[onboarding.verify] failed to refresh coverage snapshot', {\n entityType,\n tenantId: tenantKey,\n organizationId: orgScope,\n error,\n })\n }\n }\n}\n\nexport async function runDeferredProvisioning(args: {\n requestId: string\n tenantId: string\n organizationId: string\n}) {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const modules = getModules()\n\n for (const mod of modules) {\n if (!mod.setup?.seedExamples) continue\n try {\n await runModuleSetupHook({\n moduleId: mod.id,\n phase: 'seedExamples',\n timeoutMs: SEED_EXAMPLES_TIMEOUT_MS,\n run: () => mod.setup!.seedExamples!({\n em,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n container,\n }),\n })\n } catch (error) {\n if (isUniqueViolation(error)) {\n console.info('[onboarding.verify] deferred seedExamples skipped (already applied)', {\n moduleId: mod.id,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n })\n } else {\n console.error('[onboarding.verify] deferred seedExamples failed', {\n moduleId: mod.id,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n error,\n })\n }\n }\n }\n\n await markWorkspaceReady({\n requestId: args.requestId,\n })\n\n await sendWorkspaceReadyEmail({\n requestId: args.requestId,\n tenantId: args.tenantId,\n }).catch((error) => {\n console.error('[onboarding.verify] ready email failed', {\n requestId: args.requestId,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n error,\n })\n throw error\n })\n\n await rebuildTenantQueryIndexes({\n em,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n })\n\n await enqueueVectorReindex({\n container,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n }).catch((error) => {\n console.warn('[onboarding.verify] vector reindex enqueue did not complete promptly', {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n reason: error instanceof Error ? error.message : String(error),\n })\n })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { SearchIndexer } from '@open-mercato/search'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport { getModules } from '@open-mercato/shared/lib/modules/registry'\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 { reindexEntity } from '@open-mercato/core/modules/query_index/lib/reindexer'\nimport { purgeIndexScope } from '@open-mercato/core/modules/query_index/lib/purge'\nimport { refreshCoverageSnapshot } from '@open-mercato/core/modules/query_index/lib/coverage'\nimport { isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'\nimport { PREPARATION_CLAIM_STALE_MS } from '@open-mercato/onboarding/modules/onboarding/lib/preparation-claim'\n\nconst VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5_000\nconst SEED_EXAMPLES_TIMEOUT_MS = 15_000\n\nexport function resolveProvisioningIds(request: OnboardingRequest) {\n if (!request.tenantId || !request.organizationId || !request.userId) return null\n return {\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n userId: request.userId,\n }\n}\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: 'seedExamples'\n timeoutMs: number\n run: () => Promise<void>\n}) {\n const startedAt = Date.now()\n console.info('[onboarding.verify] 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 console.info('[onboarding.verify] 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 // Deferred provisioning is re-triggered on every preparing-page status\n // poll until preparationCompletedAt is set. seedExamples is not fully\n // idempotent (e.g. catalog product handles are unique-scoped), so a\n // re-run that lands before completion collides on an already-seeded row.\n // The workspace is already provisioned and the collision is expected and\n // harmless \u2014 log at info so genuine failures still stand out.\n console.info('[onboarding.verify] 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 console.error('[onboarding.verify] module hook failed', {\n moduleId: args.moduleId,\n phase: args.phase,\n durationMs: Math.max(0, Date.now() - startedAt),\n timeoutMs: args.timeoutMs,\n error,\n })\n }\n throw error\n }\n}\n\nasync function markWorkspaceReady(args: {\n requestId: string\n service: OnboardingService\n}) {\n const request = await args.service.findById(args.requestId)\n // The status guard protects a request that was re-submitted (and reset to\n // pending) while this chain was still running \u2014 completing it would let the\n // new flow skip its own deferred provisioning.\n if (!request || request.preparationCompletedAt || request.status !== 'completed') return\n await args.service.markPreparationCompleted(request, new Date())\n}\n\nasync function enqueueVectorReindex(args: {\n container: { resolve: <T = unknown>(name: string) => T }\n tenantId: string\n organizationId: string\n}) {\n let searchIndexer: SearchIndexer | null = null\n try {\n searchIndexer = args.container.resolve<SearchIndexer>('searchIndexer')\n } catch {\n searchIndexer = null\n }\n if (!searchIndexer) return\n\n await Promise.race([\n searchIndexer.reindexAllToVector({\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n purgeFirst: true,\n useQueue: true,\n }),\n createTimeoutPromise('vector reindex enqueue', VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS),\n ])\n}\n\nasync function rebuildTenantQueryIndexes(args: {\n em: EntityManager\n tenantId: string\n organizationId: string\n}) {\n const coverageRefreshKeys = new Set<string>()\n try {\n const allEntities = getEntityIds()\n const entityIds = flattenSystemEntityIds(allEntities)\n for (const entityType of entityIds) {\n try {\n await purgeIndexScope(args.em, { entityType, tenantId: args.tenantId })\n } catch (error) {\n console.error('[onboarding.verify] failed to purge query index scope', {\n entityType,\n tenantId: args.tenantId,\n error,\n })\n }\n try {\n await reindexEntity(args.em, {\n entityType,\n tenantId: args.tenantId,\n force: true,\n emitVectorizeEvents: false,\n vectorService: null,\n })\n } catch (error) {\n console.error('[onboarding.verify] failed to reindex entity', {\n entityType,\n tenantId: args.tenantId,\n error,\n })\n }\n coverageRefreshKeys.add(`${entityType}|${args.tenantId}|__null__`)\n coverageRefreshKeys.add(`${entityType}|${args.tenantId}|${args.organizationId}`)\n }\n } catch (error) {\n console.error('[onboarding.verify] failed to rebuild query indexes', { tenantId: args.tenantId, error })\n }\n\n if (!coverageRefreshKeys.size) return\n\n for (const entry of coverageRefreshKeys) {\n const [entityType, tenantKey, orgKey] = entry.split('|')\n const orgScope = orgKey === '__null__' ? null : orgKey\n try {\n await refreshCoverageSnapshot(\n args.em,\n {\n entityType,\n tenantId: tenantKey,\n organizationId: orgScope,\n withDeleted: false,\n },\n )\n } catch (error) {\n console.error('[onboarding.verify] failed to refresh coverage snapshot', {\n entityType,\n tenantId: tenantKey,\n organizationId: orgScope,\n error,\n })\n }\n }\n}\n\nexport async function runDeferredProvisioning(args: {\n requestId: string\n tenantId: string\n organizationId: string\n}) {\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = new OnboardingService(em)\n\n // Single-flight guard: the preparing page polls the status endpoint every\n // second and each poll (plus the verify handler) schedules this chain. The\n // atomic claim collapses those triggers into one run per request \u2014 without\n // it, dozens of concurrent seed + full-reindex chains exhaust the PG\n // connection pool (2026-06-11 demo outage). A stale claim (crashed runner)\n // becomes reclaimable after PREPARATION_CLAIM_STALE_MS.\n const claimedAt = new Date()\n const claimed = await service.claimPreparation(\n args.requestId,\n claimedAt,\n new Date(claimedAt.getTime() - PREPARATION_CLAIM_STALE_MS),\n )\n if (!claimed) {\n console.info('[onboarding.verify] deferred provisioning skipped (already claimed or completed)', {\n requestId: args.requestId,\n tenantId: args.tenantId,\n })\n return\n }\n\n const modules = getModules()\n\n for (const mod of modules) {\n if (!mod.setup?.seedExamples) continue\n // Heartbeat: keep the lease fresh while legitimately working so a slow run\n // (many modules \u00D7 15s seed timeout + rebuild) can never look stale and get\n // double-claimed by a later poll.\n await service.renewPreparation(args.requestId, new Date()).catch(() => {})\n try {\n await runModuleSetupHook({\n moduleId: mod.id,\n phase: 'seedExamples',\n timeoutMs: SEED_EXAMPLES_TIMEOUT_MS,\n run: () => mod.setup!.seedExamples!({\n em,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n container,\n }),\n })\n } catch (error) {\n if (isUniqueViolation(error)) {\n console.info('[onboarding.verify] deferred seedExamples skipped (already applied)', {\n moduleId: mod.id,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n })\n } else {\n console.error('[onboarding.verify] deferred seedExamples failed', {\n moduleId: mod.id,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n error,\n })\n }\n }\n }\n\n // The rebuild runs BEFORE the completion flag: preparationCompletedAt is the\n // terminal gate for both the status-route scheduling and claimPreparation,\n // so a runner that dies mid-rebuild must leave the flag unset \u2014 the stale\n // claim then makes the whole chain reclaimable and the rebuild self-heals.\n // rebuildTenantQueryIndexes never throws (it logs per-entity failures).\n await service.renewPreparation(args.requestId, new Date()).catch(() => {})\n await rebuildTenantQueryIndexes({\n em,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n })\n\n await markWorkspaceReady({\n requestId: args.requestId,\n service,\n })\n\n // Non-fatal (#2954 contract): an email failure must not abort the chain.\n // The status endpoint retries the ready email on later polls while\n // readyEmailSentAt is unset.\n await sendWorkspaceReadyEmail({\n requestId: args.requestId,\n tenantId: args.tenantId,\n }).catch((error) => {\n console.error('[onboarding.verify] ready email failed', {\n requestId: args.requestId,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n error,\n })\n })\n\n await enqueueVectorReindex({\n container,\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n }).catch((error) => {\n console.warn('[onboarding.verify] vector reindex enqueue did not complete promptly', {\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n reason: error instanceof Error ? error.message : String(error),\n })\n })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,10 @@
1
+ const PREPARATION_CLAIM_STALE_MS = 10 * 60 * 1e3;
2
+ function isPreparationClaimActive(startedAt, now = /* @__PURE__ */ new Date()) {
3
+ if (!startedAt) return false;
4
+ return startedAt.getTime() > now.getTime() - PREPARATION_CLAIM_STALE_MS;
5
+ }
6
+ export {
7
+ PREPARATION_CLAIM_STALE_MS,
8
+ isPreparationClaimActive
9
+ };
10
+ //# sourceMappingURL=preparation-claim.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/onboarding/lib/preparation-claim.ts"],
4
+ "sourcesContent": ["export const PREPARATION_CLAIM_STALE_MS = 10 * 60 * 1000\n\nexport function isPreparationClaimActive(startedAt: Date | null | undefined, now: Date = new Date()): boolean {\n if (!startedAt) return false\n return startedAt.getTime() > now.getTime() - PREPARATION_CLAIM_STALE_MS\n}\n"],
5
+ "mappings": "AAAO,MAAM,6BAA6B,KAAK,KAAK;AAE7C,SAAS,yBAAyB,WAAoC,MAAY,oBAAI,KAAK,GAAY;AAC5G,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,UAAU,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAC/C;",
6
+ "names": []
7
+ }
@@ -36,6 +36,7 @@ class OnboardingService {
36
36
  existing.organizationId = null;
37
37
  existing.userId = null;
38
38
  existing.lastEmailSentAt = now;
39
+ existing.preparationStartedAt = null;
39
40
  existing.preparationCompletedAt = null;
40
41
  existing.readyEmailSentAt = null;
41
42
  await this.em.flush();
@@ -126,8 +127,38 @@ class OnboardingService {
126
127
  request.readyEmailSentAt = sentAt;
127
128
  await this.em.flush();
128
129
  }
130
+ async claimPreparation(requestId, claimedAt, staleBefore) {
131
+ const claimedRows = await this.em.nativeUpdate(
132
+ OnboardingRequest,
133
+ {
134
+ id: requestId,
135
+ status: "completed",
136
+ preparationCompletedAt: null,
137
+ $or: [
138
+ { preparationStartedAt: null },
139
+ { preparationStartedAt: { $lt: staleBefore } }
140
+ ]
141
+ },
142
+ { preparationStartedAt: claimedAt, updatedAt: /* @__PURE__ */ new Date() }
143
+ );
144
+ return claimedRows > 0;
145
+ }
146
+ async renewPreparation(requestId, renewedAt) {
147
+ const renewedRows = await this.em.nativeUpdate(
148
+ OnboardingRequest,
149
+ {
150
+ id: requestId,
151
+ status: "completed",
152
+ preparationCompletedAt: null,
153
+ preparationStartedAt: { $ne: null }
154
+ },
155
+ { preparationStartedAt: renewedAt, updatedAt: /* @__PURE__ */ new Date() }
156
+ );
157
+ return renewedRows > 0;
158
+ }
129
159
  async markPreparationCompleted(request, completedAt) {
130
160
  request.preparationCompletedAt = completedAt;
161
+ request.preparationStartedAt = null;
131
162
  await this.em.flush();
132
163
  }
133
164
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/onboarding/lib/service.ts"],
4
- "sourcesContent": ["import { randomBytes, createHash } from 'node:crypto'\nimport { hash } from 'bcryptjs'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { OnboardingRequest } from '../data/entities'\nimport type { OnboardingStartInput } from '../data/validators'\n\ntype CreateRequestOptions = {\n expiresInHours?: number\n}\n\nexport class OnboardingService {\n constructor(private readonly em: EntityManager) {}\n\n async createOrUpdateRequest(input: OnboardingStartInput, options: CreateRequestOptions = {}) {\n const expiresInHours = options.expiresInHours ?? 24\n const token = randomBytes(32).toString('hex')\n const tokenHash = hashToken(token)\n const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000)\n const now = new Date()\n const passwordHash = await hash(input.password, 10)\n\n const existing = await this.em.findOne(OnboardingRequest, { email: input.email })\n if (existing) {\n const lastSentAt = existing.lastEmailSentAt ?? existing.updatedAt ?? existing.createdAt\n if (['pending', 'processing'].includes(existing.status) && lastSentAt && lastSentAt.getTime() > Date.now() - 10 * 60 * 1000) {\n const remainingMs = 10 * 60 * 1000 - (Date.now() - lastSentAt.getTime())\n const waitMinutes = Math.max(1, Math.ceil(remainingMs / (60 * 1000)))\n throw new Error(`PENDING_REQUEST:${waitMinutes}`)\n }\n existing.tokenHash = tokenHash\n existing.status = 'pending'\n existing.firstName = input.firstName\n existing.lastName = input.lastName\n existing.organizationName = input.organizationName\n existing.locale = input.locale ?? existing.locale ?? 'en'\n existing.termsAccepted = true\n existing.marketingConsent = input.marketingConsent ?? false\n existing.passwordHash = passwordHash\n existing.expiresAt = expiresAt\n existing.completedAt = null\n existing.processingStartedAt = null\n existing.tenantId = null\n existing.organizationId = null\n existing.userId = null\n existing.lastEmailSentAt = now\n existing.preparationCompletedAt = null\n existing.readyEmailSentAt = null\n await this.em.flush()\n return { request: existing, token }\n }\n\n const request = this.em.create(OnboardingRequest, {\n email: input.email,\n tokenHash,\n status: 'pending',\n firstName: input.firstName,\n lastName: input.lastName,\n organizationName: input.organizationName,\n locale: input.locale ?? 'en',\n termsAccepted: true,\n marketingConsent: input.marketingConsent ?? false,\n passwordHash,\n expiresAt,\n processingStartedAt: null,\n lastEmailSentAt: now,\n createdAt: now,\n updatedAt: now,\n })\n await this.em.persist(request).flush()\n return { request, token }\n }\n\n async findPendingByToken(token: string) {\n const tokenHash = hashToken(token)\n const now = new Date()\n return this.em.findOne(OnboardingRequest, {\n tokenHash,\n status: 'pending',\n expiresAt: { $gt: now } as any,\n })\n }\n\n async findByToken(token: string) {\n const tokenHash = hashToken(token)\n return this.em.findOne(OnboardingRequest, { tokenHash })\n }\n\n async findById(id: string) {\n return this.em.findOne(OnboardingRequest, { id })\n }\n\n async findLatestByTenantId(tenantId: string) {\n return this.em.findOne(\n OnboardingRequest,\n { tenantId, deletedAt: null },\n { orderBy: { updatedAt: 'DESC', createdAt: 'DESC' } },\n )\n }\n\n async startProcessing(request: OnboardingRequest, startedAt: Date): Promise<boolean> {\n const claimedRows = await this.em.nativeUpdate(\n OnboardingRequest,\n { id: request.id, status: 'pending' },\n { status: 'processing', processingStartedAt: startedAt, updatedAt: new Date() },\n )\n if (claimedRows === 0) return false\n request.status = 'processing'\n request.processingStartedAt = startedAt\n return true\n }\n\n async resetProcessing(request: OnboardingRequest): Promise<boolean> {\n const revertedRows = await this.em.nativeUpdate(\n OnboardingRequest,\n { id: request.id, status: 'processing' },\n { status: 'pending', processingStartedAt: null, updatedAt: new Date() },\n )\n if (revertedRows === 0) return false\n request.status = 'pending'\n request.processingStartedAt = null\n return true\n }\n\n async updateProvisioningIds(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {\n request.tenantId = data.tenantId\n request.organizationId = data.organizationId\n request.userId = data.userId\n await this.em.flush()\n }\n\n async markCompleted(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {\n request.status = 'completed'\n request.completedAt = new Date()\n request.tenantId = data.tenantId\n request.organizationId = data.organizationId\n request.userId = data.userId\n request.processingStartedAt = null\n request.passwordHash = null\n await this.em.flush()\n }\n\n async markReadyEmailSent(request: OnboardingRequest, sentAt: Date) {\n request.readyEmailSentAt = sentAt\n await this.em.flush()\n }\n\n async markPreparationCompleted(request: OnboardingRequest, completedAt: Date) {\n request.preparationCompletedAt = completedAt\n await this.em.flush()\n }\n}\n\nfunction hashToken(token: string) {\n return createHash('sha256').update(token).digest('hex')\n}\n"],
5
- "mappings": "AAAA,SAAS,aAAa,kBAAkB;AACxC,SAAS,YAAY;AAErB,SAAS,yBAAyB;AAO3B,MAAM,kBAAkB;AAAA,EAC7B,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,sBAAsB,OAA6B,UAAgC,CAAC,GAAG;AAC3F,UAAM,iBAAiB,QAAQ,kBAAkB;AACjD,UAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,UAAM,YAAY,UAAU,KAAK;AACjC,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,iBAAiB,KAAK,KAAK,GAAI;AACvE,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,eAAe,MAAM,KAAK,MAAM,UAAU,EAAE;AAElD,UAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,mBAAmB,EAAE,OAAO,MAAM,MAAM,CAAC;AAChF,QAAI,UAAU;AACZ,YAAM,aAAa,SAAS,mBAAmB,SAAS,aAAa,SAAS;AAC9E,UAAI,CAAC,WAAW,YAAY,EAAE,SAAS,SAAS,MAAM,KAAK,cAAc,WAAW,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,KAAK,KAAM;AAC3H,cAAM,cAAc,KAAK,KAAK,OAAQ,KAAK,IAAI,IAAI,WAAW,QAAQ;AACtE,cAAM,cAAc,KAAK,IAAI,GAAG,KAAK,KAAK,eAAe,KAAK,IAAK,CAAC;AACpE,cAAM,IAAI,MAAM,mBAAmB,WAAW,EAAE;AAAA,MAClD;AACA,eAAS,YAAY;AACrB,eAAS,SAAS;AAClB,eAAS,YAAY,MAAM;AAC3B,eAAS,WAAW,MAAM;AAC1B,eAAS,mBAAmB,MAAM;AAClC,eAAS,SAAS,MAAM,UAAU,SAAS,UAAU;AACrD,eAAS,gBAAgB;AACzB,eAAS,mBAAmB,MAAM,oBAAoB;AACtD,eAAS,eAAe;AACxB,eAAS,YAAY;AACrB,eAAS,cAAc;AACvB,eAAS,sBAAsB;AAC/B,eAAS,WAAW;AACpB,eAAS,iBAAiB;AAC1B,eAAS,SAAS;AAClB,eAAS,kBAAkB;AAC3B,eAAS,yBAAyB;AAClC,eAAS,mBAAmB;AAC5B,YAAM,KAAK,GAAG,MAAM;AACpB,aAAO,EAAE,SAAS,UAAU,MAAM;AAAA,IACpC;AAEA,UAAM,UAAU,KAAK,GAAG,OAAO,mBAAmB;AAAA,MAChD,OAAO,MAAM;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,kBAAkB,MAAM;AAAA,MACxB,QAAQ,MAAM,UAAU;AAAA,MACxB,eAAe;AAAA,MACf,kBAAkB,MAAM,oBAAoB;AAAA,MAC5C;AAAA,MACA;AAAA,MACA,qBAAqB;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,UAAM,KAAK,GAAG,QAAQ,OAAO,EAAE,MAAM;AACrC,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAAA,EAEA,MAAM,mBAAmB,OAAe;AACtC,UAAM,YAAY,UAAU,KAAK;AACjC,UAAM,MAAM,oBAAI,KAAK;AACrB,WAAO,KAAK,GAAG,QAAQ,mBAAmB;AAAA,MACxC;AAAA,MACA,QAAQ;AAAA,MACR,WAAW,EAAE,KAAK,IAAI;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,YAAY,OAAe;AAC/B,UAAM,YAAY,UAAU,KAAK;AACjC,WAAO,KAAK,GAAG,QAAQ,mBAAmB,EAAE,UAAU,CAAC;AAAA,EACzD;AAAA,EAEA,MAAM,SAAS,IAAY;AACzB,WAAO,KAAK,GAAG,QAAQ,mBAAmB,EAAE,GAAG,CAAC;AAAA,EAClD;AAAA,EAEA,MAAM,qBAAqB,UAAkB;AAC3C,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA,EAAE,UAAU,WAAW,KAAK;AAAA,MAC5B,EAAE,SAAS,EAAE,WAAW,QAAQ,WAAW,OAAO,EAAE;AAAA,IACtD;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,SAA4B,WAAmC;AACnF,UAAM,cAAc,MAAM,KAAK,GAAG;AAAA,MAChC;AAAA,MACA,EAAE,IAAI,QAAQ,IAAI,QAAQ,UAAU;AAAA,MACpC,EAAE,QAAQ,cAAc,qBAAqB,WAAW,WAAW,oBAAI,KAAK,EAAE;AAAA,IAChF;AACA,QAAI,gBAAgB,EAAG,QAAO;AAC9B,YAAQ,SAAS;AACjB,YAAQ,sBAAsB;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,gBAAgB,SAA8C;AAClE,UAAM,eAAe,MAAM,KAAK,GAAG;AAAA,MACjC;AAAA,MACA,EAAE,IAAI,QAAQ,IAAI,QAAQ,aAAa;AAAA,MACvC,EAAE,QAAQ,WAAW,qBAAqB,MAAM,WAAW,oBAAI,KAAK,EAAE;AAAA,IACxE;AACA,QAAI,iBAAiB,EAAG,QAAO;AAC/B,YAAQ,SAAS;AACjB,YAAQ,sBAAsB;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,sBAAsB,SAA4B,MAAoE;AAC1H,YAAQ,WAAW,KAAK;AACxB,YAAQ,iBAAiB,KAAK;AAC9B,YAAQ,SAAS,KAAK;AACtB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,cAAc,SAA4B,MAAoE;AAClH,YAAQ,SAAS;AACjB,YAAQ,cAAc,oBAAI,KAAK;AAC/B,YAAQ,WAAW,KAAK;AACxB,YAAQ,iBAAiB,KAAK;AAC9B,YAAQ,SAAS,KAAK;AACtB,YAAQ,sBAAsB;AAC9B,YAAQ,eAAe;AACvB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,mBAAmB,SAA4B,QAAc;AACjE,YAAQ,mBAAmB;AAC3B,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,yBAAyB,SAA4B,aAAmB;AAC5E,YAAQ,yBAAyB;AACjC,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AACF;AAEA,SAAS,UAAU,OAAe;AAChC,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;",
4
+ "sourcesContent": ["import { randomBytes, createHash } from 'node:crypto'\nimport { hash } from 'bcryptjs'\nimport { EntityManager } from '@mikro-orm/postgresql'\nimport { OnboardingRequest } from '../data/entities'\nimport type { OnboardingStartInput } from '../data/validators'\n\ntype CreateRequestOptions = {\n expiresInHours?: number\n}\n\nexport class OnboardingService {\n constructor(private readonly em: EntityManager) {}\n\n async createOrUpdateRequest(input: OnboardingStartInput, options: CreateRequestOptions = {}) {\n const expiresInHours = options.expiresInHours ?? 24\n const token = randomBytes(32).toString('hex')\n const tokenHash = hashToken(token)\n const expiresAt = new Date(Date.now() + expiresInHours * 60 * 60 * 1000)\n const now = new Date()\n const passwordHash = await hash(input.password, 10)\n\n const existing = await this.em.findOne(OnboardingRequest, { email: input.email })\n if (existing) {\n const lastSentAt = existing.lastEmailSentAt ?? existing.updatedAt ?? existing.createdAt\n if (['pending', 'processing'].includes(existing.status) && lastSentAt && lastSentAt.getTime() > Date.now() - 10 * 60 * 1000) {\n const remainingMs = 10 * 60 * 1000 - (Date.now() - lastSentAt.getTime())\n const waitMinutes = Math.max(1, Math.ceil(remainingMs / (60 * 1000)))\n throw new Error(`PENDING_REQUEST:${waitMinutes}`)\n }\n existing.tokenHash = tokenHash\n existing.status = 'pending'\n existing.firstName = input.firstName\n existing.lastName = input.lastName\n existing.organizationName = input.organizationName\n existing.locale = input.locale ?? existing.locale ?? 'en'\n existing.termsAccepted = true\n existing.marketingConsent = input.marketingConsent ?? false\n existing.passwordHash = passwordHash\n existing.expiresAt = expiresAt\n existing.completedAt = null\n existing.processingStartedAt = null\n existing.tenantId = null\n existing.organizationId = null\n existing.userId = null\n existing.lastEmailSentAt = now\n existing.preparationStartedAt = null\n existing.preparationCompletedAt = null\n existing.readyEmailSentAt = null\n await this.em.flush()\n return { request: existing, token }\n }\n\n const request = this.em.create(OnboardingRequest, {\n email: input.email,\n tokenHash,\n status: 'pending',\n firstName: input.firstName,\n lastName: input.lastName,\n organizationName: input.organizationName,\n locale: input.locale ?? 'en',\n termsAccepted: true,\n marketingConsent: input.marketingConsent ?? false,\n passwordHash,\n expiresAt,\n processingStartedAt: null,\n lastEmailSentAt: now,\n createdAt: now,\n updatedAt: now,\n })\n await this.em.persist(request).flush()\n return { request, token }\n }\n\n async findPendingByToken(token: string) {\n const tokenHash = hashToken(token)\n const now = new Date()\n return this.em.findOne(OnboardingRequest, {\n tokenHash,\n status: 'pending',\n expiresAt: { $gt: now } as any,\n })\n }\n\n async findByToken(token: string) {\n const tokenHash = hashToken(token)\n return this.em.findOne(OnboardingRequest, { tokenHash })\n }\n\n async findById(id: string) {\n return this.em.findOne(OnboardingRequest, { id })\n }\n\n async findLatestByTenantId(tenantId: string) {\n return this.em.findOne(\n OnboardingRequest,\n { tenantId, deletedAt: null },\n { orderBy: { updatedAt: 'DESC', createdAt: 'DESC' } },\n )\n }\n\n async startProcessing(request: OnboardingRequest, startedAt: Date): Promise<boolean> {\n const claimedRows = await this.em.nativeUpdate(\n OnboardingRequest,\n { id: request.id, status: 'pending' },\n { status: 'processing', processingStartedAt: startedAt, updatedAt: new Date() },\n )\n if (claimedRows === 0) return false\n request.status = 'processing'\n request.processingStartedAt = startedAt\n return true\n }\n\n async resetProcessing(request: OnboardingRequest): Promise<boolean> {\n const revertedRows = await this.em.nativeUpdate(\n OnboardingRequest,\n { id: request.id, status: 'processing' },\n { status: 'pending', processingStartedAt: null, updatedAt: new Date() },\n )\n if (revertedRows === 0) return false\n request.status = 'pending'\n request.processingStartedAt = null\n return true\n }\n\n async updateProvisioningIds(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {\n request.tenantId = data.tenantId\n request.organizationId = data.organizationId\n request.userId = data.userId\n await this.em.flush()\n }\n\n async markCompleted(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {\n request.status = 'completed'\n request.completedAt = new Date()\n request.tenantId = data.tenantId\n request.organizationId = data.organizationId\n request.userId = data.userId\n request.processingStartedAt = null\n request.passwordHash = null\n await this.em.flush()\n }\n\n async markReadyEmailSent(request: OnboardingRequest, sentAt: Date) {\n request.readyEmailSentAt = sentAt\n await this.em.flush()\n }\n\n async claimPreparation(requestId: string, claimedAt: Date, staleBefore: Date): Promise<boolean> {\n const claimedRows = await this.em.nativeUpdate(\n OnboardingRequest,\n {\n id: requestId,\n status: 'completed',\n preparationCompletedAt: null,\n $or: [\n { preparationStartedAt: null },\n { preparationStartedAt: { $lt: staleBefore } },\n ],\n },\n { preparationStartedAt: claimedAt, updatedAt: new Date() },\n )\n return claimedRows > 0\n }\n\n async renewPreparation(requestId: string, renewedAt: Date): Promise<boolean> {\n const renewedRows = await this.em.nativeUpdate(\n OnboardingRequest,\n {\n id: requestId,\n status: 'completed',\n preparationCompletedAt: null,\n preparationStartedAt: { $ne: null },\n },\n { preparationStartedAt: renewedAt, updatedAt: new Date() },\n )\n return renewedRows > 0\n }\n\n async markPreparationCompleted(request: OnboardingRequest, completedAt: Date) {\n request.preparationCompletedAt = completedAt\n request.preparationStartedAt = null\n await this.em.flush()\n }\n}\n\nfunction hashToken(token: string) {\n return createHash('sha256').update(token).digest('hex')\n}\n"],
5
+ "mappings": "AAAA,SAAS,aAAa,kBAAkB;AACxC,SAAS,YAAY;AAErB,SAAS,yBAAyB;AAO3B,MAAM,kBAAkB;AAAA,EAC7B,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA,EAEjD,MAAM,sBAAsB,OAA6B,UAAgC,CAAC,GAAG;AAC3F,UAAM,iBAAiB,QAAQ,kBAAkB;AACjD,UAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,UAAM,YAAY,UAAU,KAAK;AACjC,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,iBAAiB,KAAK,KAAK,GAAI;AACvE,UAAM,MAAM,oBAAI,KAAK;AACrB,UAAM,eAAe,MAAM,KAAK,MAAM,UAAU,EAAE;AAElD,UAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,mBAAmB,EAAE,OAAO,MAAM,MAAM,CAAC;AAChF,QAAI,UAAU;AACZ,YAAM,aAAa,SAAS,mBAAmB,SAAS,aAAa,SAAS;AAC9E,UAAI,CAAC,WAAW,YAAY,EAAE,SAAS,SAAS,MAAM,KAAK,cAAc,WAAW,QAAQ,IAAI,KAAK,IAAI,IAAI,KAAK,KAAK,KAAM;AAC3H,cAAM,cAAc,KAAK,KAAK,OAAQ,KAAK,IAAI,IAAI,WAAW,QAAQ;AACtE,cAAM,cAAc,KAAK,IAAI,GAAG,KAAK,KAAK,eAAe,KAAK,IAAK,CAAC;AACpE,cAAM,IAAI,MAAM,mBAAmB,WAAW,EAAE;AAAA,MAClD;AACA,eAAS,YAAY;AACrB,eAAS,SAAS;AAClB,eAAS,YAAY,MAAM;AAC3B,eAAS,WAAW,MAAM;AAC1B,eAAS,mBAAmB,MAAM;AAClC,eAAS,SAAS,MAAM,UAAU,SAAS,UAAU;AACrD,eAAS,gBAAgB;AACzB,eAAS,mBAAmB,MAAM,oBAAoB;AACtD,eAAS,eAAe;AACxB,eAAS,YAAY;AACrB,eAAS,cAAc;AACvB,eAAS,sBAAsB;AAC/B,eAAS,WAAW;AACpB,eAAS,iBAAiB;AAC1B,eAAS,SAAS;AAClB,eAAS,kBAAkB;AAC3B,eAAS,uBAAuB;AAChC,eAAS,yBAAyB;AAClC,eAAS,mBAAmB;AAC5B,YAAM,KAAK,GAAG,MAAM;AACpB,aAAO,EAAE,SAAS,UAAU,MAAM;AAAA,IACpC;AAEA,UAAM,UAAU,KAAK,GAAG,OAAO,mBAAmB;AAAA,MAChD,OAAO,MAAM;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,MACR,WAAW,MAAM;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,kBAAkB,MAAM;AAAA,MACxB,QAAQ,MAAM,UAAU;AAAA,MACxB,eAAe;AAAA,MACf,kBAAkB,MAAM,oBAAoB;AAAA,MAC5C;AAAA,MACA;AAAA,MACA,qBAAqB;AAAA,MACrB,iBAAiB;AAAA,MACjB,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,UAAM,KAAK,GAAG,QAAQ,OAAO,EAAE,MAAM;AACrC,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAAA,EAEA,MAAM,mBAAmB,OAAe;AACtC,UAAM,YAAY,UAAU,KAAK;AACjC,UAAM,MAAM,oBAAI,KAAK;AACrB,WAAO,KAAK,GAAG,QAAQ,mBAAmB;AAAA,MACxC;AAAA,MACA,QAAQ;AAAA,MACR,WAAW,EAAE,KAAK,IAAI;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,YAAY,OAAe;AAC/B,UAAM,YAAY,UAAU,KAAK;AACjC,WAAO,KAAK,GAAG,QAAQ,mBAAmB,EAAE,UAAU,CAAC;AAAA,EACzD;AAAA,EAEA,MAAM,SAAS,IAAY;AACzB,WAAO,KAAK,GAAG,QAAQ,mBAAmB,EAAE,GAAG,CAAC;AAAA,EAClD;AAAA,EAEA,MAAM,qBAAqB,UAAkB;AAC3C,WAAO,KAAK,GAAG;AAAA,MACb;AAAA,MACA,EAAE,UAAU,WAAW,KAAK;AAAA,MAC5B,EAAE,SAAS,EAAE,WAAW,QAAQ,WAAW,OAAO,EAAE;AAAA,IACtD;AAAA,EACF;AAAA,EAEA,MAAM,gBAAgB,SAA4B,WAAmC;AACnF,UAAM,cAAc,MAAM,KAAK,GAAG;AAAA,MAChC;AAAA,MACA,EAAE,IAAI,QAAQ,IAAI,QAAQ,UAAU;AAAA,MACpC,EAAE,QAAQ,cAAc,qBAAqB,WAAW,WAAW,oBAAI,KAAK,EAAE;AAAA,IAChF;AACA,QAAI,gBAAgB,EAAG,QAAO;AAC9B,YAAQ,SAAS;AACjB,YAAQ,sBAAsB;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,gBAAgB,SAA8C;AAClE,UAAM,eAAe,MAAM,KAAK,GAAG;AAAA,MACjC;AAAA,MACA,EAAE,IAAI,QAAQ,IAAI,QAAQ,aAAa;AAAA,MACvC,EAAE,QAAQ,WAAW,qBAAqB,MAAM,WAAW,oBAAI,KAAK,EAAE;AAAA,IACxE;AACA,QAAI,iBAAiB,EAAG,QAAO;AAC/B,YAAQ,SAAS;AACjB,YAAQ,sBAAsB;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,sBAAsB,SAA4B,MAAoE;AAC1H,YAAQ,WAAW,KAAK;AACxB,YAAQ,iBAAiB,KAAK;AAC9B,YAAQ,SAAS,KAAK;AACtB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,cAAc,SAA4B,MAAoE;AAClH,YAAQ,SAAS;AACjB,YAAQ,cAAc,oBAAI,KAAK;AAC/B,YAAQ,WAAW,KAAK;AACxB,YAAQ,iBAAiB,KAAK;AAC9B,YAAQ,SAAS,KAAK;AACtB,YAAQ,sBAAsB;AAC9B,YAAQ,eAAe;AACvB,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,mBAAmB,SAA4B,QAAc;AACjE,YAAQ,mBAAmB;AAC3B,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,iBAAiB,WAAmB,WAAiB,aAAqC;AAC9F,UAAM,cAAc,MAAM,KAAK,GAAG;AAAA,MAChC;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,wBAAwB;AAAA,QACxB,KAAK;AAAA,UACH,EAAE,sBAAsB,KAAK;AAAA,UAC7B,EAAE,sBAAsB,EAAE,KAAK,YAAY,EAAE;AAAA,QAC/C;AAAA,MACF;AAAA,MACA,EAAE,sBAAsB,WAAW,WAAW,oBAAI,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO,cAAc;AAAA,EACvB;AAAA,EAEA,MAAM,iBAAiB,WAAmB,WAAmC;AAC3E,UAAM,cAAc,MAAM,KAAK,GAAG;AAAA,MAChC;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,QAAQ;AAAA,QACR,wBAAwB;AAAA,QACxB,sBAAsB,EAAE,KAAK,KAAK;AAAA,MACpC;AAAA,MACA,EAAE,sBAAsB,WAAW,WAAW,oBAAI,KAAK,EAAE;AAAA,IAC3D;AACA,WAAO,cAAc;AAAA,EACvB;AAAA,EAEA,MAAM,yBAAyB,SAA4B,aAAmB;AAC5E,YAAQ,yBAAyB;AACjC,YAAQ,uBAAuB;AAC/B,UAAM,KAAK,GAAG,MAAM;AAAA,EACtB;AACF;AAEA,SAAS,UAAU,OAAe;AAChC,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,13 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260611120000 extends Migration {
3
+ async up() {
4
+ this.addSql(`alter table "onboarding_requests" add column "preparation_started_at" timestamptz null;`);
5
+ }
6
+ async down() {
7
+ this.addSql(`alter table "onboarding_requests" drop column "preparation_started_at";`);
8
+ }
9
+ }
10
+ export {
11
+ Migration20260611120000
12
+ };
13
+ //# sourceMappingURL=Migration20260611120000.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/onboarding/migrations/Migration20260611120000.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260611120000 extends Migration {\n\n override async up(): Promise<void> {\n this.addSql(`alter table \"onboarding_requests\" add column \"preparation_started_at\" timestamptz null;`);\n }\n\n override async down(): Promise<void> {\n this.addSql(`alter table \"onboarding_requests\" drop column \"preparation_started_at\";`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,gCAAgC,UAAU;AAAA,EAErD,MAAe,KAAoB;AACjC,SAAK,OAAO,yFAAyF;AAAA,EACvG;AAAA,EAEA,MAAe,OAAsB;AACnC,SAAK,OAAO,yEAAyE;AAAA,EACvF;AAEF;",
6
+ "names": []
7
+ }