@open-mercato/core 0.9.1-develop.7519.1.38f51f8dbb → 0.9.1-develop.7536.1.f9a5776a07

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 (105) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/logout.js +14 -6
  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/currencies/backend/currencies/[id]/page.js +5 -2
  10. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  11. package/dist/modules/currencies/commands/currencies.js +399 -137
  12. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  13. package/dist/modules/currencies/lib/seeds.js +2 -0
  14. package/dist/modules/currencies/lib/seeds.js.map +2 -2
  15. package/dist/modules/customer_accounts/api/login.js +14 -7
  16. package/dist/modules/customer_accounts/api/login.js.map +2 -2
  17. package/dist/modules/customer_accounts/services/customerUserService.js +10 -10
  18. package/dist/modules/customer_accounts/services/customerUserService.js.map +2 -2
  19. package/dist/modules/customers/commands/companies.js +10 -3
  20. package/dist/modules/customers/commands/companies.js.map +2 -2
  21. package/dist/modules/customers/commands/interactionRestoreDetails.js +75 -0
  22. package/dist/modules/customers/commands/interactionRestoreDetails.js.map +7 -0
  23. package/dist/modules/customers/commands/people.js +10 -3
  24. package/dist/modules/customers/commands/people.js.map +2 -2
  25. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js +1 -1
  26. package/dist/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.js.map +1 -1
  27. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js +12 -2
  28. package/dist/modules/forms/api/form-submissions/[id]/submit/route.js.map +2 -2
  29. package/dist/modules/forms/api/form-submissions/route.js +16 -1
  30. package/dist/modules/forms/api/form-submissions/route.js.map +2 -2
  31. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js +6 -1
  32. package/dist/modules/forms/api/public/submissions/[id]/submit/route.js.map +2 -2
  33. package/dist/modules/forms/api/runtime-helpers.js +16 -0
  34. package/dist/modules/forms/api/runtime-helpers.js.map +2 -2
  35. package/dist/modules/forms/data/validators.js.map +2 -2
  36. package/dist/modules/forms/lib/portal-subject-policy.js +17 -0
  37. package/dist/modules/forms/lib/portal-subject-policy.js.map +7 -0
  38. package/dist/modules/forms/lib/runtime-principal.js +27 -10
  39. package/dist/modules/forms/lib/runtime-principal.js.map +2 -2
  40. package/dist/modules/forms/services/distribution-service.js +35 -13
  41. package/dist/modules/forms/services/distribution-service.js.map +2 -2
  42. package/dist/modules/forms/services/submission-service.js +4 -0
  43. package/dist/modules/forms/services/submission-service.js.map +2 -2
  44. package/dist/modules/staff/commands/leave-requests.js +54 -24
  45. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  46. package/dist/modules/warranty_claims/api/portal/attachments/route.js +19 -7
  47. package/dist/modules/warranty_claims/api/portal/attachments/route.js.map +2 -2
  48. package/dist/modules/warranty_claims/commands/claims.js +1 -1
  49. package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
  50. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js +6 -6
  51. package/dist/modules/warranty_claims/subscribers/return-shipment-tracking.js.map +2 -2
  52. package/dist/modules/wms/commands/inventory-actions.js +19 -3
  53. package/dist/modules/wms/commands/inventory-actions.js.map +2 -2
  54. package/dist/modules/workflows/lib/async-activity-wait.js +43 -0
  55. package/dist/modules/workflows/lib/async-activity-wait.js.map +7 -0
  56. package/dist/modules/workflows/lib/parallel-handler.js +20 -14
  57. package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
  58. package/dist/modules/workflows/lib/workflow-executor.js +21 -23
  59. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  60. package/package.json +7 -7
  61. package/src/modules/auth/api/logout.ts +15 -6
  62. package/src/modules/auth/api/session/refresh.ts +6 -1
  63. package/src/modules/catalog/ai-tools/mutation-pack.ts +14 -5
  64. package/src/modules/catalog/ai-tools/types.ts +1 -0
  65. package/src/modules/currencies/backend/currencies/[id]/page.tsx +5 -1
  66. package/src/modules/currencies/commands/currencies.ts +475 -168
  67. package/src/modules/currencies/i18n/de.json +4 -0
  68. package/src/modules/currencies/i18n/en.json +4 -0
  69. package/src/modules/currencies/i18n/es.json +4 -0
  70. package/src/modules/currencies/i18n/ko.json +4 -0
  71. package/src/modules/currencies/i18n/pl.json +4 -0
  72. package/src/modules/currencies/lib/seeds.ts +2 -0
  73. package/src/modules/customer_accounts/api/login.ts +15 -7
  74. package/src/modules/customer_accounts/services/customerUserService.ts +34 -10
  75. package/src/modules/customers/commands/companies.ts +16 -4
  76. package/src/modules/customers/commands/interactionRestoreDetails.ts +110 -0
  77. package/src/modules/customers/commands/people.ts +16 -4
  78. package/src/modules/forms/api/distributions/[distributionId]/invitations/[invitationId]/route.ts +1 -1
  79. package/src/modules/forms/api/form-submissions/[id]/submit/route.ts +16 -3
  80. package/src/modules/forms/api/form-submissions/route.ts +21 -2
  81. package/src/modules/forms/api/public/submissions/[id]/submit/route.ts +7 -0
  82. package/src/modules/forms/api/runtime-helpers.ts +16 -0
  83. package/src/modules/forms/data/validators.ts +2 -1
  84. package/src/modules/forms/i18n/de.json +1 -0
  85. package/src/modules/forms/i18n/en.json +1 -0
  86. package/src/modules/forms/i18n/es.json +1 -0
  87. package/src/modules/forms/i18n/ko.json +1 -0
  88. package/src/modules/forms/i18n/pl.json +1 -0
  89. package/src/modules/forms/lib/portal-subject-policy.ts +42 -0
  90. package/src/modules/forms/lib/runtime-principal.ts +35 -11
  91. package/src/modules/forms/services/distribution-service.ts +35 -13
  92. package/src/modules/forms/services/submission-service.ts +12 -0
  93. package/src/modules/staff/commands/leave-requests.ts +54 -23
  94. package/src/modules/staff/i18n/de.json +2 -0
  95. package/src/modules/staff/i18n/en.json +2 -0
  96. package/src/modules/staff/i18n/es.json +2 -0
  97. package/src/modules/staff/i18n/ko.json +2 -0
  98. package/src/modules/staff/i18n/pl.json +2 -0
  99. package/src/modules/warranty_claims/api/portal/attachments/route.ts +20 -8
  100. package/src/modules/warranty_claims/commands/claims.ts +1 -1
  101. package/src/modules/warranty_claims/subscribers/return-shipment-tracking.ts +5 -5
  102. package/src/modules/wms/commands/inventory-actions.ts +30 -10
  103. package/src/modules/workflows/lib/async-activity-wait.ts +71 -0
  104. package/src/modules/workflows/lib/parallel-handler.ts +24 -15
  105. package/src/modules/workflows/lib/workflow-executor.ts +24 -27
@@ -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": "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",
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": "AAIA,SAAS,oBAAsC;AAC/C,SAAS,wBAAwB,gCAAgC;AACjE,SAAmC,kCAAkC;AACrE,SAAS,yBAAyB;AAW3B,SAAS,mBAAmB,OAA8B;AAC/D,MAAI,yBAAyB,KAAK,GAAG;AACnC,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,MAAM,MAAM,SAAS,MAAM,SAAS,SAAS,MAAM,WAAW,KAAK;AAAA,MAC5E,EAAE,QAAQ,MAAM,WAAW;AAAA,IAC7B;AAAA,EACF;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,SAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAChF;AAEO,SAAS,qBAAqB,OAA8B;AACjE,MAAI,2BAA2B,KAAK,GAAG;AACrC,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,MAAM,MAAM,SAAS,MAAM,SAAS,SAAS,MAAM,WAAW,KAAK;AAAA,MAC5E,EAAE,QAAQ,MAAM,WAAW;AAAA,IAC7B;AAAA,EACF;AACA,MAAI,yBAAyB,KAAK,GAAG;AACnC,WAAO,mBAAmB,KAAK;AAAA,EACjC;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,SAAO,aAAa,KAAK,EAAE,OAAO,kBAAkB,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAChF;AAeO,SAAS,oBACd,KACA,gBACyB;AACzB,QAAM,eAAe,IAAI,QAAQ,IAAI,iBAAiB;AACtD,QAAM,KAAK,cAAc,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAAK,IAAI,QAAQ,IAAI,WAAW,KAAK;AAClF,QAAM,YAAY,IAAI,QAAQ,IAAI,YAAY,KAAK;AACnD,SAAO;AAAA,IACL,GAAI,kBAAkB,CAAC;AAAA,IACvB;AAAA,IACA;AAAA,IACA,oBAAmB,oBAAI,KAAK,GAAE,YAAY;AAAA,EAC5C;AACF;AAcA,SAAS,mBACP,UACkD;AAClD,QAAM,aACJ,YAAY,OAAO,aAAa,YAAY,CAAC,MAAM,QAAQ,QAAQ,IAC9D,SAAqC,aACtC;AACN,MAAI,CAAC,cAAc,OAAO,eAAe,YAAY,MAAM,QAAQ,UAAU,GAAG;AAC9E,WAAO,EAAE,OAAO,MAAM,SAAS,KAAK;AAAA,EACtC;AACA,QAAM,SAAS;AACf,QAAM,QAAQ,OAAO,OAAO,UAAU,YAAY,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ;AACvF,QAAM,UACJ,OAAO,OAAO,YAAY,YAAY,OAAO,QAAQ,KAAK,IAAI,OAAO,UAAU;AACjF,SAAO,EAAE,OAAO,QAAQ;AAC1B;AAQA,SAAS,iBACP,cACyE;AACzE,QAAM,QAAQ,kBAAkB,cAAc,QAAQ;AACtD,MAAI,CAAC,SAAS,MAAM,YAAY,KAAM,QAAO;AAC7C,SAAO;AAAA,IACL,OAAO,MAAM,SAAS;AAAA,IACtB,YAAY,MAAM,eAAe;AAAA,EACnC;AACF;AAEO,SAAS,qBAAqB,MAKlC;AACD,QAAM,EAAE,MAAM,aAAa,UAAU,aAAa,IAAI;AACtD,QAAM,aAAsC,CAAC;AAC7C,aAAW,CAAC,UAAU,UAAU,KAAK,OAAO,QAAQ,SAAS,UAAU,GAAG;AACxE,eAAW,QAAQ,IAAI;AAAA,MACrB,KAAK,WAAW;AAAA,MAChB,MAAM,WAAW;AAAA,MACjB,YAAY,WAAW;AAAA,MACvB,WAAW,WAAW;AAAA,MACtB,YAAY,WAAW;AAAA,MACvB,WAAW,WAAW;AAAA,MACtB,UAAU,WAAW;AAAA,IACvB;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,KAAK,KAAK;AAAA,MACV,MAAM,KAAK;AAAA,MACX,eAAe,KAAK;AAAA,MACpB,kBAAkB,KAAK;AAAA,IACzB;AAAA,IACA,QAAQ,YAAY;AAAA,IACpB,WAAW,YAAY;AAAA,IACvB;AAAA,IACA,YAAY,mBAAmB,cAAc,QAAQ;AAAA,IACrD,cAAc,cAAc,eAAe;AAAA,IAC3C,OAAO,iBAAiB,YAAY;AAAA,EACtC;AACF;AAEA,eAAsB,aAA0B,KAA0B;AACxE,MAAI;AACF,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB,QAAQ;AACN,UAAM,IAAI,uBAAuB,qBAAqB,sBAAsB,GAAG;AAAA,EACjF;AACF;AAEO,SAAS,oBAAoB,YAA4B;AAC9D,SAAO;AAAA,IACL,IAAI,WAAW;AAAA,IACf,gBAAgB,WAAW;AAAA,IAC3B,UAAU,WAAW;AAAA,IACrB,eAAe,WAAW;AAAA,IAC1B,aAAa,WAAW;AAAA,IACxB,WAAW,WAAW;AAAA,IACtB,QAAQ,WAAW;AAAA,IACnB,mBAAmB,WAAW,qBAAqB;AAAA,IACnD,WAAW,WAAW;AAAA,IACtB,aAAa,WAAW,eAAe;AAAA,IACvC,cAAc,WAAW,cAAc,YAAY,KAAK;AAAA,IACxD,aAAa,WAAW,aAAa,YAAY,KAAK;AAAA,IACtD,gBAAgB,WAAW,kBAAkB;AAAA,IAC7C,yBAAyB,WAAW,2BAA2B;AAAA,IAC/D,cAAc,WAAW,cAAc,YAAY,KAAK;AAAA,IACxD,WAAW,WAAW,WAAW,YAAY,KAAK;AAAA,IAClD,WAAW,WAAW,WAAW,YAAY,KAAK;AAAA,EACpD;AACF;AAEO,SAAS,kBAAkB,UAAkC;AAClE,SAAO;AAAA,IACL,IAAI,SAAS;AAAA,IACb,cAAc,SAAS;AAAA,IACvB,gBAAgB,SAAS;AAAA,IACzB,sBAAsB,SAAS;AAAA,IAC/B,SAAS,SAAS,SAAS,YAAY,KAAK;AAAA,IAC5C,SAAS,SAAS;AAAA,IAClB,aAAa,SAAS;AAAA,IACtB,cAAc,SAAS;AAAA,IACvB,kBAAkB,SAAS,oBAAoB,CAAC;AAAA,IAChD,eAAe,SAAS,iBAAiB;AAAA,IACzC,cAAc,SAAS,cAAc,YAAY,KAAK;AAAA,EACxD;AACF;AAEO,SAAS,eAAe,OAA4B;AACzD,SAAO;AAAA,IACL,IAAI,MAAM;AAAA,IACV,cAAc,MAAM;AAAA,IACpB,QAAQ,MAAM;AAAA,IACd,MAAM,MAAM;AAAA,IACZ,YAAY,MAAM,YAAY,YAAY,KAAK;AAAA,IAC/C,WAAW,MAAM,WAAW,YAAY,KAAK;AAAA,EAC/C;AACF;",
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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/forms/data/validators.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { normalizeEmbedOrigin } from '../lib/embed-frame-policy'\nimport { isNavigableUrl } from '../lib/navigable-url'\n\n/**\n * Forms module shared Zod primitives + Phase 1b/1c command/API schemas.\n *\n * Phase 1a shipped only the building-block primitives. Phase 1b extends with\n * the form definition + version lifecycle command/API schemas. Phase 1c adds\n * the runtime submission schemas.\n */\n\n/**\n * Form key \u2014 stable per-organization slug. 3-64 chars; starts with a lower\n * case letter; contains only `[a-z0-9_-]`. Treated as a contract surface for\n * external integrations; uniqueness is enforced at the DB level by\n * UNIQUE (organization_id, key).\n */\nexport const formKeySchema = z\n .string()\n .min(3)\n .max(64)\n .regex(/^[a-z][a-z0-9_-]*$/, {\n message: 'Form key must start with a lowercase letter and only contain lowercase letters, digits, hyphens, and underscores.',\n })\n\n/**\n * Locale tag \u2014 minimal BCP-47 shape. `en`, `en-US`, `pl`, `pl-PL`, etc.\n * Studio (phase 1b) may expand this once the locale picker ships.\n */\nexport const localeSchema = z\n .string()\n .min(2)\n .max(10)\n .regex(/^[a-z]{2,3}(?:-[A-Z]{2})?$/, {\n message: 'Locale must be a BCP-47 tag like \"en\" or \"en-US\".',\n })\n\n/**\n * Role identifier \u2014 used in `x-om-roles`, `x-om-editable-by`, `x-om-visible-to`,\n * and the form-version `roles` column. Lower-case, slug-shaped, snake-kebab\n * mix tolerated. Common values: `patient`, `clinician`, `admin`, `guardian`.\n */\nexport const roleIdentifierSchema = z\n .string()\n .min(2)\n .max(64)\n .regex(/^[a-z][a-z0-9_-]*$/, {\n message: 'Role identifier must start with a lowercase letter and only contain lowercase letters, digits, hyphens, and underscores.',\n })\n\nexport type FormKey = z.infer<typeof formKeySchema>\nexport type Locale = z.infer<typeof localeSchema>\nexport type RoleIdentifier = z.infer<typeof roleIdentifierSchema>\n\n/**\n * Submission subject identifier. Polymorphic: subject_type is a free-form\n * string tag (`patient`, `customer`, `case`, etc.); subject_id MUST be a\n * UUID. Validation of the (subject_type, subject_id) pair against actual\n * domain entities is the responsibility of the calling module.\n */\nexport const submissionSubjectSchema = z.object({\n subject_type: z.string().min(1).max(64),\n subject_id: z.string().uuid(),\n})\n\n/**\n * Body for `POST /api/form-submissions` \u2014 runtime start.\n */\nexport const submissionStartInputSchema = z.object({\n form_key: formKeySchema,\n subject_type: z.string().min(1).max(64),\n subject_id: z.string().uuid(),\n locale: localeSchema.optional(),\n})\n\nexport type SubmissionStartInput = z.infer<typeof submissionStartInputSchema>\n\n/**\n * Body for `PATCH /api/form-submissions/:id` \u2014 autosave.\n *\n * `patch` is left as `passthrough` because the field-set is dynamic per form\n * version; the SubmissionService enforces `additionalProperties: false`\n * via the AJV validator from the compiler before persisting.\n */\nexport const submissionSaveInputSchema = z.object({\n base_revision_id: z.string().uuid(),\n patch: z.record(z.string(), z.unknown()),\n change_summary: z.string().max(500).optional(),\n})\n\nexport type SubmissionSaveInput = z.infer<typeof submissionSaveInputSchema>\n\n/**\n * Body for `POST /api/form-submissions/:id/submit` \u2014 final submit.\n */\nexport const submissionSubmitInputSchema = z.object({\n base_revision_id: z.string().uuid(),\n submit_metadata: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport type SubmissionSubmitInput = z.infer<typeof submissionSubmitInputSchema>\n\n/**\n * Body for `POST /api/forms/submissions/:submissionId/actors` \u2014 admin assigns\n * a (user, role) pair to a submission. The role MUST be one of the form\n * version's declared roles; the SubmissionService enforces this.\n */\nexport const assignActorInputSchema = z.object({\n user_id: z.string().uuid(),\n role: roleIdentifierSchema,\n})\n\nexport type AssignActorInput = z.infer<typeof assignActorInputSchema>\n\nexport const revokeActorInputSchema = z.object({\n actor_id: z.string().uuid(),\n})\n\nexport type RevokeActorInput = z.infer<typeof revokeActorInputSchema>\n\n// ============================================================================\n// Phase 1b \u2014 Definition Authoring (CRUD + Studio) command + API schemas\n// ============================================================================\n\nconst scopedFields = {\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid(),\n} as const\n\nconst optionalDescription = z\n .string()\n .trim()\n .max(2000)\n .optional()\n .nullable()\n\nconst optionalChangelog = z\n .string()\n .trim()\n .max(4000)\n .optional()\n .nullable()\n\nexport const formStatusSchema = z.enum(['draft', 'active', 'archived'])\nexport const formVersionStatusSchema = z.enum(['draft', 'published', 'archived'])\n\nconst jsonObjectSchema = z.record(z.string(), z.unknown())\n\nconst supportedLocalesSchema = z.array(localeSchema).min(1).max(20)\n\n/**\n * Per-form retention window in days (W5 / DP-6). `null` keeps submissions\n * forever; a positive integer drives the retention-purge worker. Capped at\n * ~100 years so a typo cannot disable purge by accident while still allowing\n * decade-long legal-retention windows.\n */\nconst retentionDaysSchema = z.number().int().min(1).max(36500).optional().nullable()\n\n/** Query schema for `GET /api/forms`. */\nexport const formListQuerySchema = z.object({\n status: z\n .union([formStatusSchema, z.array(formStatusSchema).max(3)])\n .optional(),\n q: z.string().trim().max(200).optional(),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n})\n\nexport type FormListQueryInput = z.infer<typeof formListQuerySchema>\n\n/** Command input for `forms.form.create`. */\nexport const formCreateCommandSchema = z.object({\n ...scopedFields,\n key: formKeySchema,\n name: z.string().trim().min(1).max(200),\n description: optionalDescription,\n defaultLocale: localeSchema,\n supportedLocales: supportedLocalesSchema,\n retentionDays: retentionDaysSchema,\n})\n\nexport type FormCreateCommandInput = z.infer<typeof formCreateCommandSchema>\n\n/**\n * Command input for `forms.form.rename`. Despite the name, this command is the\n * single form-metadata update path: name, description, and (W7) the locale set.\n * `defaultLocale` must be one of `supportedLocales` \u2014 enforced at execute time.\n */\nexport const formRenameCommandSchema = z.object({\n ...scopedFields,\n id: z.string().uuid(),\n name: z.string().trim().min(1).max(200).optional(),\n description: optionalDescription,\n defaultLocale: localeSchema.optional(),\n supportedLocales: supportedLocalesSchema.optional(),\n retentionDays: retentionDaysSchema,\n})\n\nexport type FormRenameCommandInput = z.infer<typeof formRenameCommandSchema>\n\n/** Command input for `forms.form.archive`. */\nexport const formArchiveCommandSchema = z.object({\n ...scopedFields,\n id: z.string().uuid(),\n})\n\nexport type FormArchiveCommandInput = z.infer<typeof formArchiveCommandSchema>\n\n/** Command input for `forms.form.restore`. */\nexport const formRestoreCommandSchema = z.object({\n ...scopedFields,\n id: z.string().uuid(),\n})\n\nexport type FormRestoreCommandInput = z.infer<typeof formRestoreCommandSchema>\n\n/** Command input for `forms.form_version.fork_draft`. */\nexport const formVersionForkDraftCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n fromVersionId: z.string().uuid().optional().nullable(),\n})\n\nexport type FormVersionForkDraftCommandInput = z.infer<typeof formVersionForkDraftCommandSchema>\n\n/** Command input for `forms.form_version.update_draft`. */\nexport const formVersionUpdateDraftCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n versionId: z.string().uuid(),\n schema: jsonObjectSchema.optional(),\n uiSchema: jsonObjectSchema.optional(),\n roles: z.array(roleIdentifierSchema).max(50).optional(),\n changelog: optionalChangelog,\n})\n\nexport type FormVersionUpdateDraftCommandInput = z.infer<typeof formVersionUpdateDraftCommandSchema>\n\n/** Command input for `forms.form_version.publish`. */\nexport const formVersionPublishCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n versionId: z.string().uuid(),\n changelog: z.string().trim().max(4000).optional().nullable(),\n})\n\nexport type FormVersionPublishCommandInput = z.infer<typeof formVersionPublishCommandSchema>\n\n/** Command input for `forms.form_version.archive`. */\nexport const formVersionArchiveCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n versionId: z.string().uuid(),\n})\n\nexport type FormVersionArchiveCommandInput = z.infer<typeof formVersionArchiveCommandSchema>\n\n/** POST /api/forms request body (route layer fills tenant/organization scope). */\nexport const formCreateRequestSchema = formCreateCommandSchema.omit({\n tenantId: true,\n organizationId: true,\n})\n\n/** PATCH /api/forms/:id request body. */\nexport const formPatchRequestSchema = z.object({\n name: z.string().trim().min(1).max(200).optional(),\n description: optionalDescription,\n defaultLocale: localeSchema.optional(),\n supportedLocales: supportedLocalesSchema.optional(),\n retentionDays: retentionDaysSchema,\n})\n\n/** POST /api/forms/:id/versions/fork request body. */\nexport const formVersionForkRequestSchema = z.object({\n fromVersionId: z.string().uuid().optional().nullable(),\n})\n\n/** PATCH /api/forms/:id/versions/:versionId request body. */\nexport const formVersionPatchRequestSchema = z.object({\n schema: jsonObjectSchema.optional(),\n uiSchema: jsonObjectSchema.optional(),\n roles: z.array(roleIdentifierSchema).max(50).optional(),\n changelog: optionalChangelog,\n})\n\n/** POST /api/forms/:id/versions/:versionId/publish request body. */\nexport const formVersionPublishRequestSchema = z.object({\n changelog: z.string().trim().max(4000).optional().nullable(),\n})\n\n/** GET /api/forms/:id/versions/:versionId/diff query. */\nexport const formVersionDiffQuerySchema = z.object({\n against: z.string().uuid(),\n})\n\nexport type FormCreateRequestInput = z.infer<typeof formCreateRequestSchema>\nexport type FormPatchRequestInput = z.infer<typeof formPatchRequestSchema>\nexport type FormVersionForkRequestInput = z.infer<typeof formVersionForkRequestSchema>\nexport type FormVersionPatchRequestInput = z.infer<typeof formVersionPatchRequestSchema>\nexport type FormVersionPublishRequestInput = z.infer<typeof formVersionPublishRequestSchema>\nexport type FormVersionDiffQueryInput = z.infer<typeof formVersionDiffQuerySchema>\n\nexport type FormStatus = z.infer<typeof formStatusSchema>\nexport type FormVersionStatus = z.infer<typeof formVersionStatusSchema>\n\n// ============================================================================\n// Phase 2d \u2014 Distribution & Anonymous Submission command/API schemas\n// ============================================================================\n\nexport const distributionModeSchema = z.enum(['open', 'personal'])\nexport const distributionStatusSchema = z.enum(['active', 'paused', 'closed'])\n\nexport type FormDistributionMode = z.infer<typeof distributionModeSchema>\nexport type FormDistributionStatus = z.infer<typeof distributionStatusSchema>\n\nconst isoDateTimeSchema = z.string().datetime({ offset: true })\n\n/**\n * Post-submission redirect target. `z.string().url()` alone is not enough: it\n * only asks whether `new URL(...)` parses, so `javascript:`, `data:` and\n * `vbscript:` all pass it \u2014 and this value is assigned to `window.location` by\n * the anonymous public runner, in the app's own origin, under a CSP that allows\n * `'unsafe-inline'`. The scheme check is what makes it safe.\n */\nconst optionalRedirectUrl = z\n .string()\n .url()\n .max(2000)\n .refine((value) => isNavigableUrl(value), {\n message: 'forms.errors.redirect_url_scheme',\n })\n .optional()\n .nullable()\n\n/**\n * A single embed allowlist origin. Validated to the strict origin shape\n * (scheme + host + optional port, https-only except localhost) via the shared\n * `normalizeEmbedOrigin` policy helper so the Zod layer and the runtime CSP\n * builder agree byte-for-byte (R-RS-1).\n */\nconst embedAllowedDomainSchema = z\n .string()\n .trim()\n .min(1)\n .max(253)\n .refine((value) => normalizeEmbedOrigin(value) !== null, {\n message:\n 'Allowed domain must be an https origin like \"https://www.acme.com\" (http is permitted only for localhost). No path, query, or fragment.',\n })\n\n/**\n * `settings.embed` \u2014 opt-in external-website framing config (spec D6). Rides on\n * the existing `forms_distribution.settings` JSON column; no migration. Enabling\n * embedding REQUIRES a non-empty allowlist \u2014 an enabled bag with zero allowed\n * domains is rejected at save time (R-RS-1). `allowedDomains` is NEVER echoed to\n * any public context payload.\n */\nexport const embedSettingsSchema = z\n .object({\n enabled: z.boolean().default(false),\n allowedDomains: z.array(embedAllowedDomainSchema).max(50).default([]),\n theme: z.enum(['light', 'dark', 'auto']).optional(),\n autoResize: z.boolean().default(true),\n })\n .strict()\n .refine((value) => !value.enabled || value.allowedDomains.length > 0, {\n message: 'Embedding cannot be enabled without at least one allowed domain.',\n path: ['allowedDomains'],\n })\n\nexport type EmbedSettingsInput = z.infer<typeof embedSettingsSchema>\n\n/**\n * Distribution `settings` JSON bag. Additive, backward-compatible: known keys\n * (`embed`) are validated; every other key (`captcha`, `completion`, \u2026) passes\n * through untouched so existing settings shapes keep working.\n */\nexport const distributionSettingsSchema = z\n .object({\n embed: embedSettingsSchema.optional(),\n })\n .passthrough()\n\nexport type DistributionSettingsInput = z.infer<typeof distributionSettingsSchema>\n\n/** Command input for `forms.distribution.create`. */\nexport const distributionCreateCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n mode: distributionModeSchema,\n pinnedVersionId: z.string().uuid().optional().nullable(),\n title: z.string().trim().min(1).max(200).optional().nullable(),\n defaultLocale: localeSchema,\n requireCustomerAuth: z.boolean().optional(),\n allowMultipleSubmissions: z.boolean().optional(),\n maxResponses: z.number().int().positive().optional().nullable(),\n opensAt: isoDateTimeSchema.optional().nullable(),\n closesAt: isoDateTimeSchema.optional().nullable(),\n redirectUrl: optionalRedirectUrl,\n settings: distributionSettingsSchema.optional().nullable(),\n})\n\nexport type FormDistributionCreateCommandInput = z.infer<typeof distributionCreateCommandSchema>\n\n/** Command input for `forms.distribution.update`. */\nexport const distributionUpdateCommandSchema = z.object({\n ...scopedFields,\n distributionId: z.string().uuid(),\n status: distributionStatusSchema.optional(),\n title: z.string().trim().min(1).max(200).optional().nullable(),\n maxResponses: z.number().int().positive().optional().nullable(),\n opensAt: isoDateTimeSchema.optional().nullable(),\n closesAt: isoDateTimeSchema.optional().nullable(),\n redirectUrl: optionalRedirectUrl,\n allowMultipleSubmissions: z.boolean().optional(),\n requireCustomerAuth: z.boolean().optional(),\n settings: distributionSettingsSchema.optional().nullable(),\n})\n\nexport type FormDistributionUpdateCommandInput = z.infer<typeof distributionUpdateCommandSchema>\n\n/** Command input for `forms.distribution.close`. */\nexport const distributionCloseCommandSchema = z.object({\n ...scopedFields,\n distributionId: z.string().uuid(),\n})\n\nexport type FormDistributionCloseCommandInput = z.infer<typeof distributionCloseCommandSchema>\n\nconst invitationRecipientSchema = z.object({\n email: z.string().email().max(320).optional(),\n name: z.string().trim().min(1).max(200).optional(),\n ref: z.string().trim().min(1).max(200).optional(),\n role: roleIdentifierSchema.optional(),\n locale: localeSchema.optional(),\n expiresAt: isoDateTimeSchema.optional(),\n})\n\nexport type FormInvitationRecipientInput = z.infer<typeof invitationRecipientSchema>\n\n/** Command input for `forms.invitation.create` \u2014 bulk recipient creation. */\nexport const invitationCreateCommandSchema = z.object({\n ...scopedFields,\n distributionId: z.string().uuid(),\n recipients: z.array(invitationRecipientSchema).min(1).max(1000),\n})\n\nexport type FormInvitationCreateCommandInput = z.infer<typeof invitationCreateCommandSchema>\n\n/** Command input for `forms.invitation.send`. */\nexport const invitationSendCommandSchema = z.object({\n ...scopedFields,\n invitationId: z.string().uuid(),\n})\n\nexport type FormInvitationSendCommandInput = z.infer<typeof invitationSendCommandSchema>\n\n/** Command input for `forms.invitation.revoke`. */\nexport const invitationRevokeCommandSchema = z.object({\n ...scopedFields,\n invitationId: z.string().uuid(),\n})\n\nexport type FormInvitationRevokeCommandInput = z.infer<typeof invitationRevokeCommandSchema>\n\n/**\n * Body for the public anonymous start endpoint. Exactly one of `slug`\n * (open-mode public link) or `token` (personal-mode invitation) MUST be\n * present; the refine below enforces the disjunction.\n */\nexport const publicStartInputSchema = z\n .object({\n slug: z.string().min(1).max(200).optional(),\n token: z.string().min(1).max(512).optional(),\n locale: localeSchema.optional(),\n captchaToken: z.string().min(1).max(4000).optional(),\n })\n .refine((value) => Boolean(value.slug) !== Boolean(value.token), {\n message: 'Exactly one of \"slug\" or \"token\" must be provided.',\n })\n\nexport type PublicStartInput = z.infer<typeof publicStartInputSchema>\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { normalizeEmbedOrigin } from '../lib/embed-frame-policy'\nimport { isNavigableUrl } from '../lib/navigable-url'\n\n/**\n * Forms module shared Zod primitives + Phase 1b/1c command/API schemas.\n *\n * Phase 1a shipped only the building-block primitives. Phase 1b extends with\n * the form definition + version lifecycle command/API schemas. Phase 1c adds\n * the runtime submission schemas.\n */\n\n/**\n * Form key \u2014 stable per-organization slug. 3-64 chars; starts with a lower\n * case letter; contains only `[a-z0-9_-]`. Treated as a contract surface for\n * external integrations; uniqueness is enforced at the DB level by\n * UNIQUE (organization_id, key).\n */\nexport const formKeySchema = z\n .string()\n .min(3)\n .max(64)\n .regex(/^[a-z][a-z0-9_-]*$/, {\n message: 'Form key must start with a lowercase letter and only contain lowercase letters, digits, hyphens, and underscores.',\n })\n\n/**\n * Locale tag \u2014 minimal BCP-47 shape. `en`, `en-US`, `pl`, `pl-PL`, etc.\n * Studio (phase 1b) may expand this once the locale picker ships.\n */\nexport const localeSchema = z\n .string()\n .min(2)\n .max(10)\n .regex(/^[a-z]{2,3}(?:-[A-Z]{2})?$/, {\n message: 'Locale must be a BCP-47 tag like \"en\" or \"en-US\".',\n })\n\n/**\n * Role identifier \u2014 used in `x-om-roles`, `x-om-editable-by`, `x-om-visible-to`,\n * and the form-version `roles` column. Lower-case, slug-shaped, snake-kebab\n * mix tolerated. Common values: `patient`, `clinician`, `admin`, `guardian`.\n */\nexport const roleIdentifierSchema = z\n .string()\n .min(2)\n .max(64)\n .regex(/^[a-z][a-z0-9_-]*$/, {\n message: 'Role identifier must start with a lowercase letter and only contain lowercase letters, digits, hyphens, and underscores.',\n })\n\nexport type FormKey = z.infer<typeof formKeySchema>\nexport type Locale = z.infer<typeof localeSchema>\nexport type RoleIdentifier = z.infer<typeof roleIdentifierSchema>\n\n/**\n * Submission subject identifier. Polymorphic: subject_type is a free-form\n * string tag (`patient`, `customer`, `case`, etc.); subject_id MUST be a\n * UUID. Validation of the (subject_type, subject_id) pair against actual\n * domain entities is the responsibility of the calling module; the portal\n * runtime enforces `lib/portal-subject-policy.ts`.\n */\nexport const submissionSubjectSchema = z.object({\n subject_type: z.string().min(1).max(64),\n subject_id: z.string().uuid(),\n})\n\n/**\n * Body for `POST /api/form-submissions` \u2014 runtime start.\n */\nexport const submissionStartInputSchema = z.object({\n form_key: formKeySchema,\n subject_type: z.string().min(1).max(64),\n subject_id: z.string().uuid(),\n locale: localeSchema.optional(),\n})\n\nexport type SubmissionStartInput = z.infer<typeof submissionStartInputSchema>\n\n/**\n * Body for `PATCH /api/form-submissions/:id` \u2014 autosave.\n *\n * `patch` is left as `passthrough` because the field-set is dynamic per form\n * version; the SubmissionService enforces `additionalProperties: false`\n * via the AJV validator from the compiler before persisting.\n */\nexport const submissionSaveInputSchema = z.object({\n base_revision_id: z.string().uuid(),\n patch: z.record(z.string(), z.unknown()),\n change_summary: z.string().max(500).optional(),\n})\n\nexport type SubmissionSaveInput = z.infer<typeof submissionSaveInputSchema>\n\n/**\n * Body for `POST /api/form-submissions/:id/submit` \u2014 final submit.\n */\nexport const submissionSubmitInputSchema = z.object({\n base_revision_id: z.string().uuid(),\n submit_metadata: z.record(z.string(), z.unknown()).optional(),\n})\n\nexport type SubmissionSubmitInput = z.infer<typeof submissionSubmitInputSchema>\n\n/**\n * Body for `POST /api/forms/submissions/:submissionId/actors` \u2014 admin assigns\n * a (user, role) pair to a submission. The role MUST be one of the form\n * version's declared roles; the SubmissionService enforces this.\n */\nexport const assignActorInputSchema = z.object({\n user_id: z.string().uuid(),\n role: roleIdentifierSchema,\n})\n\nexport type AssignActorInput = z.infer<typeof assignActorInputSchema>\n\nexport const revokeActorInputSchema = z.object({\n actor_id: z.string().uuid(),\n})\n\nexport type RevokeActorInput = z.infer<typeof revokeActorInputSchema>\n\n// ============================================================================\n// Phase 1b \u2014 Definition Authoring (CRUD + Studio) command + API schemas\n// ============================================================================\n\nconst scopedFields = {\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid(),\n} as const\n\nconst optionalDescription = z\n .string()\n .trim()\n .max(2000)\n .optional()\n .nullable()\n\nconst optionalChangelog = z\n .string()\n .trim()\n .max(4000)\n .optional()\n .nullable()\n\nexport const formStatusSchema = z.enum(['draft', 'active', 'archived'])\nexport const formVersionStatusSchema = z.enum(['draft', 'published', 'archived'])\n\nconst jsonObjectSchema = z.record(z.string(), z.unknown())\n\nconst supportedLocalesSchema = z.array(localeSchema).min(1).max(20)\n\n/**\n * Per-form retention window in days (W5 / DP-6). `null` keeps submissions\n * forever; a positive integer drives the retention-purge worker. Capped at\n * ~100 years so a typo cannot disable purge by accident while still allowing\n * decade-long legal-retention windows.\n */\nconst retentionDaysSchema = z.number().int().min(1).max(36500).optional().nullable()\n\n/** Query schema for `GET /api/forms`. */\nexport const formListQuerySchema = z.object({\n status: z\n .union([formStatusSchema, z.array(formStatusSchema).max(3)])\n .optional(),\n q: z.string().trim().max(200).optional(),\n page: z.coerce.number().int().min(1).optional(),\n pageSize: z.coerce.number().int().min(1).max(100).optional(),\n})\n\nexport type FormListQueryInput = z.infer<typeof formListQuerySchema>\n\n/** Command input for `forms.form.create`. */\nexport const formCreateCommandSchema = z.object({\n ...scopedFields,\n key: formKeySchema,\n name: z.string().trim().min(1).max(200),\n description: optionalDescription,\n defaultLocale: localeSchema,\n supportedLocales: supportedLocalesSchema,\n retentionDays: retentionDaysSchema,\n})\n\nexport type FormCreateCommandInput = z.infer<typeof formCreateCommandSchema>\n\n/**\n * Command input for `forms.form.rename`. Despite the name, this command is the\n * single form-metadata update path: name, description, and (W7) the locale set.\n * `defaultLocale` must be one of `supportedLocales` \u2014 enforced at execute time.\n */\nexport const formRenameCommandSchema = z.object({\n ...scopedFields,\n id: z.string().uuid(),\n name: z.string().trim().min(1).max(200).optional(),\n description: optionalDescription,\n defaultLocale: localeSchema.optional(),\n supportedLocales: supportedLocalesSchema.optional(),\n retentionDays: retentionDaysSchema,\n})\n\nexport type FormRenameCommandInput = z.infer<typeof formRenameCommandSchema>\n\n/** Command input for `forms.form.archive`. */\nexport const formArchiveCommandSchema = z.object({\n ...scopedFields,\n id: z.string().uuid(),\n})\n\nexport type FormArchiveCommandInput = z.infer<typeof formArchiveCommandSchema>\n\n/** Command input for `forms.form.restore`. */\nexport const formRestoreCommandSchema = z.object({\n ...scopedFields,\n id: z.string().uuid(),\n})\n\nexport type FormRestoreCommandInput = z.infer<typeof formRestoreCommandSchema>\n\n/** Command input for `forms.form_version.fork_draft`. */\nexport const formVersionForkDraftCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n fromVersionId: z.string().uuid().optional().nullable(),\n})\n\nexport type FormVersionForkDraftCommandInput = z.infer<typeof formVersionForkDraftCommandSchema>\n\n/** Command input for `forms.form_version.update_draft`. */\nexport const formVersionUpdateDraftCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n versionId: z.string().uuid(),\n schema: jsonObjectSchema.optional(),\n uiSchema: jsonObjectSchema.optional(),\n roles: z.array(roleIdentifierSchema).max(50).optional(),\n changelog: optionalChangelog,\n})\n\nexport type FormVersionUpdateDraftCommandInput = z.infer<typeof formVersionUpdateDraftCommandSchema>\n\n/** Command input for `forms.form_version.publish`. */\nexport const formVersionPublishCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n versionId: z.string().uuid(),\n changelog: z.string().trim().max(4000).optional().nullable(),\n})\n\nexport type FormVersionPublishCommandInput = z.infer<typeof formVersionPublishCommandSchema>\n\n/** Command input for `forms.form_version.archive`. */\nexport const formVersionArchiveCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n versionId: z.string().uuid(),\n})\n\nexport type FormVersionArchiveCommandInput = z.infer<typeof formVersionArchiveCommandSchema>\n\n/** POST /api/forms request body (route layer fills tenant/organization scope). */\nexport const formCreateRequestSchema = formCreateCommandSchema.omit({\n tenantId: true,\n organizationId: true,\n})\n\n/** PATCH /api/forms/:id request body. */\nexport const formPatchRequestSchema = z.object({\n name: z.string().trim().min(1).max(200).optional(),\n description: optionalDescription,\n defaultLocale: localeSchema.optional(),\n supportedLocales: supportedLocalesSchema.optional(),\n retentionDays: retentionDaysSchema,\n})\n\n/** POST /api/forms/:id/versions/fork request body. */\nexport const formVersionForkRequestSchema = z.object({\n fromVersionId: z.string().uuid().optional().nullable(),\n})\n\n/** PATCH /api/forms/:id/versions/:versionId request body. */\nexport const formVersionPatchRequestSchema = z.object({\n schema: jsonObjectSchema.optional(),\n uiSchema: jsonObjectSchema.optional(),\n roles: z.array(roleIdentifierSchema).max(50).optional(),\n changelog: optionalChangelog,\n})\n\n/** POST /api/forms/:id/versions/:versionId/publish request body. */\nexport const formVersionPublishRequestSchema = z.object({\n changelog: z.string().trim().max(4000).optional().nullable(),\n})\n\n/** GET /api/forms/:id/versions/:versionId/diff query. */\nexport const formVersionDiffQuerySchema = z.object({\n against: z.string().uuid(),\n})\n\nexport type FormCreateRequestInput = z.infer<typeof formCreateRequestSchema>\nexport type FormPatchRequestInput = z.infer<typeof formPatchRequestSchema>\nexport type FormVersionForkRequestInput = z.infer<typeof formVersionForkRequestSchema>\nexport type FormVersionPatchRequestInput = z.infer<typeof formVersionPatchRequestSchema>\nexport type FormVersionPublishRequestInput = z.infer<typeof formVersionPublishRequestSchema>\nexport type FormVersionDiffQueryInput = z.infer<typeof formVersionDiffQuerySchema>\n\nexport type FormStatus = z.infer<typeof formStatusSchema>\nexport type FormVersionStatus = z.infer<typeof formVersionStatusSchema>\n\n// ============================================================================\n// Phase 2d \u2014 Distribution & Anonymous Submission command/API schemas\n// ============================================================================\n\nexport const distributionModeSchema = z.enum(['open', 'personal'])\nexport const distributionStatusSchema = z.enum(['active', 'paused', 'closed'])\n\nexport type FormDistributionMode = z.infer<typeof distributionModeSchema>\nexport type FormDistributionStatus = z.infer<typeof distributionStatusSchema>\n\nconst isoDateTimeSchema = z.string().datetime({ offset: true })\n\n/**\n * Post-submission redirect target. `z.string().url()` alone is not enough: it\n * only asks whether `new URL(...)` parses, so `javascript:`, `data:` and\n * `vbscript:` all pass it \u2014 and this value is assigned to `window.location` by\n * the anonymous public runner, in the app's own origin, under a CSP that allows\n * `'unsafe-inline'`. The scheme check is what makes it safe.\n */\nconst optionalRedirectUrl = z\n .string()\n .url()\n .max(2000)\n .refine((value) => isNavigableUrl(value), {\n message: 'forms.errors.redirect_url_scheme',\n })\n .optional()\n .nullable()\n\n/**\n * A single embed allowlist origin. Validated to the strict origin shape\n * (scheme + host + optional port, https-only except localhost) via the shared\n * `normalizeEmbedOrigin` policy helper so the Zod layer and the runtime CSP\n * builder agree byte-for-byte (R-RS-1).\n */\nconst embedAllowedDomainSchema = z\n .string()\n .trim()\n .min(1)\n .max(253)\n .refine((value) => normalizeEmbedOrigin(value) !== null, {\n message:\n 'Allowed domain must be an https origin like \"https://www.acme.com\" (http is permitted only for localhost). No path, query, or fragment.',\n })\n\n/**\n * `settings.embed` \u2014 opt-in external-website framing config (spec D6). Rides on\n * the existing `forms_distribution.settings` JSON column; no migration. Enabling\n * embedding REQUIRES a non-empty allowlist \u2014 an enabled bag with zero allowed\n * domains is rejected at save time (R-RS-1). `allowedDomains` is NEVER echoed to\n * any public context payload.\n */\nexport const embedSettingsSchema = z\n .object({\n enabled: z.boolean().default(false),\n allowedDomains: z.array(embedAllowedDomainSchema).max(50).default([]),\n theme: z.enum(['light', 'dark', 'auto']).optional(),\n autoResize: z.boolean().default(true),\n })\n .strict()\n .refine((value) => !value.enabled || value.allowedDomains.length > 0, {\n message: 'Embedding cannot be enabled without at least one allowed domain.',\n path: ['allowedDomains'],\n })\n\nexport type EmbedSettingsInput = z.infer<typeof embedSettingsSchema>\n\n/**\n * Distribution `settings` JSON bag. Additive, backward-compatible: known keys\n * (`embed`) are validated; every other key (`captcha`, `completion`, \u2026) passes\n * through untouched so existing settings shapes keep working.\n */\nexport const distributionSettingsSchema = z\n .object({\n embed: embedSettingsSchema.optional(),\n })\n .passthrough()\n\nexport type DistributionSettingsInput = z.infer<typeof distributionSettingsSchema>\n\n/** Command input for `forms.distribution.create`. */\nexport const distributionCreateCommandSchema = z.object({\n ...scopedFields,\n formId: z.string().uuid(),\n mode: distributionModeSchema,\n pinnedVersionId: z.string().uuid().optional().nullable(),\n title: z.string().trim().min(1).max(200).optional().nullable(),\n defaultLocale: localeSchema,\n requireCustomerAuth: z.boolean().optional(),\n allowMultipleSubmissions: z.boolean().optional(),\n maxResponses: z.number().int().positive().optional().nullable(),\n opensAt: isoDateTimeSchema.optional().nullable(),\n closesAt: isoDateTimeSchema.optional().nullable(),\n redirectUrl: optionalRedirectUrl,\n settings: distributionSettingsSchema.optional().nullable(),\n})\n\nexport type FormDistributionCreateCommandInput = z.infer<typeof distributionCreateCommandSchema>\n\n/** Command input for `forms.distribution.update`. */\nexport const distributionUpdateCommandSchema = z.object({\n ...scopedFields,\n distributionId: z.string().uuid(),\n status: distributionStatusSchema.optional(),\n title: z.string().trim().min(1).max(200).optional().nullable(),\n maxResponses: z.number().int().positive().optional().nullable(),\n opensAt: isoDateTimeSchema.optional().nullable(),\n closesAt: isoDateTimeSchema.optional().nullable(),\n redirectUrl: optionalRedirectUrl,\n allowMultipleSubmissions: z.boolean().optional(),\n requireCustomerAuth: z.boolean().optional(),\n settings: distributionSettingsSchema.optional().nullable(),\n})\n\nexport type FormDistributionUpdateCommandInput = z.infer<typeof distributionUpdateCommandSchema>\n\n/** Command input for `forms.distribution.close`. */\nexport const distributionCloseCommandSchema = z.object({\n ...scopedFields,\n distributionId: z.string().uuid(),\n})\n\nexport type FormDistributionCloseCommandInput = z.infer<typeof distributionCloseCommandSchema>\n\nconst invitationRecipientSchema = z.object({\n email: z.string().email().max(320).optional(),\n name: z.string().trim().min(1).max(200).optional(),\n ref: z.string().trim().min(1).max(200).optional(),\n role: roleIdentifierSchema.optional(),\n locale: localeSchema.optional(),\n expiresAt: isoDateTimeSchema.optional(),\n})\n\nexport type FormInvitationRecipientInput = z.infer<typeof invitationRecipientSchema>\n\n/** Command input for `forms.invitation.create` \u2014 bulk recipient creation. */\nexport const invitationCreateCommandSchema = z.object({\n ...scopedFields,\n distributionId: z.string().uuid(),\n recipients: z.array(invitationRecipientSchema).min(1).max(1000),\n})\n\nexport type FormInvitationCreateCommandInput = z.infer<typeof invitationCreateCommandSchema>\n\n/** Command input for `forms.invitation.send`. */\nexport const invitationSendCommandSchema = z.object({\n ...scopedFields,\n invitationId: z.string().uuid(),\n})\n\nexport type FormInvitationSendCommandInput = z.infer<typeof invitationSendCommandSchema>\n\n/** Command input for `forms.invitation.revoke`. */\nexport const invitationRevokeCommandSchema = z.object({\n ...scopedFields,\n invitationId: z.string().uuid(),\n})\n\nexport type FormInvitationRevokeCommandInput = z.infer<typeof invitationRevokeCommandSchema>\n\n/**\n * Body for the public anonymous start endpoint. Exactly one of `slug`\n * (open-mode public link) or `token` (personal-mode invitation) MUST be\n * present; the refine below enforces the disjunction.\n */\nexport const publicStartInputSchema = z\n .object({\n slug: z.string().min(1).max(200).optional(),\n token: z.string().min(1).max(512).optional(),\n locale: localeSchema.optional(),\n captchaToken: z.string().min(1).max(4000).optional(),\n })\n .refine((value) => Boolean(value.slug) !== Boolean(value.token), {\n message: 'Exactly one of \"slug\" or \"token\" must be provided.',\n })\n\nexport type PublicStartInput = z.infer<typeof publicStartInputSchema>\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,17 @@
1
+ const PORTAL_SUBJECT_TYPE = "customer";
2
+ function isSameId(left, right) {
3
+ return left.toLowerCase() === right.toLowerCase();
4
+ }
5
+ function isPortalSubjectAllowed(customerUserId, subject) {
6
+ return subject.subjectType === PORTAL_SUBJECT_TYPE && isSameId(subject.subjectId, customerUserId);
7
+ }
8
+ function isPortalSubmitAllowed(customerUserId, submission) {
9
+ if (!isSameId(submission.startedBy, customerUserId)) return true;
10
+ return isPortalSubjectAllowed(customerUserId, submission);
11
+ }
12
+ export {
13
+ PORTAL_SUBJECT_TYPE,
14
+ isPortalSubjectAllowed,
15
+ isPortalSubmitAllowed
16
+ };
17
+ //# sourceMappingURL=portal-subject-policy.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/forms/lib/portal-subject-policy.ts"],
4
+ "sourcesContent": ["/**\n * Portal subject policy (#7224).\n *\n * A portal customer may start a submission, and finalize a submission they\n * started themselves, only for the subject that identifies them: the\n * `customer` subject whose id is their own customer user id (`auth.sub`).\n * Any other pair is refused before anything is persisted, because the\n * subject is what the consent projector attributes signatures to.\n *\n * A submission started by someone else reached the caller through a staff\n * actor assignment (`forms.submissions.manage`), so staff already vouched for\n * that pairing and the submit is not re-checked here. Token (invitation)\n * principals carry a server-derived subject and are out of scope.\n *\n * The start route applies `isPortalSubjectAllowed` before anything is\n * persisted; `SubmissionService.submit()` applies `isPortalSubmitAllowed`\n * inside its locked transaction when the caller is a portal customer.\n */\n\nexport const PORTAL_SUBJECT_TYPE = 'customer'\n\nexport type PortalSubject = {\n subjectType: string\n subjectId: string\n}\n\nexport type PortalSubmissionSubject = PortalSubject & {\n startedBy: string\n}\n\nfunction isSameId(left: string, right: string): boolean {\n return left.toLowerCase() === right.toLowerCase()\n}\n\nexport function isPortalSubjectAllowed(customerUserId: string, subject: PortalSubject): boolean {\n return subject.subjectType === PORTAL_SUBJECT_TYPE && isSameId(subject.subjectId, customerUserId)\n}\n\nexport function isPortalSubmitAllowed(customerUserId: string, submission: PortalSubmissionSubject): boolean {\n if (!isSameId(submission.startedBy, customerUserId)) return true\n return isPortalSubjectAllowed(customerUserId, submission)\n}\n"],
5
+ "mappings": "AAmBO,MAAM,sBAAsB;AAWnC,SAAS,SAAS,MAAc,OAAwB;AACtD,SAAO,KAAK,YAAY,MAAM,MAAM,YAAY;AAClD;AAEO,SAAS,uBAAuB,gBAAwB,SAAiC;AAC9F,SAAO,QAAQ,gBAAgB,uBAAuB,SAAS,QAAQ,WAAW,cAAc;AAClG;AAEO,SAAS,sBAAsB,gBAAwB,YAA8C;AAC1G,MAAI,CAAC,SAAS,WAAW,WAAW,cAAc,EAAG,QAAO;AAC5D,SAAO,uBAAuB,gBAAgB,UAAU;AAC1D;",
6
+ "names": []
7
+ }
@@ -1,5 +1,6 @@
1
1
  import { getCustomerAuthFromRequest } from "@open-mercato/core/modules/customer_accounts/lib/customerAuth";
2
- import { FormSubmission, FormSubmissionActor } from "../data/entities.js";
2
+ import { FormInvitation, FormSubmission, FormSubmissionActor } from "../data/entities.js";
3
+ import { INVITATION_ACCESS_CLOSED_STATUSES } from "../services/distribution-service.js";
3
4
  import { verifyAccessToken } from "../services/distribution-token.js";
4
5
  function readBearerToken(req) {
5
6
  const header = req.headers.get("authorization") ?? req.headers.get("Authorization");
@@ -19,16 +20,32 @@ async function resolveRuntimePrincipal(args) {
19
20
  id: submissionId,
20
21
  deletedAt: null
21
22
  });
22
- if (submission) {
23
- return {
24
- source: "token",
25
- principal: verified.invitationId,
26
- role: verified.role ?? null,
27
- organizationId: submission.organizationId,
28
- tenantId: submission.tenantId,
29
- submissionId
30
- };
23
+ if (!submission) return null;
24
+ const invitation = await em.findOne(FormInvitation, {
25
+ id: verified.invitationId,
26
+ organizationId: submission.organizationId,
27
+ tenantId: submission.tenantId,
28
+ deletedAt: null
29
+ });
30
+ if (!invitation || invitation.submissionId !== submissionId || INVITATION_ACCESS_CLOSED_STATUSES.has(invitation.status)) {
31
+ return null;
31
32
  }
33
+ const actor = await em.findOne(FormSubmissionActor, {
34
+ submissionId,
35
+ organizationId: submission.organizationId,
36
+ userId: invitation.id,
37
+ revokedAt: null,
38
+ deletedAt: null
39
+ });
40
+ if (!actor) return null;
41
+ return {
42
+ source: "token",
43
+ principal: verified.invitationId,
44
+ role: actor.role,
45
+ organizationId: submission.organizationId,
46
+ tenantId: submission.tenantId,
47
+ submissionId
48
+ };
32
49
  }
33
50
  return null;
34
51
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/forms/lib/runtime-principal.ts"],
4
- "sourcesContent": ["/**\n * Public runtime principal guard \u2014 phase 2d.\n *\n * Resolves the acting principal for a public-runtime submission request from\n * EITHER a submission access token (`Authorization: Bearer <token>`) OR a\n * portal customer session, in that order.\n *\n * Critical invariant (R-2d-4): when the principal comes from an access token,\n * the org/tenant are NEVER trusted from the token body \u2014 they are re-derived\n * from the persisted `forms_form_submission` row. The token only authorizes a\n * single `(submissionId, invitationId, role)` triple; it asserts the token's\n * submission matches the route's submission before trusting anything.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { FormSubmission, FormSubmissionActor } from '../data/entities'\nimport { verifyAccessToken } from '../services/distribution-token'\n\nexport type RuntimePrincipal = {\n source: 'token' | 'customer'\n principal: string\n role: string | null\n organizationId: string\n tenantId: string\n submissionId: string\n}\n\nexport type ResolveRuntimePrincipalArgs = {\n req: Request\n submissionId: string\n em: EntityManager\n}\n\nfunction readBearerToken(req: Request): string | null {\n const header = req.headers.get('authorization') ?? req.headers.get('Authorization')\n if (!header) return null\n const match = /^Bearer\\s+(.+)$/i.exec(header.trim())\n if (!match) return null\n const token = match[1].trim()\n return token.length > 0 ? token : null\n}\n\nexport async function resolveRuntimePrincipal(\n args: ResolveRuntimePrincipalArgs,\n): Promise<RuntimePrincipal | null> {\n const { req, submissionId, em } = args\n\n const bearer = readBearerToken(req)\n if (bearer) {\n const verified = verifyAccessToken(bearer)\n if (verified.ok && verified.submissionId === submissionId && verified.invitationId) {\n const submission = await em.findOne(FormSubmission, {\n id: submissionId,\n deletedAt: null,\n })\n if (submission) {\n return {\n source: 'token',\n principal: verified.invitationId,\n role: verified.role ?? null,\n organizationId: submission.organizationId,\n tenantId: submission.tenantId,\n submissionId,\n }\n }\n }\n // A present-but-invalid bearer token does not fall through to customer\n // auth \u2014 it is an explicit (failed) authorization attempt.\n return null\n }\n\n const auth = await getCustomerAuthFromRequest(req)\n if (auth) {\n // A portal session authenticates the customer; it does not by itself\n // authorize them on THIS submission. Authorization is the active actor row,\n // exactly as `SubmissionService.save()` requires \u2014 and the actor's role is\n // what the read-slice is computed from. Without this lookup the branch\n // returned `role: null`, which `getCurrent` used to read as \"do not slice\",\n // so any portal customer could read any submission in their own\n // organization fully decrypted and unsliced.\n const actor = await em.findOne(FormSubmissionActor, {\n submissionId,\n organizationId: auth.orgId,\n userId: auth.sub,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) return null\n return {\n source: 'customer',\n principal: auth.sub,\n role: actor.role,\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n submissionId,\n }\n }\n\n return null\n}\n"],
5
- "mappings": "AAeA,SAAS,kCAAkC;AAC3C,SAAS,gBAAgB,2BAA2B;AACpD,SAAS,yBAAyB;AAiBlC,SAAS,gBAAgB,KAA6B;AACpD,QAAM,SAAS,IAAI,QAAQ,IAAI,eAAe,KAAK,IAAI,QAAQ,IAAI,eAAe;AAClF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,mBAAmB,KAAK,OAAO,KAAK,CAAC;AACnD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC,EAAE,KAAK;AAC5B,SAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;AAEA,eAAsB,wBACpB,MACkC;AAClC,QAAM,EAAE,KAAK,cAAc,GAAG,IAAI;AAElC,QAAM,SAAS,gBAAgB,GAAG;AAClC,MAAI,QAAQ;AACV,UAAM,WAAW,kBAAkB,MAAM;AACzC,QAAI,SAAS,MAAM,SAAS,iBAAiB,gBAAgB,SAAS,cAAc;AAClF,YAAM,aAAa,MAAM,GAAG,QAAQ,gBAAgB;AAAA,QAClD,IAAI;AAAA,QACJ,WAAW;AAAA,MACb,CAAC;AACD,UAAI,YAAY;AACd,eAAO;AAAA,UACL,QAAQ;AAAA,UACR,WAAW,SAAS;AAAA,UACpB,MAAM,SAAS,QAAQ;AAAA,UACvB,gBAAgB,WAAW;AAAA,UAC3B,UAAU,WAAW;AAAA,UACrB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,MAAM;AAQR,UAAM,QAAQ,MAAM,GAAG,QAAQ,qBAAqB;AAAA,MAClD;AAAA,MACA,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,MAAO,QAAO;AACnB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK;AAAA,MAChB,MAAM,MAAM;AAAA,MACZ,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
4
+ "sourcesContent": ["/**\n * Public runtime principal guard \u2014 phase 2d.\n *\n * Resolves the acting principal for a public-runtime submission request from\n * EITHER a submission access token (`Authorization: Bearer <token>`) OR a\n * portal customer session, in that order.\n *\n * Critical invariant (R-2d-4): when the principal comes from an access token,\n * the org/tenant are NEVER trusted from the token body \u2014 they are re-derived\n * from the persisted `forms_form_submission` row. The token only authorizes a\n * single `(submissionId, invitationId, role)` triple; it asserts the token's\n * submission matches the route's submission before trusting anything. The\n * token is also only honoured while its invitation is live (not revoked or\n * expired) and its actor row on the submission is active; the role comes from\n * that actor row, never from the token.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { getCustomerAuthFromRequest } from '@open-mercato/core/modules/customer_accounts/lib/customerAuth'\nimport { FormInvitation, FormSubmission, FormSubmissionActor } from '../data/entities'\nimport { INVITATION_ACCESS_CLOSED_STATUSES } from '../services/distribution-service'\nimport { verifyAccessToken } from '../services/distribution-token'\n\nexport type RuntimePrincipal = {\n source: 'token' | 'customer'\n principal: string\n role: string | null\n organizationId: string\n tenantId: string\n submissionId: string\n}\n\nexport type ResolveRuntimePrincipalArgs = {\n req: Request\n submissionId: string\n em: EntityManager\n}\n\nfunction readBearerToken(req: Request): string | null {\n const header = req.headers.get('authorization') ?? req.headers.get('Authorization')\n if (!header) return null\n const match = /^Bearer\\s+(.+)$/i.exec(header.trim())\n if (!match) return null\n const token = match[1].trim()\n return token.length > 0 ? token : null\n}\n\nexport async function resolveRuntimePrincipal(\n args: ResolveRuntimePrincipalArgs,\n): Promise<RuntimePrincipal | null> {\n const { req, submissionId, em } = args\n\n const bearer = readBearerToken(req)\n if (bearer) {\n const verified = verifyAccessToken(bearer)\n if (verified.ok && verified.submissionId === submissionId && verified.invitationId) {\n const submission = await em.findOne(FormSubmission, {\n id: submissionId,\n deletedAt: null,\n })\n if (!submission) return null\n const invitation = await em.findOne(FormInvitation, {\n id: verified.invitationId,\n organizationId: submission.organizationId,\n tenantId: submission.tenantId,\n deletedAt: null,\n })\n if (\n !invitation ||\n invitation.submissionId !== submissionId ||\n INVITATION_ACCESS_CLOSED_STATUSES.has(invitation.status)\n ) {\n return null\n }\n const actor = await em.findOne(FormSubmissionActor, {\n submissionId,\n organizationId: submission.organizationId,\n userId: invitation.id,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) return null\n return {\n source: 'token',\n principal: verified.invitationId,\n role: actor.role,\n organizationId: submission.organizationId,\n tenantId: submission.tenantId,\n submissionId,\n }\n }\n // A present-but-invalid bearer token does not fall through to customer\n // auth \u2014 it is an explicit (failed) authorization attempt.\n return null\n }\n\n const auth = await getCustomerAuthFromRequest(req)\n if (auth) {\n // A portal session authenticates the customer; it does not by itself\n // authorize them on THIS submission. Authorization is the active actor row,\n // exactly as `SubmissionService.save()` requires \u2014 and the actor's role is\n // what the read-slice is computed from. Without this lookup the branch\n // returned `role: null`, which `getCurrent` used to read as \"do not slice\",\n // so any portal customer could read any submission in their own\n // organization fully decrypted and unsliced.\n const actor = await em.findOne(FormSubmissionActor, {\n submissionId,\n organizationId: auth.orgId,\n userId: auth.sub,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) return null\n return {\n source: 'customer',\n principal: auth.sub,\n role: actor.role,\n organizationId: auth.orgId,\n tenantId: auth.tenantId,\n submissionId,\n }\n }\n\n return null\n}\n"],
5
+ "mappings": "AAkBA,SAAS,kCAAkC;AAC3C,SAAS,gBAAgB,gBAAgB,2BAA2B;AACpE,SAAS,yCAAyC;AAClD,SAAS,yBAAyB;AAiBlC,SAAS,gBAAgB,KAA6B;AACpD,QAAM,SAAS,IAAI,QAAQ,IAAI,eAAe,KAAK,IAAI,QAAQ,IAAI,eAAe;AAClF,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,mBAAmB,KAAK,OAAO,KAAK,CAAC;AACnD,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC,EAAE,KAAK;AAC5B,SAAO,MAAM,SAAS,IAAI,QAAQ;AACpC;AAEA,eAAsB,wBACpB,MACkC;AAClC,QAAM,EAAE,KAAK,cAAc,GAAG,IAAI;AAElC,QAAM,SAAS,gBAAgB,GAAG;AAClC,MAAI,QAAQ;AACV,UAAM,WAAW,kBAAkB,MAAM;AACzC,QAAI,SAAS,MAAM,SAAS,iBAAiB,gBAAgB,SAAS,cAAc;AAClF,YAAM,aAAa,MAAM,GAAG,QAAQ,gBAAgB;AAAA,QAClD,IAAI;AAAA,QACJ,WAAW;AAAA,MACb,CAAC;AACD,UAAI,CAAC,WAAY,QAAO;AACxB,YAAM,aAAa,MAAM,GAAG,QAAQ,gBAAgB;AAAA,QAClD,IAAI,SAAS;AAAA,QACb,gBAAgB,WAAW;AAAA,QAC3B,UAAU,WAAW;AAAA,QACrB,WAAW;AAAA,MACb,CAAC;AACD,UACE,CAAC,cACD,WAAW,iBAAiB,gBAC5B,kCAAkC,IAAI,WAAW,MAAM,GACvD;AACA,eAAO;AAAA,MACT;AACA,YAAM,QAAQ,MAAM,GAAG,QAAQ,qBAAqB;AAAA,QAClD;AAAA,QACA,gBAAgB,WAAW;AAAA,QAC3B,QAAQ,WAAW;AAAA,QACnB,WAAW;AAAA,QACX,WAAW;AAAA,MACb,CAAC;AACD,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,WAAW,SAAS;AAAA,QACpB,MAAM,MAAM;AAAA,QACZ,gBAAgB,WAAW;AAAA,QAC3B,UAAU,WAAW;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAGA,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,MAAM,2BAA2B,GAAG;AACjD,MAAI,MAAM;AAQR,UAAM,QAAQ,MAAM,GAAG,QAAQ,qBAAqB;AAAA,MAClD;AAAA,MACA,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK;AAAA,MACb,WAAW;AAAA,MACX,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,MAAO,QAAO;AACnB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,KAAK;AAAA,MAChB,MAAM,MAAM;AAAA,MACZ,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
6
6
  "names": []
7
7
  }