@open-mercato/core 0.6.7-develop.6744.1.0483b18565 → 0.6.7-develop.6749.1.6b54c56dfe

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 (46) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/profile/route.js +8 -2
  3. package/dist/modules/auth/api/profile/route.js.map +2 -2
  4. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  5. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  6. package/dist/modules/data_sync/lib/queue-policy.js +15 -0
  7. package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
  8. package/dist/modules/data_sync/lib/queue.js +16 -1
  9. package/dist/modules/data_sync/lib/queue.js.map +2 -2
  10. package/dist/modules/data_sync/lib/start-run.js +2 -1
  11. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  12. package/dist/modules/data_sync/lib/sync-engine.js +35 -2
  13. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  14. package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
  15. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  16. package/dist/modules/data_sync/workers/sync-export.js +9 -2
  17. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  18. package/dist/modules/data_sync/workers/sync-import.js +9 -2
  19. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  20. package/dist/modules/notifications/api/route.js +3 -1
  21. package/dist/modules/notifications/api/route.js.map +2 -2
  22. package/dist/modules/notifications/api/unread-count/route.js +25 -6
  23. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  24. package/dist/modules/notifications/lib/notificationScope.js +26 -0
  25. package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
  26. package/dist/modules/notifications/lib/notificationService.js +7 -3
  27. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  28. package/dist/modules/notifications/lib/routeHelpers.js +16 -2
  29. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  30. package/package.json +7 -7
  31. package/src/modules/auth/api/profile/route.ts +8 -2
  32. package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
  33. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
  34. package/src/modules/data_sync/lib/adapter.ts +18 -0
  35. package/src/modules/data_sync/lib/queue-policy.ts +30 -0
  36. package/src/modules/data_sync/lib/queue.ts +19 -1
  37. package/src/modules/data_sync/lib/start-run.ts +2 -1
  38. package/src/modules/data_sync/lib/sync-engine.ts +46 -0
  39. package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
  40. package/src/modules/data_sync/workers/sync-export.ts +8 -1
  41. package/src/modules/data_sync/workers/sync-import.ts +8 -1
  42. package/src/modules/notifications/api/route.ts +2 -0
  43. package/src/modules/notifications/api/unread-count/route.ts +42 -5
  44. package/src/modules/notifications/lib/notificationScope.ts +40 -0
  45. package/src/modules/notifications/lib/notificationService.ts +5 -0
  46. package/src/modules/notifications/lib/routeHelpers.ts +19 -2
@@ -1,4 +1,4 @@
1
- [build:core] found 3692 entry points
1
+ [build:core] found 3695 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 197 entry points
4
4
  [build:core:generated] built successfully
@@ -12,6 +12,7 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
12
12
  const logger = createLogger("auth").child({ component: "profile" });
13
13
  const profileResponseSchema = z.object({
14
14
  email: z.string().email(),
15
+ name: z.string().nullable().optional(),
15
16
  roles: z.array(z.string())
16
17
  });
17
18
  const passwordSchema = buildPasswordSchema();
@@ -92,7 +93,12 @@ async function GET(req) {
92
93
  if (!user) {
93
94
  return NextResponse.json({ error: translate("auth.users.form.errors.notFound", "User not found") }, { status: 404 });
94
95
  }
95
- return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] });
96
+ const displayName = typeof user.name === "string" ? user.name.trim() : "";
97
+ return NextResponse.json({
98
+ email: String(user.email),
99
+ name: displayName.length > 0 ? displayName : null,
100
+ roles: auth.roles ?? []
101
+ });
96
102
  } catch (err) {
97
103
  logger.error("Profile load failed", { err });
98
104
  return NextResponse.json({ error: translate("auth.profile.form.errors.load", "Failed to load profile.") }, { status: 400 });
@@ -191,7 +197,7 @@ const openApi = {
191
197
  methods: {
192
198
  GET: {
193
199
  summary: "Get current profile",
194
- description: "Returns the email address for the signed-in user.",
200
+ description: "Returns the email address, display name, and roles for the signed-in user. The display name is null when unset.",
195
201
  responses: [
196
202
  { status: 200, description: "Profile payload", schema: profileResponseSchema },
197
203
  { status: 401, description: "Unauthorized", schema: z.object({ error: z.string() }) },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/auth/api/profile/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile' })\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchemaBase = z.object({\n email: z.string().email().optional(),\n currentPassword: z.string().trim().min(1).optional(),\n password: passwordSchema.optional(),\n})\n\nfunction buildUpdateSchema(translate: (key: string, fallback: string) => string) {\n return updateSchemaBase.superRefine((data, ctx) => {\n if (!data.email && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.emailOrPasswordRequired',\n 'Provide an email or password.',\n ),\n path: ['email'],\n })\n }\n if (data.password && !data.currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.currentPasswordRequired',\n 'Current password is required.',\n ),\n path: ['currentPassword'],\n })\n }\n if (data.currentPassword && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.newPasswordRequired',\n 'New password is required.',\n ),\n path: ['password'],\n })\n }\n })\n}\n\nconst updateSchema = buildUpdateSchema((_key, fallback) => fallback)\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n return NextResponse.json({ email: String(user.email), roles: auth.roles ?? [] })\n } catch (err) {\n logger.error('Profile load failed', { err })\n return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = buildUpdateSchema(translate).safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: translate('auth.profile.form.errors.invalid', 'Invalid profile update.'),\n issues: parsed.error.issues,\n },\n { status: 400 },\n )\n }\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const authService = container.resolve('authService') as AuthService\n if (parsed.data.password) {\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n const currentPassword = parsed.data.currentPassword?.trim() ?? ''\n const isCurrentPasswordValid = await authService.verifyPassword(user, currentPassword)\n if (!isCurrentPasswordValid) {\n const message = translate(\n 'auth.profile.form.errors.currentPasswordInvalid',\n 'Current password is incorrect.',\n )\n return NextResponse.json(\n {\n error: message,\n issues: [{ path: ['currentPassword'], message }],\n },\n { status: 400 },\n )\n }\n }\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const ctx = buildCommandContext(container, auth, req)\n const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(\n 'auth.users.update',\n {\n input: {\n id: auth.sub,\n email: parsed.data.email,\n password: parsed.data.password,\n },\n ctx,\n },\n )\n const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)\n const jwt = signJwt({\n sub: String(result.id),\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: result.tenantId ? String(result.tenantId) : null,\n orgId: result.organizationId ? String(result.organizationId) : null,\n email: result.email,\n roles,\n })\n const res = NextResponse.json({ ok: true, email: String(result.email) })\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n return res\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Profile update failed', { err })\n return NextResponse.json({ error: translate('auth.profile.form.errors.save', 'Failed to update profile.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Profile settings',\n methods: {\n GET: {\n summary: 'Get current profile',\n description: 'Returns the email address for the signed-in user.',\n responses: [\n { status: 200, description: 'Profile payload', schema: profileResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update current profile',\n description: 'Updates the email address or password for the signed-in user.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { signJwt } from '@open-mercato/shared/lib/auth/jwt'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { AuthService } from '@open-mercato/core/modules/auth/services/authService'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { buildPasswordSchema } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile' })\n\nconst profileResponseSchema = z.object({\n email: z.string().email(),\n name: z.string().nullable().optional(),\n roles: z.array(z.string()),\n})\n\nconst passwordSchema = buildPasswordSchema()\n\nconst updateSchemaBase = z.object({\n email: z.string().email().optional(),\n currentPassword: z.string().trim().min(1).optional(),\n password: passwordSchema.optional(),\n})\n\nfunction buildUpdateSchema(translate: (key: string, fallback: string) => string) {\n return updateSchemaBase.superRefine((data, ctx) => {\n if (!data.email && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.emailOrPasswordRequired',\n 'Provide an email or password.',\n ),\n path: ['email'],\n })\n }\n if (data.password && !data.currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.currentPasswordRequired',\n 'Current password is required.',\n ),\n path: ['currentPassword'],\n })\n }\n if (data.currentPassword && !data.password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: translate(\n 'auth.profile.form.errors.newPasswordRequired',\n 'New password is required.',\n ),\n path: ['password'],\n })\n }\n })\n}\n\nconst updateSchema = buildUpdateSchema((_key, fallback) => fallback)\n\nconst profileUpdateResponseSchema = z.object({\n ok: z.literal(true),\n email: z.string().email(),\n})\n\nexport const metadata = {\n GET: { requireAuth: true },\n PUT: { requireAuth: true },\n}\n\nfunction buildCommandContext(container: Awaited<ReturnType<typeof createRequestContainer>>, auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>, req: Request): CommandRuntimeContext {\n return {\n container,\n auth,\n organizationScope: null,\n selectedOrganizationId: auth.orgId ?? null,\n organizationIds: auth.orgId ? [auth.orgId] : null,\n request: req,\n }\n}\n\nexport async function GET(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n const displayName = typeof user.name === 'string' ? user.name.trim() : ''\n return NextResponse.json({\n email: String(user.email),\n name: displayName.length > 0 ? displayName : null,\n roles: auth.roles ?? [],\n })\n } catch (err) {\n logger.error('Profile load failed', { err })\n return NextResponse.json({ error: translate('auth.profile.form.errors.load', 'Failed to load profile.') }, { status: 400 })\n }\n}\n\nexport async function PUT(req: Request) {\n const { translate } = await resolveTranslations()\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: translate('api.errors.unauthorized', 'Unauthorized') }, { status: 401 })\n }\n try {\n const body = await req.json().catch(() => ({}))\n const parsed = buildUpdateSchema(translate).safeParse(body)\n if (!parsed.success) {\n return NextResponse.json(\n {\n error: translate('auth.profile.form.errors.invalid', 'Invalid profile update.'),\n issues: parsed.error.issues,\n },\n { status: 400 },\n )\n }\n const container = await createRequestContainer()\n const em = (container.resolve('em') as EntityManager)\n const authService = container.resolve('authService') as AuthService\n if (parsed.data.password) {\n const user = await findOneWithDecryption(\n em,\n User,\n { id: auth.sub, deletedAt: null },\n undefined,\n { tenantId: auth.tenantId ?? null, organizationId: auth.orgId ?? null },\n )\n if (!user) {\n return NextResponse.json({ error: translate('auth.users.form.errors.notFound', 'User not found') }, { status: 404 })\n }\n const currentPassword = parsed.data.currentPassword?.trim() ?? ''\n const isCurrentPasswordValid = await authService.verifyPassword(user, currentPassword)\n if (!isCurrentPasswordValid) {\n const message = translate(\n 'auth.profile.form.errors.currentPasswordInvalid',\n 'Current password is incorrect.',\n )\n return NextResponse.json(\n {\n error: message,\n issues: [{ path: ['currentPassword'], message }],\n },\n { status: 400 },\n )\n }\n }\n const commandBus = (container.resolve('commandBus') as CommandBus)\n const ctx = buildCommandContext(container, auth, req)\n const { result } = await commandBus.execute<{ id: string; email?: string; password?: string }, User>(\n 'auth.users.update',\n {\n input: {\n id: auth.sub,\n email: parsed.data.email,\n password: parsed.data.password,\n },\n ctx,\n },\n )\n const roles = await authService.getUserRoles(result, result.tenantId ? String(result.tenantId) : null)\n const jwt = signJwt({\n sub: String(result.id),\n sid: typeof auth.sid === 'string' ? auth.sid : undefined,\n tenantId: result.tenantId ? String(result.tenantId) : null,\n orgId: result.organizationId ? String(result.organizationId) : null,\n email: result.email,\n roles,\n })\n const res = NextResponse.json({ ok: true, email: String(result.email) })\n res.cookies.set('auth_token', jwt, {\n httpOnly: true,\n path: '/',\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n maxAge: 60 * 60 * 8,\n })\n return res\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n logger.error('Profile update failed', { err })\n return NextResponse.json({ error: translate('auth.profile.form.errors.save', 'Failed to update profile.') }, { status: 400 })\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Authentication & Accounts',\n summary: 'Profile settings',\n methods: {\n GET: {\n summary: 'Get current profile',\n description: 'Returns the email address, display name, and roles for the signed-in user. The display name is null when unset.',\n responses: [\n { status: 200, description: 'Profile payload', schema: profileResponseSchema },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n { status: 404, description: 'User not found', schema: z.object({ error: z.string() }) },\n ],\n },\n PUT: {\n summary: 'Update current profile',\n description: 'Updates the email address or password for the signed-in user.',\n requestBody: {\n contentType: 'application/json',\n schema: updateSchema,\n },\n responses: [\n { status: 200, description: 'Profile updated', schema: profileUpdateResponseSchema },\n { status: 400, description: 'Invalid payload', schema: z.object({ error: z.string() }) },\n { status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/backend/auth/profile/page.tsx"],
4
- "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile-page' })\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function AuthProfilePage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n return (\n <Page>\n <PageBody>\n {loading ? (\n <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.profile.title', 'Profile')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )}\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { Page, PageBody } from '@open-mercato/ui/backend/Page'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'profile-page' })\n\ntype ProfileResponse = {\n email?: string | null\n name?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function AuthProfilePage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n return (\n <Page>\n <PageBody>\n {loading ? (\n <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n ) : error ? (\n <ErrorMessage label={error} />\n ) : (\n <section className=\"space-y-6 rounded-lg border bg-background p-6\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.profile.title', 'Profile')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )}\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": ";AAiLU,cAMI,YANJ;AAhLV,YAAY,WAAW;AACvB,SAAS,iBAAiB;AAC1B,SAAS,SAAS;AAClB,SAAS,YAAY;AACrB,SAAS,MAAM,gBAAgB;AAC/B,SAAS,gBAAgC;AACzC,SAAS,SAAS,4BAA4B;AAC9C,SAAS,2BAA2B;AACpC,SAAS,aAAa;AACtB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,cAAc;AACvB,SAAS,YAAY;AACrB,SAAS,qBAAqB,4BAA4B,yBAAyB;AACnF,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,MAAM,EAAE,MAAM,EAAE,WAAW,eAAe,CAAC;AAmBxD,SAAR,kBAAmC;AACxC,QAAM,IAAI,KAAK;AACf,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAwB,IAAI;AAC5D,QAAM,CAAC,OAAO,QAAQ,IAAI,MAAM,SAAS,EAAE;AAC3C,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,CAAC;AAC9C,QAAM,SAAS,MAAM,MAAM;AAC3B,QAAM,iBAAiB,MAAM,QAAQ,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAClE,QAAM,uBAAuB,MAAM;AAAA,IACjC,MAAM,2BAA2B,gBAAgB,CAAC;AAAA,IAClD,CAAC,gBAAgB,CAAC;AAAA,EACpB;AACA,QAAM,sBAAsB,MAAM,QAAQ,MACxC,uBACI,EAAE,mCAAmC,yCAAyC,EAAE,cAAc,qBAAqB,CAAC,IACpH,QACH,CAAC,sBAAsB,CAAC,CAAC;AAE5B,QAAM,UAAU,MAAM;AACpB,QAAI,YAAY;AAChB,mBAAe,OAAO;AACpB,iBAAW,IAAI;AACf,eAAS,IAAI;AACb,UAAI;AACF,cAAM,EAAE,IAAI,OAAO,IAAI,MAAM,QAAyB,mBAAmB;AACzE,YAAI,CAAC,GAAI,OAAM,IAAI,MAAM,EAAE,iCAAiC,yBAAyB,CAAC;AACtF,cAAM,gBAAgB,OAAO,QAAQ,UAAU,WAAW,OAAO,QAAQ;AACzE,YAAI,CAAC,UAAW,UAAS,aAAa;AAAA,MACxC,SAAS,KAAK;AACZ,eAAO,MAAM,+BAA+B,EAAE,IAAI,CAAC;AACnD,YAAI,CAAC,UAAW,UAAS,EAAE,iCAAiC,yBAAyB,CAAC;AAAA,MACxF,UAAE;AACA,YAAI,CAAC,UAAW,YAAW,KAAK;AAAA,MAClC;AAAA,IACF;AACA,SAAK;AACL,WAAO,MAAM;AAAE,kBAAY;AAAA,IAAK;AAAA,EAClC,GAAG,CAAC,CAAC,CAAC;AAEN,QAAM,SAAS,MAAM,QAAqB,MAAM;AAAA,IAC9C,EAAE,IAAI,SAAS,OAAO,EAAE,2BAA2B,OAAO,GAAG,MAAM,QAAQ,UAAU,KAAK;AAAA,IAC1F;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,qCAAqC,kBAAkB;AAAA,MAChE,MAAM;AAAA,IACR;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,8BAA8B,cAAc;AAAA,MACrD,MAAM;AAAA,MACN,aAAa;AAAA,IACf;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,OAAO,EAAE,qCAAqC,sBAAsB;AAAA,MACpE,MAAM;AAAA,IACR;AAAA,EACF,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAE3B,QAAM,SAAS,MAAM,QAAQ,MAAM;AACjC,UAAM,iBAAiB,oBAAoB;AAAA,MACzC,QAAQ;AAAA,MACR,SAAS,EAAE,iDAAiD,sCAAsC;AAAA,IACpG,CAAC;AACD,UAAM,yBAAyB,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,EAAE,SAAS;AACjF,WAAO,EAAE,OAAO;AAAA,MACd,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,GAAG,EAAE,0CAA0C,oBAAoB,CAAC;AAAA,MACjG,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,MACrC,UAAU;AAAA,MACV,iBAAiB,EAAE,OAAO,EAAE,SAAS;AAAA,IACvC,CAAC,EAAE,YAAY,CAAC,QAAQ,QAAQ;AAC9B,YAAM,kBAAkB,OAAO,iBAAiB,KAAK,KAAK;AAC1D,YAAM,WAAW,OAAO,UAAU,KAAK,KAAK;AAC5C,YAAM,kBAAkB,OAAO,iBAAiB,KAAK,KAAK;AAC1D,YAAM,oBAAoB,QAAQ,mBAAmB,YAAY,eAAe;AAEhF,UAAI,qBAAqB,CAAC,iBAAiB;AACzC,YAAI,SAAS;AAAA,UACX,MAAM,EAAE,aAAa;AAAA,UACrB,SAAS,EAAE,oDAAoD,+BAA+B;AAAA,UAC9F,MAAM,CAAC,iBAAiB;AAAA,QAC1B,CAAC;AAAA,MACH;AACA,UAAI,qBAAqB,CAAC,UAAU;AAClC,YAAI,SAAS;AAAA,UACX,MAAM,EAAE,aAAa;AAAA,UACrB,SAAS,EAAE,gDAAgD,2BAA2B;AAAA,UACtF,MAAM,CAAC,UAAU;AAAA,QACnB,CAAC;AAAA,MACH;AACA,UAAI,qBAAqB,CAAC,iBAAiB;AACzC,YAAI,SAAS;AAAA,UACX,MAAM,EAAE,aAAa;AAAA,UACrB,SAAS,EAAE,oDAAoD,kCAAkC;AAAA,UACjG,MAAM,CAAC,iBAAiB;AAAA,QAC1B,CAAC;AAAA,MACH;AACA,UAAI,YAAY,mBAAmB,aAAa,iBAAiB;AAC/D,YAAI,SAAS;AAAA,UACX,MAAM,EAAE,aAAa;AAAA,UACrB,SAAS,EAAE,6CAA6C,yBAAyB;AAAA,UACjF,MAAM,CAAC,iBAAiB;AAAA,QAC1B,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAEtB,QAAM,eAAe,MAAM,YAAY,OAAO,WAA8B;AAC1E,UAAM,YAAY,OAAO,OAAO,KAAK,KAAK;AAC1C,UAAM,kBAAkB,OAAO,iBAAiB,KAAK,KAAK;AAC1D,UAAM,WAAW,OAAO,UAAU,KAAK,KAAK;AAE5C,QAAI,CAAC,YAAY,cAAc,OAAO;AACpC,YAAM,oBAAoB,EAAE,sCAAsC,qBAAqB,CAAC;AAAA,IAC1F;AAEA,UAAM,UAA0E,EAAE,OAAO,UAAU;AACnG,QAAI,SAAU,SAAQ,WAAW;AACjC,QAAI,SAAU,SAAQ,kBAAkB;AAExC,UAAM,SAAS,MAAM;AAAA,MACnB;AAAA,MACA;AAAA,QACE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B;AAAA,MACA,EAAE,cAAc,EAAE,iCAAiC,2BAA2B,EAAE;AAAA,IAClF;AAEA,UAAM,gBAAgB,OAAO,QAAQ,UAAU,WAAW,OAAO,QAAQ;AACzE,aAAS,aAAa;AACtB,eAAW,CAAC,SAAS,OAAO,CAAC;AAC7B,UAAM,EAAE,6BAA6B,kBAAkB,GAAG,SAAS;AACnE,WAAO,QAAQ;AAAA,EACjB,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC;AAErB,SACE,oBAAC,QACC,8BAAC,YACE,oBACC,oBAAC,kBAAe,OAAO,EAAE,6BAA6B,oBAAoB,GAAG,IAC3E,QACF,oBAAC,gBAAa,OAAO,OAAO,IAE5B,qBAAC,aAAQ,WAAU,iDACjB;AAAA,yBAAC,YAAO,WAAU,qEAChB;AAAA,2BAAC,SAAI,WAAU,aACb;AAAA,4BAAC,QAAG,WAAU,yBAAyB,YAAE,sBAAsB,SAAS,GAAE;AAAA,QAC1E,oBAAC,OAAE,WAAU,iCACV,YAAE,yBAAyB,iBAAiB,GAC/C;AAAA,SACF;AAAA,MACA,qBAAC,UAAO,MAAK,UAAS,MAAM,QAC1B;AAAA,4BAAC,QAAK,WAAU,eAAc;AAAA,QAC7B,EAAE,0BAA0B,cAAc;AAAA,SAC7C;AAAA,OACF;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA;AAAA,QACA;AAAA,QACA,eAAe;AAAA,UACb;AAAA,UACA,iBAAiB;AAAA,UACjB,UAAU;AAAA,UACV,iBAAiB;AAAA,QACnB;AAAA,QACA,aAAa,EAAE,0BAA0B,cAAc;AAAA,QACvD,UAAU;AAAA,QACV,UAAQ;AAAA,QACR,mBAAiB;AAAA;AAAA,MAbZ;AAAA,IAcP;AAAA,KACF,GAEJ,GACF;AAEJ;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/auth/backend/profile/change-password/page.tsx"],
4
- "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'change-password-page' })\n\ntype ProfileResponse = {\n email?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function ProfileChangePasswordPage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n if (loading) {\n return <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6 max-w-2xl\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.changePassword.title', 'Change Password')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\nimport * as React from 'react'\nimport { useRouter } from 'next/navigation'\nimport { z } from 'zod'\nimport { Save } from 'lucide-react'\nimport { CrudForm, type CrudField } from '@open-mercato/ui/backend/CrudForm'\nimport { apiCall, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { buildPasswordSchema, formatPasswordRequirements, getPasswordPolicy } from '@open-mercato/shared/lib/auth/passwordPolicy'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('auth').child({ component: 'change-password-page' })\n\ntype ProfileResponse = {\n email?: string | null\n name?: string | null\n}\n\ntype ProfileUpdateResponse = {\n ok?: boolean\n email?: string | null\n}\n\ntype ProfileFormValues = {\n email: string\n currentPassword?: string\n password?: string\n confirmPassword?: string\n}\n\nexport default function ProfileChangePasswordPage() {\n const t = useT()\n const router = useRouter()\n const [loading, setLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [email, setEmail] = React.useState('')\n const [formKey, setFormKey] = React.useState(0)\n const formId = React.useId()\n const passwordPolicy = React.useMemo(() => getPasswordPolicy(), [])\n const passwordRequirements = React.useMemo(\n () => formatPasswordRequirements(passwordPolicy, t),\n [passwordPolicy, t],\n )\n const passwordDescription = React.useMemo(() => (\n passwordRequirements\n ? t('auth.password.requirements.help', 'Password requirements: {requirements}', { requirements: passwordRequirements })\n : undefined\n ), [passwordRequirements, t])\n\n React.useEffect(() => {\n let cancelled = false\n async function load() {\n setLoading(true)\n setError(null)\n try {\n const { ok, result } = await apiCall<ProfileResponse>('/api/auth/profile')\n if (!ok) throw new Error(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n const resolvedEmail = typeof result?.email === 'string' ? result.email : ''\n if (!cancelled) setEmail(resolvedEmail)\n } catch (err) {\n logger.error('Failed to load auth profile', { err })\n if (!cancelled) setError(t('auth.profile.form.errors.load', 'Failed to load profile.'))\n } finally {\n if (!cancelled) setLoading(false)\n }\n }\n load()\n return () => { cancelled = true }\n }, [t])\n\n const fields = React.useMemo<CrudField[]>(() => [\n { id: 'email', label: t('auth.profile.form.email', 'Email'), type: 'text', required: true },\n {\n id: 'currentPassword',\n label: t('auth.profile.form.currentPassword', 'Current password'),\n type: 'password',\n },\n {\n id: 'password',\n label: t('auth.profile.form.password', 'New password'),\n type: 'password',\n description: passwordDescription,\n },\n {\n id: 'confirmPassword',\n label: t('auth.profile.form.confirmPassword', 'Confirm new password'),\n type: 'password',\n },\n ], [passwordDescription, t])\n\n const schema = React.useMemo(() => {\n const passwordSchema = buildPasswordSchema({\n policy: passwordPolicy,\n message: t('auth.profile.form.errors.passwordRequirements', 'Password must meet the requirements.'),\n })\n const optionalPasswordSchema = z.union([z.literal(''), passwordSchema]).optional()\n return z.object({\n email: z.string().trim().min(1, t('auth.profile.form.errors.emailRequired', 'Email is required.')),\n currentPassword: z.string().optional(),\n password: optionalPasswordSchema,\n confirmPassword: z.string().optional(),\n }).superRefine((values, ctx) => {\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n const confirmPassword = values.confirmPassword?.trim() ?? ''\n const hasPasswordIntent = Boolean(currentPassword || password || confirmPassword)\n\n if (hasPasswordIntent && !currentPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.currentPasswordRequired', 'Current password is required.'),\n path: ['currentPassword'],\n })\n }\n if (hasPasswordIntent && !password) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.newPasswordRequired', 'New password is required.'),\n path: ['password'],\n })\n }\n if (hasPasswordIntent && !confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.confirmPasswordRequired', 'Please confirm the new password.'),\n path: ['confirmPassword'],\n })\n }\n if (password && confirmPassword && password !== confirmPassword) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: t('auth.profile.form.errors.passwordMismatch', 'Passwords do not match.'),\n path: ['confirmPassword'],\n })\n }\n })\n }, [passwordPolicy, t])\n\n const handleSubmit = React.useCallback(async (values: ProfileFormValues) => {\n const nextEmail = values.email?.trim() ?? ''\n const currentPassword = values.currentPassword?.trim() ?? ''\n const password = values.password?.trim() ?? ''\n\n if (!password && nextEmail === email) {\n throw createCrudFormError(t('auth.profile.form.errors.noChanges', 'No changes to save.'))\n }\n\n const payload: { email: string; currentPassword?: string; password?: string } = { email: nextEmail }\n if (password) payload.password = password\n if (password) payload.currentPassword = currentPassword\n\n const result = await readApiResultOrThrow<ProfileUpdateResponse>(\n '/api/auth/profile',\n {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n },\n { errorMessage: t('auth.profile.form.errors.save', 'Failed to update profile.') },\n )\n\n const resolvedEmail = typeof result?.email === 'string' ? result.email : nextEmail\n setEmail(resolvedEmail)\n setFormKey((prev) => prev + 1)\n flash(t('auth.profile.form.success', 'Profile updated.'), 'success')\n router.refresh()\n }, [email, router, t])\n\n if (loading) {\n return <LoadingMessage label={t('auth.profile.form.loading', 'Loading profile...')} />\n }\n\n if (error) {\n return <ErrorMessage label={error} />\n }\n\n return (\n <section className=\"space-y-6 rounded-lg border bg-background p-6 max-w-2xl\">\n <header className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"space-y-1\">\n <h2 className=\"text-lg font-semibold\">{t('auth.changePassword.title', 'Change Password')}</h2>\n <p className=\"text-sm text-muted-foreground\">\n {t('auth.profile.subtitle', 'Change password')}\n </p>\n </div>\n <Button type=\"submit\" form={formId}>\n <Save className=\"size-4 mr-2\" />\n {t('auth.profile.form.save', 'Save changes')}\n </Button>\n </header>\n <CrudForm<ProfileFormValues>\n key={formKey}\n formId={formId}\n schema={schema}\n fields={fields}\n initialValues={{\n email,\n currentPassword: '',\n password: '',\n confirmPassword: '',\n }}\n submitLabel={t('auth.profile.form.save', 'Save changes')}\n onSubmit={handleSubmit}\n embedded\n hideFooterActions\n />\n </section>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,15 @@
1
+ const DATA_SYNC_IMPORT_QUEUE = "data-sync-import";
2
+ const DATA_SYNC_EXPORT_QUEUE = "data-sync-export";
3
+ const DATA_SYNC_RESUMABLE_QUEUES = [DATA_SYNC_IMPORT_QUEUE, DATA_SYNC_EXPORT_QUEUE];
4
+ const DATA_SYNC_QUEUE_ATTEMPTS = 3;
5
+ const DATA_SYNC_LOCK_DURATION_MS = 12e4;
6
+ const DATA_SYNC_MAX_STALLED_COUNT = 10;
7
+ export {
8
+ DATA_SYNC_EXPORT_QUEUE,
9
+ DATA_SYNC_IMPORT_QUEUE,
10
+ DATA_SYNC_LOCK_DURATION_MS,
11
+ DATA_SYNC_MAX_STALLED_COUNT,
12
+ DATA_SYNC_QUEUE_ATTEMPTS,
13
+ DATA_SYNC_RESUMABLE_QUEUES
14
+ };
15
+ //# sourceMappingURL=queue-policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/data_sync/lib/queue-policy.ts"],
4
+ "sourcesContent": ["/**\n * Queue names whose jobs resume an existing SyncRun instead of starting a fresh\n * one. The workers import these to declare their `metadata.queue`, so a rename\n * cannot silently drop the retry policy.\n */\nexport const DATA_SYNC_IMPORT_QUEUE = 'data-sync-import'\nexport const DATA_SYNC_EXPORT_QUEUE = 'data-sync-export'\n\nexport const DATA_SYNC_RESUMABLE_QUEUES: readonly string[] = [DATA_SYNC_IMPORT_QUEUE, DATA_SYNC_EXPORT_QUEUE]\n\nexport const DATA_SYNC_QUEUE_ATTEMPTS = 3\n\n/**\n * A backfill batch can hold the processor well past BullMQ's 30s default lock,\n * so these jobs stall on slow upstreams even when the worker is perfectly\n * healthy. `DATA_SYNC_LOCK_DURATION_MS` is the primary defence \u2014 it stops most\n * of those false stalls at the source rather than reacting to them.\n *\n * `DATA_SYNC_MAX_STALLED_COUNT` covers what is left. BullMQ fails a job once it\n * stalls more than this many times, and at the default of 1 a single stall\n * during a long backfill discards the run permanently. 10 buys enough\n * redeliveries to survive a rolling restart. Raising it is only safe because a\n * duplicate delivery can no longer corrupt the run: the ownership\n * compare-and-swap in `commitBatchProgress` lets exactly one worker advance a\n * run, and every other delivery aborts on its first commit. A job that poisons its\n * worker still dies \u2014 after 10 stalls, with the run left `running` and its\n * cursor unmoved, which is the state the admin runs list surfaces.\n */\nexport const DATA_SYNC_LOCK_DURATION_MS = 120_000\nexport const DATA_SYNC_MAX_STALLED_COUNT = 10\n"],
5
+ "mappings": "AAKO,MAAM,yBAAyB;AAC/B,MAAM,yBAAyB;AAE/B,MAAM,6BAAgD,CAAC,wBAAwB,sBAAsB;AAErG,MAAM,2BAA2B;AAkBjC,MAAM,6BAA6B;AACnC,MAAM,8BAA8B;",
6
+ "names": []
7
+ }
@@ -1,10 +1,25 @@
1
1
  import { createModuleQueue } from "@open-mercato/queue";
2
+ import {
3
+ DATA_SYNC_LOCK_DURATION_MS,
4
+ DATA_SYNC_MAX_STALLED_COUNT,
5
+ DATA_SYNC_QUEUE_ATTEMPTS,
6
+ DATA_SYNC_RESUMABLE_QUEUES
7
+ } from "./queue-policy.js";
2
8
  const queues = /* @__PURE__ */ new Map();
9
+ const resumableQueueNames = new Set(DATA_SYNC_RESUMABLE_QUEUES);
3
10
  function getSyncQueue(queueName) {
4
11
  const existing = queues.get(queueName);
5
12
  if (existing) return existing;
6
13
  const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? "5", 10) || 5);
7
- const created = createModuleQueue(queueName, { concurrency });
14
+ const created = createModuleQueue(
15
+ queueName,
16
+ resumableQueueNames.has(queueName) ? {
17
+ concurrency,
18
+ attempts: DATA_SYNC_QUEUE_ATTEMPTS,
19
+ lockDuration: DATA_SYNC_LOCK_DURATION_MS,
20
+ maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT
21
+ } : { concurrency }
22
+ );
8
23
  queues.set(queueName, created);
9
24
  return created;
10
25
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/queue.ts"],
4
- "sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\n\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nexport function getSyncQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<Record<string, unknown>>(queueName, { concurrency })\n\n queues.set(queueName, created)\n return created\n}\n"],
5
- "mappings": "AAAA,SAAS,yBAAqC;AAE9C,MAAM,SAAS,oBAAI,IAA4C;AAExD,SAAS,aAAa,WAAmD;AAC9E,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,cAAc,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,+BAA+B,KAAK,EAAE,KAAK,CAAC;AACxG,QAAM,UAAU,kBAA2C,WAAW,EAAE,YAAY,CAAC;AAErF,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;",
4
+ "sourcesContent": ["import { createModuleQueue, type Queue } from '@open-mercato/queue'\nimport {\n DATA_SYNC_LOCK_DURATION_MS,\n DATA_SYNC_MAX_STALLED_COUNT,\n DATA_SYNC_QUEUE_ATTEMPTS,\n DATA_SYNC_RESUMABLE_QUEUES,\n} from './queue-policy'\n\nconst queues = new Map<string, Queue<Record<string, unknown>>>()\n\nconst resumableQueueNames = new Set<string>(DATA_SYNC_RESUMABLE_QUEUES)\n\nexport function getSyncQueue(queueName: string): Queue<Record<string, unknown>> {\n const existing = queues.get(queueName)\n if (existing) return existing\n\n const concurrency = Math.max(1, Number.parseInt(process.env.DATA_SYNC_QUEUE_CONCURRENCY ?? '5', 10) || 5)\n const created = createModuleQueue<Record<string, unknown>>(\n queueName,\n resumableQueueNames.has(queueName)\n ? {\n concurrency,\n attempts: DATA_SYNC_QUEUE_ATTEMPTS,\n lockDuration: DATA_SYNC_LOCK_DURATION_MS,\n maxStalledCount: DATA_SYNC_MAX_STALLED_COUNT,\n }\n : { concurrency },\n )\n\n queues.set(queueName, created)\n return created\n}\n"],
5
+ "mappings": "AAAA,SAAS,yBAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,oBAAI,IAA4C;AAE/D,MAAM,sBAAsB,IAAI,IAAY,0BAA0B;AAE/D,SAAS,aAAa,WAAmD;AAC9E,QAAM,WAAW,OAAO,IAAI,SAAS;AACrC,MAAI,SAAU,QAAO;AAErB,QAAM,cAAc,KAAK,IAAI,GAAG,OAAO,SAAS,QAAQ,IAAI,+BAA+B,KAAK,EAAE,KAAK,CAAC;AACxG,QAAM,UAAU;AAAA,IACd;AAAA,IACA,oBAAoB,IAAI,SAAS,IAC7B;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV,cAAc;AAAA,MACd,iBAAiB;AAAA,IACnB,IACE,EAAE,YAAY;AAAA,EACpB;AAEA,SAAO,IAAI,WAAW,OAAO;AAC7B,SAAO;AACT;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { getSyncQueue } from "./queue.js";
2
+ import { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from "./queue-policy.js";
2
3
  async function startDataSyncRun(params) {
3
4
  const { syncRunService, progressService, scope, input } = params;
4
5
  const createProgressJob = input.createProgressJob !== false;
@@ -35,7 +36,7 @@ async function startDataSyncRun(params) {
35
36
  tenantId: scope.tenantId
36
37
  }
37
38
  );
38
- const queueName = input.direction === "import" ? "data-sync-import" : "data-sync-export";
39
+ const queueName = input.direction === "import" ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE;
39
40
  const queue = getSyncQueue(queueName);
40
41
  await queue.enqueue({
41
42
  runId: run.id,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/start-run.ts"],
4
- "sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? 'data-sync-import' : 'data-sync-export'\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
5
- "mappings": "AAEA,SAAS,oBAAoB;AAyB7B,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,qBAAqB;AACtE,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { ProgressService } from '../../progress/lib/progressService'\nimport type { SyncRunService } from './sync-run-service'\nimport { getSyncQueue } from './queue'\nimport { DATA_SYNC_EXPORT_QUEUE, DATA_SYNC_IMPORT_QUEUE } from './queue-policy'\n\nexport type DataSyncStartScope = {\n organizationId: string\n tenantId: string\n userId?: string | null\n}\n\nexport type StartDataSyncRunInput = {\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n cursor?: string | null\n triggeredBy?: string | null\n batchSize?: number\n createProgressJob?: boolean\n progressJob?: {\n jobType?: string\n name?: string\n description?: string\n cancellable?: boolean\n meta?: Record<string, unknown>\n }\n}\n\nexport async function startDataSyncRun(params: {\n syncRunService: SyncRunService\n progressService: ProgressService\n scope: DataSyncStartScope\n input: StartDataSyncRunInput\n}) {\n const { syncRunService, progressService, scope, input } = params\n const createProgressJob = input.createProgressJob !== false\n\n const progressJob = createProgressJob\n ? await progressService.createJob(\n {\n jobType: input.progressJob?.jobType ?? `data_sync:${input.direction}`,\n name: input.progressJob?.name ?? `Data sync ${input.integrationId} \u2014 ${input.entityType}`,\n description: input.progressJob?.description ?? `${input.entityType} ${input.direction}`,\n cancellable: input.progressJob?.cancellable ?? true,\n meta: {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n ...(input.progressJob?.meta ?? {}),\n },\n },\n {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n userId: scope.userId,\n },\n )\n : null\n\n const run = await syncRunService.createRun(\n {\n integrationId: input.integrationId,\n entityType: input.entityType,\n direction: input.direction,\n cursor: input.cursor ?? null,\n triggeredBy: input.triggeredBy ?? scope.userId ?? null,\n progressJobId: progressJob?.id ?? null,\n },\n {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n )\n\n const queueName = input.direction === 'import' ? DATA_SYNC_IMPORT_QUEUE : DATA_SYNC_EXPORT_QUEUE\n const queue = getSyncQueue(queueName)\n await queue.enqueue({\n runId: run.id,\n batchSize: input.batchSize ?? 100,\n scope: {\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n userId: scope.userId ?? null,\n },\n })\n\n return {\n run,\n progressJob,\n }\n}\n"],
5
+ "mappings": "AAEA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB,8BAA8B;AAyB/D,eAAsB,iBAAiB,QAKpC;AACD,QAAM,EAAE,gBAAgB,iBAAiB,OAAO,MAAM,IAAI;AAC1D,QAAM,oBAAoB,MAAM,sBAAsB;AAEtD,QAAM,cAAc,oBAChB,MAAM,gBAAgB;AAAA,IACtB;AAAA,MACE,SAAS,MAAM,aAAa,WAAW,aAAa,MAAM,SAAS;AAAA,MACnE,MAAM,MAAM,aAAa,QAAQ,aAAa,MAAM,aAAa,WAAM,MAAM,UAAU;AAAA,MACvF,aAAa,MAAM,aAAa,eAAe,GAAG,MAAM,UAAU,IAAI,MAAM,SAAS;AAAA,MACrF,aAAa,MAAM,aAAa,eAAe;AAAA,MAC/C,MAAM;AAAA,QACJ,eAAe,MAAM;AAAA,QACrB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,GAAI,MAAM,aAAa,QAAQ,CAAC;AAAA,MAClC;AAAA,IACF;AAAA,IACA;AAAA,MACE,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,QAAQ,MAAM;AAAA,IAChB;AAAA,EACF,IACE;AAEJ,QAAM,MAAM,MAAM,eAAe;AAAA,IAC/B;AAAA,MACE,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,WAAW,MAAM;AAAA,MACjB,QAAQ,MAAM,UAAU;AAAA,MACxB,aAAa,MAAM,eAAe,MAAM,UAAU;AAAA,MAClD,eAAe,aAAa,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,MACE,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,cAAc,WAAW,yBAAyB;AAC1E,QAAM,QAAQ,aAAa,SAAS;AACpC,QAAM,MAAM,QAAQ;AAAA,IAClB,OAAO,IAAI;AAAA,IACX,WAAW,MAAM,aAAa;AAAA,IAC9B,OAAO;AAAA,MACL,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,QAAQ,MAAM,UAAU;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -2,6 +2,7 @@ import { getIntegration } from "@open-mercato/shared/modules/integrations/types"
2
2
  import { refreshCoverageSnapshot } from "../../query_index/lib/coverage.js";
3
3
  import { emitDataSyncEvent } from "../events.js";
4
4
  import { getDataSyncAdapter } from "./adapter-registry.js";
5
+ import { SyncRunOwnershipConflictError } from "./sync-run-service.js";
5
6
  import { createLogger } from "@open-mercato/shared/lib/logger";
6
7
  const logger = createLogger("data_sync").child({ component: "sync-engine" });
7
8
  function resolveProviderKey(integrationId) {
@@ -143,6 +144,14 @@ function createSyncEngine(deps) {
143
144
  if (alreadyFinalizedWithSameStatus) {
144
145
  return;
145
146
  }
147
+ if (run.status !== status) {
148
+ logger.warn("Skipping finalization of a sync run another worker already finalized", {
149
+ runId,
150
+ requestedStatus: status,
151
+ actualStatus: run.status
152
+ });
153
+ return;
154
+ }
146
155
  if (run.progressJobId) {
147
156
  if (status === "completed") {
148
157
  await progressService.completeJob(
@@ -307,6 +316,7 @@ function createSyncEngine(deps) {
307
316
  if (!credentials) {
308
317
  throw new Error(`Integration ${run.integrationId} is missing credentials`);
309
318
  }
319
+ const resumed = run.status === "running";
310
320
  const activeRun = await syncRunService.markStatus(run.id, "running", scope);
311
321
  if (!activeRun || activeRun.status !== "running") {
312
322
  return;
@@ -316,6 +326,7 @@ function createSyncEngine(deps) {
316
326
  integrationId: run.integrationId,
317
327
  entityType: run.entityType,
318
328
  direction: run.direction,
329
+ resumed,
319
330
  tenantId: scope.tenantId,
320
331
  organizationId: scope.organizationId
321
332
  });
@@ -349,6 +360,7 @@ function createSyncEngine(deps) {
349
360
  const mapping = await resolveMapping(adapter, run.entityType, scope);
350
361
  let processedCount = 0;
351
362
  let totalCount = null;
363
+ let committedBatches = activeRun.batchesCompleted ?? 0;
352
364
  try {
353
365
  for await (const batch of adapter.streamImport({
354
366
  entityType: run.entityType,
@@ -374,8 +386,10 @@ function createSyncEngine(deps) {
374
386
  batchesCompleted: 1
375
387
  },
376
388
  batch.cursor,
377
- scope
389
+ scope,
390
+ committedBatches
378
391
  );
392
+ committedBatches += 1;
379
393
  await updateProgress(run.progressJobId, processedCount, totalCount, scope);
380
394
  await refreshCoverageSnapshots(batch.refreshCoverageEntityTypes, scope);
381
395
  await logImportItemFailures(run.id, run.integrationId, batch.items, scope);
@@ -397,6 +411,13 @@ function createSyncEngine(deps) {
397
411
  });
398
412
  }
399
413
  } catch (error) {
414
+ if (error instanceof SyncRunOwnershipConflictError) {
415
+ logger.warn("Yielding import run to a concurrent worker that already advanced it", {
416
+ runId: run.id,
417
+ expectedBatchesCompleted: error.expectedBatchesCompleted
418
+ });
419
+ return;
420
+ }
400
421
  const message = error instanceof Error ? error.message : "Sync import failed";
401
422
  await integrationLogService.write(
402
423
  {
@@ -438,6 +459,7 @@ function createSyncEngine(deps) {
438
459
  if (!credentials) {
439
460
  throw new Error(`Integration ${run.integrationId} is missing credentials`);
440
461
  }
462
+ const resumed = run.status === "running";
441
463
  const activeRun = await syncRunService.markStatus(run.id, "running", scope);
442
464
  if (!activeRun || activeRun.status !== "running") {
443
465
  return;
@@ -447,6 +469,7 @@ function createSyncEngine(deps) {
447
469
  integrationId: run.integrationId,
448
470
  entityType: run.entityType,
449
471
  direction: run.direction,
472
+ resumed,
450
473
  tenantId: scope.tenantId,
451
474
  organizationId: scope.organizationId
452
475
  });
@@ -479,6 +502,7 @@ function createSyncEngine(deps) {
479
502
  }
480
503
  const mapping = await resolveMapping(adapter, run.entityType, scope);
481
504
  let processedCount = 0;
505
+ let committedBatches = activeRun.batchesCompleted ?? 0;
482
506
  try {
483
507
  for await (const batch of adapter.streamExport({
484
508
  entityType: run.entityType,
@@ -505,8 +529,10 @@ function createSyncEngine(deps) {
505
529
  batchesCompleted: 1
506
530
  },
507
531
  batch.cursor,
508
- scope
532
+ scope,
533
+ committedBatches
509
534
  );
535
+ committedBatches += 1;
510
536
  await updateProgress(run.progressJobId, processedCount, null, scope);
511
537
  await logExportItemFailures(run.id, run.integrationId, batch.results, scope);
512
538
  await writeOperationalLog({
@@ -526,6 +552,13 @@ function createSyncEngine(deps) {
526
552
  });
527
553
  }
528
554
  } catch (error) {
555
+ if (error instanceof SyncRunOwnershipConflictError) {
556
+ logger.warn("Yielding export run to a concurrent worker that already advanced it", {
557
+ runId: run.id,
558
+ expectedBatchesCompleted: error.expectedBatchesCompleted
559
+ });
560
+ return;
561
+ }
529
562
  const message = error instanceof Error ? error.message : "Sync export failed";
530
563
  await integrationLogService.write(
531
564
  {