{
  "title": "Web Form Builder: upload, save RawJson, reload and download attachments on one shared page",
  "platform": "web",
  "sourceUrl": "https://blukavantui.blutekmedia.com/components/form-renderer#code",
  "downloadUrl": "https://blukavantui.blutekmedia.com/guides/web-form-storage/guide.json",
  "sdkVersion": "0.2.1",
  "package": "@blukavantui/pro",
  "requires": "@blukavantui/community 0.4.0",
  "edition": "pro",
  "scope": "Reusable, typed application integration for the released web SDK. Includes a complete shared workspace and an HTTP storage adapter. Implement the documented routes in your own backend; these routes are not SDK APIs. The website-only adapter is a separate disposable demonstration, not a production storage service.",
  "files": [
    "form-workspace.ts",
    "form-http-store.ts",
    "website-demo-store.ts"
  ],
  "architecture": {
    "definitions": "JSON.stringify(definition) is the whole form definition. Store it in one RawJson column per form row. JSON.parse(RawJson) becomes the web Definition object. Pages and sections are steps inside one shared workspace selected by FormId, not new routes or database tables per form.",
    "responses": "Store JSON.stringify(submittedValues) in a response row’s RawJson column (or a ValuesJson column); load it as FormValues. Keep a definition revision and a separate response revision. The example models the latest response for the authenticated owner/FormId; add an authorized ResponseId for multiple responses.",
    "files": "Store bytes in your private object/file/database storage. Form answers contain only BluFormAttachment references: Id, Name, Size, Type. Never put File objects, base64 bytes, upload URLs, cookies or tokens in RawJson/ValuesJson.",
    "demo": "The published Code preview uses the same workspace with website-demo-store.ts: browser-local JSON/answers, actual session-private website uploads, 1 MiB each, 12 files / 8 MiB per visitor, automatic two-hour expiry. These are website demo limits, not SDK or production limits. Do not call blukavantui.blutekmedia.com/demo/forms/files from another app."
  },
  "usage": "import '@blukavantui/community/theme.css';\nimport { mountFormWorkspace } from './form-workspace';\nimport { createHttpFormStore } from './form-http-store';\n\nconst store = createHttpFormStore('/api/forms', () => ({\n  'x-csrf-token': document.querySelector<HTMLMetaElement>('meta[name=\"csrf-token\"]')!.content,\n}));\nconst workspace = mountFormWorkspace(document.getElementById('forms')!, {\n  formIds: ['service_request', 'event_feedback'], // Authorized IDs loaded by your app\n  mode: 'design', // or 'fill'\n  store,\n});\n// On view/framework unmount: workspace.destroy();",
  "bindings": [
    {
      "component": "FormBuilder",
      "factory": "createFormBuilder",
      "arguments": "host, options, events",
      "options": [
        "FormId",
        "Definition",
        "ResetKey",
        "IsDisabled",
        "SaveResultRequestId",
        "SaveResultSuccess",
        "SaveResultMessage"
      ],
      "events": [
        {
          "name": "SaveRequested",
          "parameters": [
            "requestId",
            "formId",
            "definition"
          ],
          "types": [
            "string",
            "string",
            "BluFormDefinition"
          ],
          "action": "Await store.saveDefinition(formId, JSON.stringify(definition), expectedRevision, signal). Only then update SaveResultRequestId=requestId, SaveResultSuccess=true and SaveResultMessage. On rejection use the SAME requestId with success=false; retain the draft."
        },
        {
          "name": "DefinitionChanged",
          "parameters": [
            "definition"
          ],
          "types": [
            "BluFormDefinition"
          ],
          "action": "Optional draft observer; does not persist by itself."
        },
        {
          "name": "DirtyChanged",
          "parameters": [
            "isDirty"
          ],
          "types": [
            "boolean"
          ],
          "action": "Optional dirty-navigation guard; wire your application’s unsaved-changes policy."
        }
      ]
    },
    {
      "component": "FormRenderer",
      "factory": "createFormRenderer",
      "arguments": "host, options, events",
      "options": [
        "FormId",
        "Definition",
        "Values",
        "ResetKey",
        "IsDisabled",
        "UploadUrl",
        "UploadHeaders",
        "UploadWithCredentials",
        "SubmitResultRequestId",
        "SubmitResultSuccess",
        "SubmitResultMessage"
      ],
      "events": [
        {
          "name": "ValuesChanged",
          "parameters": [
            "values",
            "fieldKey"
          ],
          "types": [
            "FormValues",
            "string"
          ],
          "action": "Draft values include attachment IDs only after a successful upload. Update current download controls here. There is no separate UploadCompleted callback."
        },
        {
          "name": "SubmitRequested",
          "parameters": [
            "requestId",
            "formId",
            "values"
          ],
          "types": [
            "string",
            "string",
            "FormValues"
          ],
          "action": "Await store.saveResponse(formId, JSON.stringify(values), definitionRevision, expectedResponseRevision, signal). Only then echo SubmitResultRequestId=requestId and SubmitResultSuccess=true. On HTTP/network/validation/concurrency failure acknowledge false with the same ID and retain answers."
        }
      ]
    }
  ],
  "reload": [
    "On initial load, FormId switch, mode change or explicit Reload, await store.load(FormId). Parse RawJson to Definition and ValuesJson to Values.",
    "Use a fresh ResetKey for explicit loads, not on every keystroke. Clear old acknowledgement IDs. The supplied workspace recreates the appropriate instance only after the next load succeeds.",
    "Disable record/mode switches while a request is pending. Ignore results after unmount or a newer generation; abort outstanding fetches on destroy. Failed loads retain the previous mounted form.",
    "Reload after a successful definition change before filling. Decide whether to migrate old responses or return {} for a changed schema; do not apply incompatible answers automatically. Track response revisions independently."
  ],
  "endpoints": [
    {
      "method": "GET",
      "path": "/api/forms/:formId",
      "request": "Authenticated session; optional CSRF/auth headers",
      "response": {
        "FormId": "service_request",
        "RawJson": "JSON.stringify(completeDefinition)",
        "ValuesJson": "JSON.stringify(savedAnswers)",
        "Revision": 1,
        "ResponseRevision": 0,
        "Upload": {
          "url": "/api/forms/attachments",
          "headers": {
            "x-csrf-token": "fresh session CSRF token"
          },
          "withCredentials": false
        }
      },
      "behavior": "Authorize owner and form. Load the saved definition and compatible response. Supply fresh upload headers separately; do not persist Upload in RawJson."
    },
    {
      "method": "PUT",
      "path": "/api/forms/:formId/definition",
      "request": {
        "RawJson": "complete definition JSON string",
        "ExpectedRevision": 1
      },
      "response": {
        "Revision": 2
      },
      "behavior": "Authorize design permission, validate definition, compare revision and commit atomically. Return 409 on conflict. Changing the schema needs a response migration or invalidation policy."
    },
    {
      "method": "PUT",
      "path": "/api/forms/:formId/response",
      "request": {
        "ValuesJson": "complete answer JSON string",
        "DefinitionRevision": 2,
        "ExpectedResponseRevision": 0
      },
      "response": {
        "ResponseRevision": 1
      },
      "behavior": "Authorize the respondent. Validate against the stored definition: visible/required fields, types, choices, limits. Recheck every attachment owner/form/field/expiry and stored bytes. Compare both revisions and commit atomically. Return 409 on conflict; 422 for invalid data."
    },
    {
      "method": "POST",
      "path": "/api/forms/attachments",
      "request": "multipart/form-data: file (binary), formId (text), fieldKey (text), clientKey (text). Browser sets Content-Type and boundary.",
      "response": {
        "Id": "durable opaque storage ID",
        "stored": true
      },
      "behavior": "Authenticate and authorize. Validate the stored FileUpload field, actual size/type, file counts and quota; store bytes before returning 2xx JSON. Id must identify actual durable storage. Do not return stored:false or invent an ID. Use clientKey for idempotent retries scoped to owner/form/field; reject reuse with different content."
    },
    {
      "method": "GET",
      "path": "/api/forms/attachments/:id",
      "request": "Authenticated session and required auth headers",
      "response": "Stored file bytes; Content-Disposition: attachment; Cache-Control: private, no-store; X-Content-Type-Options: nosniff",
      "behavior": "Authorize by stored owner/form, never by caller-supplied owner ID. Return 404 for inaccessible/deleted/expired files. The workspace fetches a Blob with auth headers and downloads a short-lived object URL; it does not put credentials in URLs."
    }
  ],
  "uploadSettings": {
    "UploadUrl": "Your shared same-origin multipart endpoint",
    "UploadHeaders": "Record<string,string> containing fresh CSRF/auth headers, not UploadHeadersJson",
    "UploadWithCredentials": "false by default; same-origin cookies still work. For an explicitly supported cross-origin API use true plus credentialed CORS with an exact allowed origin.",
    "boundary": "Do not set Content-Type manually on multipart uploads. UploadFormat is not present in Pro Web 0.2.1; do not copy the newer native Binary example."
  },
  "serverChecklist": [
    "Derive owner from authenticated server context; preserve existing role checks and CSRF protection. The list of FormIds in the browser is not authorization.",
    "Keep file bytes private, use random opaque IDs, validate content as well as extensions, enforce body/field/file limits and quotas. Apply scanning/quarantine and retention appropriate to your application.",
    "Use transactional compare-and-swap for both definition and response revisions. The backend validates JSON and attachments independently of client-side helpers.",
    "Link uploaded IDs only after successful response persistence. Define orphan cleanup and expiry; removing a reference from the renderer does not delete stored bytes automatically.",
    "Register fixed /attachments routes before /:formId where your router uses first-match routing. Upload/download share the same storage and owner checks.",
    "The provided HTTP adapter is for same-origin routes. Replace authentication and transport deliberately if your application uses another deployment layout."
  ],
  "tests": [
    "Design and save a form containing FileUpload; reload and verify every field/condition/file limit is retained.",
    "Upload a small test file; verify values contain Id/Name/Size/Type and actual bytes exist server-side. Submit, fully reload the page and download; compare bytes.",
    "Reject a definition save and response save; verify the draft remains and matching failure acknowledgement is shown.",
    "Reject unauthorized downloads, invalid/oversized files, wrong form/field IDs, expired references and stale revisions. Test concurrent tabs.",
    "Switch FormId on the shared page and unmount during a pending request; late results must not update another record or removed view."
  ],
  "sourceFiles": {
    "form-workspace.ts": "import { createFormBuilder } from '@blukavantui/pro/form-builder';\nimport { createFormRenderer, type BluFormDefinition, type FormValues, type BluFormAttachment } from '@blukavantui/pro/form-renderer';\nimport '@blukavantui/pro/form-builder.css';\nimport '@blukavantui/pro/form-renderer.css';\n\n// Load @blukavantui/community/theme.css once in your application's browser entry.\n// These interfaces belong to this application example, not to the component SDK.\nexport interface SavedForm {\n  FormId: string;\n  RawJson: string;       // JSON.stringify(complete BluFormDefinition)\n  ValuesJson: string;    // JSON.stringify(complete submitted FormValues)\n  Revision: number;      // server-issued definition revision\n  ResponseRevision: number; // server-issued response revision (prevents lost edits)\n  Upload: { url: string; headers: Record<string, string>; withCredentials?: boolean };\n}\nexport interface FormStore {\n  load(formId: string, signal: AbortSignal): Promise<SavedForm>;\n  saveDefinition(formId: string, rawJson: string, expectedRevision: number, signal: AbortSignal): Promise<{ Revision: number }>;\n  saveResponse(formId: string, valuesJson: string, definitionRevision: number, expectedResponseRevision: number, signal: AbortSignal): Promise<{ ResponseRevision: number }>;\n  download(fileId: string, signal: AbortSignal): Promise<Blob>;\n}\nexport interface WorkspaceOptions {\n  formIds: string[];\n  store: FormStore;\n  mode?: 'design' | 'fill';\n}\n\n/** One mounted workspace loads any authorized FormId. No DOM tree per form. */\nexport function mountFormWorkspace(host: HTMLElement, options: WorkspaceOptions) {\n  const lifetime = new AbortController(), store = options.store;\n  const panel = document.createElement('section'), controls = document.createElement('div');\n  controls.style.cssText = 'display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-bottom:16px';\n  const select = document.createElement('select'); select.setAttribute('aria-label', 'Form to load');\n  for (const id of options.formIds) { const option = document.createElement('option'); option.value = id; option.textContent = id; select.append(option); }\n  const reload = document.createElement('button'); reload.type = 'button'; reload.textContent = 'Reload saved form (discard draft)';\n  const design = document.createElement('button'); design.type = 'button'; design.textContent = 'Design form';\n  const fill = document.createElement('button'); fill.type = 'button'; fill.textContent = 'Fill saved form';\n  for (const button of [reload, design, fill]) button.className = 'blu-forms__button';\n  const status = document.createElement('p'); status.setAttribute('role', 'status');\n  const surface = document.createElement('div'), downloads = document.createElement('div');\n  controls.append(select, reload, design, fill); panel.append(controls, status, surface, downloads); host.append(panel);\n  let record: SavedForm | undefined, activeId = options.formIds[0] || '', mode = options.mode || 'fill';\n  let builder: ReturnType<typeof createFormBuilder> | undefined, renderer: ReturnType<typeof createFormRenderer> | undefined;\n  let busy = false, generation = 0, reset = 0, disposed = false;\n  const current = (ticket: number) => !disposed && ticket === generation;\n  function message(error: unknown) { return error instanceof Error ? error.message : 'The operation failed. Please retry.'; }\n  function setBusy(value: boolean) {\n    busy = value;\n    for (const input of [select, reload, design, fill]) input.disabled = value;\n    builder?.update({ IsDisabled: value }); renderer?.update({ IsDisabled: value });\n  }\n  function fileLinks(definition: BluFormDefinition, values: FormValues) {\n    downloads.replaceChildren();\n    const fields = definition.Pages.flatMap(page => page.Sections.flatMap(section => section.Fields)).filter(field => field.Type === 'FileUpload');\n    for (const field of fields) {\n      const files = values[field.Key]; if (!Array.isArray(files)) continue;\n      for (const file of files as BluFormAttachment[]) {\n        if (!file || typeof file.Id !== 'string') continue;\n        const button = document.createElement('button'); button.type = 'button'; button.className = 'blu-forms__button';\n        button.textContent = 'Download ' + file.Name; button.style.margin = '8px';\n        button.onclick = async () => {\n          button.disabled = true; const ticket = generation;\n          try {\n            const blob = await store.download(file.Id, lifetime.signal); if (!current(ticket)) return;\n            const url = URL.createObjectURL(blob), anchor = document.createElement('a');\n            anchor.href = url; anchor.download = file.Name; anchor.hidden = true; panel.append(anchor); anchor.click(); anchor.remove();\n            setTimeout(() => URL.revokeObjectURL(url), 1000);\n          } catch (error) { if (current(ticket)) status.textContent = message(error); }\n          finally { button.disabled = false; }\n        };\n        downloads.append(button);\n      }\n    }\n  }\n  async function load(formId: string) {\n    if (busy || disposed) return;\n    let ticket = generation; setBusy(true); status.textContent = 'Loading saved form…';\n    try {\n      const loaded = await store.load(formId, lifetime.signal); if (!current(ticket)) return;\n      if (loaded.FormId !== formId) throw Error('The server returned a different form.');\n      const definition = JSON.parse(loaded.RawJson) as BluFormDefinition, values = JSON.parse(loaded.ValuesJson) as FormValues;\n      if (definition.Version !== 1 || !Array.isArray(definition.Pages) || !values || Array.isArray(values) || typeof values !== 'object') throw Error('Invalid saved form JSON.');\n      record = loaded; activeId = formId; select.value = formId;\n      ticket = ++generation;\n      // Replace the instances only on an explicit load/mode change, never on each keystroke.\n      builder?.destroy(); renderer?.destroy(); builder = undefined; renderer = undefined;\n      surface.replaceChildren(); downloads.replaceChildren();\n      const resetKey = String(++reset);\n      if (mode === 'design') {\n        builder = createFormBuilder(surface, { FormId: formId, Definition: definition, ResetKey: resetKey }, {\n          async SaveRequested(requestId, requestedFormId, requestedDefinition) {\n            if (busy || requestedFormId !== activeId || !current(ticket)) return;\n            setBusy(true);\n            try {\n              const result = await store.saveDefinition(requestedFormId, JSON.stringify(requestedDefinition), loaded.Revision, lifetime.signal);\n              if (!current(ticket)) return;\n              loaded.Revision = result.Revision; loaded.RawJson = JSON.stringify(requestedDefinition);\n              builder!.update({ SaveResultRequestId: requestId, SaveResultSuccess: true, SaveResultMessage: 'Form saved. Choose Fill saved form to use it.' });\n              status.textContent = 'Complete definition saved as RawJson.';\n            } catch (error) {\n              if (current(ticket)) { builder!.update({ SaveResultRequestId: requestId, SaveResultSuccess: false, SaveResultMessage: message(error) }); status.textContent = message(error); }\n            } finally { if (current(ticket)) setBusy(false); }\n          },\n          Error: text => { status.textContent = text; },\n        });\n      } else {\n        renderer = createFormRenderer(surface, { FormId: formId, Definition: definition, Values: values, ResetKey: resetKey,\n          UploadUrl: loaded.Upload.url, UploadHeaders: loaded.Upload.headers, UploadWithCredentials: loaded.Upload.withCredentials || false }, {\n          ValuesChanged: draft => { fileLinks(definition, draft); }, // Draft state only; uploads yield durable IDs here.\n          async SubmitRequested(requestId, requestedFormId, submittedValues) {\n            if (busy || requestedFormId !== activeId || !current(ticket)) return;\n            setBusy(true);\n            try {\n              const result = await store.saveResponse(requestedFormId, JSON.stringify(submittedValues), loaded.Revision, loaded.ResponseRevision, lifetime.signal);\n              if (!current(ticket)) return;\n              loaded.ResponseRevision = result.ResponseRevision; loaded.ValuesJson = JSON.stringify(submittedValues);\n              renderer!.update({ SubmitResultRequestId: requestId, SubmitResultSuccess: true, SubmitResultMessage: 'Answers and attachment references saved.' });\n              fileLinks(definition, submittedValues); status.textContent = 'Saved. Reload to restore the form, answers and attachments.';\n            } catch (error) {\n              if (current(ticket)) { renderer!.update({ SubmitResultRequestId: requestId, SubmitResultSuccess: false, SubmitResultMessage: message(error) }); status.textContent = message(error); }\n            } finally { if (current(ticket)) setBusy(false); }\n          },\n          Error: text => { status.textContent = text; },\n        });\n        fileLinks(definition, values);\n      }\n      design.setAttribute('aria-pressed', String(mode === 'design')); fill.setAttribute('aria-pressed', String(mode === 'fill'));\n      status.textContent = 'Loaded saved definition and answers. Files are downloaded through your application.';\n    } catch (error) { if (current(ticket)) { select.value = activeId; status.textContent = message(error); } }\n    finally { if (current(ticket)) setBusy(false); }\n  }\n  select.onchange = () => { void load(select.value); };\n  reload.onclick = () => { void load(activeId); };\n  design.onclick = () => { if (!busy) { mode = 'design'; void load(activeId); } };\n  fill.onclick = () => { if (!busy) { mode = 'fill'; void load(activeId); } };\n  void load(activeId);\n  return { reload() { return load(activeId); }, get record() { return record; }, destroy() { disposed = true; generation++; lifetime.abort(); builder?.destroy(); renderer?.destroy(); panel.remove(); } };\n}\n",
    "form-http-store.ts": "import type { FormStore, SavedForm } from './form-workspace';\n\n/** Connect the workspace to same-origin application routes documented in the guide.\n * Implement these routes on YOUR server; they are not included in the SDK.\n * The server derives the owner from its session, validates the stored definition,\n * checks attachment ownership, and uses optimistic concurrency for both records.\n */\nexport function createHttpFormStore(baseUrl: string, headers: () => Record<string, string>): FormStore {\n  const base = baseUrl.replace(/\\/$/, '');\n  async function request<T>(path: string, signal: AbortSignal, body?: unknown): Promise<T> {\n    const response = await fetch(base + path, { method: body === undefined ? 'GET' : 'PUT', signal,\n      credentials: 'same-origin', cache: 'no-store',\n      headers: { Accept: 'application/json', ...headers(), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },\n      ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n    });\n    const result = await response.json().catch(() => null);\n    if (!response.ok) throw Error(result?.error || 'Request failed (' + response.status + '). Your draft is retained.');\n    if (!result) throw Error('The server returned an invalid JSON response.');\n    return result as T;\n  }\n  return {\n    load: (formId, signal) => request<SavedForm>('/' + encodeURIComponent(formId), signal),\n    saveDefinition: (formId, RawJson, ExpectedRevision, signal) => request('/' + encodeURIComponent(formId) + '/definition', signal, { RawJson, ExpectedRevision }),\n    saveResponse: (formId, ValuesJson, DefinitionRevision, ExpectedResponseRevision, signal) => request('/' + encodeURIComponent(formId) + '/response', signal, { ValuesJson, DefinitionRevision, ExpectedResponseRevision }),\n    async download(fileId, signal) {\n      const response = await fetch(base + '/attachments/' + encodeURIComponent(fileId), { signal, credentials: 'same-origin', cache: 'no-store', headers: headers() });\n      if (!response.ok) throw Error(response.status === 404 ? 'Attachment expired or unavailable. Remove it and upload it again.' : 'Download failed. Please retry.');\n      return response.blob();\n    },\n  };\n}\n",
    "website-demo-store.ts": "import { validateFormValues, type BluFormDefinition, type FormValues } from '@blukavantui/pro/form-renderer';\nimport type { FormStore } from './form-workspace';\n\n// WEBSITE DEMONSTRATION ONLY. Your own app uses createHttpFormStore and its own\n// endpoints. /demo/forms/files is private to this site's session and is not a\n// public upload service. Definitions/answers below are browser-local, NOT a DB.\nconst definition: BluFormDefinition = { Version: 1, Key: 'web_storage_example', Title: 'Save a form with attachments',\n  Description: 'Upload a test file, submit, reload this page and download it. Demo uploads expire after two hours.',\n  SubmitLabel: 'Save response & attachments', Pages: [{ Key: 'request', Title: 'Your request', Sections: [{ Key: 'details', Fields: [\n    { Key: 'notes', Type: 'TextArea', Label: 'Notes', MaxLength: 500 },\n    { Key: 'files', Type: 'FileUpload', Label: 'Supporting files', Required: true, Accept: '.txt,.pdf,.png,.jpg,.jpeg', MaxFiles: 3, MaxFileSizeMB: 1 },\n  ] }] }] };\ntype BrowserRecord = { RawJson: string; ValuesJson: string; Revision: number; ResponseRevision: number };\nexport function createWebsiteDemoStore(scenario: string): FormStore {\n  const key = (id: string) => 'blukavantui-code-storage-v1:' + scenario + ':' + id;\n  const read = (id: string): BrowserRecord => JSON.parse(localStorage.getItem(key(id)) || 'null') || { RawJson: JSON.stringify(definition), ValuesJson: '{}', Revision: 1, ResponseRevision: 0 };\n  const save = (id: string, record: BrowserRecord) => localStorage.setItem(key(id), JSON.stringify(record));\n  async function files(signal: AbortSignal) {\n    const response = await fetch('/demo/forms/files', { signal, credentials: 'same-origin', cache: 'no-store' });\n    if (!response.ok) throw Error('Could not connect attachment storage. Reload and retry.');\n    return (await response.json()).files as { Id: string; formId: string; fieldKey: string; ExpiresAt: number }[];\n  }\n  return {\n    async load(FormId, signal) {\n      await files(signal);\n      return { FormId, ...read(FormId), Upload: { url: '/demo/forms/files', headers: { 'x-csrf-token': document.querySelector<HTMLMetaElement>('meta[name=csrf-token]')?.content || '' } } };\n    },\n    async saveDefinition(id, RawJson, expectedRevision) {\n      const old = read(id); if (old.Revision !== expectedRevision) throw Error('This definition changed in another tab. Reload before saving.');\n      JSON.parse(RawJson);\n      const next = { RawJson, Revision: old.Revision + 1, ValuesJson: '{}', ResponseRevision: old.ResponseRevision + 1 };\n      save(id, next); return { Revision: next.Revision };\n    },\n    async saveResponse(id, ValuesJson, definitionRevision, expectedResponseRevision, signal) {\n      const storedFiles = await files(signal), old = read(id), form: BluFormDefinition = JSON.parse(old.RawJson), values: FormValues = JSON.parse(ValuesJson);\n      if (old.Revision !== definitionRevision || old.ResponseRevision !== expectedResponseRevision) throw Error('This record changed in another tab. Reload before submitting.');\n      const errors = validateFormValues(form, values); if (errors.length) throw Error(errors[0].Message);\n      for (const field of form.Pages.flatMap(page => page.Sections.flatMap(section => section.Fields)).filter(field => field.Type === 'FileUpload')) {\n        const references = values[field.Key]; if (!Array.isArray(references)) continue;\n        for (const file of references) if (typeof file !== 'object' || !storedFiles.some(stored => stored.Id === file.Id && stored.formId === id && stored.fieldKey === field.Key && stored.ExpiresAt > Date.now())) throw Error('An attachment expired. Remove it and upload it again.');\n      }\n      save(id, { ...old, ValuesJson, ResponseRevision: old.ResponseRevision + 1 }); return { ResponseRevision: old.ResponseRevision + 1 };\n    },\n    async download(id, signal) {\n      const response = await fetch('/demo/forms/files/' + encodeURIComponent(id), { signal, credentials: 'same-origin', cache: 'no-store' });\n      if (!response.ok) throw Error('This attachment expired or is unavailable in this browser session.');\n      return response.blob();\n    },\n  };\n}\n"
  }
}
