@open-mercato/onboarding 0.6.5-develop.5099.1.85c0aff78c → 0.6.5-develop.5139.1.g7925348493

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.
@@ -10,9 +10,27 @@ const metadata = {
10
10
  requireAuth: false
11
11
  }
12
12
  };
13
+ const ONBOARDING_LOGIN_TENANT_COOKIE = "om_login_tenant";
13
14
  const onboardingStatusQuerySchema = z.object({
14
15
  tenantId: z.string().uuid()
15
16
  });
17
+ function readCookie(req, name) {
18
+ const header = req.headers.get("cookie");
19
+ if (!header) return null;
20
+ for (const part of header.split(";")) {
21
+ const separatorIndex = part.indexOf("=");
22
+ if (separatorIndex === -1) continue;
23
+ const key = part.slice(0, separatorIndex).trim();
24
+ if (key !== name) continue;
25
+ const rawValue = part.slice(separatorIndex + 1).trim();
26
+ try {
27
+ return decodeURIComponent(rawValue);
28
+ } catch {
29
+ return rawValue;
30
+ }
31
+ }
32
+ return null;
33
+ }
16
34
  async function GET(req) {
17
35
  const url = new URL(req.url);
18
36
  const tenantId = url.searchParams.get("tenantId") || url.searchParams.get("tenant") || "";
@@ -20,6 +38,10 @@ async function GET(req) {
20
38
  if (!parsed.success) {
21
39
  return NextResponse.json({ ok: false, error: "Invalid tenant id." }, { status: 400 });
22
40
  }
41
+ const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE);
42
+ if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {
43
+ return NextResponse.json({ ok: false, error: "Not authorized for this tenant." }, { status: 403 });
44
+ }
23
45
  let baseUrl;
24
46
  try {
25
47
  baseUrl = getSecurityEmailBaseUrl(req);
@@ -88,6 +110,7 @@ const onboardingStatusDoc = {
88
110
  ],
89
111
  errors: [
90
112
  { status: 400, description: "Invalid tenant id or request origin.", schema: onboardingStatusErrorSchema },
113
+ { status: 403, description: "Caller is not authorized for this tenant.", schema: onboardingStatusErrorSchema },
91
114
  { status: 404, description: "Onboarding request not found.", schema: onboardingStatusErrorSchema },
92
115
  { status: 500, description: "Onboarding status is not configured.", schema: onboardingStatusErrorSchema }
93
116
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/onboarding/api/get/onboarding/status.ts"],
4
- "sourcesContent": ["import { 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 { getSecurityEmailBaseUrl, 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 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 onboardingStatusQuerySchema = z.object({\n tenantId: z.string().uuid(),\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 let baseUrl: string\n try {\n baseUrl = getSecurityEmailBaseUrl(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 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 let emailSent = Boolean(request.readyEmailSentAt)\n const ready = request.status === 'completed' && Boolean(request.preparationCompletedAt)\n const loginUrl = ready && request.tenantId ? `${baseUrl}/login?tenant=${encodeURIComponent(request.tenantId)}` : null\n\n if (ready && request.tenantId && !request.readyEmailSentAt) {\n try {\n emailSent = await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: request.tenantId,\n })\n } catch (error) {\n console.error('[onboarding.status] ready email retry failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n error,\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: 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 { 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 { getSecurityEmailBaseUrl, 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 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 let baseUrl: string\n try {\n baseUrl = getSecurityEmailBaseUrl(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 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 let emailSent = Boolean(request.readyEmailSentAt)\n const ready = request.status === 'completed' && Boolean(request.preparationCompletedAt)\n const loginUrl = ready && request.tenantId ? `${baseUrl}/login?tenant=${encodeURIComponent(request.tenantId)}` : null\n\n if (ready && request.tenantId && !request.readyEmailSentAt) {\n try {\n emailSent = await sendWorkspaceReadyEmail({\n requestId: request.id,\n tenantId: request.tenantId,\n })\n } catch (error) {\n console.error('[onboarding.status] ready email retry failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n error,\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
  }
@@ -311,7 +311,14 @@ async function GET(req) {
311
311
  if (request.status !== "pending") {
312
312
  return redirectWithStatus(baseUrl, "invalid");
313
313
  }
314
- await service.startProcessing(request, /* @__PURE__ */ new Date());
314
+ const claimed = await service.startProcessing(request, /* @__PURE__ */ new Date());
315
+ if (!claimed) {
316
+ const current = await new OnboardingService(em.fork()).findById(request.id);
317
+ if (current?.status === "completed" && current.tenantId) {
318
+ return current.preparationCompletedAt ? redirectToLogin(baseUrl, current.tenantId) : redirectToPreparing(baseUrl, current.tenantId);
319
+ }
320
+ return redirectToPreparing(baseUrl, current?.tenantId ?? request.tenantId ?? null);
321
+ }
315
322
  if (!request.passwordHash) {
316
323
  console.error("[onboarding.verify] missing password hash for request", request.id);
317
324
  await service.resetProcessing(request);
@@ -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 type { SearchIndexer } from '@open-mercato/search'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n AppOriginConfigurationError,\n AppOriginRejectedError,\n getSecurityEmailBaseUrl,\n} from '@open-mercato/shared/lib/url'\nimport { onboardingVerifySchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\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 { 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 { 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 { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n path: '/onboarding/onboarding/verify',\n GET: {\n requireAuth: false,\n },\n}\n\nfunction resolveTrustedBaseUrl(req: Request): string {\n try {\n return getSecurityEmailBaseUrl(req)\n } catch (error) {\n if (error instanceof AppOriginRejectedError || error instanceof AppOriginConfigurationError) {\n console.error('[onboarding.verify] rejected request origin for redirect base', {\n requestUrl: req.url,\n reason: error.message,\n })\n return new URL(req.url).origin\n }\n throw error\n }\n}\n\nfunction clearAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n response.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n response.cookies.set('om_login_tenant', '', { path: '/', maxAge: 0 })\n}\n\nfunction redirectWithStatus(baseUrl: string, status: string) {\n const response = NextResponse.redirect(`${baseUrl}/onboarding?status=${encodeURIComponent(status)}`)\n clearAuthCookies(response)\n return response\n}\n\nfunction redirectToPreparing(baseUrl: string, tenantId: string | null) {\n const tenantParam = tenantId ? `?tenant=${encodeURIComponent(tenantId)}` : ''\n const response = NextResponse.redirect(`${baseUrl}/onboarding/preparing${tenantParam}`)\n clearAuthCookies(response)\n if (tenantId) {\n response.cookies.set('om_login_tenant', tenantId, {\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n maxAge: 60 * 60 * 24 * 14,\n })\n }\n return response\n}\n\nfunction redirectToLogin(baseUrl: string, tenantId: string | null) {\n const tenantParam = tenantId ? `?tenant=${encodeURIComponent(tenantId)}` : ''\n const response = NextResponse.redirect(`${baseUrl}/login${tenantParam}`)\n clearAuthCookies(response)\n if (tenantId) {\n response.cookies.set('om_login_tenant', tenantId, {\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n maxAge: 60 * 60 * 24 * 14,\n })\n }\n return response\n}\n\nconst VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5_000\nconst SEED_DEFAULTS_TIMEOUT_MS = 15_000\nconst SEED_EXAMPLES_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 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 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 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\nasync 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 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 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\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedBaseUrl(req)\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 console.error('[onboarding.verify] retry ready email failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n 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 return redirectToPreparing(baseUrl, request.tenantId ?? null)\n }\n if (request.status === 'processing' && !processingFresh) {\n await service.resetProcessing(request)\n }\n if (request.status !== 'pending') {\n return redirectWithStatus(baseUrl, 'invalid')\n }\n await service.startProcessing(request, new Date())\n if (!request.passwordHash) {\n console.error('[onboarding.verify] missing password hash for request', 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 // Wrap the request-em consent write in a transaction so it commits\n // all-or-nothing. setupInitialTenant already manages its own internal\n // transactions, and the seedDefaults hooks below resolve container\n // services that fork their own EM (and may enqueue work), so neither can\n // join this transaction \u2014 only the direct request-em writes are wrapped.\n await em.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 // Call module seedDefaults + seedExamples hooks\n const modules = getModules()\n for (const mod of modules) {\n if (mod.setup?.seedDefaults) {\n await runModuleSetupHook({\n moduleId: mod.id,\n phase: 'seedDefaults',\n timeoutMs: SEED_DEFAULTS_TIMEOUT_MS,\n run: () => mod.setup!.seedDefaults!({\n em,\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 console.error('[onboarding.verify] failed', 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 type { SearchIndexer } from '@open-mercato/search'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport {\n AppOriginConfigurationError,\n AppOriginRejectedError,\n getSecurityEmailBaseUrl,\n} from '@open-mercato/shared/lib/url'\nimport { onboardingVerifySchema } from '@open-mercato/onboarding/modules/onboarding/data/validators'\nimport { OnboardingService } from '@open-mercato/onboarding/modules/onboarding/lib/service'\nimport { sendWorkspaceReadyEmail } from '@open-mercato/onboarding/modules/onboarding/lib/ready-email'\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 { 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 { 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 { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\n\nexport const metadata = {\n path: '/onboarding/onboarding/verify',\n GET: {\n requireAuth: false,\n },\n}\n\nfunction resolveTrustedBaseUrl(req: Request): string {\n try {\n return getSecurityEmailBaseUrl(req)\n } catch (error) {\n if (error instanceof AppOriginRejectedError || error instanceof AppOriginConfigurationError) {\n console.error('[onboarding.verify] rejected request origin for redirect base', {\n requestUrl: req.url,\n reason: error.message,\n })\n return new URL(req.url).origin\n }\n throw error\n }\n}\n\nfunction clearAuthCookies(response: NextResponse) {\n response.cookies.set('auth_token', '', { path: '/', maxAge: 0 })\n response.cookies.set('session_token', '', { path: '/', maxAge: 0 })\n response.cookies.set('om_login_tenant', '', { path: '/', maxAge: 0 })\n}\n\nfunction redirectWithStatus(baseUrl: string, status: string) {\n const response = NextResponse.redirect(`${baseUrl}/onboarding?status=${encodeURIComponent(status)}`)\n clearAuthCookies(response)\n return response\n}\n\nfunction redirectToPreparing(baseUrl: string, tenantId: string | null) {\n const tenantParam = tenantId ? `?tenant=${encodeURIComponent(tenantId)}` : ''\n const response = NextResponse.redirect(`${baseUrl}/onboarding/preparing${tenantParam}`)\n clearAuthCookies(response)\n if (tenantId) {\n response.cookies.set('om_login_tenant', tenantId, {\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n maxAge: 60 * 60 * 24 * 14,\n })\n }\n return response\n}\n\nfunction redirectToLogin(baseUrl: string, tenantId: string | null) {\n const tenantParam = tenantId ? `?tenant=${encodeURIComponent(tenantId)}` : ''\n const response = NextResponse.redirect(`${baseUrl}/login${tenantParam}`)\n clearAuthCookies(response)\n if (tenantId) {\n response.cookies.set('om_login_tenant', tenantId, {\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n path: '/',\n maxAge: 60 * 60 * 24 * 14,\n })\n }\n return response\n}\n\nconst VECTOR_REINDEX_ENQUEUE_TIMEOUT_MS = 5_000\nconst SEED_DEFAULTS_TIMEOUT_MS = 15_000\nconst SEED_EXAMPLES_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 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 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 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\nasync 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 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 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\nexport async function GET(req: Request) {\n const url = new URL(req.url)\n const baseUrl = resolveTrustedBaseUrl(req)\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 console.error('[onboarding.verify] retry ready email failed', {\n requestId: request.id,\n tenantId: request.tenantId,\n organizationId: request.organizationId,\n 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 return redirectToPreparing(baseUrl, request.tenantId ?? null)\n }\n if (request.status === 'processing' && !processingFresh) {\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 current = await new OnboardingService(em.fork()).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 return redirectToPreparing(baseUrl, current?.tenantId ?? request.tenantId ?? null)\n }\n if (!request.passwordHash) {\n console.error('[onboarding.verify] missing password hash for request', 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 // Wrap the request-em consent write in a transaction so it commits\n // all-or-nothing. setupInitialTenant already manages its own internal\n // transactions, and the seedDefaults hooks below resolve container\n // services that fork their own EM (and may enqueue work), so neither can\n // join this transaction \u2014 only the direct request-em writes are wrapped.\n await em.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 // Call module seedDefaults + seedExamples hooks\n const modules = getModules()\n for (const mod of modules) {\n if (mod.setup?.seedDefaults) {\n await runModuleSetupHook({\n moduleId: mod.id,\n phase: 'seedDefaults',\n timeoutMs: SEED_DEFAULTS_TIMEOUT_MS,\n run: () => mod.setup!.seedDefaults!({\n em,\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 console.error('[onboarding.verify] failed', 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
  }
@@ -85,14 +85,26 @@ class OnboardingService {
85
85
  );
86
86
  }
87
87
  async startProcessing(request, startedAt) {
88
+ const claimedRows = await this.em.nativeUpdate(
89
+ OnboardingRequest,
90
+ { id: request.id, status: "pending" },
91
+ { status: "processing", processingStartedAt: startedAt, updatedAt: /* @__PURE__ */ new Date() }
92
+ );
93
+ if (claimedRows === 0) return false;
88
94
  request.status = "processing";
89
95
  request.processingStartedAt = startedAt;
90
- await this.em.flush();
96
+ return true;
91
97
  }
92
98
  async resetProcessing(request) {
99
+ const revertedRows = await this.em.nativeUpdate(
100
+ OnboardingRequest,
101
+ { id: request.id, status: "processing" },
102
+ { status: "pending", processingStartedAt: null, updatedAt: /* @__PURE__ */ new Date() }
103
+ );
104
+ if (revertedRows === 0) return false;
93
105
  request.status = "pending";
94
106
  request.processingStartedAt = null;
95
- await this.em.flush();
107
+ return true;
96
108
  }
97
109
  async updateProvisioningIds(request, data) {
98
110
  request.tenantId = data.tenantId;
@@ -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) {\n request.status = 'processing'\n request.processingStartedAt = startedAt\n await this.em.flush()\n }\n\n async resetProcessing(request: OnboardingRequest) {\n request.status = 'pending'\n request.processingStartedAt = null\n await this.em.flush()\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": "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",
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": "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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.5-develop.5099.1.85c0aff78c",
3
+ "version": "0.6.5-develop.5139.1.g7925348493",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -69,10 +69,10 @@
69
69
  }
70
70
  },
71
71
  "peerDependencies": {
72
- "@open-mercato/shared": "0.6.5-develop.5099.1.85c0aff78c"
72
+ "@open-mercato/shared": "0.6.5-develop.5139.1.g7925348493"
73
73
  },
74
74
  "devDependencies": {
75
- "@open-mercato/shared": "0.6.5-develop.5099.1.85c0aff78c",
75
+ "@open-mercato/shared": "0.6.5-develop.5139.1.g7925348493",
76
76
  "@types/jest": "^30.0.0",
77
77
  "jest": "^30.4.2",
78
78
  "ts-jest": "^29.4.11"
@@ -75,6 +75,38 @@ function createMockEm(overrides: Record<string, unknown> = {}): MockEm {
75
75
  } as unknown as MockEm
76
76
  }
77
77
 
78
+ type DbRow = { id: string; status: string; processingStartedAt: Date | null }
79
+
80
+ // EntityManager mock backed by a single persisted row. `nativeUpdate` honours the
81
+ // status guard in its WHERE clause (so it returns 0 when the row has already moved
82
+ // on), while `flush` writes the tracked managed entity's scalars back to the row —
83
+ // reproducing MikroORM's last-write-wins behaviour for the unconditional code path.
84
+ function createRowBackedEm(dbRow: DbRow, trackedEntity?: OnboardingRequest): EntityManager {
85
+ const flush = jest.fn(async () => {
86
+ if (!trackedEntity) return
87
+ dbRow.status = trackedEntity.status
88
+ dbRow.processingStartedAt = trackedEntity.processingStartedAt ?? null
89
+ })
90
+ const nativeUpdate = jest.fn(
91
+ async (_entity: unknown, where: Record<string, unknown>, data: Record<string, unknown>) => {
92
+ const matches = Object.entries(where).every(
93
+ ([key, value]) => (dbRow as unknown as Record<string, unknown>)[key] === value,
94
+ )
95
+ if (!matches) return 0
96
+ if ('status' in data) dbRow.status = data.status as string
97
+ if ('processingStartedAt' in data) dbRow.processingStartedAt = (data.processingStartedAt as Date | null) ?? null
98
+ return 1
99
+ },
100
+ )
101
+ return {
102
+ findOne: jest.fn().mockResolvedValue(null),
103
+ create: jest.fn(),
104
+ persist: jest.fn(() => ({ flush })),
105
+ flush,
106
+ nativeUpdate,
107
+ } as unknown as EntityManager
108
+ }
109
+
78
110
  describe('OnboardingService', () => {
79
111
  describe('createOrUpdateRequest', () => {
80
112
  it('creates a new pending request when no existing email found', async () => {
@@ -126,4 +158,54 @@ describe('OnboardingService', () => {
126
158
  expect(em.flush).toHaveBeenCalled()
127
159
  })
128
160
  })
161
+
162
+ // Regression coverage for #2742: two concurrent verify requests carrying the same
163
+ // token must not corrupt the persisted state. The fix makes the pending→processing
164
+ // transition an atomic claim and the processing→pending reset conditional, so a
165
+ // request that lost the race can never clobber a sibling that already completed.
166
+ describe('startProcessing (atomic claim, #2742)', () => {
167
+ it('lets only one concurrent caller claim a pending request', async () => {
168
+ const dbRow: DbRow = { id: 'req-1', status: 'pending', processingStartedAt: null }
169
+ const em = createRowBackedEm(dbRow)
170
+ const service = new OnboardingService(em)
171
+ const requestA = makeRequest({ id: 'req-1', status: 'pending', processingStartedAt: null })
172
+ const requestB = makeRequest({ id: 'req-1', status: 'pending', processingStartedAt: null })
173
+
174
+ const claimedA = await service.startProcessing(requestA, new Date())
175
+ const claimedB = await service.startProcessing(requestB, new Date())
176
+
177
+ expect(claimedA).toBe(true)
178
+ expect(claimedB).toBe(false)
179
+ expect(dbRow.status).toBe('processing')
180
+ })
181
+ })
182
+
183
+ describe('resetProcessing (conditional revert, #2742)', () => {
184
+ it('does not revert a request a concurrent worker already completed', async () => {
185
+ // A sibling worker already advanced the persisted row to 'completed'.
186
+ const dbRow: DbRow = { id: 'req-1', status: 'completed', processingStartedAt: null }
187
+ // This worker still holds an in-memory copy it believes is 'processing'.
188
+ const staleRequest = makeRequest({ id: 'req-1', status: 'processing', processingStartedAt: new Date() })
189
+ const em = createRowBackedEm(dbRow, staleRequest)
190
+ const service = new OnboardingService(em)
191
+
192
+ const reverted = await service.resetProcessing(staleRequest)
193
+
194
+ expect(reverted).toBe(false)
195
+ expect(dbRow.status).toBe('completed')
196
+ })
197
+
198
+ it('reverts a request that is still processing', async () => {
199
+ const dbRow: DbRow = { id: 'req-1', status: 'processing', processingStartedAt: new Date() }
200
+ const request = makeRequest({ id: 'req-1', status: 'processing', processingStartedAt: new Date() })
201
+ const em = createRowBackedEm(dbRow, request)
202
+ const service = new OnboardingService(em)
203
+
204
+ const reverted = await service.resetProcessing(request)
205
+
206
+ expect(reverted).toBe(true)
207
+ expect(dbRow.status).toBe('pending')
208
+ expect(dbRow.processingStartedAt).toBeNull()
209
+ })
210
+ })
129
211
  })
@@ -0,0 +1,111 @@
1
+ import { OnboardingRequest } from '../modules/onboarding/data/entities'
2
+
3
+ const findLatestByTenantId = jest.fn()
4
+ const sendWorkspaceReadyEmail = jest.fn()
5
+
6
+ jest.mock('@open-mercato/shared/lib/di/container', () => ({
7
+ createRequestContainer: jest.fn(async () => ({
8
+ resolve: (name: string) => {
9
+ if (name === 'em') return {}
10
+ throw new Error(`unexpected resolve(${name})`)
11
+ },
12
+ })),
13
+ }))
14
+
15
+ jest.mock('@open-mercato/shared/lib/url', () => ({
16
+ getSecurityEmailBaseUrl: () => 'https://app.example.com',
17
+ mapSecurityEmailUrlError: () => null,
18
+ }))
19
+
20
+ jest.mock('@open-mercato/onboarding/modules/onboarding/lib/service', () => ({
21
+ OnboardingService: jest.fn().mockImplementation(() => ({
22
+ findLatestByTenantId,
23
+ })),
24
+ }))
25
+
26
+ jest.mock('@open-mercato/onboarding/modules/onboarding/lib/ready-email', () => ({
27
+ sendWorkspaceReadyEmail,
28
+ }))
29
+
30
+ import { GET } from '../modules/onboarding/api/get/onboarding/status'
31
+
32
+ const TENANT_ID = '11111111-1111-4111-8111-111111111111'
33
+ const OTHER_TENANT_ID = '22222222-2222-4222-8222-222222222222'
34
+
35
+ function makeRequest(overrides: Record<string, unknown> = {}) {
36
+ return Object.assign(new OnboardingRequest(), {
37
+ id: 'req-1',
38
+ status: 'completed',
39
+ tenantId: TENANT_ID,
40
+ organizationId: 'org-1',
41
+ preparationCompletedAt: new Date(),
42
+ readyEmailSentAt: new Date(),
43
+ ...overrides,
44
+ })
45
+ }
46
+
47
+ function buildRequest(args: { tenantId: string; cookie?: string }) {
48
+ const headers = new Headers()
49
+ if (args.cookie !== undefined) headers.set('cookie', args.cookie)
50
+ return new Request(`https://app.example.com/api/onboarding/onboarding/status?tenantId=${args.tenantId}`, {
51
+ headers,
52
+ })
53
+ }
54
+
55
+ describe('onboarding status endpoint authorization', () => {
56
+ beforeEach(() => {
57
+ findLatestByTenantId.mockReset()
58
+ sendWorkspaceReadyEmail.mockReset()
59
+ findLatestByTenantId.mockResolvedValue(makeRequest())
60
+ })
61
+
62
+ it('returns 403 and does not look up tenant state when no om_login_tenant cookie is present', async () => {
63
+ const res = await GET(buildRequest({ tenantId: TENANT_ID }))
64
+ expect(res.status).toBe(403)
65
+ const body = await res.json()
66
+ expect(body).toEqual({ ok: false, error: 'Not authorized for this tenant.' })
67
+ expect(findLatestByTenantId).not.toHaveBeenCalled()
68
+ expect(sendWorkspaceReadyEmail).not.toHaveBeenCalled()
69
+ })
70
+
71
+ it('returns 403 when the om_login_tenant cookie is for a different tenant', async () => {
72
+ const res = await GET(
73
+ buildRequest({ tenantId: TENANT_ID, cookie: `om_login_tenant=${OTHER_TENANT_ID}` }),
74
+ )
75
+ expect(res.status).toBe(403)
76
+ expect(findLatestByTenantId).not.toHaveBeenCalled()
77
+ expect(sendWorkspaceReadyEmail).not.toHaveBeenCalled()
78
+ })
79
+
80
+ it('does not trigger the ready email side effect for an unauthorized caller', async () => {
81
+ findLatestByTenantId.mockResolvedValue(
82
+ makeRequest({ readyEmailSentAt: null }),
83
+ )
84
+ const res = await GET(
85
+ buildRequest({ tenantId: TENANT_ID, cookie: `om_login_tenant=${OTHER_TENANT_ID}` }),
86
+ )
87
+ expect(res.status).toBe(403)
88
+ expect(sendWorkspaceReadyEmail).not.toHaveBeenCalled()
89
+ })
90
+
91
+ it('returns the status when the om_login_tenant cookie matches the requested tenant', async () => {
92
+ const res = await GET(
93
+ buildRequest({ tenantId: TENANT_ID, cookie: `other=foo; om_login_tenant=${TENANT_ID}` }),
94
+ )
95
+ expect(res.status).toBe(200)
96
+ const body = await res.json()
97
+ expect(body.ok).toBe(true)
98
+ expect(body.tenantId).toBe(TENANT_ID)
99
+ expect(body.ready).toBe(true)
100
+ expect(body.loginUrl).toBe(`https://app.example.com/login?tenant=${TENANT_ID}`)
101
+ expect(findLatestByTenantId).toHaveBeenCalledWith(TENANT_ID)
102
+ })
103
+
104
+ it('returns 404 for an authorized caller when no onboarding record exists', async () => {
105
+ findLatestByTenantId.mockResolvedValue(null)
106
+ const res = await GET(
107
+ buildRequest({ tenantId: TENANT_ID, cookie: `om_login_tenant=${TENANT_ID}` }),
108
+ )
109
+ expect(res.status).toBe(404)
110
+ })
111
+ })
@@ -14,10 +14,30 @@ export const metadata = {
14
14
  },
15
15
  }
16
16
 
17
+ const ONBOARDING_LOGIN_TENANT_COOKIE = 'om_login_tenant'
18
+
17
19
  const onboardingStatusQuerySchema = z.object({
18
20
  tenantId: z.string().uuid(),
19
21
  })
20
22
 
23
+ function readCookie(req: Request, name: string): string | null {
24
+ const header = req.headers.get('cookie')
25
+ if (!header) return null
26
+ for (const part of header.split(';')) {
27
+ const separatorIndex = part.indexOf('=')
28
+ if (separatorIndex === -1) continue
29
+ const key = part.slice(0, separatorIndex).trim()
30
+ if (key !== name) continue
31
+ const rawValue = part.slice(separatorIndex + 1).trim()
32
+ try {
33
+ return decodeURIComponent(rawValue)
34
+ } catch {
35
+ return rawValue
36
+ }
37
+ }
38
+ return null
39
+ }
40
+
21
41
  export async function GET(req: Request) {
22
42
  const url = new URL(req.url)
23
43
  const tenantId = url.searchParams.get('tenantId') || url.searchParams.get('tenant') || ''
@@ -26,6 +46,11 @@ export async function GET(req: Request) {
26
46
  return NextResponse.json({ ok: false, error: 'Invalid tenant id.' }, { status: 400 })
27
47
  }
28
48
 
49
+ const loginTenantCookie = readCookie(req, ONBOARDING_LOGIN_TENANT_COOKIE)
50
+ if (!loginTenantCookie || loginTenantCookie !== parsed.data.tenantId) {
51
+ return NextResponse.json({ ok: false, error: 'Not authorized for this tenant.' }, { status: 403 })
52
+ }
53
+
29
54
  let baseUrl: string
30
55
  try {
31
56
  baseUrl = getSecurityEmailBaseUrl(req)
@@ -101,6 +126,7 @@ const onboardingStatusDoc: OpenApiMethodDoc = {
101
126
  ],
102
127
  errors: [
103
128
  { status: 400, description: 'Invalid tenant id or request origin.', schema: onboardingStatusErrorSchema },
129
+ { status: 403, description: 'Caller is not authorized for this tenant.', schema: onboardingStatusErrorSchema },
104
130
  { status: 404, description: 'Onboarding request not found.', schema: onboardingStatusErrorSchema },
105
131
  { status: 500, description: 'Onboarding status is not configured.', schema: onboardingStatusErrorSchema },
106
132
  ],
@@ -356,7 +356,20 @@ export async function GET(req: Request) {
356
356
  if (request.status !== 'pending') {
357
357
  return redirectWithStatus(baseUrl, 'invalid')
358
358
  }
359
- await service.startProcessing(request, new Date())
359
+ const claimed = await service.startProcessing(request, new Date())
360
+ if (!claimed) {
361
+ // A concurrent verify request already claimed this token (pending → processing).
362
+ // Re-read on a fresh fork — the request EM's identity map still holds the stale
363
+ // pre-claim copy — and route off the winner's committed state instead of re-running
364
+ // provisioning, which would throw USER_EXISTS and could strand the request (#2742).
365
+ const current = await new OnboardingService(em.fork()).findById(request.id)
366
+ if (current?.status === 'completed' && current.tenantId) {
367
+ return current.preparationCompletedAt
368
+ ? redirectToLogin(baseUrl, current.tenantId)
369
+ : redirectToPreparing(baseUrl, current.tenantId)
370
+ }
371
+ return redirectToPreparing(baseUrl, current?.tenantId ?? request.tenantId ?? null)
372
+ }
360
373
  if (!request.passwordHash) {
361
374
  console.error('[onboarding.verify] missing password hash for request', request.id)
362
375
  await service.resetProcessing(request)
@@ -97,16 +97,28 @@ export class OnboardingService {
97
97
  )
98
98
  }
99
99
 
100
- async startProcessing(request: OnboardingRequest, startedAt: Date) {
100
+ async startProcessing(request: OnboardingRequest, startedAt: Date): Promise<boolean> {
101
+ const claimedRows = await this.em.nativeUpdate(
102
+ OnboardingRequest,
103
+ { id: request.id, status: 'pending' },
104
+ { status: 'processing', processingStartedAt: startedAt, updatedAt: new Date() },
105
+ )
106
+ if (claimedRows === 0) return false
101
107
  request.status = 'processing'
102
108
  request.processingStartedAt = startedAt
103
- await this.em.flush()
109
+ return true
104
110
  }
105
111
 
106
- async resetProcessing(request: OnboardingRequest) {
112
+ async resetProcessing(request: OnboardingRequest): Promise<boolean> {
113
+ const revertedRows = await this.em.nativeUpdate(
114
+ OnboardingRequest,
115
+ { id: request.id, status: 'processing' },
116
+ { status: 'pending', processingStartedAt: null, updatedAt: new Date() },
117
+ )
118
+ if (revertedRows === 0) return false
107
119
  request.status = 'pending'
108
120
  request.processingStartedAt = null
109
- await this.em.flush()
121
+ return true
110
122
  }
111
123
 
112
124
  async updateProvisioningIds(request: OnboardingRequest, data: { tenantId: string; organizationId: string; userId: string }) {