@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
@@ -31,7 +31,8 @@ const noopLogger = {
31
31
  error: () => {
32
32
  }
33
33
  };
34
- const INVITATION_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["submitted", "revoked", "expired"]);
34
+ const INVITATION_ACCESS_CLOSED_STATUSES = /* @__PURE__ */ new Set(["revoked", "expired"]);
35
+ const INVITATION_TERMINAL_STATUSES = /* @__PURE__ */ new Set(["submitted", ...INVITATION_ACCESS_CLOSED_STATUSES]);
35
36
  class DistributionService {
36
37
  constructor(options) {
37
38
  this.emFactory = options.emFactory;
@@ -254,20 +255,40 @@ class DistributionService {
254
255
  // --------------------------------------------------------------------------
255
256
  async markInvitationSubmitted(args) {
256
257
  const em = this.emFactory();
257
- const invitation = await em.findOne(FormInvitation, {
258
- id: args.invitationId,
259
- organizationId: args.organizationId,
260
- tenantId: args.tenantId,
261
- deletedAt: null
258
+ const marked = await em.transactional(async (trx) => {
259
+ const invitation = await trx.findOne(
260
+ FormInvitation,
261
+ {
262
+ id: args.invitationId,
263
+ organizationId: args.organizationId,
264
+ tenantId: args.tenantId,
265
+ deletedAt: null
266
+ },
267
+ { lockMode: LockMode.PESSIMISTIC_WRITE }
268
+ );
269
+ if (!invitation) {
270
+ throw new DistributionServiceError("NOT_FOUND", "Invitation not found.", 404);
271
+ }
272
+ if (INVITATION_ACCESS_CLOSED_STATUSES.has(invitation.status)) {
273
+ this.logger.warn(
274
+ {
275
+ event: "forms.distribution.invitation_submit_skipped",
276
+ invitationId: invitation.id,
277
+ submissionId: args.submissionId,
278
+ status: invitation.status
279
+ },
280
+ "Invitation access closed before submit; status left unchanged."
281
+ );
282
+ return false;
283
+ }
284
+ invitation.status = "submitted";
285
+ invitation.submittedAt = this.now();
286
+ await trx.flush();
287
+ return true;
262
288
  });
263
- if (!invitation) {
264
- throw new DistributionServiceError("NOT_FOUND", "Invitation not found.", 404);
265
- }
266
- invitation.status = "submitted";
267
- invitation.submittedAt = this.now();
268
- await em.flush();
289
+ if (!marked) return;
269
290
  await this.safeEmit("forms.invitation.submitted", {
270
- invitationId: invitation.id,
291
+ invitationId: args.invitationId,
271
292
  submissionId: args.submissionId
272
293
  });
273
294
  }
@@ -361,6 +382,7 @@ function isDistributionServiceError(error) {
361
382
  export {
362
383
  DistributionService,
363
384
  DistributionServiceError,
385
+ INVITATION_ACCESS_CLOSED_STATUSES,
364
386
  isDistributionServiceError
365
387
  };
366
388
  //# sourceMappingURL=distribution-service.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/forms/services/distribution-service.ts"],
4
- "sourcesContent": ["/**\n * Forms DistributionService \u2014 phase 2d.\n *\n * Orchestrates the distribution / invitation layer that puts a published form\n * in front of an unauthenticated participant and bootstraps an anonymous\n * submission by delegating to the unchanged `SubmissionService`.\n *\n * Invariants:\n * - Every query is tenant-scoped (`organization_id` + `tenant_id`) EXCEPT the\n * two deliberately-global lookups \u2014 by random `public_slug` and by\n * `token_hash` \u2014 both high-entropy and globally unique in practice. After a\n * global lookup the org/tenant scope is taken from the found row and used\n * for every subsequent query.\n * - Recipient PII (`recipient_email` / `recipient_name`) is encrypted via the\n * GLOBAL pipeline; reads that need the plaintext use `findOneWithDecryption`.\n * - The submission access token never carries org/tenant \u2014 those are\n * re-derived from the persisted submission downstream (see runtime-principal).\n */\n\nimport { LockMode } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n Form,\n FormDistribution,\n FormInvitation,\n FormVersion,\n} from '../data/entities'\nimport type { CompiledFormVersion } from './form-version-compiler'\nimport type { SubmissionService, SubmissionViewModel } from './submission-service'\nimport type { StructuredLogger } from '../lib/log-redaction'\nimport { buildFrameAncestorsCsp, isEmbedEnabled, readEmbedSettings } from '../lib/embed-frame-policy'\nimport { formsEventPayloadSchemas } from '../events-payloads'\nimport {\n getAccessTokenTtlSeconds,\n hashInvitationToken,\n signAccessToken,\n} from './distribution-token'\n\nexport type DistributionServiceErrorCode =\n | 'NOT_FOUND'\n | 'GONE'\n | 'INVALID_STATUS'\n\n/**\n * `Symbol.for` so the brand survives module duplication across bundle\n * boundaries. `instanceof` does NOT: Next can load this module in more than\n * one chunk, and once a second copy of the class exists an error thrown\n * through one copy fails `instanceof` against the other \u2014 silently\n * downgrading a mapped domain error to a 500. Mirrors `CrudHttpError`.\n */\nconst DISTRIBUTION_SERVICE_ERROR_MARKER = Symbol.for('@open-mercato/forms/DistributionServiceError')\n\nexport class DistributionServiceError extends Error {\n readonly [DISTRIBUTION_SERVICE_ERROR_MARKER] = true\n readonly code: DistributionServiceErrorCode\n readonly httpStatus: number\n readonly details?: Record<string, unknown>\n\n constructor(\n code: DistributionServiceErrorCode,\n message: string,\n httpStatus: number,\n details?: Record<string, unknown>,\n ) {\n super(message)\n this.name = 'DistributionServiceError'\n this.code = code\n this.httpStatus = httpStatus\n this.details = details\n }\n}\n\ntype DistributionEvents = {\n 'forms.invitation.opened': z.infer<typeof formsEventPayloadSchemas['forms.invitation.opened']>\n 'forms.invitation.submitted': z.infer<typeof formsEventPayloadSchemas['forms.invitation.submitted']>\n}\n\nexport type DistributionServiceOptions = {\n emFactory: () => EntityManager\n submissionService: SubmissionService\n /** Pluggable, schema-validated event emitter \u2014 wired in di.ts. */\n emitEvent: <K extends keyof DistributionEvents>(eventId: K, payload: DistributionEvents[K]) => Promise<void> | void\n now?: () => Date\n logger?: StructuredLogger\n}\n\nconst noopLogger: StructuredLogger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n\nconst INVITATION_TERMINAL_STATUSES: ReadonlySet<string> = new Set(['submitted', 'revoked', 'expired'])\n\nexport type BeginAnonymousArgs = {\n distribution: FormDistribution\n invitation?: FormInvitation\n locale?: string | null\n}\n\nexport type BeginAnonymousResult = {\n view: SubmissionViewModel\n invitation: FormInvitation\n accessToken: string\n expiresAt: string\n}\n\nexport type FormContextResult = {\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n}\n\nexport class DistributionService {\n private readonly emFactory: () => EntityManager\n private readonly submissionService: SubmissionService\n private readonly emitEvent: DistributionServiceOptions['emitEvent']\n private readonly now: () => Date\n private readonly logger: StructuredLogger\n\n constructor(options: DistributionServiceOptions) {\n this.emFactory = options.emFactory\n this.submissionService = options.submissionService\n this.emitEvent = options.emitEvent\n this.now = options.now ?? (() => new Date())\n this.logger = options.logger ?? noopLogger\n }\n\n // --------------------------------------------------------------------------\n // Embed eligibility (spec D5 / R-RS-2)\n // --------------------------------------------------------------------------\n\n /**\n * True only when a distribution may be framed on a third-party website:\n * an active OPEN distribution that does not require customer auth and has\n * opted in via a valid, non-empty `settings.embed` allowlist. There is no\n * session inside a third-party iframe, so personal / auth-required / disabled\n * distributions are never embeddable.\n */\n isEmbeddable(distribution: FormDistribution): boolean {\n if (distribution.mode !== 'open') return false\n if (distribution.status !== 'active') return false\n if (distribution.requireCustomerAuth) return false\n return isEmbedEnabled(readEmbedSettings(distribution.settings))\n }\n\n /**\n * Resolves the `frame-ancestors` CSP directive for the `/embed/:slug` host\n * page WITHOUT enforcing the runtime availability window \u2014 framing is\n * permitted for any config-embeddable distribution (open + active + !auth +\n * opted-in allowlist) so the iframe can still load and render a graceful\n * \"unavailable\" state for a capped / out-of-window form. Fails closed\n * (`frame-ancestors 'none'`) for unknown or non-embeddable slugs.\n */\n async getEmbedPolicyBySlug(\n slug: string,\n ): Promise<{ frameAncestors: string; embeddable: boolean }> {\n const em = this.emFactory()\n const distribution = await em.findOne(FormDistribution, {\n publicSlug: slug,\n deletedAt: null,\n })\n if (!distribution) return { frameAncestors: \"frame-ancestors 'none'\", embeddable: false }\n const embeddable = this.isEmbeddable(distribution)\n const frameAncestors = embeddable\n ? buildFrameAncestorsCsp(readEmbedSettings(distribution.settings))\n : \"frame-ancestors 'none'\"\n return { frameAncestors, embeddable }\n }\n\n // --------------------------------------------------------------------------\n // Resolve \u2014 open link (global slug lookup)\n // --------------------------------------------------------------------------\n\n async resolveBySlug(slug: string): Promise<{ distribution: FormDistribution }> {\n const em = this.emFactory()\n const distribution = await em.findOne(FormDistribution, {\n publicSlug: slug,\n deletedAt: null,\n })\n if (!distribution) {\n throw new DistributionServiceError('NOT_FOUND', 'Distribution not found.', 404)\n }\n this.assertDistributionAvailable(distribution)\n return { distribution }\n }\n\n // --------------------------------------------------------------------------\n // Resolve \u2014 personal invitation (global token-hash lookup)\n // --------------------------------------------------------------------------\n\n async resolveByToken(rawToken: string): Promise<{ invitation: FormInvitation; distribution: FormDistribution }> {\n const em = this.emFactory()\n const tokenHash = hashInvitationToken(rawToken)\n // Locate the row first (no scope) to derive org/tenant, then re-read with\n // decryption scoped to that row so recipient PII is available to the caller.\n const located = await em.findOne(FormInvitation, {\n tokenHash,\n deletedAt: null,\n })\n if (!located) {\n throw new DistributionServiceError('NOT_FOUND', 'Invitation not found.', 404)\n }\n const invitation = await findOneWithDecryption(\n em,\n FormInvitation,\n { id: located.id, organizationId: located.organizationId, tenantId: located.tenantId, deletedAt: null },\n undefined,\n { organizationId: located.organizationId, tenantId: located.tenantId },\n )\n if (!invitation) {\n throw new DistributionServiceError('NOT_FOUND', 'Invitation not found.', 404)\n }\n\n this.assertInvitationUsable(invitation)\n\n const distribution = await em.findOne(FormDistribution, {\n id: invitation.distributionId,\n organizationId: invitation.organizationId,\n tenantId: invitation.tenantId,\n deletedAt: null,\n })\n if (!distribution) {\n throw new DistributionServiceError('NOT_FOUND', 'Distribution not found.', 404)\n }\n this.assertDistributionAvailable(distribution)\n\n // Mark opened on first resolve from a pre-fill status.\n if (invitation.status === 'pending' || invitation.status === 'sent') {\n invitation.status = 'opened'\n invitation.openedAt = this.now()\n await em.flush()\n await this.safeEmit('forms.invitation.opened', { invitationId: invitation.id })\n }\n\n return { invitation, distribution }\n }\n\n // --------------------------------------------------------------------------\n // Form context \u2014 resolve the served form version for a distribution\n // --------------------------------------------------------------------------\n\n async getFormContext(args: { distribution: FormDistribution }): Promise<FormContextResult> {\n const { distribution } = args\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n id: distribution.formId,\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n deletedAt: null,\n })\n if (!form) {\n throw new DistributionServiceError('NOT_FOUND', 'Form not found.', 404)\n }\n if (distribution.pinnedVersionId) {\n return this.submissionService.getPinnedFormVersionByKey({\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n formKey: form.key,\n pinnedVersionId: distribution.pinnedVersionId,\n })\n }\n return this.submissionService.getActiveFormVersionByKey({\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n formKey: form.key,\n })\n }\n\n // --------------------------------------------------------------------------\n // Begin anonymous \u2014 mint/link invitation, start submission, sign token\n // --------------------------------------------------------------------------\n\n async beginAnonymous(args: BeginAnonymousArgs): Promise<BeginAnonymousResult> {\n const { distribution } = args\n this.assertDistributionAvailable(distribution)\n\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n id: distribution.formId,\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n deletedAt: null,\n })\n if (!form) {\n throw new DistributionServiceError('NOT_FOUND', 'Form not found.', 404)\n }\n\n let invitation: FormInvitation\n if (args.invitation) {\n invitation = args.invitation\n this.assertInvitationUsable(invitation)\n // Resume: invitation already has a submission \u2014 return the existing one.\n if (invitation.submissionId) {\n return this.resumeExisting(distribution, form.key, invitation)\n }\n } else {\n const now = this.now()\n invitation = em.create(FormInvitation, {\n distributionId: distribution.id,\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n recipientEmail: null,\n recipientName: null,\n recipientRef: null,\n role: null,\n tokenHash: null,\n status: 'started',\n submissionId: null,\n locale: args.locale ?? distribution.defaultLocale ?? null,\n sendCount: 0,\n createdBy: null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(invitation)\n await em.flush()\n }\n\n const view = await this.submissionService.start({\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n formKey: form.key,\n subjectType: 'forms_invitation',\n subjectId: invitation.id,\n startedBy: invitation.id,\n initialRole: invitation.role ?? null,\n pinnedVersionId: distribution.pinnedVersionId ?? undefined,\n })\n\n const now = this.now()\n invitation.submissionId = view.submission.id\n invitation.status = 'started'\n invitation.startedAt = now\n if (!invitation.locale) invitation.locale = args.locale ?? distribution.defaultLocale ?? null\n await em.flush()\n\n const role = view.actors[0]?.role ?? invitation.role ?? null\n return this.issueAccess(view, invitation, role)\n }\n\n // --------------------------------------------------------------------------\n // Reserve a response slot (atomic cap enforcement \u2014 R-2d-5)\n // --------------------------------------------------------------------------\n\n async reserveResponseSlot(args: { distributionId: string; organizationId: string; tenantId: string }): Promise<void> {\n const em = this.emFactory()\n await em.transactional(async (trx) => {\n const distribution = await trx.findOne(\n FormDistribution,\n {\n id: args.distributionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!distribution) {\n throw new DistributionServiceError('NOT_FOUND', 'Distribution not found.', 404)\n }\n this.assertDistributionAvailable(distribution)\n distribution.responseCount += 1\n await trx.flush()\n })\n }\n\n // --------------------------------------------------------------------------\n // Mark invitation submitted\n // --------------------------------------------------------------------------\n\n async markInvitationSubmitted(args: {\n invitationId: string\n organizationId: string\n tenantId: string\n submissionId: string\n }): Promise<void> {\n const em = this.emFactory()\n const invitation = await em.findOne(FormInvitation, {\n id: args.invitationId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!invitation) {\n throw new DistributionServiceError('NOT_FOUND', 'Invitation not found.', 404)\n }\n invitation.status = 'submitted'\n invitation.submittedAt = this.now()\n await em.flush()\n await this.safeEmit('forms.invitation.submitted', {\n invitationId: invitation.id,\n submissionId: args.submissionId,\n })\n }\n\n // --------------------------------------------------------------------------\n // Slide the access-token expiry (used by the autosave route)\n // --------------------------------------------------------------------------\n\n refreshAccessToken(submissionId: string, invitationId: string, role: string | null): string {\n return signAccessToken({\n submissionId,\n invitationId,\n role,\n expiresAtSeconds: this.expiresAtSeconds(),\n })\n }\n\n // --------------------------------------------------------------------------\n // Helpers\n // --------------------------------------------------------------------------\n\n private async resumeExisting(\n distribution: FormDistribution,\n formKey: string,\n invitation: FormInvitation,\n ): Promise<BeginAnonymousResult> {\n const view = await this.submissionService.getCurrent({\n organizationId: invitation.organizationId,\n tenantId: invitation.tenantId,\n submissionId: invitation.submissionId as string,\n viewerRole: invitation.role ?? null,\n viewerUserId: invitation.id,\n })\n void distribution\n void formKey\n const role = view.actors.find((actor) => actor.userId === invitation.id)?.role\n ?? invitation.role\n ?? view.actors[0]?.role\n ?? null\n return this.issueAccess(view, invitation, role)\n }\n\n private issueAccess(\n view: SubmissionViewModel,\n invitation: FormInvitation,\n role: string | null,\n ): BeginAnonymousResult {\n const expiresAtSeconds = this.expiresAtSeconds()\n const accessToken = signAccessToken({\n submissionId: view.submission.id,\n invitationId: invitation.id,\n role,\n expiresAtSeconds,\n })\n return {\n view,\n invitation,\n accessToken,\n expiresAt: new Date(expiresAtSeconds * 1000).toISOString(),\n }\n }\n\n private expiresAtSeconds(): number {\n return Math.floor(this.now().getTime() / 1000) + getAccessTokenTtlSeconds()\n }\n\n private assertDistributionAvailable(distribution: FormDistribution): void {\n if (distribution.status !== 'active') {\n throw new DistributionServiceError('GONE', 'Distribution is not active.', 410, {\n status: distribution.status,\n })\n }\n const now = this.now().getTime()\n if (distribution.opensAt && now < distribution.opensAt.getTime()) {\n throw new DistributionServiceError('GONE', 'Distribution is not open yet.', 410)\n }\n if (distribution.closesAt && now > distribution.closesAt.getTime()) {\n throw new DistributionServiceError('GONE', 'Distribution is closed.', 410)\n }\n if (distribution.maxResponses != null && distribution.responseCount >= distribution.maxResponses) {\n throw new DistributionServiceError('GONE', 'Distribution response cap reached.', 410)\n }\n }\n\n private assertInvitationUsable(invitation: FormInvitation): void {\n if (INVITATION_TERMINAL_STATUSES.has(invitation.status)) {\n throw new DistributionServiceError('GONE', `Invitation is ${invitation.status}.`, 410, {\n status: invitation.status,\n })\n }\n if (invitation.expiresAt && this.now().getTime() > invitation.expiresAt.getTime()) {\n throw new DistributionServiceError('GONE', 'Invitation is expired.', 410)\n }\n }\n\n private async safeEmit<K extends keyof DistributionEvents>(\n eventId: K,\n payload: DistributionEvents[K],\n ): Promise<void> {\n try {\n await this.emitEvent(eventId, payload)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown emit error'\n this.logger.warn(\n { event: 'forms.distribution.event_emit_failed', eventId, message },\n 'forms distribution event emission failed',\n )\n }\n }\n}\n\n/** Brand check for DistributionServiceError \u2014 use instead of `instanceof`. */\nexport function isDistributionServiceError(error: unknown): error is DistributionServiceError {\n return typeof error === 'object' && error !== null && (DISTRIBUTION_SERVICE_ERROR_MARKER in error)\n}\n"],
5
- "mappings": "AAAA;AAmBA,SAAS,gBAAgB;AAGzB,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP,SAAS,wBAAwB,gBAAgB,yBAAyB;AAE1E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAcP,MAAM,oCAAoC,uBAAO,IAAI,8CAA8C;AAE5F,MAAM,kCAAiC,YAClC,wCADkC,IAAM;AAAA,EAMlD,YACE,MACA,SACA,YACA,SACA;AACA,UAAM,OAAO;AAXf,SAAU,MAAqC;AAY7C,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,aAAa;AAClB,SAAK,UAAU;AAAA,EACjB;AACF;AAgBA,MAAM,aAA+B;AAAA,EACnC,MAAM,MAAM;AAAA,EAAC;AAAA,EACb,MAAM,MAAM;AAAA,EAAC;AAAA,EACb,OAAO,MAAM;AAAA,EAAC;AAChB;AAEA,MAAM,+BAAoD,oBAAI,IAAI,CAAC,aAAa,WAAW,SAAS,CAAC;AAqB9F,MAAM,oBAAoB;AAAA,EAO/B,YAAY,SAAqC;AAC/C,SAAK,YAAY,QAAQ;AACzB,SAAK,oBAAoB,QAAQ;AACjC,SAAK,YAAY,QAAQ;AACzB,SAAK,MAAM,QAAQ,QAAQ,MAAM,oBAAI,KAAK;AAC1C,SAAK,SAAS,QAAQ,UAAU;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,aAAa,cAAyC;AACpD,QAAI,aAAa,SAAS,OAAQ,QAAO;AACzC,QAAI,aAAa,WAAW,SAAU,QAAO;AAC7C,QAAI,aAAa,oBAAqB,QAAO;AAC7C,WAAO,eAAe,kBAAkB,aAAa,QAAQ,CAAC;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,qBACJ,MAC0D;AAC1D,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,eAAe,MAAM,GAAG,QAAQ,kBAAkB;AAAA,MACtD,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,aAAc,QAAO,EAAE,gBAAgB,0BAA0B,YAAY,MAAM;AACxF,UAAM,aAAa,KAAK,aAAa,YAAY;AACjD,UAAM,iBAAiB,aACnB,uBAAuB,kBAAkB,aAAa,QAAQ,CAAC,IAC/D;AACJ,WAAO,EAAE,gBAAgB,WAAW;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,MAA2D;AAC7E,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,eAAe,MAAM,GAAG,QAAQ,kBAAkB;AAAA,MACtD,YAAY;AAAA,MACZ,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,yBAAyB,aAAa,2BAA2B,GAAG;AAAA,IAChF;AACA,SAAK,4BAA4B,YAAY;AAC7C,WAAO,EAAE,aAAa;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAe,UAA2F;AAC9G,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,YAAY,oBAAoB,QAAQ;AAG9C,UAAM,UAAU,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAC/C;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,yBAAyB,aAAa,yBAAyB,GAAG;AAAA,IAC9E;AACA,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA,EAAE,IAAI,QAAQ,IAAI,gBAAgB,QAAQ,gBAAgB,UAAU,QAAQ,UAAU,WAAW,KAAK;AAAA,MACtG;AAAA,MACA,EAAE,gBAAgB,QAAQ,gBAAgB,UAAU,QAAQ,SAAS;AAAA,IACvE;AACA,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,yBAAyB,aAAa,yBAAyB,GAAG;AAAA,IAC9E;AAEA,SAAK,uBAAuB,UAAU;AAEtC,UAAM,eAAe,MAAM,GAAG,QAAQ,kBAAkB;AAAA,MACtD,IAAI,WAAW;AAAA,MACf,gBAAgB,WAAW;AAAA,MAC3B,UAAU,WAAW;AAAA,MACrB,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,yBAAyB,aAAa,2BAA2B,GAAG;AAAA,IAChF;AACA,SAAK,4BAA4B,YAAY;AAG7C,QAAI,WAAW,WAAW,aAAa,WAAW,WAAW,QAAQ;AACnE,iBAAW,SAAS;AACpB,iBAAW,WAAW,KAAK,IAAI;AAC/B,YAAM,GAAG,MAAM;AACf,YAAM,KAAK,SAAS,2BAA2B,EAAE,cAAc,WAAW,GAAG,CAAC;AAAA,IAChF;AAEA,WAAO,EAAE,YAAY,aAAa;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAe,MAAsE;AACzF,UAAM,EAAE,aAAa,IAAI;AACzB,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,OAAO,MAAM,GAAG,QAAQ,MAAM;AAAA,MAClC,IAAI,aAAa;AAAA,MACjB,gBAAgB,aAAa;AAAA,MAC7B,UAAU,aAAa;AAAA,MACvB,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,yBAAyB,aAAa,mBAAmB,GAAG;AAAA,IACxE;AACA,QAAI,aAAa,iBAAiB;AAChC,aAAO,KAAK,kBAAkB,0BAA0B;AAAA,QACtD,gBAAgB,aAAa;AAAA,QAC7B,UAAU,aAAa;AAAA,QACvB,SAAS,KAAK;AAAA,QACd,iBAAiB,aAAa;AAAA,MAChC,CAAC;AAAA,IACH;AACA,WAAO,KAAK,kBAAkB,0BAA0B;AAAA,MACtD,gBAAgB,aAAa;AAAA,MAC7B,UAAU,aAAa;AAAA,MACvB,SAAS,KAAK;AAAA,IAChB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,eAAe,MAAyD;AAC5E,UAAM,EAAE,aAAa,IAAI;AACzB,SAAK,4BAA4B,YAAY;AAE7C,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,OAAO,MAAM,GAAG,QAAQ,MAAM;AAAA,MAClC,IAAI,aAAa;AAAA,MACjB,gBAAgB,aAAa;AAAA,MAC7B,UAAU,aAAa;AAAA,MACvB,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,yBAAyB,aAAa,mBAAmB,GAAG;AAAA,IACxE;AAEA,QAAI;AACJ,QAAI,KAAK,YAAY;AACnB,mBAAa,KAAK;AAClB,WAAK,uBAAuB,UAAU;AAEtC,UAAI,WAAW,cAAc;AAC3B,eAAO,KAAK,eAAe,cAAc,KAAK,KAAK,UAAU;AAAA,MAC/D;AAAA,IACF,OAAO;AACL,YAAMA,OAAM,KAAK,IAAI;AACrB,mBAAa,GAAG,OAAO,gBAAgB;AAAA,QACrC,gBAAgB,aAAa;AAAA,QAC7B,gBAAgB,aAAa;AAAA,QAC7B,UAAU,aAAa;AAAA,QACvB,gBAAgB;AAAA,QAChB,eAAe;AAAA,QACf,cAAc;AAAA,QACd,MAAM;AAAA,QACN,WAAW;AAAA,QACX,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,QAAQ,KAAK,UAAU,aAAa,iBAAiB;AAAA,QACrD,WAAW;AAAA,QACX,WAAW;AAAA,QACX,WAAWA;AAAA,QACX,WAAWA;AAAA,MACb,CAAC;AACD,SAAG,QAAQ,UAAU;AACrB,YAAM,GAAG,MAAM;AAAA,IACjB;AAEA,UAAM,OAAO,MAAM,KAAK,kBAAkB,MAAM;AAAA,MAC9C,gBAAgB,aAAa;AAAA,MAC7B,UAAU,aAAa;AAAA,MACvB,SAAS,KAAK;AAAA,MACd,aAAa;AAAA,MACb,WAAW,WAAW;AAAA,MACtB,WAAW,WAAW;AAAA,MACtB,aAAa,WAAW,QAAQ;AAAA,MAChC,iBAAiB,aAAa,mBAAmB;AAAA,IACnD,CAAC;AAED,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,eAAe,KAAK,WAAW;AAC1C,eAAW,SAAS;AACpB,eAAW,YAAY;AACvB,QAAI,CAAC,WAAW,OAAQ,YAAW,SAAS,KAAK,UAAU,aAAa,iBAAiB;AACzF,UAAM,GAAG,MAAM;AAEf,UAAM,OAAO,KAAK,OAAO,CAAC,GAAG,QAAQ,WAAW,QAAQ;AACxD,WAAO,KAAK,YAAY,MAAM,YAAY,IAAI;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,oBAAoB,MAA2F;AACnH,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,GAAG,cAAc,OAAO,QAAQ;AACpC,YAAM,eAAe,MAAM,IAAI;AAAA,QAC7B;AAAA,QACA;AAAA,UACE,IAAI,KAAK;AAAA,UACT,gBAAgB,KAAK;AAAA,UACrB,UAAU,KAAK;AAAA,UACf,WAAW;AAAA,QACb;AAAA,QACA,EAAE,UAAU,SAAS,kBAAkB;AAAA,MACzC;AACA,UAAI,CAAC,cAAc;AACjB,cAAM,IAAI,yBAAyB,aAAa,2BAA2B,GAAG;AAAA,MAChF;AACA,WAAK,4BAA4B,YAAY;AAC7C,mBAAa,iBAAiB;AAC9B,YAAM,IAAI,MAAM;AAAA,IAClB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,wBAAwB,MAKZ;AAChB,UAAM,KAAK,KAAK,UAAU;AAC1B,UAAM,aAAa,MAAM,GAAG,QAAQ,gBAAgB;AAAA,MAClD,IAAI,KAAK;AAAA,MACT,gBAAgB,KAAK;AAAA,MACrB,UAAU,KAAK;AAAA,MACf,WAAW;AAAA,IACb,CAAC;AACD,QAAI,CAAC,YAAY;AACf,YAAM,IAAI,yBAAyB,aAAa,yBAAyB,GAAG;AAAA,IAC9E;AACA,eAAW,SAAS;AACpB,eAAW,cAAc,KAAK,IAAI;AAClC,UAAM,GAAG,MAAM;AACf,UAAM,KAAK,SAAS,8BAA8B;AAAA,MAChD,cAAc,WAAW;AAAA,MACzB,cAAc,KAAK;AAAA,IACrB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,cAAsB,cAAsB,MAA6B;AAC1F,WAAO,gBAAgB;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA,kBAAkB,KAAK,iBAAiB;AAAA,IAC1C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,eACZ,cACA,SACA,YAC+B;AAC/B,UAAM,OAAO,MAAM,KAAK,kBAAkB,WAAW;AAAA,MACnD,gBAAgB,WAAW;AAAA,MAC3B,UAAU,WAAW;AAAA,MACrB,cAAc,WAAW;AAAA,MACzB,YAAY,WAAW,QAAQ;AAAA,MAC/B,cAAc,WAAW;AAAA,IAC3B,CAAC;AACD,SAAK;AACL,SAAK;AACL,UAAM,OAAO,KAAK,OAAO,KAAK,CAAC,UAAU,MAAM,WAAW,WAAW,EAAE,GAAG,QACrE,WAAW,QACX,KAAK,OAAO,CAAC,GAAG,QAChB;AACL,WAAO,KAAK,YAAY,MAAM,YAAY,IAAI;AAAA,EAChD;AAAA,EAEQ,YACN,MACA,YACA,MACsB;AACtB,UAAM,mBAAmB,KAAK,iBAAiB;AAC/C,UAAM,cAAc,gBAAgB;AAAA,MAClC,cAAc,KAAK,WAAW;AAAA,MAC9B,cAAc,WAAW;AAAA,MACzB;AAAA,MACA;AAAA,IACF,CAAC;AACD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,IAAI,KAAK,mBAAmB,GAAI,EAAE,YAAY;AAAA,IAC3D;AAAA,EACF;AAAA,EAEQ,mBAA2B;AACjC,WAAO,KAAK,MAAM,KAAK,IAAI,EAAE,QAAQ,IAAI,GAAI,IAAI,yBAAyB;AAAA,EAC5E;AAAA,EAEQ,4BAA4B,cAAsC;AACxE,QAAI,aAAa,WAAW,UAAU;AACpC,YAAM,IAAI,yBAAyB,QAAQ,+BAA+B,KAAK;AAAA,QAC7E,QAAQ,aAAa;AAAA,MACvB,CAAC;AAAA,IACH;AACA,UAAM,MAAM,KAAK,IAAI,EAAE,QAAQ;AAC/B,QAAI,aAAa,WAAW,MAAM,aAAa,QAAQ,QAAQ,GAAG;AAChE,YAAM,IAAI,yBAAyB,QAAQ,iCAAiC,GAAG;AAAA,IACjF;AACA,QAAI,aAAa,YAAY,MAAM,aAAa,SAAS,QAAQ,GAAG;AAClE,YAAM,IAAI,yBAAyB,QAAQ,2BAA2B,GAAG;AAAA,IAC3E;AACA,QAAI,aAAa,gBAAgB,QAAQ,aAAa,iBAAiB,aAAa,cAAc;AAChG,YAAM,IAAI,yBAAyB,QAAQ,sCAAsC,GAAG;AAAA,IACtF;AAAA,EACF;AAAA,EAEQ,uBAAuB,YAAkC;AAC/D,QAAI,6BAA6B,IAAI,WAAW,MAAM,GAAG;AACvD,YAAM,IAAI,yBAAyB,QAAQ,iBAAiB,WAAW,MAAM,KAAK,KAAK;AAAA,QACrF,QAAQ,WAAW;AAAA,MACrB,CAAC;AAAA,IACH;AACA,QAAI,WAAW,aAAa,KAAK,IAAI,EAAE,QAAQ,IAAI,WAAW,UAAU,QAAQ,GAAG;AACjF,YAAM,IAAI,yBAAyB,QAAQ,0BAA0B,GAAG;AAAA,IAC1E;AAAA,EACF;AAAA,EAEA,MAAc,SACZ,SACA,SACe;AACf,QAAI;AACF,YAAM,KAAK,UAAU,SAAS,OAAO;AAAA,IACvC,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;AACzD,WAAK,OAAO;AAAA,QACV,EAAE,OAAO,wCAAwC,SAAS,QAAQ;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,2BAA2B,OAAmD;AAC5F,SAAO,OAAO,UAAU,YAAY,UAAU,QAAS,qCAAqC;AAC9F;",
4
+ "sourcesContent": ["/**\n * Forms DistributionService \u2014 phase 2d.\n *\n * Orchestrates the distribution / invitation layer that puts a published form\n * in front of an unauthenticated participant and bootstraps an anonymous\n * submission by delegating to the unchanged `SubmissionService`.\n *\n * Invariants:\n * - Every query is tenant-scoped (`organization_id` + `tenant_id`) EXCEPT the\n * two deliberately-global lookups \u2014 by random `public_slug` and by\n * `token_hash` \u2014 both high-entropy and globally unique in practice. After a\n * global lookup the org/tenant scope is taken from the found row and used\n * for every subsequent query.\n * - Recipient PII (`recipient_email` / `recipient_name`) is encrypted via the\n * GLOBAL pipeline; reads that need the plaintext use `findOneWithDecryption`.\n * - The submission access token never carries org/tenant \u2014 those are\n * re-derived from the persisted submission downstream (see runtime-principal).\n */\n\nimport { LockMode } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n Form,\n FormDistribution,\n FormInvitation,\n FormVersion,\n} from '../data/entities'\nimport type { CompiledFormVersion } from './form-version-compiler'\nimport type { SubmissionService, SubmissionViewModel } from './submission-service'\nimport type { StructuredLogger } from '../lib/log-redaction'\nimport { buildFrameAncestorsCsp, isEmbedEnabled, readEmbedSettings } from '../lib/embed-frame-policy'\nimport { formsEventPayloadSchemas } from '../events-payloads'\nimport {\n getAccessTokenTtlSeconds,\n hashInvitationToken,\n signAccessToken,\n} from './distribution-token'\n\nexport type DistributionServiceErrorCode =\n | 'NOT_FOUND'\n | 'GONE'\n | 'INVALID_STATUS'\n\n/**\n * `Symbol.for` so the brand survives module duplication across bundle\n * boundaries. `instanceof` does NOT: Next can load this module in more than\n * one chunk, and once a second copy of the class exists an error thrown\n * through one copy fails `instanceof` against the other \u2014 silently\n * downgrading a mapped domain error to a 500. Mirrors `CrudHttpError`.\n */\nconst DISTRIBUTION_SERVICE_ERROR_MARKER = Symbol.for('@open-mercato/forms/DistributionServiceError')\n\nexport class DistributionServiceError extends Error {\n readonly [DISTRIBUTION_SERVICE_ERROR_MARKER] = true\n readonly code: DistributionServiceErrorCode\n readonly httpStatus: number\n readonly details?: Record<string, unknown>\n\n constructor(\n code: DistributionServiceErrorCode,\n message: string,\n httpStatus: number,\n details?: Record<string, unknown>,\n ) {\n super(message)\n this.name = 'DistributionServiceError'\n this.code = code\n this.httpStatus = httpStatus\n this.details = details\n }\n}\n\ntype DistributionEvents = {\n 'forms.invitation.opened': z.infer<typeof formsEventPayloadSchemas['forms.invitation.opened']>\n 'forms.invitation.submitted': z.infer<typeof formsEventPayloadSchemas['forms.invitation.submitted']>\n}\n\nexport type DistributionServiceOptions = {\n emFactory: () => EntityManager\n submissionService: SubmissionService\n /** Pluggable, schema-validated event emitter \u2014 wired in di.ts. */\n emitEvent: <K extends keyof DistributionEvents>(eventId: K, payload: DistributionEvents[K]) => Promise<void> | void\n now?: () => Date\n logger?: StructuredLogger\n}\n\nconst noopLogger: StructuredLogger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n\nexport const INVITATION_ACCESS_CLOSED_STATUSES: ReadonlySet<string> = new Set(['revoked', 'expired'])\n\nconst INVITATION_TERMINAL_STATUSES: ReadonlySet<string> = new Set(['submitted', ...INVITATION_ACCESS_CLOSED_STATUSES])\n\nexport type BeginAnonymousArgs = {\n distribution: FormDistribution\n invitation?: FormInvitation\n locale?: string | null\n}\n\nexport type BeginAnonymousResult = {\n view: SubmissionViewModel\n invitation: FormInvitation\n accessToken: string\n expiresAt: string\n}\n\nexport type FormContextResult = {\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n}\n\nexport class DistributionService {\n private readonly emFactory: () => EntityManager\n private readonly submissionService: SubmissionService\n private readonly emitEvent: DistributionServiceOptions['emitEvent']\n private readonly now: () => Date\n private readonly logger: StructuredLogger\n\n constructor(options: DistributionServiceOptions) {\n this.emFactory = options.emFactory\n this.submissionService = options.submissionService\n this.emitEvent = options.emitEvent\n this.now = options.now ?? (() => new Date())\n this.logger = options.logger ?? noopLogger\n }\n\n // --------------------------------------------------------------------------\n // Embed eligibility (spec D5 / R-RS-2)\n // --------------------------------------------------------------------------\n\n /**\n * True only when a distribution may be framed on a third-party website:\n * an active OPEN distribution that does not require customer auth and has\n * opted in via a valid, non-empty `settings.embed` allowlist. There is no\n * session inside a third-party iframe, so personal / auth-required / disabled\n * distributions are never embeddable.\n */\n isEmbeddable(distribution: FormDistribution): boolean {\n if (distribution.mode !== 'open') return false\n if (distribution.status !== 'active') return false\n if (distribution.requireCustomerAuth) return false\n return isEmbedEnabled(readEmbedSettings(distribution.settings))\n }\n\n /**\n * Resolves the `frame-ancestors` CSP directive for the `/embed/:slug` host\n * page WITHOUT enforcing the runtime availability window \u2014 framing is\n * permitted for any config-embeddable distribution (open + active + !auth +\n * opted-in allowlist) so the iframe can still load and render a graceful\n * \"unavailable\" state for a capped / out-of-window form. Fails closed\n * (`frame-ancestors 'none'`) for unknown or non-embeddable slugs.\n */\n async getEmbedPolicyBySlug(\n slug: string,\n ): Promise<{ frameAncestors: string; embeddable: boolean }> {\n const em = this.emFactory()\n const distribution = await em.findOne(FormDistribution, {\n publicSlug: slug,\n deletedAt: null,\n })\n if (!distribution) return { frameAncestors: \"frame-ancestors 'none'\", embeddable: false }\n const embeddable = this.isEmbeddable(distribution)\n const frameAncestors = embeddable\n ? buildFrameAncestorsCsp(readEmbedSettings(distribution.settings))\n : \"frame-ancestors 'none'\"\n return { frameAncestors, embeddable }\n }\n\n // --------------------------------------------------------------------------\n // Resolve \u2014 open link (global slug lookup)\n // --------------------------------------------------------------------------\n\n async resolveBySlug(slug: string): Promise<{ distribution: FormDistribution }> {\n const em = this.emFactory()\n const distribution = await em.findOne(FormDistribution, {\n publicSlug: slug,\n deletedAt: null,\n })\n if (!distribution) {\n throw new DistributionServiceError('NOT_FOUND', 'Distribution not found.', 404)\n }\n this.assertDistributionAvailable(distribution)\n return { distribution }\n }\n\n // --------------------------------------------------------------------------\n // Resolve \u2014 personal invitation (global token-hash lookup)\n // --------------------------------------------------------------------------\n\n async resolveByToken(rawToken: string): Promise<{ invitation: FormInvitation; distribution: FormDistribution }> {\n const em = this.emFactory()\n const tokenHash = hashInvitationToken(rawToken)\n // Locate the row first (no scope) to derive org/tenant, then re-read with\n // decryption scoped to that row so recipient PII is available to the caller.\n const located = await em.findOne(FormInvitation, {\n tokenHash,\n deletedAt: null,\n })\n if (!located) {\n throw new DistributionServiceError('NOT_FOUND', 'Invitation not found.', 404)\n }\n const invitation = await findOneWithDecryption(\n em,\n FormInvitation,\n { id: located.id, organizationId: located.organizationId, tenantId: located.tenantId, deletedAt: null },\n undefined,\n { organizationId: located.organizationId, tenantId: located.tenantId },\n )\n if (!invitation) {\n throw new DistributionServiceError('NOT_FOUND', 'Invitation not found.', 404)\n }\n\n this.assertInvitationUsable(invitation)\n\n const distribution = await em.findOne(FormDistribution, {\n id: invitation.distributionId,\n organizationId: invitation.organizationId,\n tenantId: invitation.tenantId,\n deletedAt: null,\n })\n if (!distribution) {\n throw new DistributionServiceError('NOT_FOUND', 'Distribution not found.', 404)\n }\n this.assertDistributionAvailable(distribution)\n\n // Mark opened on first resolve from a pre-fill status.\n if (invitation.status === 'pending' || invitation.status === 'sent') {\n invitation.status = 'opened'\n invitation.openedAt = this.now()\n await em.flush()\n await this.safeEmit('forms.invitation.opened', { invitationId: invitation.id })\n }\n\n return { invitation, distribution }\n }\n\n // --------------------------------------------------------------------------\n // Form context \u2014 resolve the served form version for a distribution\n // --------------------------------------------------------------------------\n\n async getFormContext(args: { distribution: FormDistribution }): Promise<FormContextResult> {\n const { distribution } = args\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n id: distribution.formId,\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n deletedAt: null,\n })\n if (!form) {\n throw new DistributionServiceError('NOT_FOUND', 'Form not found.', 404)\n }\n if (distribution.pinnedVersionId) {\n return this.submissionService.getPinnedFormVersionByKey({\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n formKey: form.key,\n pinnedVersionId: distribution.pinnedVersionId,\n })\n }\n return this.submissionService.getActiveFormVersionByKey({\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n formKey: form.key,\n })\n }\n\n // --------------------------------------------------------------------------\n // Begin anonymous \u2014 mint/link invitation, start submission, sign token\n // --------------------------------------------------------------------------\n\n async beginAnonymous(args: BeginAnonymousArgs): Promise<BeginAnonymousResult> {\n const { distribution } = args\n this.assertDistributionAvailable(distribution)\n\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n id: distribution.formId,\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n deletedAt: null,\n })\n if (!form) {\n throw new DistributionServiceError('NOT_FOUND', 'Form not found.', 404)\n }\n\n let invitation: FormInvitation\n if (args.invitation) {\n invitation = args.invitation\n this.assertInvitationUsable(invitation)\n // Resume: invitation already has a submission \u2014 return the existing one.\n if (invitation.submissionId) {\n return this.resumeExisting(distribution, form.key, invitation)\n }\n } else {\n const now = this.now()\n invitation = em.create(FormInvitation, {\n distributionId: distribution.id,\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n recipientEmail: null,\n recipientName: null,\n recipientRef: null,\n role: null,\n tokenHash: null,\n status: 'started',\n submissionId: null,\n locale: args.locale ?? distribution.defaultLocale ?? null,\n sendCount: 0,\n createdBy: null,\n createdAt: now,\n updatedAt: now,\n })\n em.persist(invitation)\n await em.flush()\n }\n\n const view = await this.submissionService.start({\n organizationId: distribution.organizationId,\n tenantId: distribution.tenantId,\n formKey: form.key,\n subjectType: 'forms_invitation',\n subjectId: invitation.id,\n startedBy: invitation.id,\n initialRole: invitation.role ?? null,\n pinnedVersionId: distribution.pinnedVersionId ?? undefined,\n })\n\n const now = this.now()\n invitation.submissionId = view.submission.id\n invitation.status = 'started'\n invitation.startedAt = now\n if (!invitation.locale) invitation.locale = args.locale ?? distribution.defaultLocale ?? null\n await em.flush()\n\n const role = view.actors[0]?.role ?? invitation.role ?? null\n return this.issueAccess(view, invitation, role)\n }\n\n // --------------------------------------------------------------------------\n // Reserve a response slot (atomic cap enforcement \u2014 R-2d-5)\n // --------------------------------------------------------------------------\n\n async reserveResponseSlot(args: { distributionId: string; organizationId: string; tenantId: string }): Promise<void> {\n const em = this.emFactory()\n await em.transactional(async (trx) => {\n const distribution = await trx.findOne(\n FormDistribution,\n {\n id: args.distributionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!distribution) {\n throw new DistributionServiceError('NOT_FOUND', 'Distribution not found.', 404)\n }\n this.assertDistributionAvailable(distribution)\n distribution.responseCount += 1\n await trx.flush()\n })\n }\n\n // --------------------------------------------------------------------------\n // Mark invitation submitted\n // --------------------------------------------------------------------------\n\n async markInvitationSubmitted(args: {\n invitationId: string\n organizationId: string\n tenantId: string\n submissionId: string\n }): Promise<void> {\n const em = this.emFactory()\n const marked = await em.transactional(async (trx) => {\n const invitation = await trx.findOne(\n FormInvitation,\n {\n id: args.invitationId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!invitation) {\n throw new DistributionServiceError('NOT_FOUND', 'Invitation not found.', 404)\n }\n if (INVITATION_ACCESS_CLOSED_STATUSES.has(invitation.status)) {\n this.logger.warn(\n {\n event: 'forms.distribution.invitation_submit_skipped',\n invitationId: invitation.id,\n submissionId: args.submissionId,\n status: invitation.status,\n },\n 'Invitation access closed before submit; status left unchanged.',\n )\n return false\n }\n invitation.status = 'submitted'\n invitation.submittedAt = this.now()\n await trx.flush()\n return true\n })\n if (!marked) return\n await this.safeEmit('forms.invitation.submitted', {\n invitationId: args.invitationId,\n submissionId: args.submissionId,\n })\n }\n\n // --------------------------------------------------------------------------\n // Slide the access-token expiry (used by the autosave route)\n // --------------------------------------------------------------------------\n\n refreshAccessToken(submissionId: string, invitationId: string, role: string | null): string {\n return signAccessToken({\n submissionId,\n invitationId,\n role,\n expiresAtSeconds: this.expiresAtSeconds(),\n })\n }\n\n // --------------------------------------------------------------------------\n // Helpers\n // --------------------------------------------------------------------------\n\n private async resumeExisting(\n distribution: FormDistribution,\n formKey: string,\n invitation: FormInvitation,\n ): Promise<BeginAnonymousResult> {\n const view = await this.submissionService.getCurrent({\n organizationId: invitation.organizationId,\n tenantId: invitation.tenantId,\n submissionId: invitation.submissionId as string,\n viewerRole: invitation.role ?? null,\n viewerUserId: invitation.id,\n })\n void distribution\n void formKey\n const role = view.actors.find((actor) => actor.userId === invitation.id)?.role\n ?? invitation.role\n ?? view.actors[0]?.role\n ?? null\n return this.issueAccess(view, invitation, role)\n }\n\n private issueAccess(\n view: SubmissionViewModel,\n invitation: FormInvitation,\n role: string | null,\n ): BeginAnonymousResult {\n const expiresAtSeconds = this.expiresAtSeconds()\n const accessToken = signAccessToken({\n submissionId: view.submission.id,\n invitationId: invitation.id,\n role,\n expiresAtSeconds,\n })\n return {\n view,\n invitation,\n accessToken,\n expiresAt: new Date(expiresAtSeconds * 1000).toISOString(),\n }\n }\n\n private expiresAtSeconds(): number {\n return Math.floor(this.now().getTime() / 1000) + getAccessTokenTtlSeconds()\n }\n\n private assertDistributionAvailable(distribution: FormDistribution): void {\n if (distribution.status !== 'active') {\n throw new DistributionServiceError('GONE', 'Distribution is not active.', 410, {\n status: distribution.status,\n })\n }\n const now = this.now().getTime()\n if (distribution.opensAt && now < distribution.opensAt.getTime()) {\n throw new DistributionServiceError('GONE', 'Distribution is not open yet.', 410)\n }\n if (distribution.closesAt && now > distribution.closesAt.getTime()) {\n throw new DistributionServiceError('GONE', 'Distribution is closed.', 410)\n }\n if (distribution.maxResponses != null && distribution.responseCount >= distribution.maxResponses) {\n throw new DistributionServiceError('GONE', 'Distribution response cap reached.', 410)\n }\n }\n\n private assertInvitationUsable(invitation: FormInvitation): void {\n if (INVITATION_TERMINAL_STATUSES.has(invitation.status)) {\n throw new DistributionServiceError('GONE', `Invitation is ${invitation.status}.`, 410, {\n status: invitation.status,\n })\n }\n if (invitation.expiresAt && this.now().getTime() > invitation.expiresAt.getTime()) {\n throw new DistributionServiceError('GONE', 'Invitation is expired.', 410)\n }\n }\n\n private async safeEmit<K extends keyof DistributionEvents>(\n eventId: K,\n payload: DistributionEvents[K],\n ): Promise<void> {\n try {\n await this.emitEvent(eventId, payload)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown emit error'\n this.logger.warn(\n { event: 'forms.distribution.event_emit_failed', eventId, message },\n 'forms distribution event emission failed',\n )\n }\n }\n}\n\n/** Brand check for DistributionServiceError \u2014 use instead of `instanceof`. */\nexport function isDistributionServiceError(error: unknown): error is DistributionServiceError {\n return typeof error === 'object' && error !== null && (DISTRIBUTION_SERVICE_ERROR_MARKER in error)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["now"]
7
7
  }
@@ -13,6 +13,7 @@ import {
13
13
  sliceByVisibility
14
14
  } from "./visibility-resolver.js";
15
15
  import { buildTamperingMarker } from "../lib/log-redaction.js";
16
+ import { isPortalSubmitAllowed } from "../lib/portal-subject-policy.js";
16
17
  import { validateFieldValue } from "./field-validation-service.js";
17
18
  const DEFAULT_AUTOSAVE_INTERVAL_MS = (() => {
18
19
  const raw = process.env.FORMS_AUTOSAVE_INTERVAL_MS;
@@ -459,6 +460,9 @@ class SubmissionService {
459
460
  if (!submission) {
460
461
  throw new SubmissionServiceError("NOT_FOUND", "Submission not found.", 404);
461
462
  }
463
+ if (args.submittedByPortalCustomer && !isPortalSubmitAllowed(args.submittedBy, submission)) {
464
+ throw new SubmissionServiceError("SUBJECT_FORBIDDEN", "Submission subject is not the caller's own customer subject.", 403);
465
+ }
462
466
  if (submission.status === "submitted") {
463
467
  throw new SubmissionServiceError("INVALID_STATUS", "Submission already submitted.", 422);
464
468
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/forms/services/submission-service.ts"],
4
- "sourcesContent": ["/**\n * Forms module SubmissionService \u2014 owns the start/save/submit lifecycle.\n *\n * Critical invariants enforced here:\n * - Append-only revision chain (only allowed UPDATE paths are anonymize\n * in phase 2b and the coalesce-after-cap branch in this file).\n * - Tenant-scoped queries on every read/write.\n * - Server-derived `saved_by_role` from active actor row \u2014 never read from\n * client input.\n * - Optimistic concurrency via `base_revision_id`.\n * - Tampering markers logged when patches contain non-editable field keys.\n * - Per-tenant envelope encryption of revision payloads.\n *\n * The service is intentionally thin on cross-cutting concerns: emit hooks\n * are exposed via `eventEmitter` and pluggable audit-access logger\n * (`auditAccess`) so phase 2b can replace it without rewriting the service.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport { LockMode } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport {\n Form,\n FormSubmission,\n FormSubmissionActor,\n FormSubmissionRevision,\n FormVersion,\n} from '../data/entities'\nimport {\n FormVersionCompiler,\n type CompiledFormVersion,\n} from './form-version-compiler'\nimport type { EncryptionService } from './encryption-service'\nimport { RolePolicyService } from './role-policy-service'\nimport {\n resolveVisibleFieldKeys,\n sliceByVisibility,\n type VisibilitySchemaNode,\n} from './visibility-resolver'\nimport { buildTamperingMarker, type StructuredLogger } from '../lib/log-redaction'\nimport { formsEventPayloadSchemas } from '../events-payloads'\nimport { validateFieldValue } from './field-validation-service'\nimport type { FieldNode } from '../backend/forms/[id]/studio/schema-helpers'\n\nconst DEFAULT_AUTOSAVE_INTERVAL_MS = (() => {\n const raw = process.env.FORMS_AUTOSAVE_INTERVAL_MS\n if (!raw) return 10_000\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 10_000\n})()\n\nconst DEFAULT_REVISION_CAP = (() => {\n const raw = process.env.FORMS_REVISION_CAP\n if (!raw) return 10_000\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 10_000\n})()\n\nconst MAX_SUBMISSION_PAYLOAD_BYTES = (() => {\n const raw = process.env.FORMS_MAX_SUBMISSION_PAYLOAD_BYTES\n if (!raw) return 256 * 1024\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 256 * 1024\n})()\n\nexport type SubmissionServiceErrorCode =\n | 'STALE_BASE'\n | 'VALIDATION_FAILED'\n | 'RATE_LIMITED'\n | 'NOT_FOUND'\n | 'NO_ACTOR'\n | 'INVALID_STATUS'\n | 'INVALID_ROLE'\n | 'FORM_INACTIVE'\n | 'FORM_VERSION_NOT_PUBLISHED'\n\n/**\n * `Symbol.for` so the brand survives module duplication across bundle\n * boundaries. `instanceof` does NOT: Next can load this module in more than\n * one chunk, and once a second copy of the class exists an error thrown\n * through one copy fails `instanceof` against the other \u2014 silently\n * downgrading a mapped domain error to a 500. Mirrors `CrudHttpError`.\n */\nconst SUBMISSION_SERVICE_ERROR_MARKER = Symbol.for('@open-mercato/forms/SubmissionServiceError')\n\nexport class SubmissionServiceError extends Error {\n readonly [SUBMISSION_SERVICE_ERROR_MARKER] = true\n readonly code: SubmissionServiceErrorCode\n readonly httpStatus: number\n readonly details?: Record<string, unknown>\n\n constructor(code: SubmissionServiceErrorCode, message: string, httpStatus: number, details?: Record<string, unknown>) {\n super(message)\n this.name = 'SubmissionServiceError'\n this.code = code\n this.httpStatus = httpStatus\n this.details = details\n }\n}\n\ntype Scope = {\n tenantId: string\n organizationId: string\n}\n\nexport type StartArgs = Scope & {\n formKey: string\n subjectType: string\n subjectId: string\n startedBy: string\n /** Optional initial role override \u2014 when present, MUST be in form_version.roles. */\n initialRole?: string | null\n /**\n * Optional published version to pin this submission to. When present, the\n * form is still resolved by key (for scope + status checks) but the\n * specified version is compiled instead of the form's current published\n * version. Used by distribution flows (phase 2d) to lock a campaign to a\n * particular version. When absent, behavior is unchanged.\n */\n pinnedVersionId?: string | null\n /**\n * Optional initial values keyed by field key (W8 / FD-1 prefill). Resolved\n * by the route from `x-om-prefill` declarations + the injected\n * `PrefillResolver`. Applied to the initial revision after being filtered\n * through the participant role's `filterWritePatch` (only fields the\n * participant may edit are kept) and validated by AJV. Backward-compatible:\n * absent / empty \u21D2 the initial revision payload stays `{}` as before.\n */\n prefill?: Record<string, unknown> | null\n}\n\nexport type SaveArgs = Scope & {\n submissionId: string\n baseRevisionId: string\n patch: Record<string, unknown>\n savedBy: string\n changeSummary?: string | null\n changeSource?: 'user' | 'admin' | 'system'\n}\n\nexport type SubmitArgs = Scope & {\n submissionId: string\n baseRevisionId: string\n submittedBy: string\n submitMetadata?: Record<string, unknown> | null\n}\n\nexport type ReopenArgs = Scope & {\n submissionId: string\n reopenedBy: string\n}\n\nexport type AssignActorArgs = Scope & {\n submissionId: string\n userId: string\n role: string\n assignedBy: string\n}\n\nexport type RevokeActorArgs = Scope & {\n submissionId: string\n actorId: string\n revokedBy: string\n}\n\nexport type GetCurrentArgs = Scope & {\n submissionId: string\n /** When provided, slice the response to only this role's visible fields. */\n viewerRole?: string | null\n /**\n * User reading. Used for the audit-log call site, and \u2014 when `viewerRole` is\n * absent \u2014 to derive the slice from that user's active actor row, which is\n * the authority on what role a viewer actually holds.\n */\n viewerUserId?: string | null\n /**\n * Which surface is reading, for the access-audit row. `getCurrent` used to\n * hard-code `'runtime'`, so the admin submission-detail read \u2014 the primary\n * staff read of a submission's answers \u2014 was recorded as runtime traffic or,\n * for an auditor filtering on `'admin'`, not visibly recorded at all.\n * Defaults to `'runtime'` so the public and portal call sites are unchanged.\n */\n surface?: 'admin' | 'runtime'\n /**\n * TRUSTED INTERNAL CALLERS ONLY: return the full decoded payload with no role\n * slice.\n *\n * There is exactly one legitimate use \u2014 the consent projector in `di.ts`,\n * which has no viewer at all and must see `signature` answers to write\n * `forms_consent_record` rows. It is an explicit opt-in rather than the\n * default-when-unspecified it used to be, because that default silently\n * handed the full decrypted payload to any caller that could not name a role,\n * including a portal customer with no actor row on the submission.\n *\n * Never set this from anything reachable by a request principal.\n */\n unsliced?: boolean\n}\n\nexport type SubmissionViewModel = {\n submission: FormSubmission\n revision: FormSubmissionRevision\n decodedData: Record<string, unknown>\n actors: FormSubmissionActor[]\n formVersion: FormVersion\n}\n\nexport type RevisionInsertOutcome = {\n revision: FormSubmissionRevision\n coalesced: boolean\n}\n\nexport type SubmissionEvents = {\n 'forms.submission.started': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.started']>) => Promise<void> | void\n 'forms.submission.revision_appended': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.revision_appended']>) => Promise<void> | void\n 'forms.submission.submitted': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.submitted']>) => Promise<void> | void\n 'forms.submission.reopened': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.reopened']>) => Promise<void> | void\n 'forms.submission.actor_assigned': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.actor_assigned']>) => Promise<void> | void\n}\n\nexport type AuditAccessHook = (args: {\n submissionId: string\n organizationId: string\n tenantId: string\n viewerUserId: string | null\n viewerRole: string | null\n surface: 'admin' | 'runtime'\n scope: 'submission' | 'revision'\n}) => Promise<void> | void\n\nexport type SubmissionServiceOptions = {\n emFactory: () => EntityManager\n formVersionCompiler: FormVersionCompiler\n encryptionService: EncryptionService\n rolePolicyService: RolePolicyService\n /** Pluggable event emitter \u2014 wires to the typed forms events catalog. */\n emitEvent: <K extends keyof SubmissionEvents>(eventId: K, payload: Parameters<SubmissionEvents[K]>[0]) => Promise<void> | void\n /** Phase 2b replaces this with a real audit logger. Phase 1c uses a no-op. */\n auditAccess?: AuditAccessHook\n logger?: StructuredLogger\n /** Override the clock for tests. */\n now?: () => Date\n autosaveIntervalMs?: number\n revisionCap?: number\n}\n\nconst noopAuditAccess: AuditAccessHook = () => {}\n\nconst noopLogger: StructuredLogger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n\nexport class SubmissionService {\n private readonly emFactory: () => EntityManager\n private readonly compiler: FormVersionCompiler\n private readonly encryption: EncryptionService\n private readonly rolePolicy: RolePolicyService\n private readonly emitEvent: SubmissionServiceOptions['emitEvent']\n private readonly auditAccess: AuditAccessHook\n private readonly logger: StructuredLogger\n private readonly now: () => Date\n private readonly autosaveIntervalMs: number\n private readonly revisionCap: number\n\n constructor(options: SubmissionServiceOptions) {\n this.emFactory = options.emFactory\n this.compiler = options.formVersionCompiler\n this.encryption = options.encryptionService\n this.rolePolicy = options.rolePolicyService\n this.emitEvent = options.emitEvent\n this.auditAccess = options.auditAccess ?? noopAuditAccess\n this.logger = options.logger ?? noopLogger\n this.now = options.now ?? (() => new Date())\n this.autosaveIntervalMs = options.autosaveIntervalMs ?? DEFAULT_AUTOSAVE_INTERVAL_MS\n this.revisionCap = options.revisionCap ?? DEFAULT_REVISION_CAP\n }\n\n // --------------------------------------------------------------------------\n // Active form-version lookup\n // --------------------------------------------------------------------------\n\n async getActiveFormVersionByKey(args: Scope & { formKey: string }): Promise<{\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n }> {\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n key: args.formKey,\n deletedAt: null,\n })\n if (!form) {\n throw new SubmissionServiceError('NOT_FOUND', `Form \"${args.formKey}\" not found.`, 404)\n }\n if (form.status !== 'active') {\n throw new SubmissionServiceError('FORM_INACTIVE', `Form \"${args.formKey}\" is not active.`, 422)\n }\n if (!form.currentPublishedVersionId) {\n throw new SubmissionServiceError(\n 'FORM_VERSION_NOT_PUBLISHED',\n `Form \"${args.formKey}\" has no published version.`,\n 422,\n )\n }\n const formVersion = await em.findOne(FormVersion, {\n id: form.currentPublishedVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Pinned form version not found.', 404)\n }\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n return { form, formVersion, compiled }\n }\n\n /**\n * Resolve a specific published form version pinned to a key. The form is\n * resolved by key (so org/tenant scope and active status are validated) but\n * the supplied version is compiled instead of the form's current published\n * version. Asserts the version is published and belongs to the resolved\n * form. Used by phase 2d distribution flows.\n */\n async getPinnedFormVersionByKey(args: Scope & { formKey: string; pinnedVersionId: string }): Promise<{\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n }> {\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n key: args.formKey,\n deletedAt: null,\n })\n if (!form) {\n throw new SubmissionServiceError('NOT_FOUND', `Form \"${args.formKey}\" not found.`, 404)\n }\n if (form.status !== 'active') {\n throw new SubmissionServiceError('FORM_INACTIVE', `Form \"${args.formKey}\" is not active.`, 422)\n }\n const formVersion = await em.findOne(FormVersion, {\n id: args.pinnedVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion || formVersion.formId !== form.id) {\n throw new SubmissionServiceError('NOT_FOUND', 'Pinned form version not found.', 404)\n }\n if (formVersion.status !== 'published') {\n throw new SubmissionServiceError(\n 'FORM_VERSION_NOT_PUBLISHED',\n 'Pinned form version is not published.',\n 422,\n )\n }\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n return { form, formVersion, compiled }\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: start\n // --------------------------------------------------------------------------\n\n async start(args: StartArgs): Promise<SubmissionViewModel> {\n const { form, formVersion, compiled } = args.pinnedVersionId\n ? await this.getPinnedFormVersionByKey({\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n formKey: args.formKey,\n pinnedVersionId: args.pinnedVersionId,\n })\n : await this.getActiveFormVersionByKey({\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n formKey: args.formKey,\n })\n const declaredRoles = readDeclaredRoles(formVersion)\n const role = args.initialRole\n ?? readDefaultActorRole(formVersion)\n ?? declaredRoles[0]\n ?? null\n if (!role) {\n throw new SubmissionServiceError('INVALID_ROLE', 'Form version does not declare any roles.', 422)\n }\n if (!declaredRoles.includes(role) && role !== 'admin') {\n throw new SubmissionServiceError('INVALID_ROLE', `Role \"${role}\" is not declared on the form version.`, 422)\n }\n\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const now = this.now()\n // Assign UUID PKs in app code so cross-row FKs (actor/revision \u2192\n // submission, submission \u2192 revision) are known before flush. The DB\n // `defaultRaw` only fills them on insert, which is too late here.\n const submissionId = randomUUID()\n const revisionId = randomUUID()\n const submission = trx.create(FormSubmission, {\n id: submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n formVersionId: formVersion.id,\n subjectType: args.subjectType,\n subjectId: args.subjectId,\n status: 'draft',\n startedBy: args.startedBy,\n firstSavedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(submission)\n\n const actor = trx.create(FormSubmissionActor, {\n submissionId,\n organizationId: args.organizationId,\n userId: args.startedBy,\n role,\n assignedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(actor)\n\n // Seed prefill values (W8 / FD-1) \u2014 only fields the participant role may\n // write, then validate the seed against AJV. A failing seed is dropped\n // (start must never fail because of prefill); the submission opens empty.\n const initialPayload = this.buildInitialPayload(compiled, role, args.prefill ?? null)\n const ciphertext = await this.encryption.encrypt(\n args.organizationId,\n Buffer.from(JSON.stringify(initialPayload), 'utf8'),\n )\n const keyVersion = await this.encryption.currentKeyVersion(args.organizationId)\n const revision = trx.create(FormSubmissionRevision, {\n id: revisionId,\n submissionId,\n organizationId: args.organizationId,\n revisionNumber: 1,\n data: ciphertext,\n encryptionKeyVersion: keyVersion,\n savedAt: now,\n savedBy: args.startedBy,\n savedByRole: role,\n changeSource: 'system',\n changedFieldKeys: [],\n changeSummary: null,\n })\n trx.persist(revision)\n\n submission.currentRevisionId = revisionId\n await trx.flush()\n\n return { submission, actor, revision, initialPayload }\n })\n\n await this.safeEmit('forms.submission.started', {\n submissionId: result.submission.id,\n formVersionId: formVersion.id,\n })\n\n return {\n submission: result.submission,\n revision: result.revision,\n decodedData: this.rolePolicy.resolve(compiled, role).sliceReadPayload(result.initialPayload),\n actors: [result.actor],\n formVersion,\n }\n }\n\n /**\n * Builds the initial revision payload from optional prefill values (W8).\n * Filters through the participant role's write policy (only editable fields\n * seed) and validates the result against AJV. Returns `{}` when there is no\n * prefill or the seeded payload fails validation \u2014 start must stay\n * backward-compatible and never fail because of prefill.\n */\n private buildInitialPayload(\n compiled: CompiledFormVersion,\n role: string,\n prefill: Record<string, unknown> | null,\n ): Record<string, unknown> {\n if (!prefill || Object.keys(prefill).length === 0) return {}\n const { accepted } = this.rolePolicy.resolve(compiled, role).filterWritePatch(prefill)\n const seed: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(accepted)) {\n if (value === null || value === undefined) continue\n seed[key] = value\n }\n if (Object.keys(seed).length === 0) return {}\n const valid = compiled.ajv(seed)\n if (!valid) {\n this.logger.warn(\n { event: 'forms.submission.prefill_rejected', role, fieldKeys: Object.keys(seed) },\n 'Forms prefill seed failed validation \u2014 starting with an empty payload.',\n )\n return {}\n }\n return seed\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: save\n // --------------------------------------------------------------------------\n\n async save(args: SaveArgs): Promise<RevisionInsertOutcome> {\n assertSubmissionPayloadSize(args.patch)\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const submission = await trx.findOne(\n FormSubmission,\n {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n if (submission.status === 'archived') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Submission is archived.', 422)\n }\n // A submitted submission is a signed record: `submittedAt` and\n // `submitMetadata` attest to what was submitted. Without this arm a\n // participant holding a live access token could keep appending revisions\n // afterwards \u2014 the attestation would still point at the original submit\n // while the answers moved underneath it, and the lazily-rendered PDF\n // snapshot could capture content the signer never submitted. Re-editing\n // has a deliberate, audited path: `reopen()`, gated on\n // `forms.submissions.manage`. `submit()` and `reopen()` both already\n // guard this status; `save()` was the gap.\n if (submission.status === 'submitted') {\n throw new SubmissionServiceError(\n 'INVALID_STATUS',\n 'Submission is already submitted \u2014 reopen it before saving further changes.',\n 422,\n )\n }\n\n const formVersion = await trx.findOne(FormVersion, {\n id: submission.formVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Form version not found.', 404)\n }\n\n // Active actor for this user\n const actor = await trx.findOne(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.savedBy,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) {\n throw new SubmissionServiceError('NO_ACTOR', 'No active actor row for this user on this submission.', 403)\n }\n\n // Optimistic concurrency\n if (submission.currentRevisionId !== args.baseRevisionId) {\n throw new SubmissionServiceError('STALE_BASE', 'base_revision_id is stale.', 409, {\n currentRevisionId: submission.currentRevisionId,\n })\n }\n\n const currentRevision = await trx.findOne(FormSubmissionRevision, {\n id: args.baseRevisionId,\n submissionId: submission.id,\n organizationId: args.organizationId,\n })\n if (!currentRevision) {\n throw new SubmissionServiceError('NOT_FOUND', 'Base revision not found.', 404)\n }\n\n // Rate limit (skip for system change source). When autosaveIntervalMs\n // is configured to 0 the rate limit is disabled \u2014 useful in tests.\n const changeSource = args.changeSource ?? 'user'\n if (changeSource !== 'system' && this.autosaveIntervalMs > 0) {\n const minIntervalMs = Math.floor(this.autosaveIntervalMs / 2)\n if (minIntervalMs > 0) {\n const elapsed = this.now().getTime() - currentRevision.savedAt.getTime()\n if (elapsed < minIntervalMs) {\n throw new SubmissionServiceError('RATE_LIMITED', 'Save rate limit exceeded.', 429, {\n retryAfterMs: minIntervalMs - elapsed,\n })\n }\n }\n }\n\n // Compile schema + filter patch by role\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n const policy = this.rolePolicy.resolve(compiled, actor.role)\n const { accepted, droppedFieldKeys } = policy.filterWritePatch(args.patch)\n if (droppedFieldKeys.length > 0) {\n this.logger.warn(\n buildTamperingMarker({\n submissionId: submission.id,\n userId: args.savedBy,\n role: actor.role,\n droppedFieldKeys,\n }),\n 'Forms tampering marker \u2014 fields outside actor editable set were dropped.',\n )\n }\n\n // Decrypt + merge\n const priorPlain = await this.decodeRevision(args.organizationId, currentRevision)\n const merged: Record<string, unknown> = { ...priorPlain }\n for (const [key, value] of Object.entries(accepted)) {\n if (value === null || value === undefined) {\n delete merged[key]\n } else {\n merged[key] = value\n }\n }\n assertSubmissionPayloadSize(merged)\n\n // Validate merged payload\n const ajvValid = compiled.ajv(merged)\n if (!ajvValid) {\n const errors = Array.isArray(compiled.ajv.errors) ? compiled.ajv.errors : []\n throw new SubmissionServiceError('VALIDATION_FAILED', 'Merged payload failed validation.', 422, {\n errors: errors.map((err) => ({\n instancePath: err.instancePath,\n keyword: err.keyword,\n message: err.message,\n params: err.params,\n })),\n })\n }\n validateCompiledFieldRules(compiled, formVersion.schema, merged)\n\n // Compute changed field keys vs prior\n const changedFieldKeys = computeChangedFieldKeys(priorPlain, merged)\n\n // Encrypt merged payload\n const ciphertext = await this.encryption.encrypt(\n args.organizationId,\n Buffer.from(JSON.stringify(merged), 'utf8'),\n )\n const keyVersion = await this.encryption.currentKeyVersion(args.organizationId)\n\n const nextRevisionNumber = currentRevision.revisionNumber + 1\n const coalesce = nextRevisionNumber > this.revisionCap\n let outcome: RevisionInsertOutcome\n if (coalesce) {\n // Coalesce-after-cap: UPDATE the latest revision in place. The only\n // UPDATE path other than anonymize. Bump saved_at, replace data,\n // accumulate changedFieldKeys, force changeSource = 'system'.\n currentRevision.data = ciphertext\n currentRevision.encryptionKeyVersion = keyVersion\n currentRevision.savedAt = this.now()\n currentRevision.savedBy = args.savedBy\n currentRevision.savedByRole = actor.role\n currentRevision.changeSource = 'system'\n currentRevision.changedFieldKeys = uniqueStrings([\n ...(currentRevision.changedFieldKeys ?? []),\n ...changedFieldKeys,\n ])\n if (typeof args.changeSummary === 'string') currentRevision.changeSummary = args.changeSummary\n submission.updatedAt = this.now()\n await trx.flush()\n outcome = { revision: currentRevision, coalesced: true }\n } else {\n const savedAt = this.now()\n // Assign the PK in app code so currentRevisionId points at it before\n // flush \u2014 the DB `defaultRaw` only fills the id on insert, which would\n // make `submission.currentRevisionId = revision.id` persist as NULL.\n const revisionId = randomUUID()\n const revision = trx.create(FormSubmissionRevision, {\n id: revisionId,\n submissionId: submission.id,\n organizationId: args.organizationId,\n revisionNumber: nextRevisionNumber,\n data: ciphertext,\n encryptionKeyVersion: keyVersion,\n savedAt,\n savedBy: args.savedBy,\n savedByRole: actor.role,\n changeSource,\n changedFieldKeys,\n changeSummary: args.changeSummary ?? null,\n })\n trx.persist(revision)\n submission.currentRevisionId = revisionId\n submission.updatedAt = this.now()\n await trx.flush()\n outcome = { revision, coalesced: false }\n }\n\n return {\n outcome,\n savedByRole: actor.role,\n }\n })\n\n await this.safeEmit('forms.submission.revision_appended', {\n submissionId: args.submissionId,\n revisionId: result.outcome.revision.id,\n savedBy: args.savedBy,\n savedByRole: result.savedByRole,\n changedFieldKeys: result.outcome.revision.changedFieldKeys ?? [],\n })\n\n return result.outcome\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: submit\n // --------------------------------------------------------------------------\n\n async submit(args: SubmitArgs): Promise<FormSubmission> {\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const submission = await trx.findOne(\n FormSubmission,\n {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n if (submission.status === 'submitted') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Submission already submitted.', 422)\n }\n if (submission.status === 'archived') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Submission is archived.', 422)\n }\n\n // Active actor for this user \u2014 the same authorization `save()` requires.\n // Without it a caller who may not EDIT a submission could still freeze\n // someone else's draft by submitting it. `start()` always creates an actor\n // row for `startedBy`, and that is the principal every flow submits as\n // (`auth.sub` for a portal customer, the invitation id for an anonymous\n // link or invite), so this denies only a genuinely unauthorized caller.\n const actor = await trx.findOne(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.submittedBy,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) {\n throw new SubmissionServiceError('NO_ACTOR', 'No active actor row for this user on this submission.', 403)\n }\n\n if (submission.currentRevisionId !== args.baseRevisionId) {\n throw new SubmissionServiceError('STALE_BASE', 'base_revision_id is stale.', 409, {\n currentRevisionId: submission.currentRevisionId,\n })\n }\n submission.status = 'submitted'\n submission.submittedAt = this.now()\n submission.submittedBy = args.submittedBy\n if (args.submitMetadata) submission.submitMetadata = args.submitMetadata\n await trx.flush()\n return submission\n })\n\n await this.safeEmit('forms.submission.submitted', { submissionId: result.id })\n return result\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: reopen\n // --------------------------------------------------------------------------\n\n async reopen(args: ReopenArgs): Promise<FormSubmission> {\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const submission = await trx.findOne(\n FormSubmission,\n {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n if (submission.status !== 'submitted') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Only submitted submissions can be reopened.', 422)\n }\n submission.status = 'reopened'\n submission.submittedAt = null\n submission.submittedBy = null\n await trx.flush()\n return submission\n })\n await this.safeEmit('forms.submission.reopened', { submissionId: result.id })\n return result\n }\n\n // --------------------------------------------------------------------------\n // Actors\n // --------------------------------------------------------------------------\n\n async assignActor(args: AssignActorArgs): Promise<FormSubmissionActor> {\n const em = this.emFactory()\n const submission = await em.findOne(FormSubmission, {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n const formVersion = await em.findOne(FormVersion, {\n id: submission.formVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Form version not found.', 404)\n }\n const declaredRoles = readDeclaredRoles(formVersion)\n if (args.role !== 'admin' && !declaredRoles.includes(args.role)) {\n throw new SubmissionServiceError('INVALID_ROLE', `Role \"${args.role}\" is not declared on the form version.`, 422)\n }\n const existing = await em.findOne(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.userId,\n revokedAt: null,\n deletedAt: null,\n })\n if (existing) {\n if (existing.role === args.role) return existing\n // Different role on the same user \u2014 revoke the previous assignment.\n existing.revokedAt = this.now()\n em.persist(existing)\n }\n const nowDate = this.now()\n const actor = em.create(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.userId,\n role: args.role,\n assignedAt: nowDate,\n createdAt: nowDate,\n updatedAt: nowDate,\n })\n em.persist(actor)\n await em.flush()\n await this.safeEmit('forms.submission.actor_assigned', {\n submissionId: submission.id,\n userId: args.userId,\n role: args.role,\n })\n return actor\n }\n\n async revokeActor(args: RevokeActorArgs): Promise<void> {\n const em = this.emFactory()\n const actor = await em.findOne(FormSubmissionActor, {\n id: args.actorId,\n organizationId: args.organizationId,\n submissionId: args.submissionId,\n deletedAt: null,\n })\n if (!actor) {\n throw new SubmissionServiceError('NOT_FOUND', 'Actor row not found.', 404)\n }\n if (actor.revokedAt) return\n actor.revokedAt = this.now()\n em.persist(actor)\n await em.flush()\n }\n\n // --------------------------------------------------------------------------\n // Reads\n // --------------------------------------------------------------------------\n\n async getCurrent(args: GetCurrentArgs): Promise<SubmissionViewModel> {\n const em = this.emFactory()\n const submission = await em.findOne(FormSubmission, {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n const formVersion = await em.findOne(FormVersion, {\n id: submission.formVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Form version not found.', 404)\n }\n const revision = submission.currentRevisionId\n ? await em.findOne(FormSubmissionRevision, {\n id: submission.currentRevisionId,\n submissionId: submission.id,\n organizationId: args.organizationId,\n })\n : null\n if (!revision) {\n throw new SubmissionServiceError('NOT_FOUND', 'Current revision not found.', 404)\n }\n const actors = await em.find(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n deletedAt: null,\n })\n\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n const decodedFull = await this.decodeRevision(args.organizationId, revision)\n // Slicing fails closed. An absent `viewerRole` used to skip it entirely and\n // return the full decrypted payload, so any caller that could not name a\n // role \u2014 notably a portal customer with no actor row on this submission \u2014\n // read everything.\n //\n // Resolution order, most specific first:\n // 1. `unsliced: true` \u2014 the one trusted internal caller (see the type).\n // 2. an explicit `viewerRole`.\n // 3. the active actor row for `viewerUserId`. This is what an invitation\n // resume needs: `FormInvitation.role` is null for every invitation the\n // admin UI creates (it posts only `{ email, name }`), so the actor row\n // is the only authority on the respondent's role.\n // 4. otherwise nothing is readable.\n const viewerRole = args.viewerRole\n ?? (args.viewerUserId\n ? actors.find((actor) => actor.userId === args.viewerUserId && !actor.revokedAt)?.role ?? null\n : null)\n\n const decodedData = args.unsliced === true\n ? decodedFull\n // Fail-closed by construction: with no role there is nothing to slice\n // by, so the answer is the empty payload rather than a role-shaped\n // lookup that happens to match no field.\n : viewerRole === null\n ? {}\n : sliceForRole({\n rolePolicy: this.rolePolicy,\n compiled,\n schema: formVersion.schema as { properties?: Record<string, VisibilitySchemaNode> },\n role: viewerRole,\n decodedFull,\n })\n\n // TODO(forms-2b): write form_access_audit row via auditAccess hook.\n await this.auditAccess({\n submissionId: submission.id,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n viewerUserId: args.viewerUserId ?? null,\n viewerRole: args.viewerRole ?? null,\n surface: args.surface ?? 'runtime',\n scope: 'submission',\n })\n\n return {\n submission,\n revision,\n decodedData,\n actors,\n formVersion,\n }\n }\n\n async listRevisions(args: Scope & { submissionId: string; viewerUserId?: string | null; viewerRole?: string | null }): Promise<{\n submission: FormSubmission\n revisions: FormSubmissionRevision[]\n }> {\n const em = this.emFactory()\n const submission = await em.findOne(FormSubmission, {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n const revisions = await em.find(\n FormSubmissionRevision,\n { submissionId: submission.id, organizationId: args.organizationId },\n { orderBy: { revisionNumber: 'asc' } },\n )\n\n // TODO(forms-2b): write form_access_audit row via auditAccess hook.\n await this.auditAccess({\n submissionId: submission.id,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n viewerUserId: args.viewerUserId ?? null,\n viewerRole: args.viewerRole ?? null,\n surface: 'admin',\n scope: 'revision',\n })\n\n return { submission, revisions }\n }\n\n async listSubmissionsBySubject(args: Scope & { subjectType: string; subjectId: string }): Promise<FormSubmission[]> {\n const em = this.emFactory()\n return em.find(\n FormSubmission,\n {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n subjectType: args.subjectType,\n subjectId: args.subjectId,\n deletedAt: null,\n },\n { orderBy: { firstSavedAt: 'desc' } },\n )\n }\n\n async listSubmissionsByForm(args: Scope & {\n formId: string\n page: number\n pageSize: number\n status?: string\n }): Promise<{ items: FormSubmission[]; total: number; page: number; pageSize: number }> {\n const em = this.emFactory()\n const where: Record<string, unknown> = {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n }\n if (args.status) where.status = args.status\n // Filter by all versions belonging to the form\n const versions = await em.find(FormVersion, {\n formId: args.formId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n const versionIds = versions.map((v) => v.id)\n if (versionIds.length === 0) {\n return { items: [], total: 0, page: args.page, pageSize: args.pageSize }\n }\n where.formVersionId = { $in: versionIds }\n const offset = Math.max(0, (args.page - 1) * args.pageSize)\n const [items, total] = await em.findAndCount<FormSubmission>(\n FormSubmission,\n where as never,\n {\n orderBy: { firstSavedAt: 'desc' },\n limit: args.pageSize,\n offset,\n },\n )\n return { items, total, page: args.page, pageSize: args.pageSize }\n }\n\n // --------------------------------------------------------------------------\n // Helpers\n // --------------------------------------------------------------------------\n\n private async decodeRevision(\n organizationId: string,\n revision: FormSubmissionRevision,\n ): Promise<Record<string, unknown>> {\n const ciphertext = ensureBuffer(revision.data)\n if (ciphertext.length === 0) return {}\n const plain = await this.encryption.decrypt(organizationId, ciphertext)\n if (plain.length === 0) return {}\n try {\n return JSON.parse(plain.toString('utf8'))\n } catch {\n return {}\n }\n }\n\n private async safeEmit<K extends keyof SubmissionEvents>(\n eventId: K,\n payload: Parameters<SubmissionEvents[K]>[0],\n ): Promise<void> {\n try {\n await this.emitEvent(eventId, payload)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown emit error'\n this.logger.warn({ event: 'forms.submission.event_emit_failed', eventId, message }, 'forms event emission failed')\n }\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction ensureBuffer(value: Buffer | Uint8Array | string): Buffer {\n if (Buffer.isBuffer(value)) return value\n if (value instanceof Uint8Array) return Buffer.from(value)\n if (typeof value === 'string') return Buffer.from(value, 'binary')\n return Buffer.alloc(0)\n}\n\nfunction readDeclaredRoles(formVersion: FormVersion): string[] {\n // Union the persisted `roles` column with the schema's `x-om-roles`. Both are\n // authoritative declarations; the column can lag the schema if a publish\n // predates the roles-sync, so a role declared in the published schema must\n // never be rejected at runtime.\n const declared = new Set<string>()\n const roles = formVersion.roles\n if (Array.isArray(roles)) {\n for (const entry of roles) if (typeof entry === 'string') declared.add(entry)\n }\n const fromSchema = (formVersion.schema as Record<string, unknown> | null | undefined)?.['x-om-roles']\n if (Array.isArray(fromSchema)) {\n for (const entry of fromSchema) if (typeof entry === 'string') declared.add(entry)\n }\n return Array.from(declared)\n}\n\nfunction readDefaultActorRole(formVersion: FormVersion): string | null {\n const value = (formVersion.schema as Record<string, unknown> | null | undefined)?.['x-om-default-actor-role']\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction computeChangedFieldKeys(\n prior: Record<string, unknown>,\n next: Record<string, unknown>,\n): string[] {\n const keys = new Set<string>([...Object.keys(prior), ...Object.keys(next)])\n const changed: string[] = []\n for (const key of keys) {\n const a = prior[key]\n const b = next[key]\n if (!deepEqual(a, b)) changed.push(key)\n }\n return changed\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a === null || b === null) return false\n if (typeof a !== typeof b) return false\n if (typeof a !== 'object') return false\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false\n if (a.length !== b.length) return false\n return a.every((entry, index) => deepEqual(entry, b[index]))\n }\n const aKeys = Object.keys(a as Record<string, unknown>)\n const bKeys = Object.keys(b as Record<string, unknown>)\n if (aKeys.length !== bKeys.length) return false\n return aKeys.every((key) => deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]))\n}\n\nfunction uniqueStrings(values: string[]): string[] {\n return Array.from(new Set(values))\n}\n\nfunction assertSubmissionPayloadSize(payload: Record<string, unknown>): void {\n const bytes = Buffer.byteLength(JSON.stringify(payload), 'utf8')\n if (bytes <= MAX_SUBMISSION_PAYLOAD_BYTES) return\n throw new SubmissionServiceError(\n 'VALIDATION_FAILED',\n 'Submission payload exceeds the configured size limit.',\n 413,\n { maxBytes: MAX_SUBMISSION_PAYLOAD_BYTES },\n )\n}\n\nfunction validateCompiledFieldRules(\n compiled: CompiledFormVersion,\n schema: Record<string, unknown>,\n payload: Record<string, unknown>,\n): void {\n const properties =\n schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)\n ? (schema.properties as Record<string, unknown>)\n : {}\n for (const [fieldKey, descriptor] of Object.entries(compiled.fieldIndex)) {\n if (!(fieldKey in payload)) continue\n const rawNode = properties[fieldKey]\n const fieldNode =\n rawNode && typeof rawNode === 'object' && !Array.isArray(rawNode)\n ? (rawNode as FieldNode)\n : undefined\n const result = validateFieldValue(\n payload[fieldKey],\n descriptor.validations,\n 'en',\n descriptor.validationMessages,\n fieldNode,\n )\n if (result.valid) continue\n throw new SubmissionServiceError(\n 'VALIDATION_FAILED',\n 'Submission field failed validation.',\n 422,\n { errors: [{ fieldKey, rule: result.rule, message: result.message }] },\n )\n }\n}\n\n/**\n * Role read-slice plus the visibility pass, in that order.\n *\n * Visibility is evaluated against the FULL decoded payload: an\n * `x-om-visibility-if` predicate may reference a field the role-sliced subset\n * omits, and it must see the real answer. The resulting visible set then\n * narrows the role-sliced payload. A form with no `x-om-visibility-if`\n * resolves to all readable keys, so the second pass is a no-op for it.\n */\nfunction sliceForRole(args: {\n rolePolicy: RolePolicyService\n compiled: CompiledFormVersion\n schema: { properties?: Record<string, VisibilitySchemaNode> }\n role: string\n decodedFull: Record<string, unknown>\n}): Record<string, unknown> {\n const roleSliced = args.rolePolicy.resolve(args.compiled, args.role).sliceReadPayload(args.decodedFull)\n const visible = resolveVisibleFieldKeys({\n compiled: args.compiled,\n schema: args.schema,\n role: args.role,\n data: args.decodedFull,\n })\n return sliceByVisibility(roleSliced, visible)\n}\n\n/** Brand check for SubmissionServiceError \u2014 use instead of `instanceof`. */\nexport function isSubmissionServiceError(error: unknown): error is SubmissionServiceError {\n return typeof error === 'object' && error !== null && (SUBMISSION_SERVICE_ERROR_MARKER in error)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Forms module SubmissionService \u2014 owns the start/save/submit lifecycle.\n *\n * Critical invariants enforced here:\n * - Append-only revision chain (only allowed UPDATE paths are anonymize\n * in phase 2b and the coalesce-after-cap branch in this file).\n * - Tenant-scoped queries on every read/write.\n * - Server-derived `saved_by_role` from active actor row \u2014 never read from\n * client input.\n * - Optimistic concurrency via `base_revision_id`.\n * - Tampering markers logged when patches contain non-editable field keys.\n * - Per-tenant envelope encryption of revision payloads.\n *\n * The service is intentionally thin on cross-cutting concerns: emit hooks\n * are exposed via `eventEmitter` and pluggable audit-access logger\n * (`auditAccess`) so phase 2b can replace it without rewriting the service.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport { LockMode } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport {\n Form,\n FormSubmission,\n FormSubmissionActor,\n FormSubmissionRevision,\n FormVersion,\n} from '../data/entities'\nimport {\n FormVersionCompiler,\n type CompiledFormVersion,\n} from './form-version-compiler'\nimport type { EncryptionService } from './encryption-service'\nimport { RolePolicyService } from './role-policy-service'\nimport {\n resolveVisibleFieldKeys,\n sliceByVisibility,\n type VisibilitySchemaNode,\n} from './visibility-resolver'\nimport { buildTamperingMarker, type StructuredLogger } from '../lib/log-redaction'\nimport { isPortalSubmitAllowed } from '../lib/portal-subject-policy'\nimport { formsEventPayloadSchemas } from '../events-payloads'\nimport { validateFieldValue } from './field-validation-service'\nimport type { FieldNode } from '../backend/forms/[id]/studio/schema-helpers'\n\nconst DEFAULT_AUTOSAVE_INTERVAL_MS = (() => {\n const raw = process.env.FORMS_AUTOSAVE_INTERVAL_MS\n if (!raw) return 10_000\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 10_000\n})()\n\nconst DEFAULT_REVISION_CAP = (() => {\n const raw = process.env.FORMS_REVISION_CAP\n if (!raw) return 10_000\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 10_000\n})()\n\nconst MAX_SUBMISSION_PAYLOAD_BYTES = (() => {\n const raw = process.env.FORMS_MAX_SUBMISSION_PAYLOAD_BYTES\n if (!raw) return 256 * 1024\n const parsed = Number.parseInt(raw, 10)\n return Number.isFinite(parsed) && parsed > 0 ? parsed : 256 * 1024\n})()\n\nexport type SubmissionServiceErrorCode =\n | 'STALE_BASE'\n | 'VALIDATION_FAILED'\n | 'RATE_LIMITED'\n | 'NOT_FOUND'\n | 'NO_ACTOR'\n | 'INVALID_STATUS'\n | 'INVALID_ROLE'\n | 'FORM_INACTIVE'\n | 'FORM_VERSION_NOT_PUBLISHED'\n | 'SUBJECT_FORBIDDEN'\n\n/**\n * `Symbol.for` so the brand survives module duplication across bundle\n * boundaries. `instanceof` does NOT: Next can load this module in more than\n * one chunk, and once a second copy of the class exists an error thrown\n * through one copy fails `instanceof` against the other \u2014 silently\n * downgrading a mapped domain error to a 500. Mirrors `CrudHttpError`.\n */\nconst SUBMISSION_SERVICE_ERROR_MARKER = Symbol.for('@open-mercato/forms/SubmissionServiceError')\n\nexport class SubmissionServiceError extends Error {\n readonly [SUBMISSION_SERVICE_ERROR_MARKER] = true\n readonly code: SubmissionServiceErrorCode\n readonly httpStatus: number\n readonly details?: Record<string, unknown>\n\n constructor(code: SubmissionServiceErrorCode, message: string, httpStatus: number, details?: Record<string, unknown>) {\n super(message)\n this.name = 'SubmissionServiceError'\n this.code = code\n this.httpStatus = httpStatus\n this.details = details\n }\n}\n\ntype Scope = {\n tenantId: string\n organizationId: string\n}\n\nexport type StartArgs = Scope & {\n formKey: string\n subjectType: string\n subjectId: string\n startedBy: string\n /** Optional initial role override \u2014 when present, MUST be in form_version.roles. */\n initialRole?: string | null\n /**\n * Optional published version to pin this submission to. When present, the\n * form is still resolved by key (for scope + status checks) but the\n * specified version is compiled instead of the form's current published\n * version. Used by distribution flows (phase 2d) to lock a campaign to a\n * particular version. When absent, behavior is unchanged.\n */\n pinnedVersionId?: string | null\n /**\n * Optional initial values keyed by field key (W8 / FD-1 prefill). Resolved\n * by the route from `x-om-prefill` declarations + the injected\n * `PrefillResolver`. Applied to the initial revision after being filtered\n * through the participant role's `filterWritePatch` (only fields the\n * participant may edit are kept) and validated by AJV. Backward-compatible:\n * absent / empty \u21D2 the initial revision payload stays `{}` as before.\n */\n prefill?: Record<string, unknown> | null\n}\n\nexport type SaveArgs = Scope & {\n submissionId: string\n baseRevisionId: string\n patch: Record<string, unknown>\n savedBy: string\n changeSummary?: string | null\n changeSource?: 'user' | 'admin' | 'system'\n}\n\nexport type SubmitArgs = Scope & {\n submissionId: string\n baseRevisionId: string\n submittedBy: string\n submitMetadata?: Record<string, unknown> | null\n /**\n * Set when `submittedBy` is a signed-in portal customer. A submission that\n * customer started themselves must then carry their own `customer` subject\n * (`lib/portal-subject-policy.ts`), otherwise 403 SUBJECT_FORBIDDEN. Left\n * unset for invitation (token) principals, whose subject is server-derived.\n */\n submittedByPortalCustomer?: boolean\n}\n\nexport type ReopenArgs = Scope & {\n submissionId: string\n reopenedBy: string\n}\n\nexport type AssignActorArgs = Scope & {\n submissionId: string\n userId: string\n role: string\n assignedBy: string\n}\n\nexport type RevokeActorArgs = Scope & {\n submissionId: string\n actorId: string\n revokedBy: string\n}\n\nexport type GetCurrentArgs = Scope & {\n submissionId: string\n /** When provided, slice the response to only this role's visible fields. */\n viewerRole?: string | null\n /**\n * User reading. Used for the audit-log call site, and \u2014 when `viewerRole` is\n * absent \u2014 to derive the slice from that user's active actor row, which is\n * the authority on what role a viewer actually holds.\n */\n viewerUserId?: string | null\n /**\n * Which surface is reading, for the access-audit row. `getCurrent` used to\n * hard-code `'runtime'`, so the admin submission-detail read \u2014 the primary\n * staff read of a submission's answers \u2014 was recorded as runtime traffic or,\n * for an auditor filtering on `'admin'`, not visibly recorded at all.\n * Defaults to `'runtime'` so the public and portal call sites are unchanged.\n */\n surface?: 'admin' | 'runtime'\n /**\n * TRUSTED INTERNAL CALLERS ONLY: return the full decoded payload with no role\n * slice.\n *\n * There is exactly one legitimate use \u2014 the consent projector in `di.ts`,\n * which has no viewer at all and must see `signature` answers to write\n * `forms_consent_record` rows. It is an explicit opt-in rather than the\n * default-when-unspecified it used to be, because that default silently\n * handed the full decrypted payload to any caller that could not name a role,\n * including a portal customer with no actor row on the submission.\n *\n * Never set this from anything reachable by a request principal.\n */\n unsliced?: boolean\n}\n\nexport type SubmissionViewModel = {\n submission: FormSubmission\n revision: FormSubmissionRevision\n decodedData: Record<string, unknown>\n actors: FormSubmissionActor[]\n formVersion: FormVersion\n}\n\nexport type RevisionInsertOutcome = {\n revision: FormSubmissionRevision\n coalesced: boolean\n}\n\nexport type SubmissionEvents = {\n 'forms.submission.started': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.started']>) => Promise<void> | void\n 'forms.submission.revision_appended': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.revision_appended']>) => Promise<void> | void\n 'forms.submission.submitted': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.submitted']>) => Promise<void> | void\n 'forms.submission.reopened': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.reopened']>) => Promise<void> | void\n 'forms.submission.actor_assigned': (payload: z.infer<typeof formsEventPayloadSchemas['forms.submission.actor_assigned']>) => Promise<void> | void\n}\n\nexport type AuditAccessHook = (args: {\n submissionId: string\n organizationId: string\n tenantId: string\n viewerUserId: string | null\n viewerRole: string | null\n surface: 'admin' | 'runtime'\n scope: 'submission' | 'revision'\n}) => Promise<void> | void\n\nexport type SubmissionServiceOptions = {\n emFactory: () => EntityManager\n formVersionCompiler: FormVersionCompiler\n encryptionService: EncryptionService\n rolePolicyService: RolePolicyService\n /** Pluggable event emitter \u2014 wires to the typed forms events catalog. */\n emitEvent: <K extends keyof SubmissionEvents>(eventId: K, payload: Parameters<SubmissionEvents[K]>[0]) => Promise<void> | void\n /** Phase 2b replaces this with a real audit logger. Phase 1c uses a no-op. */\n auditAccess?: AuditAccessHook\n logger?: StructuredLogger\n /** Override the clock for tests. */\n now?: () => Date\n autosaveIntervalMs?: number\n revisionCap?: number\n}\n\nconst noopAuditAccess: AuditAccessHook = () => {}\n\nconst noopLogger: StructuredLogger = {\n info: () => {},\n warn: () => {},\n error: () => {},\n}\n\nexport class SubmissionService {\n private readonly emFactory: () => EntityManager\n private readonly compiler: FormVersionCompiler\n private readonly encryption: EncryptionService\n private readonly rolePolicy: RolePolicyService\n private readonly emitEvent: SubmissionServiceOptions['emitEvent']\n private readonly auditAccess: AuditAccessHook\n private readonly logger: StructuredLogger\n private readonly now: () => Date\n private readonly autosaveIntervalMs: number\n private readonly revisionCap: number\n\n constructor(options: SubmissionServiceOptions) {\n this.emFactory = options.emFactory\n this.compiler = options.formVersionCompiler\n this.encryption = options.encryptionService\n this.rolePolicy = options.rolePolicyService\n this.emitEvent = options.emitEvent\n this.auditAccess = options.auditAccess ?? noopAuditAccess\n this.logger = options.logger ?? noopLogger\n this.now = options.now ?? (() => new Date())\n this.autosaveIntervalMs = options.autosaveIntervalMs ?? DEFAULT_AUTOSAVE_INTERVAL_MS\n this.revisionCap = options.revisionCap ?? DEFAULT_REVISION_CAP\n }\n\n // --------------------------------------------------------------------------\n // Active form-version lookup\n // --------------------------------------------------------------------------\n\n async getActiveFormVersionByKey(args: Scope & { formKey: string }): Promise<{\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n }> {\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n key: args.formKey,\n deletedAt: null,\n })\n if (!form) {\n throw new SubmissionServiceError('NOT_FOUND', `Form \"${args.formKey}\" not found.`, 404)\n }\n if (form.status !== 'active') {\n throw new SubmissionServiceError('FORM_INACTIVE', `Form \"${args.formKey}\" is not active.`, 422)\n }\n if (!form.currentPublishedVersionId) {\n throw new SubmissionServiceError(\n 'FORM_VERSION_NOT_PUBLISHED',\n `Form \"${args.formKey}\" has no published version.`,\n 422,\n )\n }\n const formVersion = await em.findOne(FormVersion, {\n id: form.currentPublishedVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Pinned form version not found.', 404)\n }\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n return { form, formVersion, compiled }\n }\n\n /**\n * Resolve a specific published form version pinned to a key. The form is\n * resolved by key (so org/tenant scope and active status are validated) but\n * the supplied version is compiled instead of the form's current published\n * version. Asserts the version is published and belongs to the resolved\n * form. Used by phase 2d distribution flows.\n */\n async getPinnedFormVersionByKey(args: Scope & { formKey: string; pinnedVersionId: string }): Promise<{\n form: Form\n formVersion: FormVersion\n compiled: CompiledFormVersion\n }> {\n const em = this.emFactory()\n const form = await em.findOne(Form, {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n key: args.formKey,\n deletedAt: null,\n })\n if (!form) {\n throw new SubmissionServiceError('NOT_FOUND', `Form \"${args.formKey}\" not found.`, 404)\n }\n if (form.status !== 'active') {\n throw new SubmissionServiceError('FORM_INACTIVE', `Form \"${args.formKey}\" is not active.`, 422)\n }\n const formVersion = await em.findOne(FormVersion, {\n id: args.pinnedVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion || formVersion.formId !== form.id) {\n throw new SubmissionServiceError('NOT_FOUND', 'Pinned form version not found.', 404)\n }\n if (formVersion.status !== 'published') {\n throw new SubmissionServiceError(\n 'FORM_VERSION_NOT_PUBLISHED',\n 'Pinned form version is not published.',\n 422,\n )\n }\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n return { form, formVersion, compiled }\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: start\n // --------------------------------------------------------------------------\n\n async start(args: StartArgs): Promise<SubmissionViewModel> {\n const { form, formVersion, compiled } = args.pinnedVersionId\n ? await this.getPinnedFormVersionByKey({\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n formKey: args.formKey,\n pinnedVersionId: args.pinnedVersionId,\n })\n : await this.getActiveFormVersionByKey({\n tenantId: args.tenantId,\n organizationId: args.organizationId,\n formKey: args.formKey,\n })\n const declaredRoles = readDeclaredRoles(formVersion)\n const role = args.initialRole\n ?? readDefaultActorRole(formVersion)\n ?? declaredRoles[0]\n ?? null\n if (!role) {\n throw new SubmissionServiceError('INVALID_ROLE', 'Form version does not declare any roles.', 422)\n }\n if (!declaredRoles.includes(role) && role !== 'admin') {\n throw new SubmissionServiceError('INVALID_ROLE', `Role \"${role}\" is not declared on the form version.`, 422)\n }\n\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const now = this.now()\n // Assign UUID PKs in app code so cross-row FKs (actor/revision \u2192\n // submission, submission \u2192 revision) are known before flush. The DB\n // `defaultRaw` only fills them on insert, which is too late here.\n const submissionId = randomUUID()\n const revisionId = randomUUID()\n const submission = trx.create(FormSubmission, {\n id: submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n formVersionId: formVersion.id,\n subjectType: args.subjectType,\n subjectId: args.subjectId,\n status: 'draft',\n startedBy: args.startedBy,\n firstSavedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(submission)\n\n const actor = trx.create(FormSubmissionActor, {\n submissionId,\n organizationId: args.organizationId,\n userId: args.startedBy,\n role,\n assignedAt: now,\n createdAt: now,\n updatedAt: now,\n })\n trx.persist(actor)\n\n // Seed prefill values (W8 / FD-1) \u2014 only fields the participant role may\n // write, then validate the seed against AJV. A failing seed is dropped\n // (start must never fail because of prefill); the submission opens empty.\n const initialPayload = this.buildInitialPayload(compiled, role, args.prefill ?? null)\n const ciphertext = await this.encryption.encrypt(\n args.organizationId,\n Buffer.from(JSON.stringify(initialPayload), 'utf8'),\n )\n const keyVersion = await this.encryption.currentKeyVersion(args.organizationId)\n const revision = trx.create(FormSubmissionRevision, {\n id: revisionId,\n submissionId,\n organizationId: args.organizationId,\n revisionNumber: 1,\n data: ciphertext,\n encryptionKeyVersion: keyVersion,\n savedAt: now,\n savedBy: args.startedBy,\n savedByRole: role,\n changeSource: 'system',\n changedFieldKeys: [],\n changeSummary: null,\n })\n trx.persist(revision)\n\n submission.currentRevisionId = revisionId\n await trx.flush()\n\n return { submission, actor, revision, initialPayload }\n })\n\n await this.safeEmit('forms.submission.started', {\n submissionId: result.submission.id,\n formVersionId: formVersion.id,\n })\n\n return {\n submission: result.submission,\n revision: result.revision,\n decodedData: this.rolePolicy.resolve(compiled, role).sliceReadPayload(result.initialPayload),\n actors: [result.actor],\n formVersion,\n }\n }\n\n /**\n * Builds the initial revision payload from optional prefill values (W8).\n * Filters through the participant role's write policy (only editable fields\n * seed) and validates the result against AJV. Returns `{}` when there is no\n * prefill or the seeded payload fails validation \u2014 start must stay\n * backward-compatible and never fail because of prefill.\n */\n private buildInitialPayload(\n compiled: CompiledFormVersion,\n role: string,\n prefill: Record<string, unknown> | null,\n ): Record<string, unknown> {\n if (!prefill || Object.keys(prefill).length === 0) return {}\n const { accepted } = this.rolePolicy.resolve(compiled, role).filterWritePatch(prefill)\n const seed: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(accepted)) {\n if (value === null || value === undefined) continue\n seed[key] = value\n }\n if (Object.keys(seed).length === 0) return {}\n const valid = compiled.ajv(seed)\n if (!valid) {\n this.logger.warn(\n { event: 'forms.submission.prefill_rejected', role, fieldKeys: Object.keys(seed) },\n 'Forms prefill seed failed validation \u2014 starting with an empty payload.',\n )\n return {}\n }\n return seed\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: save\n // --------------------------------------------------------------------------\n\n async save(args: SaveArgs): Promise<RevisionInsertOutcome> {\n assertSubmissionPayloadSize(args.patch)\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const submission = await trx.findOne(\n FormSubmission,\n {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n if (submission.status === 'archived') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Submission is archived.', 422)\n }\n // A submitted submission is a signed record: `submittedAt` and\n // `submitMetadata` attest to what was submitted. Without this arm a\n // participant holding a live access token could keep appending revisions\n // afterwards \u2014 the attestation would still point at the original submit\n // while the answers moved underneath it, and the lazily-rendered PDF\n // snapshot could capture content the signer never submitted. Re-editing\n // has a deliberate, audited path: `reopen()`, gated on\n // `forms.submissions.manage`. `submit()` and `reopen()` both already\n // guard this status; `save()` was the gap.\n if (submission.status === 'submitted') {\n throw new SubmissionServiceError(\n 'INVALID_STATUS',\n 'Submission is already submitted \u2014 reopen it before saving further changes.',\n 422,\n )\n }\n\n const formVersion = await trx.findOne(FormVersion, {\n id: submission.formVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Form version not found.', 404)\n }\n\n // Active actor for this user\n const actor = await trx.findOne(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.savedBy,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) {\n throw new SubmissionServiceError('NO_ACTOR', 'No active actor row for this user on this submission.', 403)\n }\n\n // Optimistic concurrency\n if (submission.currentRevisionId !== args.baseRevisionId) {\n throw new SubmissionServiceError('STALE_BASE', 'base_revision_id is stale.', 409, {\n currentRevisionId: submission.currentRevisionId,\n })\n }\n\n const currentRevision = await trx.findOne(FormSubmissionRevision, {\n id: args.baseRevisionId,\n submissionId: submission.id,\n organizationId: args.organizationId,\n })\n if (!currentRevision) {\n throw new SubmissionServiceError('NOT_FOUND', 'Base revision not found.', 404)\n }\n\n // Rate limit (skip for system change source). When autosaveIntervalMs\n // is configured to 0 the rate limit is disabled \u2014 useful in tests.\n const changeSource = args.changeSource ?? 'user'\n if (changeSource !== 'system' && this.autosaveIntervalMs > 0) {\n const minIntervalMs = Math.floor(this.autosaveIntervalMs / 2)\n if (minIntervalMs > 0) {\n const elapsed = this.now().getTime() - currentRevision.savedAt.getTime()\n if (elapsed < minIntervalMs) {\n throw new SubmissionServiceError('RATE_LIMITED', 'Save rate limit exceeded.', 429, {\n retryAfterMs: minIntervalMs - elapsed,\n })\n }\n }\n }\n\n // Compile schema + filter patch by role\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n const policy = this.rolePolicy.resolve(compiled, actor.role)\n const { accepted, droppedFieldKeys } = policy.filterWritePatch(args.patch)\n if (droppedFieldKeys.length > 0) {\n this.logger.warn(\n buildTamperingMarker({\n submissionId: submission.id,\n userId: args.savedBy,\n role: actor.role,\n droppedFieldKeys,\n }),\n 'Forms tampering marker \u2014 fields outside actor editable set were dropped.',\n )\n }\n\n // Decrypt + merge\n const priorPlain = await this.decodeRevision(args.organizationId, currentRevision)\n const merged: Record<string, unknown> = { ...priorPlain }\n for (const [key, value] of Object.entries(accepted)) {\n if (value === null || value === undefined) {\n delete merged[key]\n } else {\n merged[key] = value\n }\n }\n assertSubmissionPayloadSize(merged)\n\n // Validate merged payload\n const ajvValid = compiled.ajv(merged)\n if (!ajvValid) {\n const errors = Array.isArray(compiled.ajv.errors) ? compiled.ajv.errors : []\n throw new SubmissionServiceError('VALIDATION_FAILED', 'Merged payload failed validation.', 422, {\n errors: errors.map((err) => ({\n instancePath: err.instancePath,\n keyword: err.keyword,\n message: err.message,\n params: err.params,\n })),\n })\n }\n validateCompiledFieldRules(compiled, formVersion.schema, merged)\n\n // Compute changed field keys vs prior\n const changedFieldKeys = computeChangedFieldKeys(priorPlain, merged)\n\n // Encrypt merged payload\n const ciphertext = await this.encryption.encrypt(\n args.organizationId,\n Buffer.from(JSON.stringify(merged), 'utf8'),\n )\n const keyVersion = await this.encryption.currentKeyVersion(args.organizationId)\n\n const nextRevisionNumber = currentRevision.revisionNumber + 1\n const coalesce = nextRevisionNumber > this.revisionCap\n let outcome: RevisionInsertOutcome\n if (coalesce) {\n // Coalesce-after-cap: UPDATE the latest revision in place. The only\n // UPDATE path other than anonymize. Bump saved_at, replace data,\n // accumulate changedFieldKeys, force changeSource = 'system'.\n currentRevision.data = ciphertext\n currentRevision.encryptionKeyVersion = keyVersion\n currentRevision.savedAt = this.now()\n currentRevision.savedBy = args.savedBy\n currentRevision.savedByRole = actor.role\n currentRevision.changeSource = 'system'\n currentRevision.changedFieldKeys = uniqueStrings([\n ...(currentRevision.changedFieldKeys ?? []),\n ...changedFieldKeys,\n ])\n if (typeof args.changeSummary === 'string') currentRevision.changeSummary = args.changeSummary\n submission.updatedAt = this.now()\n await trx.flush()\n outcome = { revision: currentRevision, coalesced: true }\n } else {\n const savedAt = this.now()\n // Assign the PK in app code so currentRevisionId points at it before\n // flush \u2014 the DB `defaultRaw` only fills the id on insert, which would\n // make `submission.currentRevisionId = revision.id` persist as NULL.\n const revisionId = randomUUID()\n const revision = trx.create(FormSubmissionRevision, {\n id: revisionId,\n submissionId: submission.id,\n organizationId: args.organizationId,\n revisionNumber: nextRevisionNumber,\n data: ciphertext,\n encryptionKeyVersion: keyVersion,\n savedAt,\n savedBy: args.savedBy,\n savedByRole: actor.role,\n changeSource,\n changedFieldKeys,\n changeSummary: args.changeSummary ?? null,\n })\n trx.persist(revision)\n submission.currentRevisionId = revisionId\n submission.updatedAt = this.now()\n await trx.flush()\n outcome = { revision, coalesced: false }\n }\n\n return {\n outcome,\n savedByRole: actor.role,\n }\n })\n\n await this.safeEmit('forms.submission.revision_appended', {\n submissionId: args.submissionId,\n revisionId: result.outcome.revision.id,\n savedBy: args.savedBy,\n savedByRole: result.savedByRole,\n changedFieldKeys: result.outcome.revision.changedFieldKeys ?? [],\n })\n\n return result.outcome\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: submit\n // --------------------------------------------------------------------------\n\n async submit(args: SubmitArgs): Promise<FormSubmission> {\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const submission = await trx.findOne(\n FormSubmission,\n {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n if (args.submittedByPortalCustomer && !isPortalSubmitAllowed(args.submittedBy, submission)) {\n throw new SubmissionServiceError('SUBJECT_FORBIDDEN', 'Submission subject is not the caller\\'s own customer subject.', 403)\n }\n if (submission.status === 'submitted') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Submission already submitted.', 422)\n }\n if (submission.status === 'archived') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Submission is archived.', 422)\n }\n\n // Active actor for this user \u2014 the same authorization `save()` requires.\n // Without it a caller who may not EDIT a submission could still freeze\n // someone else's draft by submitting it. `start()` always creates an actor\n // row for `startedBy`, and that is the principal every flow submits as\n // (`auth.sub` for a portal customer, the invitation id for an anonymous\n // link or invite), so this denies only a genuinely unauthorized caller.\n const actor = await trx.findOne(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.submittedBy,\n revokedAt: null,\n deletedAt: null,\n })\n if (!actor) {\n throw new SubmissionServiceError('NO_ACTOR', 'No active actor row for this user on this submission.', 403)\n }\n\n if (submission.currentRevisionId !== args.baseRevisionId) {\n throw new SubmissionServiceError('STALE_BASE', 'base_revision_id is stale.', 409, {\n currentRevisionId: submission.currentRevisionId,\n })\n }\n submission.status = 'submitted'\n submission.submittedAt = this.now()\n submission.submittedBy = args.submittedBy\n if (args.submitMetadata) submission.submitMetadata = args.submitMetadata\n await trx.flush()\n return submission\n })\n\n await this.safeEmit('forms.submission.submitted', { submissionId: result.id })\n return result\n }\n\n // --------------------------------------------------------------------------\n // Lifecycle: reopen\n // --------------------------------------------------------------------------\n\n async reopen(args: ReopenArgs): Promise<FormSubmission> {\n const em = this.emFactory()\n const result = await em.transactional(async (trx) => {\n const submission = await trx.findOne(\n FormSubmission,\n {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n },\n { lockMode: LockMode.PESSIMISTIC_WRITE },\n )\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n if (submission.status !== 'submitted') {\n throw new SubmissionServiceError('INVALID_STATUS', 'Only submitted submissions can be reopened.', 422)\n }\n submission.status = 'reopened'\n submission.submittedAt = null\n submission.submittedBy = null\n await trx.flush()\n return submission\n })\n await this.safeEmit('forms.submission.reopened', { submissionId: result.id })\n return result\n }\n\n // --------------------------------------------------------------------------\n // Actors\n // --------------------------------------------------------------------------\n\n async assignActor(args: AssignActorArgs): Promise<FormSubmissionActor> {\n const em = this.emFactory()\n const submission = await em.findOne(FormSubmission, {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n const formVersion = await em.findOne(FormVersion, {\n id: submission.formVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Form version not found.', 404)\n }\n const declaredRoles = readDeclaredRoles(formVersion)\n if (args.role !== 'admin' && !declaredRoles.includes(args.role)) {\n throw new SubmissionServiceError('INVALID_ROLE', `Role \"${args.role}\" is not declared on the form version.`, 422)\n }\n const existing = await em.findOne(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.userId,\n revokedAt: null,\n deletedAt: null,\n })\n if (existing) {\n if (existing.role === args.role) return existing\n // Different role on the same user \u2014 revoke the previous assignment.\n existing.revokedAt = this.now()\n em.persist(existing)\n }\n const nowDate = this.now()\n const actor = em.create(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n userId: args.userId,\n role: args.role,\n assignedAt: nowDate,\n createdAt: nowDate,\n updatedAt: nowDate,\n })\n em.persist(actor)\n await em.flush()\n await this.safeEmit('forms.submission.actor_assigned', {\n submissionId: submission.id,\n userId: args.userId,\n role: args.role,\n })\n return actor\n }\n\n async revokeActor(args: RevokeActorArgs): Promise<void> {\n const em = this.emFactory()\n const actor = await em.findOne(FormSubmissionActor, {\n id: args.actorId,\n organizationId: args.organizationId,\n submissionId: args.submissionId,\n deletedAt: null,\n })\n if (!actor) {\n throw new SubmissionServiceError('NOT_FOUND', 'Actor row not found.', 404)\n }\n if (actor.revokedAt) return\n actor.revokedAt = this.now()\n em.persist(actor)\n await em.flush()\n }\n\n // --------------------------------------------------------------------------\n // Reads\n // --------------------------------------------------------------------------\n\n async getCurrent(args: GetCurrentArgs): Promise<SubmissionViewModel> {\n const em = this.emFactory()\n const submission = await em.findOne(FormSubmission, {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n const formVersion = await em.findOne(FormVersion, {\n id: submission.formVersionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n if (!formVersion) {\n throw new SubmissionServiceError('NOT_FOUND', 'Form version not found.', 404)\n }\n const revision = submission.currentRevisionId\n ? await em.findOne(FormSubmissionRevision, {\n id: submission.currentRevisionId,\n submissionId: submission.id,\n organizationId: args.organizationId,\n })\n : null\n if (!revision) {\n throw new SubmissionServiceError('NOT_FOUND', 'Current revision not found.', 404)\n }\n const actors = await em.find(FormSubmissionActor, {\n submissionId: submission.id,\n organizationId: args.organizationId,\n deletedAt: null,\n })\n\n const compiled = this.compiler.compile({\n id: formVersion.id,\n updatedAt: formVersion.updatedAt,\n schema: formVersion.schema,\n uiSchema: formVersion.uiSchema,\n })\n const decodedFull = await this.decodeRevision(args.organizationId, revision)\n // Slicing fails closed. An absent `viewerRole` used to skip it entirely and\n // return the full decrypted payload, so any caller that could not name a\n // role \u2014 notably a portal customer with no actor row on this submission \u2014\n // read everything.\n //\n // Resolution order, most specific first:\n // 1. `unsliced: true` \u2014 the one trusted internal caller (see the type).\n // 2. an explicit `viewerRole`.\n // 3. the active actor row for `viewerUserId`. This is what an invitation\n // resume needs: `FormInvitation.role` is null for every invitation the\n // admin UI creates (it posts only `{ email, name }`), so the actor row\n // is the only authority on the respondent's role.\n // 4. otherwise nothing is readable.\n const viewerRole = args.viewerRole\n ?? (args.viewerUserId\n ? actors.find((actor) => actor.userId === args.viewerUserId && !actor.revokedAt)?.role ?? null\n : null)\n\n const decodedData = args.unsliced === true\n ? decodedFull\n // Fail-closed by construction: with no role there is nothing to slice\n // by, so the answer is the empty payload rather than a role-shaped\n // lookup that happens to match no field.\n : viewerRole === null\n ? {}\n : sliceForRole({\n rolePolicy: this.rolePolicy,\n compiled,\n schema: formVersion.schema as { properties?: Record<string, VisibilitySchemaNode> },\n role: viewerRole,\n decodedFull,\n })\n\n // TODO(forms-2b): write form_access_audit row via auditAccess hook.\n await this.auditAccess({\n submissionId: submission.id,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n viewerUserId: args.viewerUserId ?? null,\n viewerRole: args.viewerRole ?? null,\n surface: args.surface ?? 'runtime',\n scope: 'submission',\n })\n\n return {\n submission,\n revision,\n decodedData,\n actors,\n formVersion,\n }\n }\n\n async listRevisions(args: Scope & { submissionId: string; viewerUserId?: string | null; viewerRole?: string | null }): Promise<{\n submission: FormSubmission\n revisions: FormSubmissionRevision[]\n }> {\n const em = this.emFactory()\n const submission = await em.findOne(FormSubmission, {\n id: args.submissionId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n })\n if (!submission) {\n throw new SubmissionServiceError('NOT_FOUND', 'Submission not found.', 404)\n }\n const revisions = await em.find(\n FormSubmissionRevision,\n { submissionId: submission.id, organizationId: args.organizationId },\n { orderBy: { revisionNumber: 'asc' } },\n )\n\n // TODO(forms-2b): write form_access_audit row via auditAccess hook.\n await this.auditAccess({\n submissionId: submission.id,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n viewerUserId: args.viewerUserId ?? null,\n viewerRole: args.viewerRole ?? null,\n surface: 'admin',\n scope: 'revision',\n })\n\n return { submission, revisions }\n }\n\n async listSubmissionsBySubject(args: Scope & { subjectType: string; subjectId: string }): Promise<FormSubmission[]> {\n const em = this.emFactory()\n return em.find(\n FormSubmission,\n {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n subjectType: args.subjectType,\n subjectId: args.subjectId,\n deletedAt: null,\n },\n { orderBy: { firstSavedAt: 'desc' } },\n )\n }\n\n async listSubmissionsByForm(args: Scope & {\n formId: string\n page: number\n pageSize: number\n status?: string\n }): Promise<{ items: FormSubmission[]; total: number; page: number; pageSize: number }> {\n const em = this.emFactory()\n const where: Record<string, unknown> = {\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n deletedAt: null,\n }\n if (args.status) where.status = args.status\n // Filter by all versions belonging to the form\n const versions = await em.find(FormVersion, {\n formId: args.formId,\n organizationId: args.organizationId,\n tenantId: args.tenantId,\n })\n const versionIds = versions.map((v) => v.id)\n if (versionIds.length === 0) {\n return { items: [], total: 0, page: args.page, pageSize: args.pageSize }\n }\n where.formVersionId = { $in: versionIds }\n const offset = Math.max(0, (args.page - 1) * args.pageSize)\n const [items, total] = await em.findAndCount<FormSubmission>(\n FormSubmission,\n where as never,\n {\n orderBy: { firstSavedAt: 'desc' },\n limit: args.pageSize,\n offset,\n },\n )\n return { items, total, page: args.page, pageSize: args.pageSize }\n }\n\n // --------------------------------------------------------------------------\n // Helpers\n // --------------------------------------------------------------------------\n\n private async decodeRevision(\n organizationId: string,\n revision: FormSubmissionRevision,\n ): Promise<Record<string, unknown>> {\n const ciphertext = ensureBuffer(revision.data)\n if (ciphertext.length === 0) return {}\n const plain = await this.encryption.decrypt(organizationId, ciphertext)\n if (plain.length === 0) return {}\n try {\n return JSON.parse(plain.toString('utf8'))\n } catch {\n return {}\n }\n }\n\n private async safeEmit<K extends keyof SubmissionEvents>(\n eventId: K,\n payload: Parameters<SubmissionEvents[K]>[0],\n ): Promise<void> {\n try {\n await this.emitEvent(eventId, payload)\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown emit error'\n this.logger.warn({ event: 'forms.submission.event_emit_failed', eventId, message }, 'forms event emission failed')\n }\n }\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction ensureBuffer(value: Buffer | Uint8Array | string): Buffer {\n if (Buffer.isBuffer(value)) return value\n if (value instanceof Uint8Array) return Buffer.from(value)\n if (typeof value === 'string') return Buffer.from(value, 'binary')\n return Buffer.alloc(0)\n}\n\nfunction readDeclaredRoles(formVersion: FormVersion): string[] {\n // Union the persisted `roles` column with the schema's `x-om-roles`. Both are\n // authoritative declarations; the column can lag the schema if a publish\n // predates the roles-sync, so a role declared in the published schema must\n // never be rejected at runtime.\n const declared = new Set<string>()\n const roles = formVersion.roles\n if (Array.isArray(roles)) {\n for (const entry of roles) if (typeof entry === 'string') declared.add(entry)\n }\n const fromSchema = (formVersion.schema as Record<string, unknown> | null | undefined)?.['x-om-roles']\n if (Array.isArray(fromSchema)) {\n for (const entry of fromSchema) if (typeof entry === 'string') declared.add(entry)\n }\n return Array.from(declared)\n}\n\nfunction readDefaultActorRole(formVersion: FormVersion): string | null {\n const value = (formVersion.schema as Record<string, unknown> | null | undefined)?.['x-om-default-actor-role']\n return typeof value === 'string' && value.length > 0 ? value : null\n}\n\nfunction computeChangedFieldKeys(\n prior: Record<string, unknown>,\n next: Record<string, unknown>,\n): string[] {\n const keys = new Set<string>([...Object.keys(prior), ...Object.keys(next)])\n const changed: string[] = []\n for (const key of keys) {\n const a = prior[key]\n const b = next[key]\n if (!deepEqual(a, b)) changed.push(key)\n }\n return changed\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a === null || b === null) return false\n if (typeof a !== typeof b) return false\n if (typeof a !== 'object') return false\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false\n if (a.length !== b.length) return false\n return a.every((entry, index) => deepEqual(entry, b[index]))\n }\n const aKeys = Object.keys(a as Record<string, unknown>)\n const bKeys = Object.keys(b as Record<string, unknown>)\n if (aKeys.length !== bKeys.length) return false\n return aKeys.every((key) => deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]))\n}\n\nfunction uniqueStrings(values: string[]): string[] {\n return Array.from(new Set(values))\n}\n\nfunction assertSubmissionPayloadSize(payload: Record<string, unknown>): void {\n const bytes = Buffer.byteLength(JSON.stringify(payload), 'utf8')\n if (bytes <= MAX_SUBMISSION_PAYLOAD_BYTES) return\n throw new SubmissionServiceError(\n 'VALIDATION_FAILED',\n 'Submission payload exceeds the configured size limit.',\n 413,\n { maxBytes: MAX_SUBMISSION_PAYLOAD_BYTES },\n )\n}\n\nfunction validateCompiledFieldRules(\n compiled: CompiledFormVersion,\n schema: Record<string, unknown>,\n payload: Record<string, unknown>,\n): void {\n const properties =\n schema.properties && typeof schema.properties === 'object' && !Array.isArray(schema.properties)\n ? (schema.properties as Record<string, unknown>)\n : {}\n for (const [fieldKey, descriptor] of Object.entries(compiled.fieldIndex)) {\n if (!(fieldKey in payload)) continue\n const rawNode = properties[fieldKey]\n const fieldNode =\n rawNode && typeof rawNode === 'object' && !Array.isArray(rawNode)\n ? (rawNode as FieldNode)\n : undefined\n const result = validateFieldValue(\n payload[fieldKey],\n descriptor.validations,\n 'en',\n descriptor.validationMessages,\n fieldNode,\n )\n if (result.valid) continue\n throw new SubmissionServiceError(\n 'VALIDATION_FAILED',\n 'Submission field failed validation.',\n 422,\n { errors: [{ fieldKey, rule: result.rule, message: result.message }] },\n )\n }\n}\n\n/**\n * Role read-slice plus the visibility pass, in that order.\n *\n * Visibility is evaluated against the FULL decoded payload: an\n * `x-om-visibility-if` predicate may reference a field the role-sliced subset\n * omits, and it must see the real answer. The resulting visible set then\n * narrows the role-sliced payload. A form with no `x-om-visibility-if`\n * resolves to all readable keys, so the second pass is a no-op for it.\n */\nfunction sliceForRole(args: {\n rolePolicy: RolePolicyService\n compiled: CompiledFormVersion\n schema: { properties?: Record<string, VisibilitySchemaNode> }\n role: string\n decodedFull: Record<string, unknown>\n}): Record<string, unknown> {\n const roleSliced = args.rolePolicy.resolve(args.compiled, args.role).sliceReadPayload(args.decodedFull)\n const visible = resolveVisibleFieldKeys({\n compiled: args.compiled,\n schema: args.schema,\n role: args.role,\n data: args.decodedFull,\n })\n return sliceByVisibility(roleSliced, visible)\n}\n\n/** Brand check for SubmissionServiceError \u2014 use instead of `instanceof`. */\nexport function isSubmissionServiceError(error: unknown): error is SubmissionServiceError {\n return typeof error === 'object' && error !== null && (SUBMISSION_SERVICE_ERROR_MARKER in error)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }