@open-mercato/core 0.9.1-develop.7518.1.343d9198f1 → 0.9.1-develop.7532.1.8eac7e198f

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 (88) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/logout.js +6 -1
  3. package/dist/modules/auth/api/logout.js.map +2 -2
  4. package/dist/modules/auth/api/session/refresh.js +6 -1
  5. package/dist/modules/auth/api/session/refresh.js.map +2 -2
  6. package/dist/modules/catalog/ai-tools/mutation-pack.js +10 -5
  7. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +2 -2
  8. package/dist/modules/catalog/ai-tools/types.js.map +2 -2
  9. package/dist/modules/customer_accounts/api/login.js +14 -7
  10. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  11. package/dist/modules/customer_accounts/services/customerUserService.js +10 -10
  12. package/dist/modules/customer_accounts/services/customerUserService.js.map +2 -2
  13. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js +1 -1
  14. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js.map +1 -1
  15. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js +12 -2
  16. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js.map +2 -2
  17. package/dist/modules/forms/api/form-submissions/route.js +16 -1
  18. package/dist/modules/forms/api/form-submissions/route.js.map +2 -2
  19. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js +6 -1
  20. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js.map +2 -2
  21. package/dist/modules/forms/api/runtime-helpers.js +16 -0
  22. package/dist/modules/forms/api/runtime-helpers.js.map +2 -2
  23. package/dist/modules/forms/data/validators.js.map +2 -2
  24. package/dist/modules/forms/lib/portal-subject-policy.js +17 -0
  25. package/dist/modules/forms/lib/portal-subject-policy.js.map +7 -0
  26. package/dist/modules/forms/lib/runtime-principal.js +27 -10
  27. package/dist/modules/forms/lib/runtime-principal.js.map +2 -2
  28. package/dist/modules/forms/services/distribution-service.js +35 -13
  29. package/dist/modules/forms/services/distribution-service.js.map +2 -2
  30. package/dist/modules/forms/services/submission-service.js +4 -0
  31. package/dist/modules/forms/services/submission-service.js.map +2 -2
  32. package/dist/modules/staff/commands/leave-requests.js +54 -24
  33. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  34. package/dist/modules/staff/commands/timesheets-entries.js +4 -0
  35. package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
  36. package/dist/modules/staff/commands/timesheets-tags.js +27 -8
  37. package/dist/modules/staff/commands/timesheets-tags.js.map +2 -2
  38. package/dist/modules/warranty_claims/api/portal/attachments/route.js +19 -7
  39. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
  40. package/dist/modules/warranty_claims/commands/claims.js +1 -1
  41. package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
  42. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +6 -6
  43. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +2 -2
  44. package/dist/modules/wms/commands/inventory-actions.js +19 -3
  45. package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
  46. package/dist/modules/workflows/lib/async-activity-wait.js +43 -0
  47. package/dist/modules/workflows/lib/async-activity-wait.js.map +7 -0
  48. package/dist/modules/workflows/lib/parallel-handler.js +20 -14
  49. package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
  50. package/dist/modules/workflows/lib/workflow-executor.js +21 -23
  51. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  52. package/package.json +7 -7
  53. package/src/modules/auth/api/logout.ts +6 -1
  54. package/src/modules/auth/api/session/refresh.ts +6 -1
  55. package/src/modules/catalog/ai-tools/mutation-pack.ts +14 -5
  56. package/src/modules/catalog/ai-tools/types.ts +1 -0
  57. package/src/modules/customer_accounts/api/login.ts +15 -7
  58. package/src/modules/customer_accounts/services/customerUserService.ts +34 -10
  59. package/src/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.ts +1 -1
  60. package/src/modules/forms/api/form-submissions/[id]/submit/route.ts +16 -3
  61. package/src/modules/forms/api/form-submissions/route.ts +21 -2
  62. package/src/modules/forms/api/public/submissions/[id]/submit/route.ts +7 -0
  63. package/src/modules/forms/api/runtime-helpers.ts +16 -0
  64. package/src/modules/forms/data/validators.ts +2 -1
  65. package/src/modules/forms/i18n/de.json +1 -0
  66. package/src/modules/forms/i18n/en.json +1 -0
  67. package/src/modules/forms/i18n/es.json +1 -0
  68. package/src/modules/forms/i18n/ko.json +1 -0
  69. package/src/modules/forms/i18n/pl.json +1 -0
  70. package/src/modules/forms/lib/portal-subject-policy.ts +42 -0
  71. package/src/modules/forms/lib/runtime-principal.ts +35 -11
  72. package/src/modules/forms/services/distribution-service.ts +35 -13
  73. package/src/modules/forms/services/submission-service.ts +12 -0
  74. package/src/modules/staff/commands/leave-requests.ts +54 -23
  75. package/src/modules/staff/commands/timesheets-entries.ts +4 -0
  76. package/src/modules/staff/commands/timesheets-tags.ts +37 -8
  77. package/src/modules/staff/i18n/de.json +2 -0
  78. package/src/modules/staff/i18n/en.json +2 -0
  79. package/src/modules/staff/i18n/es.json +2 -0
  80. package/src/modules/staff/i18n/ko.json +2 -0
  81. package/src/modules/staff/i18n/pl.json +2 -0
  82. package/src/modules/warranty_claims/api/portal/attachments/route.ts +20 -8
  83. package/src/modules/warranty_claims/commands/claims.ts +1 -1
  84. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +5 -5
  85. package/src/modules/wms/commands/inventory-actions.ts +30 -10
  86. package/src/modules/workflows/lib/async-activity-wait.ts +71 -0
  87. package/src/modules/workflows/lib/parallel-handler.ts +24 -15
  88. package/src/modules/workflows/lib/workflow-executor.ts +24 -27
@@ -64,22 +64,22 @@ class CustomerUserService {
64
64
  return false;
65
65
  }
66
66
  async incrementFailedAttempts(user) {
67
- const newCount = (user.failedLoginAttempts || 0) + 1;
68
- const updates = { failedLoginAttempts: newCount };
69
- if (newCount >= MAX_FAILED_ATTEMPTS) {
70
- updates.lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS);
71
- }
72
- await this.em.nativeUpdate(CustomerUser, { id: user.id }, updates);
73
- user.failedLoginAttempts = newCount;
74
- if (updates.lockedUntil) user.lockedUntil = updates.lockedUntil;
67
+ const lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS);
68
+ await this.em.getKysely().updateTable("customer_users").set((eb) => ({
69
+ failed_login_attempts: eb("failed_login_attempts", "+", 1),
70
+ locked_until: eb.case().when("failed_login_attempts", ">=", MAX_FAILED_ATTEMPTS - 1).then(eb.fn("greatest", [eb.ref("locked_until"), eb.val(lockedUntil)])).else(eb.ref("locked_until")).end()
71
+ })).where("id", "=", user.id).execute();
72
+ }
73
+ async resetFailedAttemptsUnlessLocked(user) {
74
+ const now = /* @__PURE__ */ new Date();
75
+ const row = await this.em.getKysely().updateTable("customer_users").set({ failed_login_attempts: 0, locked_until: null }).where("id", "=", user.id).where((eb) => eb.or([eb("locked_until", "is", null), eb("locked_until", "<=", now)])).returning("id").executeTakeFirst();
76
+ return row !== void 0;
75
77
  }
76
78
  async resetFailedAttempts(user) {
77
79
  await this.em.nativeUpdate(CustomerUser, { id: user.id }, {
78
80
  failedLoginAttempts: 0,
79
81
  lockedUntil: null
80
82
  });
81
- user.failedLoginAttempts = 0;
82
- user.lockedUntil = null;
83
83
  }
84
84
  async updatePassword(user, newPassword, em) {
85
85
  const passwordHash = await hash(newPassword, BCRYPT_COST);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customer_accounts/services/customerUserService.ts"],
4
- "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hashForLookup, lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst MAX_FAILED_ATTEMPTS = 5\nconst LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes\n\nexport class CustomerUserService {\n constructor(private em: EntityManager) {}\n\n async createUser(\n email: string,\n password: string,\n displayName: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerUser> {\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(email)\n const user = this.em.create(CustomerUser, {\n email: email.toLowerCase().trim(),\n emailHash,\n passwordHash,\n displayName,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any)\n return user as CustomerUser\n }\n\n async findByEmail(email: string, tenantId: string): Promise<CustomerUser | null> {\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n {\n emailHash: { $in: lookupHashCandidates(email) },\n tenantId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async findById(\n id: string,\n tenantId: string,\n organizationId?: string | null,\n ): Promise<CustomerUser | null> {\n const where: Record<string, unknown> = { id, tenantId, deletedAt: null }\n if (organizationId !== undefined) where.organizationId = organizationId\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n where as any,\n undefined,\n { tenantId, organizationId },\n )\n }\n\n async verifyPassword(user: CustomerUser, password: string): Promise<boolean> {\n if (!user.passwordHash) return false\n return compare(password, user.passwordHash)\n }\n\n async updateLastLoginAt(user: CustomerUser): Promise<void> {\n const now = new Date()\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n checkLockout(user: CustomerUser): boolean {\n if (!user.lockedUntil) return false\n if (user.lockedUntil.getTime() > Date.now()) return true\n return false\n }\n\n async incrementFailedAttempts(user: CustomerUser): Promise<void> {\n const newCount = (user.failedLoginAttempts || 0) + 1\n const updates: Record<string, unknown> = { failedLoginAttempts: newCount }\n if (newCount >= MAX_FAILED_ATTEMPTS) {\n updates.lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS)\n }\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, updates)\n user.failedLoginAttempts = newCount\n if (updates.lockedUntil) user.lockedUntil = updates.lockedUntil as Date\n }\n\n async resetFailedAttempts(user: CustomerUser): Promise<void> {\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n })\n user.failedLoginAttempts = 0\n user.lockedUntil = null\n }\n\n async updatePassword(user: CustomerUser, newPassword: string, em?: EntityManager): Promise<void> {\n const passwordHash = await hash(newPassword, BCRYPT_COST)\n await (em ?? this.em).nativeUpdate(CustomerUser, {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n }, { passwordHash })\n user.passwordHash = passwordHash\n }\n\n // `display_name` is encrypted at rest. `nativeUpdate` issues raw SQL and fires none of the\n // flush hooks the tenant-encryption subscriber depends on, so writing it that way persists\n // plaintext PII into a ciphertext column (#3837). Assign it on the managed entity and flush\n // so `beforeUpdate` encrypts the value on its way to the database.\n async updateProfile(user: CustomerUser, data: { displayName?: string }): Promise<void> {\n if (data.displayName === undefined) return\n const managed = await findOneWithDecryption(\n this.em,\n CustomerUser,\n { id: user.id, tenantId: user.tenantId, organizationId: user.organizationId, deletedAt: null } as any,\n undefined,\n { tenantId: user.tenantId, organizationId: user.organizationId },\n )\n if (!managed) return\n managed.displayName = data.displayName\n await this.em.flush()\n user.displayName = data.displayName\n }\n\n async softDelete(\n userId: string,\n scope?: { tenantId: string; organizationId: string | null },\n ): Promise<void> {\n const where: Record<string, unknown> = { id: userId }\n if (scope) {\n where.tenantId = scope.tenantId\n where.organizationId = scope.organizationId\n }\n await this.em.nativeUpdate(CustomerUser, where, {\n deletedAt: new Date(),\n isActive: false,\n })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { EntityManager } from '@mikro-orm/postgresql'\nimport { hash, compare } from 'bcryptjs'\nimport { CustomerUser } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { hashForLookup, lookupHashCandidates } from '@open-mercato/shared/lib/encryption/aes'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst BCRYPT_COST = 10\nconst MAX_FAILED_ATTEMPTS = 5\nconst LOCKOUT_DURATION_MS = 15 * 60 * 1000 // 15 minutes\n\ntype CustomerUserLockoutDatabase = {\n customer_users: {\n id: string\n failed_login_attempts: number\n locked_until: Date | null\n }\n}\n\nexport class CustomerUserService {\n constructor(private em: EntityManager) {}\n\n async createUser(\n email: string,\n password: string,\n displayName: string,\n scope: { tenantId: string; organizationId: string },\n ): Promise<CustomerUser> {\n const passwordHash = await hash(password, BCRYPT_COST)\n const emailHash = hashForLookup(email)\n const user = this.em.create(CustomerUser, {\n email: email.toLowerCase().trim(),\n emailHash,\n passwordHash,\n displayName,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n isActive: true,\n failedLoginAttempts: 0,\n createdAt: new Date(),\n } as any)\n return user as CustomerUser\n }\n\n async findByEmail(email: string, tenantId: string): Promise<CustomerUser | null> {\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n {\n emailHash: { $in: lookupHashCandidates(email) },\n tenantId,\n deletedAt: null,\n } as any,\n undefined,\n { tenantId },\n )\n }\n\n async findById(\n id: string,\n tenantId: string,\n organizationId?: string | null,\n ): Promise<CustomerUser | null> {\n const where: Record<string, unknown> = { id, tenantId, deletedAt: null }\n if (organizationId !== undefined) where.organizationId = organizationId\n return findOneWithDecryption(\n this.em,\n CustomerUser,\n where as any,\n undefined,\n { tenantId, organizationId },\n )\n }\n\n async verifyPassword(user: CustomerUser, password: string): Promise<boolean> {\n if (!user.passwordHash) return false\n return compare(password, user.passwordHash)\n }\n\n async updateLastLoginAt(user: CustomerUser): Promise<void> {\n const now = new Date()\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, { lastLoginAt: now })\n user.lastLoginAt = now\n }\n\n checkLockout(user: CustomerUser): boolean {\n if (!user.lockedUntil) return false\n if (user.lockedUntil.getTime() > Date.now()) return true\n return false\n }\n\n async incrementFailedAttempts(user: CustomerUser): Promise<void> {\n const lockedUntil = new Date(Date.now() + LOCKOUT_DURATION_MS)\n await this.em.getKysely<CustomerUserLockoutDatabase>()\n .updateTable('customer_users')\n .set((eb) => ({\n failed_login_attempts: eb('failed_login_attempts', '+', 1),\n locked_until: eb\n .case()\n .when('failed_login_attempts', '>=', MAX_FAILED_ATTEMPTS - 1)\n .then(eb.fn<Date>('greatest', [eb.ref('locked_until'), eb.val(lockedUntil)]))\n .else(eb.ref('locked_until'))\n .end(),\n }))\n .where('id', '=', user.id)\n .execute()\n }\n\n async resetFailedAttemptsUnlessLocked(user: CustomerUser): Promise<boolean> {\n const now = new Date()\n const row = await this.em.getKysely<CustomerUserLockoutDatabase>()\n .updateTable('customer_users')\n .set({ failed_login_attempts: 0, locked_until: null })\n .where('id', '=', user.id)\n .where((eb) => eb.or([eb('locked_until', 'is', null), eb('locked_until', '<=', now)]))\n .returning('id')\n .executeTakeFirst()\n return row !== undefined\n }\n\n async resetFailedAttempts(user: CustomerUser): Promise<void> {\n await this.em.nativeUpdate(CustomerUser, { id: user.id }, {\n failedLoginAttempts: 0,\n lockedUntil: null,\n })\n }\n\n async updatePassword(user: CustomerUser, newPassword: string, em?: EntityManager): Promise<void> {\n const passwordHash = await hash(newPassword, BCRYPT_COST)\n await (em ?? this.em).nativeUpdate(CustomerUser, {\n id: user.id,\n tenantId: user.tenantId,\n organizationId: user.organizationId,\n }, { passwordHash })\n user.passwordHash = passwordHash\n }\n\n // `display_name` is encrypted at rest. `nativeUpdate` issues raw SQL and fires none of the\n // flush hooks the tenant-encryption subscriber depends on, so writing it that way persists\n // plaintext PII into a ciphertext column (#3837). Assign it on the managed entity and flush\n // so `beforeUpdate` encrypts the value on its way to the database.\n async updateProfile(user: CustomerUser, data: { displayName?: string }): Promise<void> {\n if (data.displayName === undefined) return\n const managed = await findOneWithDecryption(\n this.em,\n CustomerUser,\n { id: user.id, tenantId: user.tenantId, organizationId: user.organizationId, deletedAt: null } as any,\n undefined,\n { tenantId: user.tenantId, organizationId: user.organizationId },\n )\n if (!managed) return\n managed.displayName = data.displayName\n await this.em.flush()\n user.displayName = data.displayName\n }\n\n async softDelete(\n userId: string,\n scope?: { tenantId: string; organizationId: string | null },\n ): Promise<void> {\n const where: Record<string, unknown> = { id: userId }\n if (scope) {\n where.tenantId = scope.tenantId\n where.organizationId = scope.organizationId\n }\n await this.em.nativeUpdate(CustomerUser, where, {\n deletedAt: new Date(),\n isActive: false,\n })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -70,7 +70,7 @@ const openApi = {
70
70
  methods: {
71
71
  DELETE: {
72
72
  summary: "Revoke invitation",
73
- description: "Soft-revokes an invitation \u2014 the row is retained and its status becomes revoked so a revoked token resolves to 410 (not 404).",
73
+ description: "Soft-revokes an invitation \u2014 the row is retained and its status becomes revoked so a revoked token resolves to 410 (not 404); access tokens already issued for the invitation stop working (401).",
74
74
  responses: [{ status: 200, description: "Invitation revoked", schema: deleteResponseSchema }],
75
75
  errors: [{ status: 404, description: "Invitation not found", schema: errorSchema }]
76
76
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/forms/api/distributions/%5BdistributionId%5D/invitations/%5BinvitationId%5D/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n invitationRevokeCommandSchema,\n type FormInvitationRevokeCommandInput,\n} from '../../../../../data/validators'\nimport {\n attachOperationMetadata,\n buildFormsRouteContext,\n handleRouteError,\n jsonError,\n withMutationGuard,\n withScopedPayload,\n} from '../../../../helpers'\nimport { FORM_INVITATION_RESOURCE_KIND } from '../../../../../commands/invitation'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['forms.distribute'] },\n}\n\nconst deleteResponseSchema = z.object({ id: z.string().uuid() })\nconst errorSchema = z.object({ error: z.string() })\n\nfunction extractInvitationId(req: Request): string {\n const url = new URL(req.url)\n const segments = url.pathname.split('/').filter(Boolean)\n const idx = segments.findIndex((segment) => segment === 'invitations')\n return idx >= 0 ? segments[idx + 1] ?? '' : ''\n}\n\nexport async function DELETE(req: Request) {\n try {\n const { ctx, organizationId, tenantId, translate } = await buildFormsRouteContext(req)\n if (!organizationId || !tenantId) {\n return jsonError(400, 'forms.errors.organization_required')\n }\n const invitationId = extractInvitationId(req)\n if (!invitationId) return jsonError(400, 'forms.errors.invalid_id')\n\n const scoped = withScopedPayload({ invitationId }, ctx, translate)\n const input = invitationRevokeCommandSchema.parse(scoped) satisfies FormInvitationRevokeCommandInput\n\n return await withMutationGuard({\n ctx,\n tenantId,\n organizationId,\n resourceKind: FORM_INVITATION_RESOURCE_KIND,\n resourceId: invitationId,\n operation: 'delete',\n request: req,\n payload: { invitationId },\n run: async () => {\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n FormInvitationRevokeCommandInput,\n { invitationId: string }\n >('forms.invitation.revoke', { input, ctx })\n const response = NextResponse.json({ id: result?.invitationId ?? invitationId })\n return attachOperationMetadata(\n response,\n logEntry,\n FORM_INVITATION_RESOURCE_KIND,\n result?.invitationId ?? invitationId,\n )\n },\n })\n } catch (error) {\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return handleRouteError('invitations.DELETE', error)\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Forms',\n summary: 'Revoke an invitation',\n pathParams: z.object({ distributionId: z.string().uuid(), invitationId: z.string().uuid() }),\n methods: {\n DELETE: {\n summary: 'Revoke invitation',\n description:\n 'Soft-revokes an invitation \u2014 the row is retained and its status becomes revoked so a revoked token resolves to 410 (not 404).',\n responses: [{ status: 200, description: 'Invitation revoked', schema: deleteResponseSchema }],\n errors: [{ status: 404, description: 'Invitation not found', schema: errorSchema }],\n },\n },\n}\n"],
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport {\n invitationRevokeCommandSchema,\n type FormInvitationRevokeCommandInput,\n} from '../../../../../data/validators'\nimport {\n attachOperationMetadata,\n buildFormsRouteContext,\n handleRouteError,\n jsonError,\n withMutationGuard,\n withScopedPayload,\n} from '../../../../helpers'\nimport { FORM_INVITATION_RESOURCE_KIND } from '../../../../../commands/invitation'\n\nexport const metadata = {\n DELETE: { requireAuth: true, requireFeatures: ['forms.distribute'] },\n}\n\nconst deleteResponseSchema = z.object({ id: z.string().uuid() })\nconst errorSchema = z.object({ error: z.string() })\n\nfunction extractInvitationId(req: Request): string {\n const url = new URL(req.url)\n const segments = url.pathname.split('/').filter(Boolean)\n const idx = segments.findIndex((segment) => segment === 'invitations')\n return idx >= 0 ? segments[idx + 1] ?? '' : ''\n}\n\nexport async function DELETE(req: Request) {\n try {\n const { ctx, organizationId, tenantId, translate } = await buildFormsRouteContext(req)\n if (!organizationId || !tenantId) {\n return jsonError(400, 'forms.errors.organization_required')\n }\n const invitationId = extractInvitationId(req)\n if (!invitationId) return jsonError(400, 'forms.errors.invalid_id')\n\n const scoped = withScopedPayload({ invitationId }, ctx, translate)\n const input = invitationRevokeCommandSchema.parse(scoped) satisfies FormInvitationRevokeCommandInput\n\n return await withMutationGuard({\n ctx,\n tenantId,\n organizationId,\n resourceKind: FORM_INVITATION_RESOURCE_KIND,\n resourceId: invitationId,\n operation: 'delete',\n request: req,\n payload: { invitationId },\n run: async () => {\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const { result, logEntry } = await commandBus.execute<\n FormInvitationRevokeCommandInput,\n { invitationId: string }\n >('forms.invitation.revoke', { input, ctx })\n const response = NextResponse.json({ id: result?.invitationId ?? invitationId })\n return attachOperationMetadata(\n response,\n logEntry,\n FORM_INVITATION_RESOURCE_KIND,\n result?.invitationId ?? invitationId,\n )\n },\n })\n } catch (error) {\n const interceptorRejection = getCommandInterceptorHttpRejection(error)\n if (interceptorRejection) {\n return NextResponse.json(interceptorRejection.body, { status: interceptorRejection.status })\n }\n return handleRouteError('invitations.DELETE', error)\n }\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'Forms',\n summary: 'Revoke an invitation',\n pathParams: z.object({ distributionId: z.string().uuid(), invitationId: z.string().uuid() }),\n methods: {\n DELETE: {\n summary: 'Revoke invitation',\n description:\n 'Soft-revokes an invitation \u2014 the row is retained and its status becomes revoked so a revoked token resolves to 410 (not 404); access tokens already issued for the invitation stop working (401).',\n responses: [{ status: 200, description: 'Invitation revoked', schema: deleteResponseSchema }],\n errors: [{ status: 404, description: 'Invitation not found', schema: errorSchema }],\n },\n },\n}\n"],
5
5
  "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAElB,SAAS,0CAA0C;AAEnD;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,qCAAqC;AAEvC,MAAM,WAAW;AAAA,EACtB,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,kBAAkB,EAAE;AACrE;AAEA,MAAM,uBAAuB,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC/D,MAAM,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;AAElD,SAAS,oBAAoB,KAAsB;AACjD,QAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAC3B,QAAM,WAAW,IAAI,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AACvD,QAAM,MAAM,SAAS,UAAU,CAAC,YAAY,YAAY,aAAa;AACrE,SAAO,OAAO,IAAI,SAAS,MAAM,CAAC,KAAK,KAAK;AAC9C;AAEA,eAAsB,OAAO,KAAc;AACzC,MAAI;AACF,UAAM,EAAE,KAAK,gBAAgB,UAAU,UAAU,IAAI,MAAM,uBAAuB,GAAG;AACrF,QAAI,CAAC,kBAAkB,CAAC,UAAU;AAChC,aAAO,UAAU,KAAK,oCAAoC;AAAA,IAC5D;AACA,UAAM,eAAe,oBAAoB,GAAG;AAC5C,QAAI,CAAC,aAAc,QAAO,UAAU,KAAK,yBAAyB;AAElE,UAAM,SAAS,kBAAkB,EAAE,aAAa,GAAG,KAAK,SAAS;AACjE,UAAM,QAAQ,8BAA8B,MAAM,MAAM;AAExD,WAAO,MAAM,kBAAkB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,MACA,cAAc;AAAA,MACd,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,SAAS;AAAA,MACT,SAAS,EAAE,aAAa;AAAA,MACxB,KAAK,YAAY;AACf,cAAM,aAAa,IAAI,UAAU,QAAQ,YAAY;AACrD,cAAM,EAAE,QAAQ,SAAS,IAAI,MAAM,WAAW,QAG5C,2BAA2B,EAAE,OAAO,IAAI,CAAC;AAC3C,cAAM,WAAW,aAAa,KAAK,EAAE,IAAI,QAAQ,gBAAgB,aAAa,CAAC;AAC/E,eAAO;AAAA,UACL;AAAA,UACA;AAAA,UACA;AAAA,UACA,QAAQ,gBAAgB;AAAA,QAC1B;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,UAAM,uBAAuB,mCAAmC,KAAK;AACrE,QAAI,sBAAsB;AACxB,aAAO,aAAa,KAAK,qBAAqB,MAAM,EAAE,QAAQ,qBAAqB,OAAO,CAAC;AAAA,IAC7F;AACA,WAAO,iBAAiB,sBAAsB,KAAK;AAAA,EACrD;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,YAAY,EAAE,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,KAAK,GAAG,cAAc,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAAA,EAC3F,SAAS;AAAA,IACP,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,sBAAsB,QAAQ,qBAAqB,CAAC;AAAA,MAC5F,QAAQ,CAAC,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,YAAY,CAAC;AAAA,IACpF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -3,7 +3,14 @@ import { z } from "zod";
3
3
  import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
4
4
  import { getCustomerAuthFromRequest } from "@open-mercato/core/modules/customer_accounts/lib/customerAuth";
5
5
  import { submissionSubmitInputSchema } from "../../../../data/validators.js";
6
- import { buildSubmitMetadata, mapSubmissionError, readJsonBody, serializeSubmission } from "../../../runtime-helpers.js";
6
+ import {
7
+ buildSubmitMetadata,
8
+ isPortalSubjectForbiddenError,
9
+ mapSubmissionError,
10
+ portalSubjectForbiddenResponse,
11
+ readJsonBody,
12
+ serializeSubmission
13
+ } from "../../../runtime-helpers.js";
7
14
  const metadata = {
8
15
  POST: { requireAuth: false }
9
16
  };
@@ -36,10 +43,12 @@ async function POST(req, context) {
36
43
  tenantId: auth.tenantId,
37
44
  baseRevisionId: parsed.data.base_revision_id,
38
45
  submittedBy: auth.sub,
39
- submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata)
46
+ submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata),
47
+ submittedByPortalCustomer: true
40
48
  });
41
49
  return NextResponse.json({ submission: serializeSubmission(submission) });
42
50
  } catch (error) {
51
+ if (isPortalSubjectForbiddenError(error)) return portalSubjectForbiddenResponse();
43
52
  return mapSubmissionError(error);
44
53
  }
45
54
  }
@@ -57,6 +66,7 @@ const postMethodDoc = {
57
66
  responses: [{ status: 200, description: "Submission submitted", schema: responseSchema }],
58
67
  errors: [
59
68
  { status: 401, description: "Not authenticated", schema: errorSchema },
69
+ { status: 403, description: "SUBJECT_FORBIDDEN: caller-started submission is not on the caller's own customer subject", schema: errorSchema },
60
70
  { status: 404, description: "Submission not found", schema: errorSchema },
61
71
  { status: 409, description: "Stale base_revision_id", schema: errorSchema },
62
72
  { status: 422, description: "Submission already in terminal state", schema: errorSchema }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/forms/api/form-submissions/%5Bid%5D/submit/route.ts"],
4
- "sourcesContent": ["/**\n * Runtime API \u2014 POST /api/form-submissions/:id/submit\n *\n * Final submit. Body: `{ base_revision_id, submit_metadata? }`. Sets\n * status = submitted, captures `submit_metadata` (locale, IP, UA), emits\n * `forms.submission.submitted`. Idempotency hardening lands in 1d once the\n * client renderer is in place; phase 1c rejects double-submits with 422.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { SubmissionService } from '../../../../services/submission-service'\nimport { submissionSubmitInputSchema } from '../../../../data/validators'\nimport { buildSubmitMetadata, mapSubmissionError, readJsonBody, serializeSubmission } from '../../../runtime-helpers'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(\n req: NextRequest,\n context: { params: { id: string } | Promise<{ id: string }> },\n) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n }\n const params = await Promise.resolve(context.params)\n const submissionId = String(params.id)\n\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (error) {\n return mapSubmissionError(error)\n }\n const parsed = submissionSubmitInputSchema.safeParse(raw)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'VALIDATION_FAILED', details: parsed.error.flatten() },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('formsSubmissionService') as SubmissionService\n\n try {\n const submission = await service.submit({\n submissionId,\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n baseRevisionId: parsed.data.base_revision_id,\n submittedBy: auth.sub,\n submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata),\n })\n return NextResponse.json({ submission: serializeSubmission(submission) })\n } catch (error) {\n return mapSubmissionError(error)\n }\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n details: z.unknown().optional(),\n})\n\nconst responseSchema = z.object({ submission: z.record(z.string(), z.unknown()) })\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Submit a form submission',\n description: 'Final submission of a draft submission. Verifies base_revision_id and stores the submit metadata.',\n tags: ['Forms Runtime'],\n requestBody: { schema: submissionSubmitInputSchema },\n responses: [{ status: 200, description: 'Submission submitted', schema: responseSchema }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 404, description: 'Submission not found', schema: errorSchema },\n { status: 409, description: 'Stale base_revision_id', schema: errorSchema },\n { status: 422, description: 'Submission already in terminal state', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Submit a form submission',\n methods: { POST: postMethodDoc },\n}\n"],
5
- "mappings": "AASA,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,kCAAkC;AAE3C,SAAS,mCAAmC;AAC5C,SAAS,qBAAqB,oBAAoB,cAAc,2BAA2B;AAEpF,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,eAAsB,KACpB,KACA,SACA;AACA,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAChF;AACA,QAAM,SAAS,MAAM,QAAQ,QAAQ,QAAQ,MAAM;AACnD,QAAM,eAAe,OAAO,OAAO,EAAE;AAErC,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,aAAa,GAAG;AAAA,EAC9B,SAAS,OAAO;AACd,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACA,QAAM,SAAS,4BAA4B,UAAU,GAAG;AACxD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,UAAU,UAAU,QAAQ,wBAAwB;AAE1D,MAAI;AACF,UAAM,aAAa,MAAM,QAAQ,OAAO;AAAA,MACtC;AAAA,MACA,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,gBAAgB,OAAO,KAAK;AAAA,MAC5B,aAAa,KAAK;AAAA,MAClB,gBAAgB,oBAAoB,KAAK,OAAO,KAAK,eAAe;AAAA,IACtE,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,YAAY,oBAAoB,UAAU,EAAE,CAAC;AAAA,EAC1E,SAAS,OAAO;AACd,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACF;AAEA,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAAA,EAChB,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;AAEjF,MAAM,gBAAkC;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,eAAe;AAAA,EACtB,aAAa,EAAE,QAAQ,4BAA4B;AAAA,EACnD,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,eAAe,CAAC;AAAA,EACxF,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,YAAY;AAAA,IACrE,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,YAAY;AAAA,IACxE,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,YAAY;AAAA,IAC1E,EAAE,QAAQ,KAAK,aAAa,wCAAwC,QAAQ,YAAY;AAAA,EAC1F;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,cAAc;AACjC;",
4
+ "sourcesContent": ["/**\n * Runtime API \u2014 POST /api/form-submissions/:id/submit\n *\n * Final submit. Body: `{ base_revision_id, submit_metadata? }`. Sets\n * status = submitted, captures `submit_metadata` (locale, IP, UA), emits\n * `forms.submission.submitted`. A submission the caller started themselves\n * must carry their own subject (`lib/portal-subject-policy.ts`), so a draft\n * started for someone else before that check existed cannot be finalized.\n * Idempotency hardening lands in 1d once the client renderer is in place;\n * phase 1c rejects double-submits with 422.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { SubmissionService } from '../../../../services/submission-service'\nimport { submissionSubmitInputSchema } from '../../../../data/validators'\nimport {\n buildSubmitMetadata,\n isPortalSubjectForbiddenError,\n mapSubmissionError,\n portalSubjectForbiddenResponse,\n readJsonBody,\n serializeSubmission,\n} from '../../../runtime-helpers'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(\n req: NextRequest,\n context: { params: { id: string } | Promise<{ id: string }> },\n) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n }\n const params = await Promise.resolve(context.params)\n const submissionId = String(params.id)\n\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (error) {\n return mapSubmissionError(error)\n }\n const parsed = submissionSubmitInputSchema.safeParse(raw)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'VALIDATION_FAILED', details: parsed.error.flatten() },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('formsSubmissionService') as SubmissionService\n\n try {\n const submission = await service.submit({\n submissionId,\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n baseRevisionId: parsed.data.base_revision_id,\n submittedBy: auth.sub,\n submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata),\n submittedByPortalCustomer: true,\n })\n return NextResponse.json({ submission: serializeSubmission(submission) })\n } catch (error) {\n if (isPortalSubjectForbiddenError(error)) return portalSubjectForbiddenResponse()\n return mapSubmissionError(error)\n }\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n details: z.unknown().optional(),\n})\n\nconst responseSchema = z.object({ submission: z.record(z.string(), z.unknown()) })\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Submit a form submission',\n description: 'Final submission of a draft submission. Verifies base_revision_id and stores the submit metadata.',\n tags: ['Forms Runtime'],\n requestBody: { schema: submissionSubmitInputSchema },\n responses: [{ status: 200, description: 'Submission submitted', schema: responseSchema }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'SUBJECT_FORBIDDEN: caller-started submission is not on the caller\\'s own customer subject', schema: errorSchema },\n { status: 404, description: 'Submission not found', schema: errorSchema },\n { status: 409, description: 'Stale base_revision_id', schema: errorSchema },\n { status: 422, description: 'Submission already in terminal state', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Submit a form submission',\n methods: { POST: postMethodDoc },\n}\n"],
5
+ "mappings": "AAYA,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAElB,SAAS,8BAA8B;AACvC,SAAS,kCAAkC;AAE3C,SAAS,mCAAmC;AAC5C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEA,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,eAAsB,KACpB,KACA,SACA;AACA,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,CAAC,MAAM;AACT,WAAO,aAAa,KAAK,EAAE,OAAO,0BAA0B,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAChF;AACA,QAAM,SAAS,MAAM,QAAQ,QAAQ,QAAQ,MAAM;AACnD,QAAM,eAAe,OAAO,OAAO,EAAE;AAErC,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,aAAa,GAAG;AAAA,EAC9B,SAAS,OAAO;AACd,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACA,QAAM,SAAS,4BAA4B,UAAU,GAAG;AACxD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,UAAU,UAAU,QAAQ,wBAAwB;AAE1D,MAAI;AACF,UAAM,aAAa,MAAM,QAAQ,OAAO;AAAA,MACtC;AAAA,MACA,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,gBAAgB,OAAO,KAAK;AAAA,MAC5B,aAAa,KAAK;AAAA,MAClB,gBAAgB,oBAAoB,KAAK,OAAO,KAAK,eAAe;AAAA,MACpE,2BAA2B;AAAA,IAC7B,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,YAAY,oBAAoB,UAAU,EAAE,CAAC;AAAA,EAC1E,SAAS,OAAO;AACd,QAAI,8BAA8B,KAAK,EAAG,QAAO,+BAA+B;AAChF,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACF;AAEA,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAAA,EAChB,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC;AAEjF,MAAM,gBAAkC;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,eAAe;AAAA,EACtB,aAAa,EAAE,QAAQ,4BAA4B;AAAA,EACnD,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,eAAe,CAAC;AAAA,EACxF,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,qBAAqB,QAAQ,YAAY;AAAA,IACrE,EAAE,QAAQ,KAAK,aAAa,4FAA6F,QAAQ,YAAY;AAAA,IAC7I,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,YAAY;AAAA,IACxE,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,YAAY;AAAA,IAC1E,EAAE,QAAQ,KAAK,aAAa,wCAAwC,QAAQ,YAAY;AAAA,EAC1F;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,cAAc;AACjC;",
6
6
  "names": []
7
7
  }
@@ -6,7 +6,15 @@ import {
6
6
  resolvePrefillSeed
7
7
  } from "../../services/prefill-resolver.js";
8
8
  import { submissionStartInputSchema } from "../../data/validators.js";
9
- import { mapSubmissionError, readJsonBody, serializeActor, serializeRevision, serializeSubmission } from "../runtime-helpers.js";
9
+ import { isPortalSubjectAllowed } from "../../lib/portal-subject-policy.js";
10
+ import {
11
+ mapSubmissionError,
12
+ portalSubjectForbiddenResponse,
13
+ readJsonBody,
14
+ serializeActor,
15
+ serializeRevision,
16
+ serializeSubmission
17
+ } from "../runtime-helpers.js";
10
18
  const metadata = {
11
19
  POST: { requireAuth: false }
12
20
  };
@@ -28,6 +36,12 @@ async function POST(req) {
28
36
  { status: 422 }
29
37
  );
30
38
  }
39
+ if (!isPortalSubjectAllowed(auth.sub, {
40
+ subjectType: parsed.data.subject_type,
41
+ subjectId: parsed.data.subject_id
42
+ })) {
43
+ return portalSubjectForbiddenResponse();
44
+ }
31
45
  const container = await createRequestContainer();
32
46
  const service = container.resolve("formsSubmissionService");
33
47
  const prefillResolver = container.resolve("formsPrefillResolver");
@@ -97,6 +111,7 @@ const postMethodDoc = {
97
111
  responses: [{ status: 201, description: "Submission started", schema: submissionResponseSchema }],
98
112
  errors: [
99
113
  { status: 401, description: "Not authenticated", schema: errorSchema },
114
+ { status: 403, description: "SUBJECT_FORBIDDEN: subject is not the caller's own customer subject", schema: errorSchema },
100
115
  { status: 404, description: "Form not found", schema: errorSchema },
101
116
  { status: 422, description: "Validation or form-state error", schema: errorSchema }
102
117
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/forms/api/form-submissions/route.ts"],
4
- "sourcesContent": ["/**\n * Runtime API \u2014 POST /api/form-submissions\n *\n * Starts a new submission for the active version of a form. Customer auth is\n * required. The starter is auto-assigned the form version's\n * `x-om-default-actor-role` (or first declared role) \u2014 admin assignment of\n * additional actors is the admin route.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { SubmissionService } from '../../services/submission-service'\nimport {\n resolvePrefillSeed,\n type PrefillResolver,\n} from '../../services/prefill-resolver'\nimport { submissionStartInputSchema } from '../../data/validators'\nimport { mapSubmissionError, readJsonBody, serializeActor, serializeRevision, serializeSubmission } from '../runtime-helpers'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n }\n\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (error) {\n return mapSubmissionError(error)\n }\n const parsed = submissionStartInputSchema.safeParse(raw)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'VALIDATION_FAILED', details: parsed.error.flatten() },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('formsSubmissionService') as SubmissionService\n const prefillResolver = container.resolve('formsPrefillResolver') as PrefillResolver\n\n try {\n // Resolve prefill values (W8 / FD-1) for fields declaring `x-om-prefill`.\n // No declarations \u21D2 no resolver call. Seeds are role-filtered + AJV-validated\n // inside `start`; resolution failures are non-fatal (submission opens empty).\n let prefill: Record<string, unknown> | null = null\n try {\n const { compiled } = await service.getActiveFormVersionByKey({\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n formKey: parsed.data.form_key,\n })\n const seed = await resolvePrefillSeed({\n compiled,\n resolver: prefillResolver,\n principal: {\n sub: auth.sub,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n email: auth.email,\n displayName: auth.displayName,\n customerEntityId: auth.customerEntityId ?? null,\n personEntityId: auth.personEntityId ?? null,\n },\n })\n if (Object.keys(seed).length > 0) prefill = seed\n } catch {\n // Prefill is best-effort \u2014 fall through to start with no seed.\n prefill = null\n }\n\n const view = await service.start({\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n formKey: parsed.data.form_key,\n subjectType: parsed.data.subject_type,\n subjectId: parsed.data.subject_id,\n startedBy: auth.sub,\n prefill,\n })\n return NextResponse.json(\n {\n submission: serializeSubmission(view.submission),\n revision: serializeRevision(view.revision),\n decoded_data: view.decodedData,\n actors: view.actors.map(serializeActor),\n },\n { status: 201 },\n )\n } catch (error) {\n return mapSubmissionError(error)\n }\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n details: z.unknown().optional(),\n})\n\nconst submissionResponseSchema = z.object({\n submission: z.record(z.string(), z.unknown()),\n revision: z.record(z.string(), z.unknown()),\n decoded_data: z.record(z.string(), z.unknown()),\n actors: z.array(z.record(z.string(), z.unknown())),\n})\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Start a new form submission',\n description: 'Starts a submission for the active version of a form, auto-assigning the starter to the default actor role.',\n tags: ['Forms Runtime'],\n requestBody: { schema: submissionStartInputSchema },\n responses: [{ status: 201, description: 'Submission started', schema: submissionResponseSchema }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 404, description: 'Form not found', schema: errorSchema },\n { status: 422, description: 'Validation or form-state error', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Form submission lifecycle',\n methods: { POST: postMethodDoc },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Runtime API \u2014 POST /api/form-submissions\n *\n * Starts a new submission for the active version of a form. Customer auth is\n * required, and the subject must be the caller's own `customer` subject\n * (`lib/portal-subject-policy.ts`); any other pair is refused with 403 before\n * anything is persisted. The starter is auto-assigned the form version's\n * `x-om-default-actor-role` (or first declared role) \u2014 admin assignment of\n * additional actors is the admin route.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { SubmissionService } from '../../services/submission-service'\nimport {\n resolvePrefillSeed,\n type PrefillResolver,\n} from '../../services/prefill-resolver'\nimport { submissionStartInputSchema } from '../../data/validators'\nimport { isPortalSubjectAllowed } from '../../lib/portal-subject-policy'\nimport {\n mapSubmissionError,\n portalSubjectForbiddenResponse,\n readJsonBody,\n serializeActor,\n serializeRevision,\n serializeSubmission,\n} from '../runtime-helpers'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(req: NextRequest) {\n const auth = await getCustomerAuthFromRequest(req)\n if (!auth) {\n return NextResponse.json({ error: 'Authentication required' }, { status: 401 })\n }\n\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (error) {\n return mapSubmissionError(error)\n }\n const parsed = submissionStartInputSchema.safeParse(raw)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'VALIDATION_FAILED', details: parsed.error.flatten() },\n { status: 422 },\n )\n }\n if (\n !isPortalSubjectAllowed(auth.sub, {\n subjectType: parsed.data.subject_type,\n subjectId: parsed.data.subject_id,\n })\n ) {\n return portalSubjectForbiddenResponse()\n }\n\n const container = await createRequestContainer()\n const service = container.resolve('formsSubmissionService') as SubmissionService\n const prefillResolver = container.resolve('formsPrefillResolver') as PrefillResolver\n\n try {\n // Resolve prefill values (W8 / FD-1) for fields declaring `x-om-prefill`.\n // No declarations \u21D2 no resolver call. Seeds are role-filtered + AJV-validated\n // inside `start`; resolution failures are non-fatal (submission opens empty).\n let prefill: Record<string, unknown> | null = null\n try {\n const { compiled } = await service.getActiveFormVersionByKey({\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n formKey: parsed.data.form_key,\n })\n const seed = await resolvePrefillSeed({\n compiled,\n resolver: prefillResolver,\n principal: {\n sub: auth.sub,\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n email: auth.email,\n displayName: auth.displayName,\n customerEntityId: auth.customerEntityId ?? null,\n personEntityId: auth.personEntityId ?? null,\n },\n })\n if (Object.keys(seed).length > 0) prefill = seed\n } catch {\n // Prefill is best-effort \u2014 fall through to start with no seed.\n prefill = null\n }\n\n const view = await service.start({\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n formKey: parsed.data.form_key,\n subjectType: parsed.data.subject_type,\n subjectId: parsed.data.subject_id,\n startedBy: auth.sub,\n prefill,\n })\n return NextResponse.json(\n {\n submission: serializeSubmission(view.submission),\n revision: serializeRevision(view.revision),\n decoded_data: view.decodedData,\n actors: view.actors.map(serializeActor),\n },\n { status: 201 },\n )\n } catch (error) {\n return mapSubmissionError(error)\n }\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n details: z.unknown().optional(),\n})\n\nconst submissionResponseSchema = z.object({\n submission: z.record(z.string(), z.unknown()),\n revision: z.record(z.string(), z.unknown()),\n decoded_data: z.record(z.string(), z.unknown()),\n actors: z.array(z.record(z.string(), z.unknown())),\n})\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Start a new form submission',\n description: 'Starts a submission for the active version of a form, auto-assigning the starter to the default actor role.',\n tags: ['Forms Runtime'],\n requestBody: { schema: submissionStartInputSchema },\n responses: [{ status: 201, description: 'Submission started', schema: submissionResponseSchema }],\n errors: [\n { status: 401, description: 'Not authenticated', schema: errorSchema },\n { status: 403, description: 'SUBJECT_FORBIDDEN: subject is not the caller\\'s own customer subject', schema: errorSchema },\n { status: 404, description: 'Form not found', schema: errorSchema },\n { status: 422, description: 'Validation or form-state error', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Form submission lifecycle',\n methods: { POST: postMethodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,7 +6,9 @@ import { submissionSubmitInputSchema } from "../../../../../data/validators.js";
6
6
  import { resolveRuntimePrincipal } from "../../../../../lib/runtime-principal.js";
7
7
  import {
8
8
  buildSubmitMetadata,
9
+ isPortalSubjectForbiddenError,
9
10
  mapDistributionError,
11
+ portalSubjectForbiddenResponse,
10
12
  readJsonBody,
11
13
  serializeSubmission
12
14
  } from "../../../../runtime-helpers.js";
@@ -71,7 +73,8 @@ async function POST(req, context) {
71
73
  submissionId,
72
74
  baseRevisionId: parsed.data.base_revision_id,
73
75
  submittedBy: principal.principal,
74
- submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata)
76
+ submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata),
77
+ submittedByPortalCustomer: principal.source === "customer"
75
78
  });
76
79
  if (principal.source === "token") {
77
80
  const distributionService = container.resolve("formsDistributionService");
@@ -87,6 +90,7 @@ async function POST(req, context) {
87
90
  redirect_url: distribution?.redirectUrl ?? null
88
91
  });
89
92
  } catch (error) {
93
+ if (isPortalSubjectForbiddenError(error)) return portalSubjectForbiddenResponse();
90
94
  return mapDistributionError(error);
91
95
  }
92
96
  }
@@ -107,6 +111,7 @@ const postMethodDoc = {
107
111
  responses: [{ status: 200, description: "Submission submitted", schema: responseSchema }],
108
112
  errors: [
109
113
  { status: 401, description: "Missing or invalid access token / session", schema: errorSchema },
114
+ { status: 403, description: "SUBJECT_FORBIDDEN: customer-started submission is not on the caller's own customer subject", schema: errorSchema },
110
115
  { status: 404, description: "Submission or invitation not found", schema: errorSchema },
111
116
  { status: 409, description: "Stale base_revision_id", schema: errorSchema },
112
117
  { status: 410, description: "Distribution response cap reached or unavailable", schema: errorSchema },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../../src/modules/forms/api/public/submissions/%5Bid%5D/submit/route.ts"],
4
- "sourcesContent": ["/**\n * Public runtime API \u2014 POST /api/forms/public/submissions/:id/submit\n *\n * Finalizes an anonymous (token) or customer-session submission.\n *\n * For token-authorized submits the order is load-bearing for atomic cap\n * enforcement (R-2d-5): FIRST reserve a response slot on the distribution\n * (derived from the invitation), and only if that succeeds proceed to submit,\n * then mark the invitation submitted. A capped distribution returns 410 BEFORE\n * any submission state changes. Customer-session submits skip the\n * reserve/mark-invitation steps \u2014 those are anonymous-distribution concerns.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { SubmissionService } from '../../../../../services/submission-service'\nimport type { DistributionService } from '../../../../../services/distribution-service'\nimport { FormDistribution, FormInvitation } from '../../../../../data/entities'\nimport { submissionSubmitInputSchema } from '../../../../../data/validators'\nimport { resolveRuntimePrincipal } from '../../../../../lib/runtime-principal'\nimport {\n buildSubmitMetadata,\n mapDistributionError,\n readJsonBody,\n serializeSubmission,\n} from '../../../../runtime-helpers'\nimport { buildPublicRateLimitKey, enforcePublicRateLimit } from '../../../rate-limit'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(\n req: NextRequest,\n context: { params: { id: string } | Promise<{ id: string }> },\n) {\n const params = await Promise.resolve(context.params)\n const submissionId = String(params.id)\n\n const limited = await enforcePublicRateLimit(\n req,\n buildPublicRateLimitKey('submission-submit', submissionId),\n )\n if (limited) return limited\n\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (error) {\n return mapDistributionError(error)\n }\n const parsed = submissionSubmitInputSchema.safeParse(raw)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'VALIDATION_FAILED', details: parsed.error.flatten() },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = container.resolve('formsSubmissionService') as SubmissionService\n\n const principal = await resolveRuntimePrincipal({ req, submissionId, em })\n if (!principal) return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 })\n\n try {\n let distribution: FormDistribution | null = null\n\n if (principal.source === 'token') {\n const invitation = await em.findOne(FormInvitation, {\n id: principal.principal,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n deletedAt: null,\n })\n if (!invitation) {\n return NextResponse.json({ error: 'NOT_FOUND', message: 'Invitation not found.' }, { status: 404 })\n }\n distribution = await em.findOne(FormDistribution, {\n id: invitation.distributionId,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n deletedAt: null,\n })\n\n const distributionService = container.resolve('formsDistributionService') as DistributionService\n // Reserve BEFORE submitting \u2014 a capped distribution must 410 without\n // mutating the submission.\n await distributionService.reserveResponseSlot({\n distributionId: invitation.distributionId,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n })\n }\n\n const submission = await service.submit({\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n submissionId,\n baseRevisionId: parsed.data.base_revision_id,\n submittedBy: principal.principal,\n submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata),\n })\n\n if (principal.source === 'token') {\n const distributionService = container.resolve('formsDistributionService') as DistributionService\n await distributionService.markInvitationSubmitted({\n invitationId: principal.principal,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n submissionId,\n })\n }\n\n return NextResponse.json({\n submission: serializeSubmission(submission),\n redirect_url: distribution?.redirectUrl ?? null,\n })\n } catch (error) {\n return mapDistributionError(error)\n }\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n details: z.unknown().optional(),\n})\n\nconst responseSchema = z.object({\n submission: z.record(z.string(), z.unknown()),\n redirect_url: z.string().nullable(),\n})\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Submit a public submission',\n description: 'Finalizes the submission named in the access token / session, enforcing the distribution response cap atomically before submitting.',\n tags: ['Forms Public Runtime'],\n requestBody: { schema: submissionSubmitInputSchema },\n responses: [{ status: 200, description: 'Submission submitted', schema: responseSchema }],\n errors: [\n { status: 401, description: 'Missing or invalid access token / session', schema: errorSchema },\n { status: 404, description: 'Submission or invitation not found', schema: errorSchema },\n { status: 409, description: 'Stale base_revision_id', schema: errorSchema },\n { status: 410, description: 'Distribution response cap reached or unavailable', schema: errorSchema },\n { status: 422, description: 'Validation failed or submission in terminal state', schema: errorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: errorSchema },\n { status: 503, description: 'Rate limiting unavailable', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Submit a public submission',\n methods: { POST: postMethodDoc },\n}\n"],
5
- "mappings": "AAaA,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,8BAA8B;AAGvC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,mCAAmC;AAC5C,SAAS,+BAA+B;AACxC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,yBAAyB,8BAA8B;AAEzD,MAAM,WAAW;AAAA,EACtB,MAAM,EAAE,aAAa,MAAM;AAC7B;AAEA,eAAsB,KACpB,KACA,SACA;AACA,QAAM,SAAS,MAAM,QAAQ,QAAQ,QAAQ,MAAM;AACnD,QAAM,eAAe,OAAO,OAAO,EAAE;AAErC,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA,wBAAwB,qBAAqB,YAAY;AAAA,EAC3D;AACA,MAAI,QAAS,QAAO;AAEpB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,aAAa,GAAG;AAAA,EAC9B,SAAS,OAAO;AACd,WAAO,qBAAqB,KAAK;AAAA,EACnC;AACA,QAAM,SAAS,4BAA4B,UAAU,GAAG;AACxD,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,qBAAqB,SAAS,OAAO,MAAM,QAAQ,EAAE;AAAA,MAC9D,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,KAAK,UAAU,QAAQ,IAAI;AACjC,QAAM,UAAU,UAAU,QAAQ,wBAAwB;AAE1D,QAAM,YAAY,MAAM,wBAAwB,EAAE,KAAK,cAAc,GAAG,CAAC;AACzE,MAAI,CAAC,UAAW,QAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAEnF,MAAI;AACF,QAAI,eAAwC;AAE5C,QAAI,UAAU,WAAW,SAAS;AAChC,YAAM,aAAa,MAAM,GAAG,QAAQ,gBAAgB;AAAA,QAClD,IAAI,UAAU;AAAA,QACd,gBAAgB,UAAU;AAAA,QAC1B,UAAU,UAAU;AAAA,QACpB,WAAW;AAAA,MACb,CAAC;AACD,UAAI,CAAC,YAAY;AACf,eAAO,aAAa,KAAK,EAAE,OAAO,aAAa,SAAS,wBAAwB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,MACpG;AACA,qBAAe,MAAM,GAAG,QAAQ,kBAAkB;AAAA,QAChD,IAAI,WAAW;AAAA,QACf,gBAAgB,UAAU;AAAA,QAC1B,UAAU,UAAU;AAAA,QACpB,WAAW;AAAA,MACb,CAAC;AAED,YAAM,sBAAsB,UAAU,QAAQ,0BAA0B;AAGxE,YAAM,oBAAoB,oBAAoB;AAAA,QAC5C,gBAAgB,WAAW;AAAA,QAC3B,gBAAgB,UAAU;AAAA,QAC1B,UAAU,UAAU;AAAA,MACtB,CAAC;AAAA,IACH;AAEA,UAAM,aAAa,MAAM,QAAQ,OAAO;AAAA,MACtC,gBAAgB,UAAU;AAAA,MAC1B,UAAU,UAAU;AAAA,MACpB;AAAA,MACA,gBAAgB,OAAO,KAAK;AAAA,MAC5B,aAAa,UAAU;AAAA,MACvB,gBAAgB,oBAAoB,KAAK,OAAO,KAAK,eAAe;AAAA,IACtE,CAAC;AAED,QAAI,UAAU,WAAW,SAAS;AAChC,YAAM,sBAAsB,UAAU,QAAQ,0BAA0B;AACxE,YAAM,oBAAoB,wBAAwB;AAAA,QAChD,cAAc,UAAU;AAAA,QACxB,gBAAgB,UAAU;AAAA,QAC1B,UAAU,UAAU;AAAA,QACpB;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO,aAAa,KAAK;AAAA,MACvB,YAAY,oBAAoB,UAAU;AAAA,MAC1C,cAAc,cAAc,eAAe;AAAA,IAC7C,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,qBAAqB,KAAK;AAAA,EACnC;AACF;AAEA,MAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,OAAO,EAAE,OAAO;AAAA,EAChB,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC;AAED,MAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EAC5C,cAAc,EAAE,OAAO,EAAE,SAAS;AACpC,CAAC;AAED,MAAM,gBAAkC;AAAA,EACtC,SAAS;AAAA,EACT,aAAa;AAAA,EACb,MAAM,CAAC,sBAAsB;AAAA,EAC7B,aAAa,EAAE,QAAQ,4BAA4B;AAAA,EACnD,WAAW,CAAC,EAAE,QAAQ,KAAK,aAAa,wBAAwB,QAAQ,eAAe,CAAC;AAAA,EACxF,QAAQ;AAAA,IACN,EAAE,QAAQ,KAAK,aAAa,6CAA6C,QAAQ,YAAY;AAAA,IAC7F,EAAE,QAAQ,KAAK,aAAa,sCAAsC,QAAQ,YAAY;AAAA,IACtF,EAAE,QAAQ,KAAK,aAAa,0BAA0B,QAAQ,YAAY;AAAA,IAC1E,EAAE,QAAQ,KAAK,aAAa,oDAAoD,QAAQ,YAAY;AAAA,IACpG,EAAE,QAAQ,KAAK,aAAa,qDAAqD,QAAQ,YAAY;AAAA,IACrG,EAAE,QAAQ,KAAK,aAAa,uBAAuB,QAAQ,YAAY;AAAA,IACvE,EAAE,QAAQ,KAAK,aAAa,6BAA6B,QAAQ,YAAY;AAAA,EAC/E;AACF;AAEO,MAAM,UAA2B;AAAA,EACtC,SAAS;AAAA,EACT,SAAS,EAAE,MAAM,cAAc;AACjC;",
4
+ "sourcesContent": ["/**\n * Public runtime API \u2014 POST /api/forms/public/submissions/:id/submit\n *\n * Finalizes an anonymous (token) or customer-session submission.\n *\n * For token-authorized submits the order is load-bearing for atomic cap\n * enforcement (R-2d-5): FIRST reserve a response slot on the distribution\n * (derived from the invitation), and only if that succeeds proceed to submit,\n * then mark the invitation submitted. A capped distribution returns 410 BEFORE\n * any submission state changes. Customer-session submits skip the\n * reserve/mark-invitation steps \u2014 those are anonymous-distribution concerns.\n * A customer-session submit of a submission the caller started themselves\n * must carry their own subject (`lib/portal-subject-policy.ts`).\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { SubmissionService } from '../../../../../services/submission-service'\nimport type { DistributionService } from '../../../../../services/distribution-service'\nimport { FormDistribution, FormInvitation } from '../../../../../data/entities'\nimport { submissionSubmitInputSchema } from '../../../../../data/validators'\nimport { resolveRuntimePrincipal } from '../../../../../lib/runtime-principal'\nimport {\n buildSubmitMetadata,\n isPortalSubjectForbiddenError,\n mapDistributionError,\n portalSubjectForbiddenResponse,\n readJsonBody,\n serializeSubmission,\n} from '../../../../runtime-helpers'\nimport { buildPublicRateLimitKey, enforcePublicRateLimit } from '../../../rate-limit'\n\nexport const metadata = {\n POST: { requireAuth: false },\n}\n\nexport async function POST(\n req: NextRequest,\n context: { params: { id: string } | Promise<{ id: string }> },\n) {\n const params = await Promise.resolve(context.params)\n const submissionId = String(params.id)\n\n const limited = await enforcePublicRateLimit(\n req,\n buildPublicRateLimitKey('submission-submit', submissionId),\n )\n if (limited) return limited\n\n let raw: unknown\n try {\n raw = await readJsonBody(req)\n } catch (error) {\n return mapDistributionError(error)\n }\n const parsed = submissionSubmitInputSchema.safeParse(raw)\n if (!parsed.success) {\n return NextResponse.json(\n { error: 'VALIDATION_FAILED', details: parsed.error.flatten() },\n { status: 422 },\n )\n }\n\n const container = await createRequestContainer()\n const em = container.resolve('em') as EntityManager\n const service = container.resolve('formsSubmissionService') as SubmissionService\n\n const principal = await resolveRuntimePrincipal({ req, submissionId, em })\n if (!principal) return NextResponse.json({ error: 'UNAUTHORIZED' }, { status: 401 })\n\n try {\n let distribution: FormDistribution | null = null\n\n if (principal.source === 'token') {\n const invitation = await em.findOne(FormInvitation, {\n id: principal.principal,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n deletedAt: null,\n })\n if (!invitation) {\n return NextResponse.json({ error: 'NOT_FOUND', message: 'Invitation not found.' }, { status: 404 })\n }\n distribution = await em.findOne(FormDistribution, {\n id: invitation.distributionId,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n deletedAt: null,\n })\n\n const distributionService = container.resolve('formsDistributionService') as DistributionService\n // Reserve BEFORE submitting \u2014 a capped distribution must 410 without\n // mutating the submission.\n await distributionService.reserveResponseSlot({\n distributionId: invitation.distributionId,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n })\n }\n\n const submission = await service.submit({\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n submissionId,\n baseRevisionId: parsed.data.base_revision_id,\n submittedBy: principal.principal,\n submitMetadata: buildSubmitMetadata(req, parsed.data.submit_metadata),\n submittedByPortalCustomer: principal.source === 'customer',\n })\n\n if (principal.source === 'token') {\n const distributionService = container.resolve('formsDistributionService') as DistributionService\n await distributionService.markInvitationSubmitted({\n invitationId: principal.principal,\n organizationId: principal.organizationId,\n tenantId: principal.tenantId,\n submissionId,\n })\n }\n\n return NextResponse.json({\n submission: serializeSubmission(submission),\n redirect_url: distribution?.redirectUrl ?? null,\n })\n } catch (error) {\n if (isPortalSubjectForbiddenError(error)) return portalSubjectForbiddenResponse()\n return mapDistributionError(error)\n }\n}\n\nconst errorSchema = z.object({\n error: z.string(),\n message: z.string().optional(),\n details: z.unknown().optional(),\n})\n\nconst responseSchema = z.object({\n submission: z.record(z.string(), z.unknown()),\n redirect_url: z.string().nullable(),\n})\n\nconst postMethodDoc: OpenApiMethodDoc = {\n summary: 'Submit a public submission',\n description: 'Finalizes the submission named in the access token / session, enforcing the distribution response cap atomically before submitting.',\n tags: ['Forms Public Runtime'],\n requestBody: { schema: submissionSubmitInputSchema },\n responses: [{ status: 200, description: 'Submission submitted', schema: responseSchema }],\n errors: [\n { status: 401, description: 'Missing or invalid access token / session', schema: errorSchema },\n { status: 403, description: 'SUBJECT_FORBIDDEN: customer-started submission is not on the caller\\'s own customer subject', schema: errorSchema },\n { status: 404, description: 'Submission or invitation not found', schema: errorSchema },\n { status: 409, description: 'Stale base_revision_id', schema: errorSchema },\n { status: 410, description: 'Distribution response cap reached or unavailable', schema: errorSchema },\n { status: 422, description: 'Validation failed or submission in terminal state', schema: errorSchema },\n { status: 429, description: 'Rate limit exceeded', schema: errorSchema },\n { status: 503, description: 'Rate limiting unavailable', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Submit a public submission',\n methods: { POST: postMethodDoc },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
2
3
  import { SubmissionServiceError, isSubmissionServiceError } from "../services/submission-service.js";
3
4
  import { isDistributionServiceError } from "../services/distribution-service.js";
4
5
  import { readEmbedSettings } from "../lib/embed-frame-policy.js";
@@ -12,6 +13,19 @@ function mapSubmissionError(error) {
12
13
  const message = error instanceof Error ? error.message : "Unknown error";
13
14
  return NextResponse.json({ error: "INTERNAL_ERROR", message }, { status: 500 });
14
15
  }
16
+ function isPortalSubjectForbiddenError(error) {
17
+ return isSubmissionServiceError(error) && error.code === "SUBJECT_FORBIDDEN";
18
+ }
19
+ async function portalSubjectForbiddenResponse() {
20
+ const { translate } = await resolveTranslations();
21
+ return NextResponse.json(
22
+ {
23
+ error: "SUBJECT_FORBIDDEN",
24
+ message: translate("forms.errors.subject_forbidden", "You can only start or submit this form for yourself.")
25
+ },
26
+ { status: 403 }
27
+ );
28
+ }
15
29
  function mapDistributionError(error) {
16
30
  if (isDistributionServiceError(error)) {
17
31
  return NextResponse.json(
@@ -138,8 +152,10 @@ function serializeActor(actor) {
138
152
  }
139
153
  export {
140
154
  buildSubmitMetadata,
155
+ isPortalSubjectForbiddenError,
141
156
  mapDistributionError,
142
157
  mapSubmissionError,
158
+ portalSubjectForbiddenResponse,
143
159
  readJsonBody,
144
160
  serializeActor,
145
161
  serializeFormContext,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/forms/api/runtime-helpers.ts"],
4
- "sourcesContent": ["/**\n * Shared helpers for forms runtime API routes.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { SubmissionServiceError, isSubmissionServiceError } from '../services/submission-service'\nimport { DistributionServiceError, isDistributionServiceError } from '../services/distribution-service'\nimport { readEmbedSettings } from '../lib/embed-frame-policy'\nimport type { CompiledFormVersion } from '../services/form-version-compiler'\nimport type {\n Form,\n FormDistribution,\n FormSubmission,\n FormSubmissionActor,\n FormSubmissionRevision,\n FormVersion,\n} from '../data/entities'\n\nexport function mapSubmissionError(error: unknown): NextResponse {\n if (isSubmissionServiceError(error)) {\n return NextResponse.json(\n { error: error.code, message: error.message, details: error.details ?? null },\n { status: error.httpStatus },\n )\n }\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: 'INTERNAL_ERROR', message }, { status: 500 })\n}\n\nexport function mapDistributionError(error: unknown): NextResponse {\n if (isDistributionServiceError(error)) {\n return NextResponse.json(\n { error: error.code, message: error.message, details: error.details ?? null },\n { status: error.httpStatus },\n )\n }\n if (isSubmissionServiceError(error)) {\n return mapSubmissionError(error)\n }\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: 'INTERNAL_ERROR', message }, { status: 500 })\n}\n\n/**\n * Builds the authoritative submit metadata recorded on `form_submission`.\n *\n * Signing audit (CN-3): the IP, user-agent, and UTC submit timestamp are\n * derived SERVER-SIDE here \u2014 the client cannot forge them. Any client-supplied\n * `submit_metadata` (e.g. locale, timezone hints) is preserved but the\n * server-derived `ip` / `userAgent` / `serverSubmittedAt` always win.\n *\n * Together with the version-pinned (immutable) schema and the encrypted,\n * append-only revision answers (which carry the signature payload incl. the\n * consent clause SHA-256 and the signer's affirmation), this metadata forms\n * the tamper-evident signed record. Full PDF reproduction of that bundle is W3.\n */\nexport function buildSubmitMetadata(\n req: NextRequest,\n clientMetadata: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const forwardedFor = req.headers.get('x-forwarded-for')\n const ip = forwardedFor?.split(',')[0]?.trim() || req.headers.get('x-real-ip') || null\n const userAgent = req.headers.get('user-agent') || null\n return {\n ...(clientMetadata ?? {}),\n ip,\n userAgent,\n serverSubmittedAt: new Date().toISOString(),\n }\n}\n\n/**\n * Serializes the published form version a distribution serves into the same\n * shape the public renderer already understands (mirrors\n * `/api/forms/by-key/:key/active`), exposing the JSON schema, ui schema, and a\n * flattened field index. Anonymous participants are never sliced by caller\n * role here \u2014 the distribution carries the default actor role.\n */\n/**\n * Reads the optional `settings.completion.{title,message}` from a distribution\n * `settings` json bag, guarding for the untyped shape. Empty / whitespace-only\n * strings collapse to `null` so the renderer falls back to its default copy.\n */\nfunction readCompletionCopy(\n settings: Record<string, unknown> | null | undefined,\n): { title: string | null; message: string | null } {\n const completion =\n settings && typeof settings === 'object' && !Array.isArray(settings)\n ? (settings as Record<string, unknown>).completion\n : undefined\n if (!completion || typeof completion !== 'object' || Array.isArray(completion)) {\n return { title: null, message: null }\n }\n const record = completion as Record<string, unknown>\n const title = typeof record.title === 'string' && record.title.trim() ? record.title : null\n const message =\n typeof record.message === 'string' && record.message.trim() ? record.message : null\n return { title, message }\n}\n\n/**\n * Reads the PUBLIC-SAFE embed display hints from a distribution's `settings`\n * bag \u2014 `theme` and `autoResize` only, and only when embedding is enabled. The\n * framing allowlist (`allowedDomains`) is NEVER echoed to a public context\n * payload (R-RS-1); it is consumed exclusively server-side by the CSP builder.\n */\nfunction readEmbedContext(\n distribution: FormDistribution | undefined,\n): { theme: 'light' | 'dark' | 'auto' | null; autoResize: boolean } | null {\n const embed = readEmbedSettings(distribution?.settings)\n if (!embed || embed.enabled !== true) return null\n return {\n theme: embed.theme ?? null,\n autoResize: embed.autoResize !== false,\n }\n}\n\nexport function serializeFormContext(args: {\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n distribution?: FormDistribution\n}) {\n const { form, formVersion, compiled, distribution } = args\n const fieldIndex: Record<string, unknown> = {}\n for (const [fieldKey, descriptor] of Object.entries(compiled.fieldIndex)) {\n fieldIndex[fieldKey] = {\n key: descriptor.key,\n type: descriptor.type,\n sectionKey: descriptor.sectionKey,\n sensitive: descriptor.sensitive,\n editableBy: descriptor.editableBy,\n visibleTo: descriptor.visibleTo,\n required: descriptor.required,\n }\n }\n return {\n form: {\n key: form.key,\n name: form.name,\n defaultLocale: form.defaultLocale,\n supportedLocales: form.supportedLocales,\n },\n schema: formVersion.schema,\n ui_schema: formVersion.uiSchema,\n fieldIndex,\n completion: readCompletionCopy(distribution?.settings),\n redirect_url: distribution?.redirectUrl ?? null,\n embed: readEmbedContext(distribution),\n }\n}\n\nexport async function readJsonBody<T = unknown>(req: Request): Promise<T> {\n try {\n return (await req.json()) as T\n } catch {\n throw new SubmissionServiceError('VALIDATION_FAILED', 'Invalid JSON body.', 400)\n }\n}\n\nexport function serializeSubmission(submission: FormSubmission) {\n return {\n id: submission.id,\n organizationId: submission.organizationId,\n tenantId: submission.tenantId,\n formVersionId: submission.formVersionId,\n subjectType: submission.subjectType,\n subjectId: submission.subjectId,\n status: submission.status,\n currentRevisionId: submission.currentRevisionId ?? null,\n startedBy: submission.startedBy,\n submittedBy: submission.submittedBy ?? null,\n firstSavedAt: submission.firstSavedAt?.toISOString() ?? null,\n submittedAt: submission.submittedAt?.toISOString() ?? null,\n submitMetadata: submission.submitMetadata ?? null,\n pdfSnapshotAttachmentId: submission.pdfSnapshotAttachmentId ?? null,\n anonymizedAt: submission.anonymizedAt?.toISOString() ?? null,\n createdAt: submission.createdAt?.toISOString() ?? null,\n updatedAt: submission.updatedAt?.toISOString() ?? null,\n }\n}\n\nexport function serializeRevision(revision: FormSubmissionRevision) {\n return {\n id: revision.id,\n submissionId: revision.submissionId,\n revisionNumber: revision.revisionNumber,\n encryptionKeyVersion: revision.encryptionKeyVersion,\n savedAt: revision.savedAt?.toISOString() ?? null,\n savedBy: revision.savedBy,\n savedByRole: revision.savedByRole,\n changeSource: revision.changeSource,\n changedFieldKeys: revision.changedFieldKeys ?? [],\n changeSummary: revision.changeSummary ?? null,\n anonymizedAt: revision.anonymizedAt?.toISOString() ?? null,\n }\n}\n\nexport function serializeActor(actor: FormSubmissionActor) {\n return {\n id: actor.id,\n submissionId: actor.submissionId,\n userId: actor.userId,\n role: actor.role,\n assignedAt: actor.assignedAt?.toISOString() ?? null,\n revokedAt: actor.revokedAt?.toISOString() ?? null,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Shared helpers for forms runtime API routes.\n */\n\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { SubmissionServiceError, isSubmissionServiceError } from '../services/submission-service'\nimport { DistributionServiceError, isDistributionServiceError } from '../services/distribution-service'\nimport { readEmbedSettings } from '../lib/embed-frame-policy'\nimport type { CompiledFormVersion } from '../services/form-version-compiler'\nimport type {\n Form,\n FormDistribution,\n FormSubmission,\n FormSubmissionActor,\n FormSubmissionRevision,\n FormVersion,\n} from '../data/entities'\n\nexport function mapSubmissionError(error: unknown): NextResponse {\n if (isSubmissionServiceError(error)) {\n return NextResponse.json(\n { error: error.code, message: error.message, details: error.details ?? null },\n { status: error.httpStatus },\n )\n }\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: 'INTERNAL_ERROR', message }, { status: 500 })\n}\n\nexport function isPortalSubjectForbiddenError(error: unknown): boolean {\n return isSubmissionServiceError(error) && error.code === 'SUBJECT_FORBIDDEN'\n}\n\nexport async function portalSubjectForbiddenResponse(): Promise<NextResponse> {\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n {\n error: 'SUBJECT_FORBIDDEN',\n message: translate('forms.errors.subject_forbidden', 'You can only start or submit this form for yourself.'),\n },\n { status: 403 },\n )\n}\n\nexport function mapDistributionError(error: unknown): NextResponse {\n if (isDistributionServiceError(error)) {\n return NextResponse.json(\n { error: error.code, message: error.message, details: error.details ?? null },\n { status: error.httpStatus },\n )\n }\n if (isSubmissionServiceError(error)) {\n return mapSubmissionError(error)\n }\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: 'INTERNAL_ERROR', message }, { status: 500 })\n}\n\n/**\n * Builds the authoritative submit metadata recorded on `form_submission`.\n *\n * Signing audit (CN-3): the IP, user-agent, and UTC submit timestamp are\n * derived SERVER-SIDE here \u2014 the client cannot forge them. Any client-supplied\n * `submit_metadata` (e.g. locale, timezone hints) is preserved but the\n * server-derived `ip` / `userAgent` / `serverSubmittedAt` always win.\n *\n * Together with the version-pinned (immutable) schema and the encrypted,\n * append-only revision answers (which carry the signature payload incl. the\n * consent clause SHA-256 and the signer's affirmation), this metadata forms\n * the tamper-evident signed record. Full PDF reproduction of that bundle is W3.\n */\nexport function buildSubmitMetadata(\n req: NextRequest,\n clientMetadata: Record<string, unknown> | null | undefined,\n): Record<string, unknown> {\n const forwardedFor = req.headers.get('x-forwarded-for')\n const ip = forwardedFor?.split(',')[0]?.trim() || req.headers.get('x-real-ip') || null\n const userAgent = req.headers.get('user-agent') || null\n return {\n ...(clientMetadata ?? {}),\n ip,\n userAgent,\n serverSubmittedAt: new Date().toISOString(),\n }\n}\n\n/**\n * Serializes the published form version a distribution serves into the same\n * shape the public renderer already understands (mirrors\n * `/api/forms/by-key/:key/active`), exposing the JSON schema, ui schema, and a\n * flattened field index. Anonymous participants are never sliced by caller\n * role here \u2014 the distribution carries the default actor role.\n */\n/**\n * Reads the optional `settings.completion.{title,message}` from a distribution\n * `settings` json bag, guarding for the untyped shape. Empty / whitespace-only\n * strings collapse to `null` so the renderer falls back to its default copy.\n */\nfunction readCompletionCopy(\n settings: Record<string, unknown> | null | undefined,\n): { title: string | null; message: string | null } {\n const completion =\n settings && typeof settings === 'object' && !Array.isArray(settings)\n ? (settings as Record<string, unknown>).completion\n : undefined\n if (!completion || typeof completion !== 'object' || Array.isArray(completion)) {\n return { title: null, message: null }\n }\n const record = completion as Record<string, unknown>\n const title = typeof record.title === 'string' && record.title.trim() ? record.title : null\n const message =\n typeof record.message === 'string' && record.message.trim() ? record.message : null\n return { title, message }\n}\n\n/**\n * Reads the PUBLIC-SAFE embed display hints from a distribution's `settings`\n * bag \u2014 `theme` and `autoResize` only, and only when embedding is enabled. The\n * framing allowlist (`allowedDomains`) is NEVER echoed to a public context\n * payload (R-RS-1); it is consumed exclusively server-side by the CSP builder.\n */\nfunction readEmbedContext(\n distribution: FormDistribution | undefined,\n): { theme: 'light' | 'dark' | 'auto' | null; autoResize: boolean } | null {\n const embed = readEmbedSettings(distribution?.settings)\n if (!embed || embed.enabled !== true) return null\n return {\n theme: embed.theme ?? null,\n autoResize: embed.autoResize !== false,\n }\n}\n\nexport function serializeFormContext(args: {\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n distribution?: FormDistribution\n}) {\n const { form, formVersion, compiled, distribution } = args\n const fieldIndex: Record<string, unknown> = {}\n for (const [fieldKey, descriptor] of Object.entries(compiled.fieldIndex)) {\n fieldIndex[fieldKey] = {\n key: descriptor.key,\n type: descriptor.type,\n sectionKey: descriptor.sectionKey,\n sensitive: descriptor.sensitive,\n editableBy: descriptor.editableBy,\n visibleTo: descriptor.visibleTo,\n required: descriptor.required,\n }\n }\n return {\n form: {\n key: form.key,\n name: form.name,\n defaultLocale: form.defaultLocale,\n supportedLocales: form.supportedLocales,\n },\n schema: formVersion.schema,\n ui_schema: formVersion.uiSchema,\n fieldIndex,\n completion: readCompletionCopy(distribution?.settings),\n redirect_url: distribution?.redirectUrl ?? null,\n embed: readEmbedContext(distribution),\n }\n}\n\nexport async function readJsonBody<T = unknown>(req: Request): Promise<T> {\n try {\n return (await req.json()) as T\n } catch {\n throw new SubmissionServiceError('VALIDATION_FAILED', 'Invalid JSON body.', 400)\n }\n}\n\nexport function serializeSubmission(submission: FormSubmission) {\n return {\n id: submission.id,\n organizationId: submission.organizationId,\n tenantId: submission.tenantId,\n formVersionId: submission.formVersionId,\n subjectType: submission.subjectType,\n subjectId: submission.subjectId,\n status: submission.status,\n currentRevisionId: submission.currentRevisionId ?? null,\n startedBy: submission.startedBy,\n submittedBy: submission.submittedBy ?? null,\n firstSavedAt: submission.firstSavedAt?.toISOString() ?? null,\n submittedAt: submission.submittedAt?.toISOString() ?? null,\n submitMetadata: submission.submitMetadata ?? null,\n pdfSnapshotAttachmentId: submission.pdfSnapshotAttachmentId ?? null,\n anonymizedAt: submission.anonymizedAt?.toISOString() ?? null,\n createdAt: submission.createdAt?.toISOString() ?? null,\n updatedAt: submission.updatedAt?.toISOString() ?? null,\n }\n}\n\nexport function serializeRevision(revision: FormSubmissionRevision) {\n return {\n id: revision.id,\n submissionId: revision.submissionId,\n revisionNumber: revision.revisionNumber,\n encryptionKeyVersion: revision.encryptionKeyVersion,\n savedAt: revision.savedAt?.toISOString() ?? null,\n savedBy: revision.savedBy,\n savedByRole: revision.savedByRole,\n changeSource: revision.changeSource,\n changedFieldKeys: revision.changedFieldKeys ?? [],\n changeSummary: revision.changeSummary ?? null,\n anonymizedAt: revision.anonymizedAt?.toISOString() ?? null,\n }\n}\n\nexport function serializeActor(actor: FormSubmissionActor) {\n return {\n id: actor.id,\n submissionId: actor.submissionId,\n userId: actor.userId,\n role: actor.role,\n assignedAt: actor.assignedAt?.toISOString() ?? null,\n revokedAt: actor.revokedAt?.toISOString() ?? null,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }