@declarion/react 0.4.1 → 0.4.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"value-CiwnEAde.js","names":[],"sources":["../src/types/api.ts","../src/api/auth-redirect.ts","../src/embed/protocol.ts","../src/embed/token.ts","../src/lib/versionTracker.ts","../src/api/client.ts","../src/embed/file-src.ts","../src/components/file-widgets/upload.ts","../src/components/file-widgets/helpers.ts","../src/components/file-widgets/draft-scope.tsx","../src/components/file-widgets/useEmbedFileSrc.ts","../src/components/file-widgets/value.ts"],"sourcesContent":["import type { AccentDef, LocalizedString } from \"./schema\";\nimport type { HydratedFile } from \"./files\";\n\nexport interface ListMeta {\n /**\n * Total matching rows. Present only when the request asked for a count\n * (`count=with` / `count=only` / `count=exact`). Absent (undefined) on a\n * window-only response (`count` omitted) - the page of rows arrives\n * without ever blocking on the count. Treat `count_mode === undefined` as\n * \"no count yet\" and ignore `total` then.\n */\n total?: number;\n page: number;\n per_page: number;\n /** Present only when a count was computed (see `total`). */\n total_pages?: number;\n has_more?: boolean;\n /**\n * How `total` was produced: \"exact\" (an exact COUNT(*)) or \"estimated\"\n * (an O(1) query-planner estimate). Absent when no count was computed.\n * The SDK keys display precision on this, NOT on the entity schema, so a\n * forced exact count renders precisely even on a `count: estimated`\n * entity.\n */\n count_mode?: \"exact\" | \"estimated\";\n}\n\nexport interface ListResponse<T = Record<string, unknown>> {\n data: T[];\n meta: ListMeta;\n $refs?: Record<string, Record<string, Record<string, unknown>>>;\n}\n\nexport interface DataResponse<T = Record<string, unknown>> {\n data: T;\n}\n\nexport interface ApiErrorBody {\n error: {\n message: string;\n code: string;\n field?: string;\n };\n /** Bulk operation item index (present on validation errors for array requests). */\n index?: number;\n}\n\nexport class ApiError extends Error {\n status: number;\n code: string;\n /** Bulk operation item index (present on validation errors for array requests). */\n index?: number;\n /** Raw JSON body for endpoints that return a non-standard error envelope (e.g. invoke:each batch). */\n raw?: unknown;\n\n constructor(status: number, message: string, code: string, index?: number, raw?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.code = code;\n this.index = index;\n this.raw = raw;\n }\n}\n\n/**\n * Stable error codes for cursor-based list pagination. The server returns\n * these on ValidationError{Field:\"after\"} when an `after` token cannot be\n * resumed under current request conditions:\n *\n * - CURSOR_SEMANTICS_MISMATCH: the cursor was minted under different sort\n * semantics (multi-column ordering, NULL placement). Server bumped its\n * cursor format version since the cursor was issued.\n * - CURSOR_LOCALE_CHANGED: the cursor was minted under one active locale\n * and the request now carries a different one. Issued so multilang sort\n * cannot silently re-order mid-pagination.\n *\n * UX contract for both: clear the `after` token and refetch page 1 under\n * the current conditions. EntityClient.list does this transparently;\n * direct apiFetch callers can recognise these codes to drive their own\n * retry logic.\n */\nexport const CURSOR_SEMANTICS_MISMATCH = \"cursor_semantics_mismatch\";\nexport const CURSOR_LOCALE_CHANGED = \"cursor_locale_changed\";\n\n/** True if the error is a recoverable cursor-stale signal that warrants\n * refetching page 1 instead of surfacing to the user. */\nexport function isCursorStaleError(err: unknown): boolean {\n return (\n err instanceof ApiError &&\n (err.code === CURSOR_SEMANTICS_MISMATCH || err.code === CURSOR_LOCALE_CHANGED)\n );\n}\n\nexport type TenantEnv = \"production\" | \"staging\" | \"dev\" | string;\n\nexport interface Tenant {\n id: string;\n code: string;\n name: string;\n role: string;\n env?: TenantEnv;\n /**\n * Whether the caller has a `tenant_users` row for this tenant. Always\n * true for non-superadmins. For superadmins, `auth.list_tenants` also\n * returns non-membership tenants reachable via cross-tenant authority;\n * the switcher splits them into a separate \"All tenants\" section.\n */\n is_member?: boolean;\n}\n\nexport interface AuthUser {\n id: string;\n email: string;\n display_name: string;\n is_active: boolean;\n roles: string[];\n permissions?: string[];\n tenant?: Tenant;\n /** Populated only when the request is acting under an impersonation session. */\n impersonation?: ImpersonationInfo;\n created_at: string;\n updated_at: string;\n}\n\n/**\n * ImpersonationInfo is the read-side projection of the active session\n * surfaced by auth.me. Drives the banner, topbar chip, viewport frame,\n * and the \"Exit impersonation\" menu item.\n */\nexport interface ImpersonationInfo {\n session_id: string;\n real_user_id: string;\n real_user_email?: string;\n target_user_id: string;\n target_user_email?: string;\n reason: string;\n started_at: string;\n expires_at: string;\n}\n\nexport interface LoginResponse {\n expires_at: string;\n user: AuthUser;\n tenant?: Tenant;\n}\n\nexport interface CheckSetupResponse {\n needs_setup: boolean;\n}\n\nexport interface StatusInfo {\n group_code: string;\n status_code: string;\n status_name: LocalizedString;\n color?: string;\n set_by?: string;\n set_at: string;\n note?: string;\n}\n\nexport interface PropertyValue {\n property_code: string;\n name: LocalizedString;\n type: string;\n value: unknown;\n}\n\nexport interface PublicParam {\n code: string;\n name: LocalizedString;\n type: string;\n category: string;\n description?: LocalizedString;\n value: unknown;\n}\n\n/** Per-app legal/support URL set rendered in the auth-shell footer. */\nexport interface PublicLegalLinks {\n security_url?: string;\n privacy_url?: string;\n terms_url?: string;\n docs_url?: string;\n}\n\n/** Subset of branding exposed pre-login by /api/params/public. */\nexport interface PublicBranding {\n app_name?: string;\n app_logo?: string;\n app_initial?: string;\n accent_color?: string;\n ai_name?: string;\n wordmark?: string;\n version_label?: string;\n status_url?: string;\n auth_tagline?: LocalizedString;\n auth_subtagline?: LocalizedString;\n auth_badges?: LocalizedString[];\n legal_entity?: string;\n workspace_domain?: string;\n legal_links?: PublicLegalLinks;\n}\n\n/**\n * Deploy-time identity exposed pre-login by /api/params/public so the\n * auth shell can render real region/build values (no mockup placeholders).\n * Each field is empty when unconfigured/unset; consumers hide the pill\n * when the entire block is missing or empty.\n */\nexport interface PublicMeta {\n version?: string;\n revision?: string;\n build_time?: string;\n region?: string;\n}\n\n/** Envelope returned by GET /api/params/public. */\nexport interface PublicParamsResponse {\n parameters: PublicParam[];\n accents?: Record<string, AccentDef>;\n messages?: Record<string, LocalizedString>;\n branding?: PublicBranding;\n meta?: PublicMeta;\n}\n\n/**\n * Wire envelope for GET /api/params/{code}.\n *\n * The server always responds with HTTP 200. `found:false` means the\n * platform has no value for the code (undeclared, restricted-category,\n * or no value at any resolution layer); the caller substitutes its own\n * default. `found:true` means `value` is authoritative - even if it is\n * null/zero (an explicit nil value is a real value, not absence).\n *\n * The default never crosses the wire; servers don't track caller\n * defaults, so two callers with different defaults always see consistent\n * server state.\n */\nexport interface ParamLookup {\n code: string;\n found: boolean;\n value?: unknown;\n source?: \"object\" | \"user\" | \"role\" | \"tenant\" | \"env\" | \"yaml\";\n type?: string;\n}\n\n/** @deprecated Prefer `ParamLookup`. Retained for callers still inspecting `source`. */\nexport interface ResolvedParam {\n code: string;\n name: LocalizedString;\n type: string;\n category?: string;\n description?: LocalizedString;\n value: unknown;\n source: \"object\" | \"user\" | \"role\" | \"tenant\" | \"env\" | \"yaml\" | \"none\";\n}\n\nexport interface ParamOverride {\n param_code: string;\n value: unknown;\n}\n\nexport interface SSOProvider {\n name: string;\n slug: string;\n}\n\nexport interface ListParams {\n page?: number;\n per_page?: number;\n sort?: string;\n search?: string;\n /** JSON filter nodes (recursive AND/OR tree). */\n filters?: FilterNodeParam[];\n include_deleted?: boolean;\n expand?: string[];\n screen?: string;\n /** Return bounded display previews for fields that declare display.list_preview. */\n preview?: boolean;\n /**\n * Field whitelist. When set, the server returns only these columns plus\n * primary-key field(s) (auto-injected server-side). Filters/search/sort\n * still operate against all fields regardless of select. Omit to fetch\n * every column (default, backwards-compatible).\n */\n select?: string[];\n /**\n * Row-count strategy for this request. Maps to the `?count=` query param.\n * omitted / \"window\" - rows only, no total (the page-window request;\n * the table never blocks on a count).\n * \"only\" - total only, no rows (the follow-up count request).\n * \"with\" - rows + total, using the entity's declared strategy.\n * \"exact\" - rows + forced exact total.\n * See docs/plans/2026-05-18-declarative-row-count.md.\n */\n count?: \"window\" | \"only\" | \"with\" | \"exact\";\n}\n\n/** Minimal filter node type for API params (matches backend FilterNode). */\nexport interface FilterNodeParam {\n field?: string;\n op?: string;\n value?: unknown;\n or?: FilterNodeParam[][];\n and?: FilterNodeParam[];\n}\n\n// ---------------------------------------------------------------------------\n// v2 API types - bulk operations, cursor pagination, sub-resource shapes\n// ---------------------------------------------------------------------------\n\n/** Cursor-based pagination metadata returned by v2 List. */\nexport interface V2Meta {\n total: number;\n limit: number;\n has_more: boolean;\n cursor?: string;\n}\n\n/**\n * Refs map: keyed by entity code (or $-prefixed virtual entity code),\n * then by record ID, then field values.\n * Example: { company: { \"uuid-1\": { id: \"uuid-1\", name: \"Acme\" } } }\n */\nexport type Refs = Record<string, Record<string, Record<string, unknown>>>;\n\n/** Per-row status instance. */\nexport interface V2StatusInstance {\n group_code: string;\n status_code: string;\n status_name?: Record<string, string>;\n color?: string;\n set_at?: string;\n set_by?: string;\n note?: string;\n}\n\n/** Per-row property instance. */\nexport interface V2PropertyInstance {\n property_code: string;\n value: unknown;\n updated_at?: string;\n updated_by?: string;\n}\n\n/** Per-row parameter override. */\nexport interface V2ParamInstance {\n param_code: string;\n value: unknown;\n}\n\n/** v2 List response with cursor pagination and response-level $refs. */\nexport interface V2ListResponse<T = Record<string, unknown>> {\n data: T[];\n meta: V2Meta;\n $refs?: Refs;\n}\n\n/** v2 List query parameters. */\nexport interface V2ListParams {\n select?: string[];\n expand?: string[];\n sort?: string;\n search?: string;\n filters?: FilterNodeParam[];\n limit?: number;\n after?: string;\n include_deleted?: boolean;\n}\n\n/** Per-field resolved display unit codes. */\nexport type Units = Record<string, string>;\n\n/** A row enriched with $-prefixed sub-resources (as returned by Get or write responses). */\nexport interface EnrichedRow extends Record<string, unknown> {\n $refs?: Refs;\n $files?: Record<string, HydratedFile | HydratedFile[] | null>;\n $statuses?: V2StatusInstance[];\n $properties?: V2PropertyInstance[];\n $params?: V2ParamInstance[];\n $children?: Record<string, Record<string, unknown>[]>;\n $units?: Units;\n}\n","const REDIRECT_FALLBACK = \"/\";\nconst LOGIN_PATH = \"/login\";\nconst SIGNUP_PATH = \"/signup\";\nconst PARSE_BASE = \"https://declarion.local\";\n\nfunction isAuthRoute(path: string): boolean {\n const pathname = path.split(/[?#]/, 1)[0];\n return (\n pathname === LOGIN_PATH ||\n pathname.startsWith(`${LOGIN_PATH}/`) ||\n pathname === SIGNUP_PATH ||\n pathname.startsWith(`${SIGNUP_PATH}/`)\n );\n}\n\nexport function sanitizeRedirectPath(value: string | null | undefined): string {\n if (!value) return REDIRECT_FALLBACK;\n\n const lower = value.toLowerCase();\n if (\n !value.startsWith(\"/\") ||\n value.startsWith(\"//\") ||\n value.includes(\"\\\\\") ||\n lower.includes(\"%5c\")\n ) {\n return REDIRECT_FALLBACK;\n }\n\n try {\n const parsed = new URL(value, PARSE_BASE);\n if (parsed.origin !== PARSE_BASE) return REDIRECT_FALLBACK;\n\n const path = `${parsed.pathname}${parsed.search}${parsed.hash}`;\n if (!path.startsWith(\"/\") || path.startsWith(\"//\") || path.includes(\"\\\\\")) {\n return REDIRECT_FALLBACK;\n }\n return path || REDIRECT_FALLBACK;\n } catch {\n return REDIRECT_FALLBACK;\n }\n}\n\nexport function loginPathForRedirectPath(value: string | null | undefined): string {\n const redirectPath = sanitizeRedirectPath(value);\n if (redirectPath === REDIRECT_FALLBACK || isAuthRoute(redirectPath)) {\n return LOGIN_PATH;\n }\n return `${LOGIN_PATH}?next=${encodeURIComponent(redirectPath)}`;\n}\n\nexport function loginPathForCurrentLocation(): string {\n if (typeof window === \"undefined\") return LOGIN_PATH;\n const { pathname, search, hash } = window.location;\n return loginPathForRedirectPath(`${pathname}${search}${hash}`);\n}\n","// Embed postMessage protocol.\n//\n// The iframe (this Declarion app) and the white-label host page exchange a\n// small, versioned set of `postMessage` frames: auth handoff, resize, theme,\n// navigation, and unsaved-changes signals. This module is the protocol\n// foundation - the typed envelope, the message-type constants, the per-type\n// payload interfaces, and the single outbound sender `postToParent`.\n//\n// Naming contract (developer-facing - keep it consistent):\n// - A message the iframe SENDS to the host is an EVENT, named in the past\n// tense - \"this happened\": `resized`, `navigated`, `navigation-requested`,\n// `dirty-changed`, `token-expired`, `reload-required`, `ready`.\n// - A message the host SENDS to the iframe is a COMMAND, named as an\n// imperative - \"do this\": `set-token`, `navigate`, `set-theme`.\n//\n// Security model: every outbound frame targets the EXACT parent origin, never\n// `\"*\"`. A frame is sent only when the iframe is genuinely framed\n// (`window.parent !== window`) and a concrete `parentOrigin` is known. The\n// inbound side (origin-checked listener) lives in `handshake.ts`; this module\n// owns the outbound half plus the shared types both halves build on.\n\n/**\n * The `source` discriminator stamped on every embed frame. The host SDK and\n * the iframe both filter inbound messages on this value so unrelated\n * `postMessage` traffic (browser extensions, other widgets) is ignored.\n */\nexport const EMBED_MESSAGE_SOURCE = \"declarion-embed\" as const;\n\n/**\n * The protocol version. Bumped only on a breaking envelope/payload change.\n * The host SDK warns when its version and the iframe's version differ.\n */\nexport const EMBED_PROTOCOL_VERSION = 1 as const;\n\n/**\n * Every protocol message `type`. The object key is the camelCase name used in\n * code; the value is the on-wire string. Events (iframe -> host) are past\n * tense; commands (host -> iframe) are imperative - see the naming contract\n * at the top of this file.\n */\nexport const EMBED_MESSAGE_TYPES = {\n /** iframe -> host: SDK mounted, requests the first token. */\n ready: \"ready\",\n /** host -> iframe: deliver or refresh the embed token. */\n setToken: \"set-token\",\n /** iframe -> host: token rejected or near expiry. */\n tokenExpired: \"token-expired\",\n /** iframe -> host: the iframe must be reloaded. */\n reloadRequired: \"reload-required\",\n /** iframe -> host: embed content height changed. */\n resized: \"resized\",\n /** iframe -> host: `self` mode internal navigation happened. */\n navigated: \"navigated\",\n /** iframe -> host: `delegated` mode navigation requested, iframe stayed. */\n navigationRequested: \"navigation-requested\",\n /** iframe -> host: the embedded screen's unsaved-edits state flipped. */\n dirtyChanged: \"dirty-changed\",\n /** host -> iframe: drive the iframe to a screen (deep-linking). */\n navigate: \"navigate\",\n /** host -> iframe: runtime theme switch. */\n setTheme: \"set-theme\",\n} as const;\n\n/** The union of all on-wire message `type` strings. */\nexport type EmbedMessageType =\n (typeof EMBED_MESSAGE_TYPES)[keyof typeof EMBED_MESSAGE_TYPES];\n\n// --- Payload interfaces, one per message type ---\n\n/** `ready` payload: empty - the frame itself is the signal. */\nexport type EmbedReadyPayload = Record<string, never>;\n\n/** `set-token` payload: the embed token and its absolute expiry. */\nexport interface EmbedSetTokenPayload {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** RFC 3339 expiry timestamp of the token. */\n readonly expires_at: string;\n}\n\n/** `token-expired` payload: empty - the host re-mints via `getToken`. */\nexport type EmbedTokenExpiredPayload = Record<string, never>;\n\n/** `reload-required` payload: a human-readable reason for the reload. */\nexport interface EmbedReloadRequiredPayload {\n /** Why the iframe must be reloaded (asset drift, terminal auth failure). */\n readonly reason: string;\n}\n\n/** `resized` payload: the measured content height in CSS pixels. */\nexport interface EmbedResizedPayload {\n /** Content height in CSS pixels. */\n readonly height: number;\n}\n\n/**\n * `navigated` / `navigation-requested` payload: the screen route and, when\n * resolvable, the bound entity code and record id.\n *\n * `entity` and `recordId` are optional: a `custom` screen has no entity, and\n * a list route has no record id.\n */\nexport interface EmbedNavigationPayload {\n /** The Declarion screen route path the navigation targets. */\n readonly route: string;\n /** The bound entity code, when the route resolves to one. */\n readonly entity?: string;\n /** The record id, when the route is a detail route with an id. */\n readonly recordId?: string;\n}\n\n/**\n * `dirty-changed` payload: whether the embedded screen currently has unsaved\n * edits. The host tracks this so it can guard its own navigation (its menu,\n * a host-initiated `navigate`) before moving the iframe away from a dirty\n * screen - the iframe never pops a dialog for host-driven navigation.\n */\nexport interface EmbedDirtyChangedPayload {\n /** True when the embedded screen has unsaved edits. */\n readonly dirty: boolean;\n}\n\n/** `navigate` payload: the route the host drives the iframe to. */\nexport interface EmbedNavigatePayload {\n /** The Declarion screen route the host wants opened. */\n readonly route: string;\n}\n\n/** `set-theme` payload: the theme the host switches the iframe to. */\nexport interface EmbedSetThemePayload {\n /** The requested theme. */\n readonly theme: \"light\" | \"dark\";\n}\n\n/**\n * Maps each message type to its payload shape. Used to type `postToParent`\n * so the payload argument is checked against the message type.\n */\nexport interface EmbedMessagePayloadMap {\n [EMBED_MESSAGE_TYPES.ready]: EmbedReadyPayload;\n [EMBED_MESSAGE_TYPES.setToken]: EmbedSetTokenPayload;\n [EMBED_MESSAGE_TYPES.tokenExpired]: EmbedTokenExpiredPayload;\n [EMBED_MESSAGE_TYPES.reloadRequired]: EmbedReloadRequiredPayload;\n [EMBED_MESSAGE_TYPES.resized]: EmbedResizedPayload;\n [EMBED_MESSAGE_TYPES.navigated]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.navigationRequested]: EmbedNavigationPayload;\n [EMBED_MESSAGE_TYPES.dirtyChanged]: EmbedDirtyChangedPayload;\n [EMBED_MESSAGE_TYPES.navigate]: EmbedNavigatePayload;\n [EMBED_MESSAGE_TYPES.setTheme]: EmbedSetThemePayload;\n}\n\n/**\n * The wire envelope. Every embed frame is exactly this shape: a fixed\n * `source` + `protocol` pair, a discriminating `type`, and the typed\n * `payload` for that type.\n */\nexport interface EmbedMessage<T extends EmbedMessageType = EmbedMessageType> {\n readonly source: typeof EMBED_MESSAGE_SOURCE;\n readonly protocol: typeof EMBED_PROTOCOL_VERSION;\n readonly type: T;\n readonly payload: EmbedMessagePayloadMap[T];\n}\n\n/**\n * Post an enveloped embed message to the host page.\n *\n * Targets the EXACT `parentOrigin` - never `\"*\"` - so a malicious page that\n * re-frames the iframe cannot intercept auth or navigation frames.\n *\n * No-op (returns false) when:\n * - `parentOrigin` is null or empty (no trusted origin to target), or\n * - the document is not framed (`window.parent === window`), or\n * - there is no `window` (SSR / unit environment without a DOM).\n *\n * Returns true when a message was posted.\n */\nexport function postToParent<T extends EmbedMessageType>(\n parentOrigin: string | null,\n type: T,\n payload: EmbedMessagePayloadMap[T],\n): boolean {\n if (!parentOrigin) return false;\n if (typeof window === \"undefined\") return false;\n // Not framed: `window.parent` is the window itself. Posting to self would\n // be a no-op at best and a self-message loop at worst.\n if (window.parent === window) return false;\n\n const message: EmbedMessage<T> = {\n source: EMBED_MESSAGE_SOURCE,\n protocol: EMBED_PROTOCOL_VERSION,\n type,\n payload,\n };\n window.parent.postMessage(message, parentOrigin);\n return true;\n}\n","// In-memory embed token state.\n//\n// In cross-site embed mode the iframe authenticates with a short-lived,\n// refresh-less Bearer token delivered by the host through a `postMessage`\n// `auth` frame (Section 4 of the embed plan). This module is the single\n// owner of that token:\n//\n// - it holds the current `{ token, expiresAt }` IN MEMORY ONLY - never\n// localStorage / sessionStorage. A persisted embed token would outlive\n// the iframe load and could be replayed; the host re-mints on every\n// load, so persistence has no benefit and real risk.\n// - it coordinates a SINGLE-FLIGHT wait: when several transports hit a 401\n// at once, exactly one `token-expired` frame is emitted and every caller\n// shares one in-flight wait for the next `auth` frame.\n//\n// Dependency discipline: this is a LEAF module. It imports only\n// `embed/protocol.ts` (pure types + the outbound sender). It MUST NOT import\n// `api/client.ts` - `client.ts` imports THIS module for the Bearer transport,\n// so importing back would create a cycle.\n\nimport { EMBED_MESSAGE_TYPES, postToParent } from \"./protocol\";\n\n/** The current embed token and its absolute expiry. */\ninterface EmbedTokenState {\n /** The scoped, refresh-less embed JWT. */\n readonly token: string;\n /** Absolute expiry as epoch milliseconds. */\n readonly expiresAt: number;\n}\n\n/**\n * How long `awaitEmbedToken()` waits for the host to deliver a fresh `auth`\n * frame before giving up. The host owns re-minting and is normally instant;\n * a bound is required so a transport caller does not hang forever if the\n * host never responds (host crashed, message dropped, iframe detached).\n *\n * 15s is generous for a backend round-trip yet short enough that a stuck\n * request surfaces as a real error instead of an indefinite spinner.\n */\nconst TOKEN_WAIT_TIMEOUT_MS = 15_000;\n\n/**\n * Skew applied when deciding whether the held token is still usable. A token\n * within this window of its expiry is treated as already expired so a\n * request does not leave with a token that dies in flight.\n */\nconst TOKEN_EXPIRY_SKEW_MS = 5_000;\n\ninterface PendingWait {\n /** The promise every concurrent caller awaits. */\n readonly promise: Promise<string>;\n /** Resolves the promise with a usable token (set on the next `auth`). */\n resolve: (token: string) => void;\n /** Rejects the promise (set on the wait timeout). */\n reject: (err: Error) => void;\n /** The timeout handle, cleared when the wait settles. */\n timer: ReturnType<typeof setTimeout>;\n}\n\ninterface ModuleState {\n /** True once embed token mode is activated at bootstrap. */\n active: boolean;\n /** The exact parent origin to emit `token-expired` frames to. */\n parentOrigin: string | null;\n /** The current token, or null before the first `auth` frame. */\n current: EmbedTokenState | null;\n /** The single in-flight wait shared by every concurrent caller, or null. */\n pending: PendingWait | null;\n}\n\n// All state is in-memory and module-scoped. There is one embed iframe per\n// document, so a module singleton is the correct scope.\nconst state: ModuleState = {\n active: false,\n parentOrigin: null,\n current: null,\n pending: null,\n};\n\n/**\n * Activate embed token mode. Called once at bootstrap by the embed auth\n * handshake. `parentOrigin` is the exact host origin that `token-expired`\n * frames are emitted to.\n *\n * Idempotent: re-activation only refreshes the parent origin.\n */\nexport function initEmbedTokenMode(parentOrigin: string | null): void {\n state.active = true;\n state.parentOrigin = parentOrigin;\n}\n\n/**\n * True when embed token mode is active. Transports branch on this to switch\n * from the cookie path to the Bearer path. False everywhere outside an\n * embed iframe, so the cookie path stays byte-equivalent to today.\n */\nexport function isEmbedTokenMode(): boolean {\n return state.active;\n}\n\n/** The current embed token string, or null before the first `auth` frame. */\nexport function getEmbedToken(): string | null {\n return state.current?.token ?? null;\n}\n\n/**\n * The exact parent origin embed frames are emitted to, or null before\n * `initEmbedTokenMode` runs. Used by transports outside this module (e.g.\n * the version probe) that need to post a frame to the host.\n */\nexport function getEmbedParentOrigin(): string | null {\n return state.parentOrigin;\n}\n\n/**\n * True when there is no token, or the held token is within the expiry skew\n * window of dying. A usable token is required for a request to leave.\n */\nfunction tokenIsStale(): boolean {\n if (!state.current) return true;\n return Date.now() >= state.current.expiresAt - TOKEN_EXPIRY_SKEW_MS;\n}\n\n/**\n * Store a token delivered by the host's `auth` frame.\n *\n * Resolves any in-flight `awaitEmbedToken()` wait so every transport that\n * was blocked on a fresh token unblocks at once. A malformed payload\n * (missing token, unparseable expiry) is ignored - the wait stays pending\n * until a well-formed frame arrives or the timeout fires.\n */\nexport function setEmbedToken(payload: {\n token: string;\n expires_at: string;\n}): void {\n if (!payload.token || typeof payload.token !== \"string\") return;\n const expiresAt = Date.parse(payload.expires_at);\n if (Number.isNaN(expiresAt)) return;\n\n state.current = { token: payload.token, expiresAt };\n\n // Unblock every concurrent waiter with the freshly delivered token.\n if (state.pending) {\n const pending = state.pending;\n state.pending = null;\n clearTimeout(pending.timer);\n pending.resolve(payload.token);\n }\n}\n\n/**\n * Clear the held token. Used when a terminal failure makes the current\n * token unusable; the next transport call will await a fresh one.\n */\nexport function clearEmbedToken(): void {\n state.current = null;\n}\n\n/**\n * Return a usable embed token, requesting a fresh one from the host when\n * needed.\n *\n * SINGLE-FLIGHT: when the held token is still usable it resolves\n * immediately. When the token is missing or stale, it emits exactly one\n * `token-expired` frame to the host and waits for the next `auth` frame.\n * Concurrent callers during that window share the one in-flight wait - the\n * `token-expired` frame is emitted once, not once per caller.\n *\n * The wait is bounded by `TOKEN_WAIT_TIMEOUT_MS`; on timeout the promise\n * rejects so the transport caller fails loudly instead of hanging.\n */\nexport function awaitEmbedToken(): Promise<string> {\n // Fast path: a usable token is already held.\n if (!tokenIsStale() && state.current) {\n return Promise.resolve(state.current.token);\n }\n\n // A wait is already in flight: join it. The `token-expired` frame was\n // already emitted by the first caller; do not emit a second.\n if (state.pending) {\n return state.pending.promise;\n }\n\n // First caller: open the single-flight wait and ask the host to re-mint.\n let resolve!: (token: string) => void;\n let reject!: (err: Error) => void;\n const promise = new Promise<string>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n\n const timer = setTimeout(() => {\n // The host never delivered a fresh `auth` frame in time. Fail the wait\n // so the transport caller surfaces a real error rather than hanging.\n if (state.pending && state.pending.promise === promise) {\n state.pending = null;\n }\n reject(\n new Error(\n `embed token wait timed out after ${TOKEN_WAIT_TIMEOUT_MS}ms`,\n ),\n );\n }, TOKEN_WAIT_TIMEOUT_MS);\n\n state.pending = { promise, resolve, reject, timer };\n\n // Emit `token-expired` exactly once. The host's `getToken` callback\n // re-mints and posts a fresh `auth` frame, which resolves the wait.\n postToParent(state.parentOrigin, EMBED_MESSAGE_TYPES.tokenExpired, {});\n\n return promise;\n}\n\n/**\n * Test-only: reset all module state. Not exposed via package exports.\n */\nexport function __resetEmbedTokenForTests(): void {\n if (state.pending) {\n clearTimeout(state.pending.timer);\n state.pending.reject(new Error(\"embed token state reset\"));\n }\n state.active = false;\n state.parentOrigin = null;\n state.current = null;\n state.pending = null;\n}\n","/**\n * versionTracker — central staleness-detection state for the SPA.\n *\n * Every API response carries `X-Declarion-Version: <combined-fingerprint>`\n * (set by the server's version_header middleware). The FIRST arrival\n * in a bundle's lifetime is captured as the baseline; any later\n * response with a different value triggers drift detection — the SPA\n * either silently invalidates the schema query (if only schema differs)\n * or prompts the user to reload (if assets/binary changed).\n *\n * **State is in-memory only — no sessionStorage / localStorage.** Each\n * bundle load captures its own baseline from its first response. The\n * bundle IS its own version contract; the live server's first answer\n * defines what version the bundle should consider itself. Persisting\n * across reloads is actively harmful: a stale baseline from a previous\n * bundle could override the live answer, producing a \"click Reload →\n * page reloads → toast pops again\" infinite loop. This is the same\n * lesson behind why `index.html` is `Cache-Control: no-cache` — the\n * bootstrap manifest must always be live, never replayed.\n *\n * Defense in depth runs three sources into the same comparator:\n * 1. observe() — header read on every API response (apiFetch hook).\n * 2. fetchProbe() — explicit GET /api/version (focus / online / 60s).\n * 3. (optional) SSE `version-changed` event from useSSE.\n *\n * Cross-tab coordination via BroadcastChannel('declarion-version') —\n * runtime IPC only, not persistence. When one tab detects drift it\n * fans the decision out so all tabs reload together (avoids the\n * half-old / half-new tab anti-pattern). Each tab still bootstraps\n * its own baseline independently.\n */\n\nimport { EMBED_MESSAGE_TYPES, postToParent } from \"@/embed/protocol\";\nimport { getEmbedParentOrigin, isEmbedTokenMode } from \"@/embed/token\";\n\nexport interface VersionDelta {\n /** Reason the delta was emitted. Lets subscribers decide UX. */\n kind: \"schema-only\" | \"asset-or-binary\" | \"unknown\";\n /** Latest combined fingerprint observed. Drives the next baseline. */\n observed: string;\n /** Per-component breakdown when /api/version was reached. */\n components?: VersionComponents;\n}\n\nexport interface VersionComponents {\n schema: string;\n asset: string;\n binary: string;\n}\n\nexport type VersionDeltaListener = (delta: VersionDelta) => void;\n\nconst HEADER_NAME = \"X-Declarion-Version\";\n\n/**\n * BroadcastChannel name. Browser support: Chrome 54+, Firefox 38+,\n * Safari 15.4+ — within Declarion's stated browser matrix. The\n * tracker tolerates a missing API (e.g. SSR / older Safari) by\n * skipping cross-tab fan-out; in-tab detection still works.\n */\nconst BROADCAST_CHANNEL = \"declarion-version\";\n\ninterface InternalState {\n firstSeen: string | null;\n firstComponents: VersionComponents | null;\n /** True once we've reported drift; debounces repeat events. */\n hasReportedDrift: boolean;\n listeners: Set<VersionDeltaListener>;\n channel: BroadcastChannel | null;\n}\n\nfunction makeChannel(): BroadcastChannel | null {\n if (typeof BroadcastChannel === \"undefined\") return null;\n try {\n return new BroadcastChannel(BROADCAST_CHANNEL);\n } catch {\n return null;\n }\n}\n\n// All state is in-memory. See header comment for why we deliberately\n// don't persist `firstSeen` across reloads.\nconst state: InternalState = {\n firstSeen: null,\n firstComponents: null,\n hasReportedDrift: false,\n listeners: new Set(),\n channel: null,\n};\n\n// Lazy channel init — defers cross-tab wiring until the SDK is actually\n// running in a browser tab (skipped for SSR / unit tests).\nfunction ensureChannel(): void {\n if (state.channel || typeof BroadcastChannel === \"undefined\") return;\n state.channel = makeChannel();\n if (!state.channel) return;\n state.channel.addEventListener(\"message\", (ev) => {\n const data = ev.data as { type?: string; delta?: VersionDelta } | null;\n if (!data || data.type !== \"drift\" || !data.delta) return;\n // Another tab reported drift; mirror locally so this tab also\n // reloads / invalidates. Do NOT re-broadcast (idempotency).\n if (state.hasReportedDrift) return;\n state.hasReportedDrift = true;\n for (const cb of state.listeners) cb(data.delta);\n });\n}\n\n/**\n * Observe an API response. Captures the first-seen fingerprint and\n * compares subsequent ones. Idempotent — safe to call from every\n * apiFetch round-trip; backward-compatible — no-op when the header\n * is absent (older server).\n */\nexport function observe(headers: Headers | undefined | null): void {\n ensureChannel();\n if (!headers) return;\n const value = headers.get(HEADER_NAME);\n if (!value) return;\n if (!state.firstSeen) {\n state.firstSeen = value;\n return;\n }\n if (value === state.firstSeen) return;\n if (state.hasReportedDrift) return;\n // Drift detected via header. We don't yet know the per-component\n // breakdown — fetch it from /api/version so subscribers can classify\n // schema-only vs asset/binary, then emit.\n fetchProbe().catch(() => {\n // Probe failed → still emit with kind=unknown so the SPA at\n // least prompts for reload. Better than swallowing drift.\n emitDrift({ kind: \"unknown\", observed: value });\n });\n}\n\n/**\n * Hit /api/version with If-None-Match (cheap when nothing changed —\n * 304 with no body). On any change, emit a typed delta.\n */\nexport async function fetchProbe(): Promise<void> {\n ensureChannel();\n let res: Response;\n try {\n const headers: Record<string, string> = {};\n if (state.firstSeen) {\n headers[\"If-None-Match\"] = `\"${state.firstSeen}\"`;\n }\n // Embed token mode: omit cookies. The iframe is same-origin to\n // Declarion; sending the cookie would carry the viewer's session.\n // The version probe needs no auth (it returns only fingerprints), so\n // omitting credentials is the correct, minimal-trust choice here too.\n res = await fetch(\"/api/version\", {\n method: \"GET\",\n credentials: isEmbedTokenMode() ? \"omit\" : \"include\",\n headers,\n });\n } catch {\n // Network error — not our problem, the watcher backs off.\n throw new Error(\"version probe network error\");\n }\n\n if (res.status === 304) {\n // Server confirmed: nothing changed. Reset the drift flag so a\n // future actual change will emit (it shouldn't have been set,\n // but defense in depth).\n return;\n }\n if (!res.ok) {\n // 404 → server is older than this SDK; gracefully disable.\n // 5xx → backoff at the watcher level.\n throw new Error(`version probe ${res.status}`);\n }\n let body: {\n fingerprint?: string;\n schema_fingerprint?: string;\n asset_fingerprint?: string;\n binary_fingerprint?: string;\n };\n try {\n body = (await res.json()) as typeof body;\n } catch {\n throw new Error(\"version probe malformed body\");\n }\n\n const observed = body.fingerprint;\n if (typeof observed !== \"string\" || observed === \"\") {\n throw new Error(\"version probe missing fingerprint\");\n }\n\n const components: VersionComponents | undefined =\n typeof body.schema_fingerprint === \"string\" &&\n typeof body.asset_fingerprint === \"string\" &&\n typeof body.binary_fingerprint === \"string\"\n ? {\n schema: body.schema_fingerprint,\n asset: body.asset_fingerprint,\n binary: body.binary_fingerprint,\n }\n : undefined;\n\n // First probe in this bundle's lifetime → capture baseline (both\n // fingerprint and components). `firstSeen` may already be set by\n // an earlier observe() that ran from a regular API response — in\n // that case `firstComponents` is null and we backfill it here.\n // Without this backfill, every later drift would emit `kind:\n // \"unknown\"` because `classifyDelta(null, current)` has no\n // baseline, and the silent schema-only invalidation path never\n // fires.\n if (!state.firstSeen) {\n state.firstSeen = observed;\n }\n if (!state.firstComponents && components && observed === state.firstSeen) {\n state.firstComponents = components;\n }\n\n if (observed === state.firstSeen) return;\n\n const kind = classifyDelta(state.firstComponents, components);\n emitDrift({ kind, observed, components });\n}\n\n/**\n * Schema-only delta: schema differs, asset+binary unchanged → silent\n * React Query invalidation. Anything else (asset, binary, missing\n * baseline) is \"asset-or-binary\" → reload prompt.\n */\nfunction classifyDelta(\n baseline: VersionComponents | null,\n current: VersionComponents | undefined,\n): VersionDelta[\"kind\"] {\n if (!baseline || !current) return \"unknown\";\n const schemaChanged = baseline.schema !== current.schema;\n const assetChanged = baseline.asset !== current.asset;\n const binaryChanged = baseline.binary !== current.binary;\n if (schemaChanged && !assetChanged && !binaryChanged) return \"schema-only\";\n if (assetChanged || binaryChanged) return \"asset-or-binary\";\n // Combined differs but no component changed? Shouldn't happen with\n // a correct server; treat conservatively.\n return \"unknown\";\n}\n\nfunction emitDrift(delta: VersionDelta): void {\n if (state.hasReportedDrift) return;\n state.hasReportedDrift = true;\n for (const cb of state.listeners) cb(delta);\n if (state.channel) {\n try {\n state.channel.postMessage({ type: \"drift\", delta });\n } catch {\n /* channel may be closed during shutdown */\n }\n }\n\n // Embed token mode: the host owns the white-label chrome, so the iframe\n // cannot render its own reload prompt. Asset or binary drift (the SPA\n // bundle changed on the server) requires a full reload that only the\n // host can perform - notify it with a `reload-required` frame. Schema-only\n // drift stays silent here: the subscriber path already invalidates the\n // React Query schema cache in place, which needs no reload.\n if (isEmbedTokenMode() && delta.kind !== \"schema-only\") {\n postToParent(getEmbedParentOrigin(), EMBED_MESSAGE_TYPES.reloadRequired, {\n reason: `version drift: ${delta.kind}`,\n });\n }\n}\n\n/**\n * Subscribe to drift events. Called once from useVersionWatcher; the\n * subscriber renders the UpdatePrompt (toast or silent invalidation).\n */\nexport function subscribe(cb: VersionDeltaListener): () => void {\n state.listeners.add(cb);\n return () => {\n state.listeners.delete(cb);\n };\n}\n\n/**\n * Test-only: reset internal state. Not exposed via package exports.\n */\nexport function __resetForTests(): void {\n state.firstSeen = null;\n state.firstComponents = null;\n state.hasReportedDrift = false;\n state.listeners.clear();\n if (state.channel) {\n state.channel.close();\n state.channel = null;\n }\n}\n","import { ApiError, type ApiErrorBody } from \"@/types/api\";\nimport { loginPathForCurrentLocation } from \"./auth-redirect\";\nimport { observe as observeVersion } from \"@/lib/versionTracker\";\nimport {\n awaitEmbedToken,\n clearEmbedToken,\n getEmbedToken,\n isEmbedTokenMode,\n} from \"@/embed/token\";\n\n// The access and refresh tokens live in httpOnly cookies managed entirely by\n// the backend (__Host-declarion-access-token, __Host-declarion-refresh-token).\n// The browser attaches them automatically on every request when\n// credentials: \"include\" is set. JavaScript cannot read these cookies, which\n// is the whole point - an XSS cannot exfiltrate the session.\n//\n// The backend collapsed every auth/SSO/api-key endpoint into the generic\n// handler dispatcher at POST /api/actions/{code}. `callHandler` below is\n// the single entry point from the frontend; every wrapper in api/auth.ts\n// etc. uses it. `apiFetch` is kept for the handful of platform endpoints\n// that still live at named URLs (schema, data CRUD, params, etc.).\n\n// POST /api/actions/{code} bodies are flat: the handler's params sit at\n// the top level. Global handlers (auth.*, etc.) pass their args as-is.\nconst flatActionBody = (args: unknown) =>\n JSON.stringify(args ?? {});\n\nlet refreshPromise: Promise<void> | null = null;\n\n// refreshAccessToken stays on raw fetch (NOT apiFetch / callHandler) to\n// avoid recursion: apiFetch's 401 retry path waits on refreshPromise, so\n// if the refresh call itself went through apiFetch it would deadlock.\n/** Signals that auth.refresh failed terminally (refresh cookie missing, token\n * expired/reused, session revoked). Callers MUST stop retrying and route the\n * user back to /login — retrying produces an infinite 401/4xx storm against\n * the rate-limited refresh endpoint. Distinct from generic network errors so\n * useSSE can branch on it. */\nexport class RefreshDeadError extends Error {\n constructor(public status: number) {\n super(`refresh failed terminally (${status})`);\n this.name = \"RefreshDeadError\";\n }\n}\n\n// --- Proactive token refresh ------------------------------------------------\n// The reactive 401 -> refresh -> retry path is the correctness backstop: it\n// covers clock skew, suspended laptops, and any missed timer. Layered on top,\n// a proactive timer refreshes the access token shortly BEFORE it expires so\n// steady-state requests never pay a 401 + retry round-trip. Server-driven -\n// the schedule comes from the `expires_at` (access-token expiry) the backend\n// returns on every login / refresh.\n\nlet proactiveRefreshTimer: ReturnType<typeof setTimeout> | null = null;\nlet accessTokenExpiryMs: number | null = null;\n// Fire the proactive refresh this far before the token's actual expiry.\nconst PROACTIVE_REFRESH_MARGIN_MS = 30_000;\n// Floor on the scheduled delay - avoids a hot loop if the server hands back\n// an already-near-expiry token.\nconst PROACTIVE_REFRESH_MIN_DELAY_MS = 5_000;\n\n// triggerProactiveRefresh runs a refresh outside the reactive request path. A\n// terminal failure routes to /login (same as reactive); a transient one is\n// left for the reactive backstop to retry on the next request.\nfunction triggerProactiveRefresh(): void {\n void ensureRefresh().catch((e) => {\n if (e instanceof RefreshDeadError) onPersistentAuthFailure();\n });\n}\n\n/** Arm the proactive refresh timer from a server `expires_at` (the access\n * token expiry, ISO-8601). Call after every login / signup / switch / the\n * refresh itself. An undefined or unparseable value just clears the timer -\n * the reactive 401 path still guarantees correctness. */\nexport function scheduleProactiveRefresh(expiresAt: string | undefined): void {\n if (proactiveRefreshTimer) {\n clearTimeout(proactiveRefreshTimer);\n proactiveRefreshTimer = null;\n }\n accessTokenExpiryMs = null;\n if (!expiresAt || typeof window === \"undefined\") return;\n const expiry = Date.parse(expiresAt);\n if (Number.isNaN(expiry)) return;\n accessTokenExpiryMs = expiry;\n const delay = Math.max(\n expiry - Date.now() - PROACTIVE_REFRESH_MARGIN_MS,\n PROACTIVE_REFRESH_MIN_DELAY_MS,\n );\n proactiveRefreshTimer = setTimeout(() => {\n proactiveRefreshTimer = null;\n triggerProactiveRefresh();\n }, delay);\n}\n\n/** Stop proactive refresh and forget the tracked expiry. Called on logout. */\nexport function cancelProactiveRefresh(): void {\n if (proactiveRefreshTimer) {\n clearTimeout(proactiveRefreshTimer);\n proactiveRefreshTimer = null;\n }\n accessTokenExpiryMs = null;\n}\n\n// A backgrounded tab throttles timers and a suspended laptop pauses them, so a\n// scheduled refresh can fire late. When the tab becomes visible again, refresh\n// at once if the token is already at/past its renewal point - this keeps a\n// user returning after a long idle from ever landing on /login.\nif (typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", () => {\n if (document.visibilityState !== \"visible\") return;\n if (\n accessTokenExpiryMs !== null &&\n Date.now() >= accessTokenExpiryMs - PROACTIVE_REFRESH_MARGIN_MS\n ) {\n triggerProactiveRefresh();\n }\n });\n}\n\n// rescheduleAfterRefresh re-arms the proactive timer from the fresh\n// access-token expiry in a successful auth.refresh response.\nasync function rescheduleAfterRefresh(res: Response): Promise<void> {\n try {\n const body = (await res.json()) as {\n result?: { expires_at?: string };\n expires_at?: string;\n };\n scheduleProactiveRefresh(body?.result?.expires_at ?? body?.expires_at);\n } catch {\n // No / malformed body - the reactive 401 path still covers correctness.\n }\n}\n\nasync function refreshAccessToken(): Promise<void> {\n const init: RequestInit = {\n method: \"POST\",\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/json\" },\n body: flatActionBody({}),\n };\n const res = await fetch(\"/api/actions/auth.refresh\", init);\n\n // Grace-period replay: backend says retry shortly. ONLY 429 is retried —\n // any other non-OK status indicates the session is dead and we must escalate.\n if (res.status === 429) {\n const retryAfter = parseInt(res.headers.get(\"Retry-After\") || \"1\", 10);\n await new Promise((r) => setTimeout(r, retryAfter * 1000));\n const retryRes = await fetch(\"/api/actions/auth.refresh\", init);\n if (!retryRes.ok) {\n throw new RefreshDeadError(retryRes.status);\n }\n await rescheduleAfterRefresh(retryRes);\n return;\n }\n\n if (!res.ok) {\n throw new RefreshDeadError(res.status);\n }\n await rescheduleAfterRefresh(res);\n}\n\nexport async function ensureRefresh(): Promise<void> {\n if (refreshPromise) return refreshPromise;\n refreshPromise = refreshAccessToken().finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n}\n\n// Handler invoked when a persistent 401 requires sending the user back to\n// the login flow. Injected (not hardcoded) so tests can substitute a spy\n// and non-browser consumers (e.g. a worker) can route the signal their own\n// way. Default implementation assigns window.location.href — same behavior\n// as before this was extracted.\ntype PersistentAuthFailureHandler = () => void;\n\nconst defaultPersistentAuthFailureHandler: PersistentAuthFailureHandler = () => {\n if (typeof window === \"undefined\") return;\n const { pathname } = window.location;\n if (\n pathname.startsWith(\"/login\") ||\n pathname.startsWith(\"/signup\") ||\n pathname === \"/reset\" ||\n pathname === \"/forgot\"\n ) {\n return;\n }\n window.location.href = loginPathForCurrentLocation();\n};\n\nlet onPersistentAuthFailure: PersistentAuthFailureHandler =\n defaultPersistentAuthFailureHandler;\n\n/** Replace the handler invoked on persistent 401. Intended for tests\n * and non-browser consumers. Returns the previous handler so callers\n * can restore it if needed. */\nexport function setPersistentAuthFailureHandler(\n h: PersistentAuthFailureHandler,\n): PersistentAuthFailureHandler {\n const prev = onPersistentAuthFailure;\n onPersistentAuthFailure = h;\n return prev;\n}\n\n/** Returns the currently registered persistent-auth-failure handler. useSSE\n * reads this lazily so a setPersistentAuthFailureHandler() call from a test\n * is observed by the SSE retry loop without re-importing the module. */\nexport function getPersistentAuthFailureHandler(): PersistentAuthFailureHandler {\n return onPersistentAuthFailure;\n}\n\n// Structured non-2xx report. Routed through an injectable reporter so the\n// SDK does not hardcode `console.error` — production keeps the existing\n// in-browser log, tests install a no-op (clean stderr), and downstream\n// consumers can plug in observability backends (Sentry, Datadog, etc.)\n// without forking the client.\nexport type ApiErrorReport = {\n status: number;\n code: string;\n message: string;\n};\n\ntype ApiErrorReporter = (report: ApiErrorReport) => void;\n\nconst defaultApiErrorReporter: ApiErrorReporter = ({ status, code, message }) => {\n console.error(`[API ${status}] ${code}: ${message}`);\n};\n\nlet apiErrorReporter: ApiErrorReporter = defaultApiErrorReporter;\n\n/** Replace the reporter invoked for every non-2xx response. Returns the\n * previous reporter so callers can restore it. Tests should install a\n * spy or no-op via this hook instead of stubbing `console.error`. */\nexport function setApiErrorReporter(r: ApiErrorReporter): ApiErrorReporter {\n const prev = apiErrorReporter;\n apiErrorReporter = r;\n return prev;\n}\n\n// parseBody turns a 2xx Response into the caller's payload type. apiFetch\n// passes a JSON parser (with 204/207 handling); apiFetchBlob passes a parser\n// that reads the body as a Blob and pulls the filename from headers. The core\n// `request` function owns everything else (header setup, the !res.ok\n// error-envelope parse, and the 401 → ensureRefresh → retry →\n// onPersistentAuthFailure block).\ntype BodyParser<T> = (res: Response) => T | Promise<T>;\n\n// Options that vary between the JSON and blob request variants. `setAccept`\n// adds `Accept: */*` to the request and retry headers — the CSV export\n// endpoint needs it; the JSON path does not send it.\ninterface RequestVariant {\n setAccept: boolean;\n}\n\n// composeHeaders builds the Headers for a request: caller headers plus the\n// default Content-Type (only when there is a body) and, for the blob variant,\n// `Accept: */*`. In embed token mode it also stamps `Authorization: Bearer\n// <embed token>` - the request is sent with `credentials: \"omit\"` (see\n// `request`), so the cookie cannot carry the session and the Bearer header\n// is the sole credential. Called once for the initial request and again for\n// the retry so both carry identical headers; `bearerToken` lets the retry\n// stamp the freshly re-minted token instead of the stale one.\nfunction composeHeaders(\n callerHeaders: HeadersInit | undefined,\n hasBody: boolean,\n variant: RequestVariant,\n bearerToken: string | null,\n): Headers {\n const headers = new Headers(callerHeaders);\n if (!headers.has(\"Content-Type\") && hasBody) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n if (variant.setAccept && !headers.has(\"Accept\")) {\n headers.set(\"Accept\", \"*/*\");\n }\n if (bearerToken) {\n headers.set(\"Authorization\", `Bearer ${bearerToken}`);\n }\n return headers;\n}\n\nasync function request<T>(\n path: string,\n options: RequestInit,\n variant: RequestVariant,\n parseBody: BodyParser<T>,\n): Promise<T> {\n // Embed token mode: the iframe is same-origin to Declarion. The auth\n // middleware checks the `__Host-` cookie BEFORE the Authorization header,\n // so `credentials: \"include\"` would send the viewer's full-authority\n // cookie and silently bypass the scoped embed token. `credentials: \"omit\"`\n // plus the Bearer header is the security boundary, not hygiene. Outside\n // embed mode this branch is inert and the cookie path is byte-equivalent\n // to before.\n const embed = isEmbedTokenMode();\n const requestCredentials: RequestCredentials = embed ? \"omit\" : \"include\";\n\n const headers = composeHeaders(\n options.headers,\n !!options.body,\n variant,\n embed ? getEmbedToken() : null,\n );\n\n // No client-side request deadline. The server's declared handler timeout is\n // the sole authority on how long an operation runs; the real budget (handler\n // + on_success chain + sync subscribers) cannot be computed here. A caller-\n // supplied options.signal (user cancel / navigate-away) rides the `...options`\n // spread and still aborts the request.\n const res = await fetch(path, {\n ...options,\n headers,\n credentials: requestCredentials,\n });\n\n // Drift detection — every API response carries the server's\n // X-Declarion-Version header (set by middleware/version_header.go).\n // observeVersion is a one-shot capture on first arrival and a\n // comparison thereafter; it no-ops when the header is absent\n // (older servers — backward compatible) and its emission flow is\n // debounced so this runs cheaply on every round-trip.\n observeVersion(res.headers);\n\n if (!res.ok) {\n let message = \"Request failed\";\n let code = \"UNKNOWN\";\n let index: number | undefined;\n let raw: unknown;\n\n try {\n raw = await res.json();\n const body = raw as ApiErrorBody;\n if (body && body.error) {\n message = body.error.message;\n code = body.error.code;\n if (body.index != null) index = body.index;\n }\n } catch {\n // Ignore JSON parse errors\n }\n\n // 401 with a refresh hint: attempt exactly one token refresh + retry.\n //\n // Embed token mode: NEVER call `auth.refresh` - the embed token is\n // refresh-less by design (the host owns re-minting). Instead emit\n // `token-expired` to the host and wait for a fresh `auth` frame via\n // `awaitEmbedToken()`. Outside embed mode the cookie path runs\n // `ensureRefresh()` exactly as before.\n if (res.status === 401 && (code === \"TOKEN_REFRESH_NEEDED\" || code === \"UNAUTHORIZED\")) {\n try {\n let retryBearer: string | null = null;\n if (embed) {\n // The server rejected the held token even though its clock-expiry\n // may not have passed (revoked session, permission change). Drop\n // it so `awaitEmbedToken` cannot return the dead token and instead\n // emits `token-expired` and waits for the host to re-mint.\n clearEmbedToken();\n retryBearer = await awaitEmbedToken();\n } else {\n await ensureRefresh();\n }\n const retryHeaders = composeHeaders(\n options.headers,\n !!options.body,\n variant,\n retryBearer,\n );\n const retryRes = await fetch(path, {\n ...options,\n headers: retryHeaders,\n credentials: requestCredentials,\n });\n if (!retryRes.ok) {\n onPersistentAuthFailure();\n const retryBody = (await retryRes\n .json()\n .catch(() => ({ error: { message: \"Request failed\", code: \"UNKNOWN\" } }))) as ApiErrorBody;\n throw new ApiError(retryRes.status, retryBody.error.message, retryBody.error.code, retryBody.index);\n }\n return parseBody(retryRes);\n } catch (refreshErr) {\n if (refreshErr instanceof ApiError) {\n throw refreshErr;\n }\n onPersistentAuthFailure();\n throw new ApiError(401, message, code, index);\n }\n }\n\n // Session revoked or other 401 - redirect to login.\n if (res.status === 401) {\n onPersistentAuthFailure();\n }\n\n apiErrorReporter({ status: res.status, code, message });\n throw new ApiError(res.status, message, code, index, raw);\n }\n\n return parseBody(res);\n}\n\n// parseJsonBody is the success-path parser for apiFetch. 204 No Content yields\n// `undefined`; 207 Multi-Status is a success status whose body is a well-formed\n// payload (invoke:each partial) and flows through the normal JSON parse so\n// callers can inspect status/errors themselves.\nfunction parseJsonBody<T>(res: Response): Promise<T> {\n if (res.status === 204) {\n return Promise.resolve(undefined as T);\n }\n return res.json() as Promise<T>;\n}\n\nexport async function apiFetch<T>(\n path: string,\n options: RequestInit = {},\n): Promise<T> {\n return request<T>(path, options, { setAccept: false }, parseJsonBody);\n}\n\n// Action endpoint response envelope. The actions dispatcher always wraps\n// successful handler results as { status: \"success\", result: <data>, ... }.\ninterface ActionEnvelope<R> {\n status: string;\n result: R;\n audit_operation_id?: string;\n object_count?: number;\n}\n\n// apiFetchBlob is the binary-payload sibling of apiFetch. Same auth-refresh +\n// error-envelope contract; differs only in the success path: it returns the\n// response body as a Blob and parses Content-Disposition for the filename.\n//\n// Used by /api/data/{entity}/export (CSV download). apiFetch stays JSON-only\n// so widening it would touch every call site.\n//\n// Returns:\n// - blob: the response body as a Blob (Content-Type preserved by the server,\n// e.g. text/csv; charset=utf-8).\n// - filename: parsed from Content-Disposition. RFC 5987 `filename*=UTF-8''<...>`\n// wins over the legacy `filename=\"...\"`. Empty string when neither is set.\n// - headers: the raw Response headers, in case callers need additional\n// metadata (e.g. X-Declarion-Audit-Operation-ID).\n//\n// On non-2xx: parses the same {error: {message, code}} envelope as apiFetch\n// and throws ApiError. On 401 with TOKEN_REFRESH_NEEDED / UNAUTHORIZED:\n// silent refresh + one retry, identical to apiFetch.\nexport interface BlobResponse {\n blob: Blob;\n filename: string;\n headers: Headers;\n}\n\n// parseBlobBody is the success-path parser for apiFetchBlob: it reads the\n// response body as a Blob and pulls the download filename from\n// Content-Disposition. apiFetchBlob has no 204/207 path.\nasync function parseBlobBody(res: Response): Promise<BlobResponse> {\n const blob = await res.blob();\n return { blob, filename: parseContentDispositionFilename(res.headers), headers: res.headers };\n}\n\nexport async function apiFetchBlob(\n path: string,\n options: RequestInit = {},\n): Promise<BlobResponse> {\n return request<BlobResponse>(path, options, { setAccept: true }, parseBlobBody);\n}\n\n// parseContentDispositionFilename extracts a filename from Content-Disposition\n// per RFC 6266. The modern `filename*=UTF-8''<percent-encoded>` form wins\n// over the legacy `filename=\"...\"` so non-ASCII filenames decode correctly.\n// Returns \"\" when no filename is set so callers can supply a fallback.\nexport function parseContentDispositionFilename(headers: Headers): string {\n const cd = headers.get(\"Content-Disposition\");\n if (!cd) return \"\";\n\n const star = cd.match(/filename\\*\\s*=\\s*([^']*)'([^']*)'([^;]+)/i);\n if (star) {\n try {\n return decodeURIComponent(star[3].trim());\n } catch {\n // Malformed encoding — fall through to the legacy form.\n }\n }\n\n const legacy = cd.match(/filename\\s*=\\s*(\"([^\"]+)\"|([^;]+))/i);\n if (legacy) {\n return (legacy[2] ?? legacy[3] ?? \"\").trim();\n }\n return \"\";\n}\n\n// callHandler is the single entry point for every auth/SSO/api-key\n// operation. It POSTs to /api/actions/{code} with a flat body (handler\n// params at the top level) and unwraps the result envelope, so callers\n// see the handler's return type directly.\n//\n// Cookie side effects (login/signup/refresh/switch_tenant) are set by the\n// backend via the HTTPCustomizer interface on *auth.LoginResult; the\n// browser stores them automatically because `credentials: \"include\"` is\n// already set by apiFetch. Frontend does not need any special handling.\nexport async function callHandler<R = unknown>(\n code: string,\n args: unknown = {},\n): Promise<R> {\n const env = await apiFetch<ActionEnvelope<R>>(\n `/api/actions/${encodeURIComponent(code)}`,\n {\n method: \"POST\",\n body: flatActionBody(args),\n },\n );\n return env.result;\n}\n","// Embed-aware file URL resolution - the PURE, React-free core.\n//\n// In cross-site embed mode the iframe authenticates with an\n// `Authorization: Bearer` header and `credentials: \"omit\"` (Section 4 of the\n// embed plan). Native browser elements - `<img src>`, `window.open()` - CANNOT\n// attach an `Authorization` header, so the static file URLs that the hydrated\n// `HydratedFile` carries (`/api/files/{id}/download`, `/api/files/{id}/url`)\n// render as broken media: the request leaves with no credential and the server\n// returns 401.\n//\n// This module resolves a file (or a derivation of it) into a URL a native\n// element CAN load, by first calling the file URL endpoint WITH the Bearer\n// header. Two outcomes, branching on the endpoint's `proxy` flag:\n//\n// - proxy: false -> the backend issued a presigned URL (S3 / SeaweedFS).\n// The URL embeds its own short-lived signature, so a native element can\n// load it cross-origin with no header. Use it directly.\n//\n// - proxy: true -> the local_fs backend cannot presign; the endpoint\n// returned a Declarion proxy path (`/api/files/{id}/download`) that needs\n// the Bearer header. A native element cannot send that header, so this\n// module fetches the bytes itself (Bearer + `credentials: \"omit\"`),\n// wraps them in a `blob:` object URL, and returns that.\n//\n// The response contract is fixed by `golang/internal/files/download.go`:\n// GET /api/files/{id}/url -> 200 { url, expires_at, proxy }\n// GET /api/files/{id}/derived/{key}/url -> 200 { url, expires_at, proxy }\n// | 202 { status: \"generating\", ... }\n//\n// Dependency discipline: this is a LEAF module. It imports only `embed/token`\n// (token state) and pure types. It MUST NOT import React or the file-widget\n// component layer - the React hook in `components/file-widgets/useEmbedFileSrc`\n// builds on this.\n\nimport { awaitEmbedToken, clearEmbedToken, isEmbedTokenMode } from \"./token\";\n\n/** Base path of the files HTTP surface. Mirrors the Go `RoutePrefix`. */\nconst FILES_ROUTE_PREFIX = \"/api/files\";\n\n/**\n * The kind of URL `resolveEmbedFileSrc` produced, so the caller knows whether\n * an object URL was created and must be revoked.\n *\n * - `direct`: a presigned (non-proxy) URL or a `data:` URL. Nothing to revoke.\n * - `blob`: a `blob:` object URL created from fetched bytes. The caller MUST\n * call `URL.revokeObjectURL` on it when the file changes or the component\n * unmounts.\n */\nexport type EmbedFileSrcKind = \"direct\" | \"blob\";\n\n/** The resolved source for a file, plus the cleanup obligation. */\nexport interface EmbedFileSrc {\n /** The URL to assign to `<img src>` / pass to `window.open`. */\n readonly url: string;\n /** Whether `url` is a revocable `blob:` object URL. */\n readonly kind: EmbedFileSrcKind;\n}\n\n/**\n * What to resolve for a given file: the primary file, a named derivation\n * (thumbnail), or the download (attachment-disposition) form.\n *\n * - `preview` -> `/api/files/{id}/url`, inline disposition.\n * - `download` -> `/api/files/{id}/url`; when the result is a proxy URL the\n * `disposition=attachment` query is preserved so the proxy stream forces a\n * download. A presigned URL already carries its disposition.\n * - a derivation key string -> `/api/files/{id}/derived/{key}/url`.\n */\nexport type EmbedFileVariant = \"preview\" | \"download\" | { derivation: string };\n\n/** Shape of the `/url` and `/derived/{key}/url` 200 response body. */\ninterface FileURLResponse {\n url: string;\n expires_at: string;\n proxy: boolean;\n}\n\n/**\n * True when the value is a well-formed `{ url, proxy }` body. `expires_at` is\n * present in the contract but unused here, so it is not asserted.\n */\nfunction isFileURLResponse(value: unknown): value is FileURLResponse {\n if (typeof value !== \"object\" || value === null) return false;\n const v = value as Record<string, unknown>;\n return typeof v.url === \"string\" && typeof v.proxy === \"boolean\";\n}\n\n/** Build the `/url` or `/derived/{key}/url` endpoint path for a file. */\nfunction fileURLEndpoint(fileId: string, variant: EmbedFileVariant): string {\n const id = encodeURIComponent(fileId);\n if (typeof variant === \"object\") {\n return `${FILES_ROUTE_PREFIX}/${id}/derived/${encodeURIComponent(variant.derivation)}/url`;\n }\n return `${FILES_ROUTE_PREFIX}/${id}/url`;\n}\n\n/**\n * A 202 `{ status: \"generating\" }` from the derived endpoint - the async\n * generator has not produced the derivation yet. Thrown so the React hook can\n * fall back to the primary file rather than rendering broken media.\n */\nexport class DerivationPendingError extends Error {\n constructor(public readonly fileId: string, public readonly key: string) {\n super(`derivation \"${key}\" of file ${fileId} is still generating`);\n this.name = \"DerivationPendingError\";\n }\n}\n\n/**\n * Build the request init for an embed file request: the Bearer header and\n * `credentials: \"omit\"`. The cookie is omitted deliberately - the auth\n * middleware checks the `__Host-` cookie BEFORE the Bearer header, so a sent\n * cookie would silently replace the scoped embed token with the viewer's\n * full-authority session (Section 4, \"Cookie wins over Bearer\").\n */\nfunction embedRequestInit(token: string, signal?: AbortSignal): RequestInit {\n return {\n method: \"GET\",\n credentials: \"omit\",\n headers: { Authorization: `Bearer ${token}` },\n signal,\n };\n}\n\n/**\n * Append `disposition=attachment` to a proxy URL so the Declarion proxy\n * stream forces a download. Only meaningful for the `download` variant on a\n * proxy-backed file; a presigned URL already carries its own disposition.\n */\nfunction withAttachmentDisposition(url: string): string {\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n return `${url}${separator}disposition=attachment`;\n}\n\n/**\n * GET `url` with the embed Bearer header and `credentials: \"omit\"`, mirroring\n * `apiFetch`'s contract (see `api/client.ts`): the token comes from\n * `awaitEmbedToken()` - which waits out a missing or stale token via the\n * single-flight host handshake - and a 401 triggers one `clearEmbedToken()` +\n * re-mint + retry.\n *\n * Without the 401 retry an embed token that expired or was revoked\n * mid-session would leave every file widget stuck on a placeholder until the\n * embedded record changed. Every other embed transport (fetch, SSE, upload)\n * self-heals on a 401; file resolution must too.\n */\nasync function embedAuthedFetch(\n url: string,\n signal?: AbortSignal,\n): Promise<Response> {\n const token = await awaitEmbedToken();\n const res = await fetch(url, embedRequestInit(token, signal));\n if (res.status !== 401) return res;\n // The server rejected the token even though it may still be clock-valid\n // (revoked session / permission change). Clear it so `awaitEmbedToken()`\n // takes the wait path and emits `token-expired`, then retry once.\n clearEmbedToken();\n const retryToken = await awaitEmbedToken();\n return fetch(url, embedRequestInit(retryToken, signal));\n}\n\n/**\n * Resolve a file (or one of its derivations) into a URL a native element can\n * load under Bearer-only embed auth.\n *\n * Step 1: call the file URL endpoint WITH the Bearer header and\n * `credentials: \"omit\"`. A 202 from the derived endpoint throws\n * `DerivationPendingError`.\n * Step 2: when `proxy: false`, the endpoint returned a presigned URL - return\n * it directly (`kind: \"direct\"`), nothing to revoke.\n * Step 3: when `proxy: true`, fetch the proxy bytes WITH the Bearer header,\n * wrap them in a `blob:` object URL, and return it (`kind: \"blob\"`).\n * The caller owns revocation.\n *\n * Both requests go through `embedAuthedFetch`, so a missing/stale token is\n * waited out and a 401 is retried once with a freshly minted token.\n *\n * `signal` aborts both the metadata request and the bytes fetch, so a file\n * change or unmount mid-flight cancels the in-flight work cleanly.\n *\n * Throws when not in embed token mode - callers outside embed mode MUST use\n * the static URL path instead and never reach this function.\n */\nexport async function resolveEmbedFileSrc(\n fileId: string,\n variant: EmbedFileVariant,\n signal?: AbortSignal,\n): Promise<EmbedFileSrc> {\n if (!isEmbedTokenMode()) {\n throw new Error(\"resolveEmbedFileSrc called outside embed token mode\");\n }\n\n // Step 1: ask the endpoint for the file URL, authenticated with the Bearer\n // header. The endpoint resolves permissions and tells us, via `proxy`,\n // whether the URL is directly loadable or needs proxying.\n const metaRes = await embedAuthedFetch(fileURLEndpoint(fileId, variant), signal);\n\n // 202 from the derived endpoint: the derivation has not been generated yet.\n if (metaRes.status === 202 && typeof variant === \"object\") {\n throw new DerivationPendingError(fileId, variant.derivation);\n }\n if (!metaRes.ok) {\n throw new Error(\n `embed file url request failed: ${metaRes.status} ${metaRes.statusText}`,\n );\n }\n\n const body: unknown = await metaRes.json();\n if (!isFileURLResponse(body)) {\n throw new Error(\"embed file url response is malformed\");\n }\n\n // Step 2: presigned URL. It carries its own signature; a native element can\n // load it cross-origin with no header. Nothing to revoke.\n if (!body.proxy) {\n return { url: body.url, kind: \"direct\" };\n }\n\n // Step 3: proxy-backed file. `body.url` is a `/api/files/{id}/download`\n // path that needs the Bearer header - a native element cannot send it. Fetch\n // the bytes ourselves and wrap them in a revocable object URL.\n //\n // Same-origin guard (Decision 2026-05-27 security followup MEDIUM 5):\n // the backend always returns a relative `/api/files/{id}/download` path\n // for proxy responses (download.go::issuePresignedOrProxyURL). Reject\n // any absolute URL here as defense-in-depth — if a future change ever\n // surfaces a foreign-origin URL, this would otherwise leak the bearer\n // token to that origin.\n if (!body.url.startsWith(FILES_ROUTE_PREFIX + \"/\")) {\n throw new Error(\n `embed file proxy: refusing non-files-route URL ${JSON.stringify(body.url)}`,\n );\n }\n const proxyURL =\n variant === \"download\" ? withAttachmentDisposition(body.url) : body.url;\n const bytesRes = await embedAuthedFetch(proxyURL, signal);\n if (!bytesRes.ok) {\n throw new Error(\n `embed file proxy fetch failed: ${bytesRes.status} ${bytesRes.statusText}`,\n );\n }\n const blob = await bytesRes.blob();\n return { url: URL.createObjectURL(blob), kind: \"blob\" };\n}\n","import { callHandler, ensureRefresh, getPersistentAuthFailureHandler } from \"@/api/client\";\nimport { awaitEmbedToken, clearEmbedToken, isEmbedTokenMode } from \"@/embed/token\";\nimport type { UploadProgress, UploadedFile } from \"./types\";\n\n// uploadFile sends a multipart/form-data POST to /api/files/upload via XHR\n// (so the widget can report progress and abort mid-flight). The single\n// \"file\" part carries the binary payload — the server's MultipartReader\n// reads it as a stream. The endpoint is mounted via roles.RegisterModuleRoute\n// per the /api/files/* exception in CLAUDE.md.\n//\n// On 4xx the server returns the standard ApiError envelope:\n// { error: { code, message }, ... }. We surface code + message so widgets\n// can render specific copy (FILE_TOO_LARGE → \"Image too large\", etc.).\nexport interface UploadOptions {\n file: File;\n entityCode?: string;\n fieldCode?: string;\n slotCode?: string;\n onProgress?: (p: UploadProgress) => void;\n signal?: AbortSignal;\n}\n\nexport class UploadError extends Error {\n code: string;\n status: number;\n constructor(status: number, code: string, message: string) {\n super(message);\n this.code = code;\n this.status = status;\n }\n}\n\nexport interface UploadEnvelope {\n status: string;\n result: UploadedFile;\n}\n\n// uploadFile sends the multipart upload and transparently refreshes the\n// access token once on a 401. The browser access token is short-lived\n// (5 min); without this retry a file upload started after a brief idle\n// fails outright even though the refresh cookie is still valid. This\n// mirrors apiFetch's \"401 -> ensureRefresh -> retry\" contract (api/client.ts,\n// docs/gotchas.md \"Security & Auth\") - the raw XHR transport below cannot go\n// through apiFetch, so the retry is reproduced here explicitly.\n//\n// Embed token mode: the XHR carries the scoped embed JWT as a Bearer header\n// with `withCredentials = false` (cookies omitted - see sendUpload). On a\n// 401 it NEVER POSTs `auth.refresh`; it asks the host to re-mint via\n// `awaitEmbedToken()` and retries once with the fresh token.\nexport async function uploadFile(opts: UploadOptions): Promise<UploadedFile> {\n const embed = isEmbedTokenMode();\n try {\n const initialBearer = embed ? await awaitEmbedToken() : null;\n return await sendUpload(opts, initialBearer);\n } catch (err) {\n const isRefreshable =\n err instanceof UploadError &&\n err.status === 401 &&\n (err.code === \"TOKEN_REFRESH_NEEDED\" || err.code === \"UNAUTHORIZED\");\n if (!isRefreshable) throw err;\n\n let retryBearer: string | null = null;\n try {\n if (embed) {\n // The 401 means the server rejected the held token even if it is\n // still clock-valid (revoked session / permission change). Clear it\n // so awaitEmbedToken() takes the wait path and emits `token-expired`\n // for the host to re-mint, rather than fast-returning the dead token.\n clearEmbedToken();\n retryBearer = await awaitEmbedToken();\n } else {\n await ensureRefresh();\n }\n } catch {\n // Refresh failed terminally (cookie missing/expired, session revoked,\n // or - in embed mode - the host never re-minted in time). Route\n // through the persistent-auth-failure handler, the same escalation\n // apiFetch performs.\n getPersistentAuthFailureHandler()();\n throw err;\n }\n\n try {\n return await sendUpload(opts, retryBearer);\n } catch (retryErr) {\n if (retryErr instanceof UploadError && retryErr.status === 401) {\n getPersistentAuthFailureHandler()();\n }\n throw retryErr;\n }\n }\n}\n\n// sendUpload performs a single multipart/form-data POST attempt via XHR (so\n// the widget can report progress and abort mid-flight). uploadFile wraps it\n// with the one-shot 401 refresh+retry above.\n//\n// `bearerToken` is the embed JWT in embed token mode, null otherwise. When\n// set, the request carries `Authorization: Bearer <token>` and omits cookies\n// (`withCredentials = false`); the cookie wins over the Bearer header in the\n// auth middleware, so cookies MUST be omitted or the scoped embed token is\n// bypassed. Outside embed mode `bearerToken` is null and the cookie path\n// runs unchanged (`withCredentials = true`, no Authorization header).\nfunction sendUpload(\n opts: UploadOptions,\n bearerToken: string | null,\n): Promise<UploadedFile> {\n const { file, entityCode, fieldCode, slotCode, onProgress, signal } = opts;\n return new Promise<UploadedFile>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhr.open(\"POST\", uploadURL(entityCode, fieldCode, slotCode));\n xhr.withCredentials = bearerToken === null;\n if (bearerToken) {\n xhr.setRequestHeader(\"Authorization\", `Bearer ${bearerToken}`);\n }\n\n xhr.upload.onprogress = (ev) => {\n if (!onProgress) return;\n const total = ev.lengthComputable ? ev.total : file.size;\n const pct = total > 0 ? Math.min(100, Math.round((ev.loaded / total) * 100)) : 0;\n onProgress({ loaded: ev.loaded, total, pct });\n };\n\n xhr.onload = () => {\n const txt = xhr.responseText || \"\";\n let parsed: unknown = null;\n try {\n parsed = txt ? JSON.parse(txt) : null;\n } catch {\n // tolerate non-JSON 5xx\n }\n if (xhr.status >= 200 && xhr.status < 300) {\n const out = normalizeUploadedFile(parsed);\n if (!out || !out.id) {\n reject(new UploadError(xhr.status, \"BAD_RESPONSE\", \"Upload returned no file id\"));\n return;\n }\n resolve(out);\n return;\n }\n const body = parsed as { error?: { code?: string; message?: string } } | null;\n const code = body?.error?.code ?? \"UPLOAD_FAILED\";\n const message = body?.error?.message ?? `Upload failed (${xhr.status})`;\n reject(new UploadError(xhr.status, code, message));\n };\n\n xhr.onerror = () => {\n reject(new UploadError(0, \"NETWORK_ERROR\", \"Network error during upload\"));\n };\n xhr.onabort = () => {\n reject(new UploadError(0, \"ABORTED\", \"Upload aborted\"));\n };\n\n if (signal) {\n if (signal.aborted) {\n xhr.abort();\n reject(new UploadError(0, \"ABORTED\", \"Upload aborted\"));\n return;\n }\n signal.addEventListener(\"abort\", () => xhr.abort(), { once: true });\n }\n\n const fd = new FormData();\n fd.append(\"file\", file, file.name);\n xhr.send(fd);\n });\n}\n\nfunction uploadURL(entityCode?: string, fieldCode?: string, slotCode?: string): string {\n const qs = new URLSearchParams();\n if (entityCode && fieldCode) {\n qs.set(\"entity_code\", entityCode);\n qs.set(\"field_code\", fieldCode);\n if (slotCode) qs.set(\"slot_code\", slotCode);\n }\n const suffix = qs.toString();\n return suffix ? `/api/files/upload?${suffix}` : \"/api/files/upload\";\n}\n\nfunction normalizeUploadedFile(payload: unknown): UploadedFile | null {\n if (!isRecord(payload)) return null;\n if (isRecord(payload.result)) {\n return normalizeUploadedFile(payload.result);\n }\n if (isRecord(payload.file)) {\n return normalizeRouteUpload(payload.file);\n }\n if (typeof payload.id === \"string\") {\n return payload as unknown as UploadedFile;\n }\n return null;\n}\n\nfunction normalizeRouteUpload(payload: Record<string, unknown>): UploadedFile | null {\n const id = asString(payload.file_id);\n if (!id) return null;\n const filename = asString(payload.filename_original) ?? asString(payload.filename_normalized) ?? id;\n return {\n id,\n filename,\n content_type: asString(payload.content_type) ?? \"application/octet-stream\",\n size_bytes: asNumber(payload.size_bytes) ?? 0,\n sha256: asString(payload.sha256) ?? \"\",\n dedup: asBoolean(payload.dedup),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction asNumber(value: unknown): number | undefined {\n return typeof value === \"number\" ? value : undefined;\n}\n\nfunction asBoolean(value: unknown): boolean | undefined {\n return typeof value === \"boolean\" ? value : undefined;\n}\n\n// discardFile asks the server to drop a draft file row + its blob (if no\n// other draft references the same bytes). Bound to the `files.discard`\n// handler registered in Task 7. Errors are swallowed by callers — discard\n// is best-effort cleanup; the GC sweep eventually catches leaked drafts.\nexport async function discardFile(fileId: string): Promise<void> {\n await callHandler(\"files.discard\", { file_id: fileId });\n}\n\n// validateClientSide mirrors the server's accepts + max_size_bytes checks\n// so the widget can reject before issuing a network call. Returns null on\n// pass, an error object on fail. Wildcards in `accepts` follow the standard\n// HTTP convention: \"image/*\" matches any image type.\nexport function validateClientSide(\n file: File,\n accepts: string[] | undefined,\n maxBytes: number | undefined,\n): { code: string; message: string } | null {\n if (maxBytes && file.size > maxBytes) {\n return {\n code: \"FILE_TOO_LARGE\",\n message: `File is ${formatBytes(file.size)}, max ${formatBytes(maxBytes)}`,\n };\n }\n if (accepts && accepts.length > 0) {\n const ct = file.type || \"application/octet-stream\";\n const ok = accepts.some((acc) => mimeMatches(acc, ct));\n if (!ok) {\n return {\n code: \"MIME_NOT_ACCEPTED\",\n message: `File type ${ct} is not accepted`,\n };\n }\n }\n return null;\n}\n\nfunction mimeMatches(pattern: string, ct: string): boolean {\n if (pattern === \"*/*\" || pattern === \"*\") return true;\n if (pattern.endsWith(\"/*\")) {\n const prefix = pattern.slice(0, pattern.length - 1);\n return ct.startsWith(prefix);\n }\n return pattern === ct;\n}\n\nexport function formatBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n const units = [\"KB\", \"MB\", \"GB\"];\n let v = n / 1024;\n let i = 0;\n while (v >= 1024 && i < units.length - 1) {\n v /= 1024;\n i++;\n }\n return `${v.toFixed(v >= 10 ? 0 : 1)} ${units[i]}`;\n}\n","import type { EmbedFileVariant } from \"@/embed/file-src\";\nimport type { HydratedFile } from \"./types\";\n\nexport interface FocalPoint {\n x: number;\n y: number;\n}\n\n// Built-in derivation keys the thumbnail path prefers, in order. Mirrors the\n// fallback chain in `preferredListThumbnailURL` so the embed resolver picks\n// the SAME derivation the static URL would have used. Kept as a named\n// constant so the two code paths cannot drift.\nconst BUILTIN_THUMBNAIL_KEYS = [\"thumb_200\", \"thumb_300\"] as const;\n\nfunction preferredLocales(): string[] {\n if (typeof navigator === \"undefined\") return [\"en\"];\n const locales = navigator.languages?.length ? navigator.languages : [navigator.language];\n return locales.filter((value): value is string => typeof value === \"string\" && value.length > 0);\n}\n\nfunction resolveLocalizedText(value: Record<string, string>, locales: string[]): string | undefined {\n for (const locale of locales) {\n if (value[locale]) return value[locale];\n const base = locale.split(\"-\")[0];\n if (base && value[base]) return value[base];\n }\n for (const entry of Object.values(value)) {\n if (typeof entry === \"string\" && entry.length > 0) return entry;\n }\n return undefined;\n}\n\nexport function resolveFileAltText(\n file: HydratedFile | null,\n alt: string | undefined,\n fallback: string,\n): string {\n if (alt) return alt;\n const meta = file?.metadata as Record<string, unknown> | undefined;\n const raw = meta?.alt_text;\n if (typeof raw === \"string\" && raw.length > 0) return raw;\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n const localized = Object.fromEntries(\n Object.entries(raw).filter(\n (entry): entry is [string, string] => entry[0].length > 0 && typeof entry[1] === \"string\",\n ),\n );\n const resolved = resolveLocalizedText(localized, preferredLocales());\n if (resolved) return resolved;\n }\n return fallback;\n}\n\n// Server inline safelist mirror. The platform's /download response only\n// honors `inline` for these types; any other type is force-downgraded\n// to attachment regardless of disposition= query. Keep this list in\n// exact sync with golang/internal/fileutil/active_content.go\n// (inlineSafeMIMEs). No prefix matching — `image/svg+xml` is XML with\n// scripting and lives on the active-content blocklist, not here.\nconst INLINE_SAFE_MIMES = new Set([\n \"image/png\",\n \"image/jpeg\",\n \"image/webp\",\n \"image/gif\",\n \"image/avif\",\n \"application/pdf\",\n]);\n\nexport function canPreviewFile(file: HydratedFile): boolean {\n const ct = file.content_type?.toLowerCase().split(\";\")[0]?.trim() ?? \"\";\n return INLINE_SAFE_MIMES.has(ct);\n}\n\nexport function previewFileURL(file: HydratedFile): string | undefined {\n return file.url ?? file.presigned_url;\n}\n\nexport function downloadFileURL(file: HydratedFile): string | undefined {\n if (file.url) return withDisposition(file.url, \"attachment\");\n return file.presigned_url;\n}\n\nexport function preferredListThumbnailURL(\n file: HydratedFile | null,\n preferredDerivationKey?: string,\n): string | undefined {\n if (!file) return undefined;\n const derivations = file.derivations ?? {};\n if (preferredDerivationKey && derivations[preferredDerivationKey]) {\n return derivations[preferredDerivationKey];\n }\n for (const key of BUILTIN_THUMBNAIL_KEYS) {\n if (derivations[key]) return derivations[key];\n }\n return file.url ?? file.presigned_url;\n}\n\n// preferredThumbnailVariant is the embed-mode twin of preferredListThumbnailURL:\n// it returns the `EmbedFileVariant` the embed resolver must request so the\n// resolved blob/presigned URL points at the SAME derivation (or primary file)\n// the static URL above would have selected. The two MUST stay in lockstep -\n// both consult `preferredDerivationKey`, then BUILTIN_THUMBNAIL_KEYS, then the\n// primary file.\nexport function preferredThumbnailVariant(\n file: HydratedFile | null,\n preferredDerivationKey?: string,\n): EmbedFileVariant {\n const derivations = file?.derivations ?? {};\n if (preferredDerivationKey && derivations[preferredDerivationKey]) {\n return { derivation: preferredDerivationKey };\n }\n for (const key of BUILTIN_THUMBNAIL_KEYS) {\n if (derivations[key]) return { derivation: key };\n }\n return \"preview\";\n}\n\nfunction withDisposition(url: string, disposition: \"attachment\"): string {\n try {\n const parsed = new URL(url, typeof window !== \"undefined\" ? window.location.origin : \"http://localhost\");\n parsed.searchParams.set(\"disposition\", disposition);\n if (/^https?:\\/\\//i.test(url)) return parsed.toString();\n return `${parsed.pathname}${parsed.search}${parsed.hash}`;\n } catch {\n const separator = url.includes(\"?\") ? \"&\" : \"?\";\n return `${url}${separator}disposition=${disposition}`;\n }\n}\n\nexport function readFocalPoint(file: HydratedFile | null): FocalPoint | null {\n const raw = (file?.metadata as Record<string, unknown> | undefined)?.focal_point;\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return null;\n const candidate = raw as Record<string, unknown>;\n const x = typeof candidate.x === \"number\" ? candidate.x : Number.NaN;\n const y = typeof candidate.y === \"number\" ? candidate.y : Number.NaN;\n if (!Number.isFinite(x) || !Number.isFinite(y)) return null;\n if (x < 0 || x > 1 || y < 0 || y > 1) return null;\n return { x, y };\n}\n\nexport function focalPointObjectPosition(point: FocalPoint | null): string | undefined {\n if (!point) return undefined;\n return `${point.x * 100}% ${point.y * 100}%`;\n}\n\nexport function withFocalPoint(file: HydratedFile, point: FocalPoint): HydratedFile {\n return {\n ...file,\n metadata: {\n ...(file.metadata ?? {}),\n focal_point: point,\n },\n };\n}\n","import { createContext, useCallback, useContext, useMemo, useRef } from \"react\";\nimport type { ReactNode } from \"react\";\n\n// FileDraftScope tracks pending file drafts so a parent form can discard them\n// if the user clicks Cancel before saving. Drafts are uploaded immediately\n// (so the user sees the preview) but only become committed when the form\n// save runs the entity update with the $files payload. Until then they are\n// \"draft\" rows in declarion.file (uploaded_by=actor, object_id=NULL) which\n// the GC sweep would eventually trash anyway — but explicit Cancel discards\n// trim the leakage window from hours to instant.\n//\n// Usage:\n// <FileDraftScope>\n// <Form>\n// <SquareImage ... />\n// <button onClick={() => scope.flushAllDrafts()}>Cancel</button>\n// </Form>\n// </FileDraftScope>\n//\n// Widgets self-register their pending drafts on upload-success and unregister\n// once the form save completes (the parent form invokes\n// `scope.markCommitted(fileId)` after PATCH /api/data/... succeeds).\n\nexport interface DraftEntry {\n fileId: string;\n discard: () => Promise<void> | void;\n}\n\nexport interface FileDraftScopeApi {\n registerDraft(entry: DraftEntry): void;\n unregisterDraft(fileId: string): void;\n hasDraft(fileId: string): boolean;\n // flushAllDrafts fires `files.discard` on every still-pending draft and\n // clears the scope. Safe to call multiple times.\n flushAllDrafts(): Promise<void>;\n // markCommitted removes a draft without discarding. Forms call this on\n // save success, when the file row has been adopted by the entity.\n markCommitted(fileId: string): void;\n}\n\nconst noopApi: FileDraftScopeApi = {\n registerDraft() {},\n unregisterDraft() {},\n hasDraft() { return false; },\n flushAllDrafts: async () => {},\n markCommitted() {},\n};\n\nconst FileDraftScopeContext = createContext<FileDraftScopeApi>(noopApi);\n\nexport function FileDraftScope({ children }: { children: ReactNode }) {\n const draftsRef = useRef<Map<string, DraftEntry>>(new Map());\n\n const api = useMemo<FileDraftScopeApi>(() => ({\n registerDraft(entry) {\n draftsRef.current.set(entry.fileId, entry);\n },\n unregisterDraft(fileId) {\n draftsRef.current.delete(fileId);\n },\n hasDraft(fileId) {\n return draftsRef.current.has(fileId);\n },\n markCommitted(fileId) {\n draftsRef.current.delete(fileId);\n },\n async flushAllDrafts() {\n const entries = Array.from(draftsRef.current.values());\n draftsRef.current.clear();\n // Best-effort parallel discard; ignore individual failures because\n // the GC sweep is the source of truth for orphan cleanup.\n await Promise.allSettled(entries.map((e) => Promise.resolve(e.discard())));\n },\n }), []);\n\n return (\n <FileDraftScopeContext.Provider value={api}>\n {children}\n </FileDraftScopeContext.Provider>\n );\n}\n\nexport function useFileDraftScope(): FileDraftScopeApi {\n return useContext(FileDraftScopeContext);\n}\n\n// useDraftRegistration is a convenience for widgets: returns stable\n// register/unregister callbacks scoped to the active FileDraftScope.\nexport function useDraftRegistration() {\n const scope = useFileDraftScope();\n const register = useCallback(\n (fileId: string, discard: () => Promise<void> | void) => {\n scope.registerDraft({ fileId, discard });\n },\n [scope],\n );\n const unregister = useCallback((fileId: string) => {\n scope.unregisterDraft(fileId);\n }, [scope]);\n const markCommitted = useCallback((fileId: string) => {\n scope.markCommitted(fileId);\n }, [scope]);\n const isDraft = useCallback((fileId: string) => {\n return scope.hasDraft(fileId);\n }, [scope]);\n return { register, unregister, markCommitted, isDraft };\n}\n","// React bindings for embed-aware file URL resolution.\n//\n// The pure resolver lives in `embed/file-src.ts`. This module wraps it for\n// the file widgets:\n//\n// - `useEmbedFileSrc` - reactive `src` for `<img>` / preview elements.\n// - `useEmbedFileOpener` - imperative open for `window.open` (Download /\n// Preview buttons in DocumentCard / FileList).\n//\n// Both keep NORMAL (non-embed, cookie) mode byte-equivalent to before: the\n// static URL string the caller already computed is returned as-is, no fetch,\n// no effect work, no object URL. The embed branch only activates when\n// `isEmbedTokenMode()` is true.\n//\n// Object-URL lifecycle: when the embed resolver produces a `blob:` URL it is\n// revoked on the next file change and on unmount, so a long-lived embedded\n// screen does not leak memory as the user scrolls or replaces files.\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n DerivationPendingError,\n resolveEmbedFileSrc,\n type EmbedFileSrc,\n type EmbedFileVariant,\n} from \"@/embed/file-src\";\nimport { isEmbedTokenMode } from \"@/embed/token\";\n\n/**\n * Serialize a variant to a stable primitive string. `EmbedFileVariant` is\n * either a string literal (`\"preview\"` / `\"download\"`) or a `{ derivation }`\n * object; a fresh object each render would otherwise re-trigger effects. The\n * serialized form is both the effect dependency AND parseable back into the\n * variant, so the hook depends only on primitives and never on the object\n * identity - no `exhaustive-deps` suppression is needed.\n */\nfunction variantToKey(variant: EmbedFileVariant): string {\n return typeof variant === \"object\"\n ? `derivation:${variant.derivation}`\n : variant;\n}\n\n/** Parse a key produced by `variantToKey` back into an `EmbedFileVariant`. */\nfunction keyToVariant(key: string): EmbedFileVariant {\n if (key.startsWith(\"derivation:\")) {\n return { derivation: key.slice(\"derivation:\".length) };\n }\n // The only non-derivation keys are the two string-literal variants.\n return key === \"download\" ? \"download\" : \"preview\";\n}\n\n/**\n * Reactively resolve a file `src` for a native `<img>` element.\n *\n * Outside embed token mode the hook returns `staticUrl` unchanged and does no\n * work - the cookie carries auth and the browser loads the URL directly, so\n * behavior is identical to passing `staticUrl` straight to `<img src>`.\n *\n * In embed token mode the hook resolves through `resolveEmbedFileSrc`:\n * - a presigned (non-proxy) URL is used directly;\n * - a proxy-backed file is fetched as bytes and rendered from a `blob:` URL.\n * The hook returns `undefined` until the async resolution settles, so the\n * widget shows its placeholder rather than a broken image during the fetch.\n *\n * `fileId` may be undefined when the widget is empty; the hook then returns\n * `staticUrl` (also typically undefined) and does nothing.\n *\n * A `blob:` object URL is revoked when `fileId` / `variant` changes and on\n * unmount. A `DerivationPendingError` (the derived endpoint returned 202)\n * resolves to `undefined` so the caller can fall back to the primary file.\n */\nexport function useEmbedFileSrc(\n fileId: string | undefined,\n variant: EmbedFileVariant,\n staticUrl: string | undefined,\n): string | undefined {\n const variantKey = variantToKey(variant);\n\n const [resolved, setResolved] = useState<string | undefined>(() =>\n isEmbedTokenMode() ? undefined : staticUrl,\n );\n\n // Non-embed (cookie) mode: the static URL is authoritative. No fetch, no\n // blob - byte-equivalent to assigning `staticUrl` directly to `<img src>`.\n // Kept as a separate effect so it depends ONLY on `staticUrl`, and the embed\n // effect below depends ONLY on `[fileId, variantKey]` - a rotating presigned\n // URL on a list re-hydration must not needlessly re-resolve a blob in embed\n // mode. `isEmbedTokenMode()` is fixed for the document's lifetime, so\n // exactly one of these two effects ever does work.\n useEffect(() => {\n if (isEmbedTokenMode()) return;\n setResolved(staticUrl);\n }, [staticUrl]);\n\n useEffect(() => {\n if (!isEmbedTokenMode()) return;\n\n // Embed mode but the widget is empty: nothing to resolve.\n if (!fileId) {\n setResolved(undefined);\n return;\n }\n\n let objectUrl: string | null = null;\n let cancelled = false;\n const controller = new AbortController();\n\n // Hold the placeholder while the async resolution runs.\n setResolved(undefined);\n\n resolveEmbedFileSrc(fileId, keyToVariant(variantKey), controller.signal)\n .then((src: EmbedFileSrc) => {\n if (cancelled) {\n // The file/variant changed (or the component unmounted) before this\n // settled. Revoke a freshly-created object URL so it does not leak.\n if (src.kind === \"blob\") URL.revokeObjectURL(src.url);\n return;\n }\n if (src.kind === \"blob\") objectUrl = src.url;\n setResolved(src.url);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n // A pending derivation (202) is not an error - fall back to no src so\n // the caller renders the primary file or a placeholder instead.\n if (err instanceof DerivationPendingError) {\n setResolved(undefined);\n return;\n }\n // An aborted fetch is the expected outcome of cancellation; the\n // `cancelled` guard above already covers the common case, but the\n // abort can also reject after the guard - swallow it quietly.\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n console.error(\"[declarion-embed] file src resolution failed:\", err);\n setResolved(undefined);\n });\n\n return () => {\n cancelled = true;\n controller.abort();\n // Revoke the object URL created by THIS effect run when the file or\n // variant changes, or the component unmounts.\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n };\n }, [fileId, variantKey]);\n\n return resolved;\n}\n\n/**\n * An imperative opener for the download / preview buttons.\n *\n * `window.open(url)` cannot attach an `Authorization` header, so in embed\n * token mode a click must FIRST resolve the file to a presigned or `blob:`\n * URL and THEN open that. Outside embed mode the returned function opens the\n * static URL directly, unchanged from before.\n *\n * The returned `open(fileId, variant, staticUrl)` function:\n * - non-embed mode: `window.open(staticUrl)` synchronously, as before;\n * - embed mode: resolves via `resolveEmbedFileSrc`, then `window.open` the\n * result. A `blob:` URL opened this way is revoked on unmount (it must\n * outlive the click so the new tab can load it; revoking immediately would\n * break the open).\n *\n * Returns `{ open }`. `open` is a no-op when `fileId` / `staticUrl` is absent.\n */\nexport function useEmbedFileOpener(): {\n open: (\n fileId: string | undefined,\n variant: EmbedFileVariant,\n staticUrl: string | undefined,\n ) => void;\n} {\n // Object URLs created for opened files. They must outlive the click so the\n // opened tab can load them, so they are revoked only on unmount.\n const openedObjectUrls = useRef<string[]>([]);\n\n useEffect(() => {\n return () => {\n for (const url of openedObjectUrls.current) URL.revokeObjectURL(url);\n openedObjectUrls.current = [];\n };\n }, []);\n\n const open = useCallback(\n (\n fileId: string | undefined,\n variant: EmbedFileVariant,\n staticUrl: string | undefined,\n ): void => {\n // Non-embed mode: the static URL works directly with the cookie.\n if (!isEmbedTokenMode()) {\n if (staticUrl) window.open(staticUrl, \"_blank\", \"noopener\");\n return;\n }\n if (!fileId) return;\n\n resolveEmbedFileSrc(fileId, variant)\n .then((src: EmbedFileSrc) => {\n if (src.kind === \"blob\") openedObjectUrls.current.push(src.url);\n window.open(src.url, \"_blank\", \"noopener\");\n })\n .catch((err: unknown) => {\n if (err instanceof DerivationPendingError) return;\n console.error(\"[declarion-embed] file open failed:\", err);\n });\n },\n [],\n );\n\n return { open };\n}\n","import type { FileFieldValue, HydratedFile } from \"./types\";\n\nexport function isHydratedFile(value: unknown): value is HydratedFile {\n return typeof value === \"object\" && value !== null && typeof (value as HydratedFile).id === \"string\";\n}\n\nexport function singleFileFromValue(value: FileFieldValue): HydratedFile | null {\n if (!value) return null;\n if (Array.isArray(value)) return value.find(isHydratedFile) ?? null;\n return isHydratedFile(value) ? value : null;\n}\n\nexport function fileArrayFromValue(value: FileFieldValue): HydratedFile[] {\n if (!value) return [];\n if (Array.isArray(value)) return value.filter(isHydratedFile);\n return isHydratedFile(value) ? [value] : [];\n}\n"],"mappings":";;;AA+CA,IAAa,IAAb,cAA8B,MAAM;CAClC;CACA;CAEA;CAEA;CAEA,YAAY,GAAgB,GAAiB,GAAc,GAAgB,GAAe;EAMxF,AALA,MAAM,CAAO,GACb,KAAK,OAAO,YACZ,KAAK,SAAS,GACd,KAAK,OAAO,GACZ,KAAK,QAAQ,GACb,KAAK,MAAM;CACb;AACF,GAmBa,IAA4B,6BAC5B,IAAwB;AAIrC,SAAgB,EAAmB,GAAuB;CACxD,OACE,aAAe,MACd,EAAI,SAAA,+BAAsC,EAAI,SAAA;AAEnD;;;AC5FA,IAAM,IAAoB,KACpB,IAAa,UACb,IAAc,WACd,IAAa;AAEnB,SAAS,GAAY,GAAuB;CAC1C,IAAM,IAAW,EAAK,MAAM,QAAQ,CAAC,EAAE;CACvC,OACE,MAAa,KACb,EAAS,WAAW,GAAG,EAAW,EAAE,KACpC,MAAa,KACb,EAAS,WAAW,GAAG,EAAY,EAAE;AAEzC;AAEA,SAAgB,GAAqB,GAA0C;CAC7E,IAAI,CAAC,GAAO,OAAO;CAEnB,IAAM,IAAQ,EAAM,YAAY;CAChC,IACE,CAAC,EAAM,WAAW,GAAG,KACrB,EAAM,WAAW,IAAI,KACrB,EAAM,SAAS,IAAI,KACnB,EAAM,SAAS,KAAK,GAEpB,OAAO;CAGT,IAAI;EACF,IAAM,IAAS,IAAI,IAAI,GAAO,CAAU;EACxC,IAAI,EAAO,WAAW,GAAY,OAAO;EAEzC,IAAM,IAAO,GAAG,EAAO,WAAW,EAAO,SAAS,EAAO;EAIzD,OAHI,CAAC,EAAK,WAAW,GAAG,KAAK,EAAK,WAAW,IAAI,KAAK,EAAK,SAAS,IAAI,IAC/D,IAEF,KAAQ;CACjB,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,GAAyB,GAA0C;CACjF,IAAM,IAAe,GAAqB,CAAK;CAI/C,OAHI,MAAiB,KAAqB,GAAY,CAAY,IACzD,IAEF,GAAG,EAAW,QAAQ,mBAAmB,CAAY;AAC9D;AAEA,SAAgB,KAAsC;CACpD,IAAI,OAAO,SAAW,KAAa,OAAO;CAC1C,IAAM,EAAE,aAAU,WAAQ,YAAS,OAAO;CAC1C,OAAO,GAAyB,GAAG,IAAW,IAAS,GAAM;AAC/D;;;AC5BA,IAAa,IAAuB,mBAcvB,IAAsB;CAEjC,OAAO;CAEP,UAAU;CAEV,cAAc;CAEd,gBAAgB;CAEhB,SAAS;CAET,WAAW;CAEX,qBAAqB;CAErB,cAAc;CAEd,UAAU;CAEV,UAAU;AACZ;AAmHA,SAAgB,EACd,GACA,GACA,GACS;CAKT,IAJI,CAAC,KACD,OAAO,SAAW,OAGlB,OAAO,WAAW,QAAQ,OAAO;CAErC,IAAM,IAA2B;EAC/B,QAAQ;EACR,UAAA;EACA;EACA;CACF;CAEA,OADA,OAAO,OAAO,YAAY,GAAS,CAAY,GACxC;AACT;;;AC5JA,IAAM,KAAwB,MAOxB,KAAuB,KA0BvB,IAAqB;CACzB,QAAQ;CACR,cAAc;CACd,SAAS;CACT,SAAS;AACX;AASA,SAAgB,GAAmB,GAAmC;CAEpE,AADA,EAAM,SAAS,IACf,EAAM,eAAe;AACvB;AAOA,SAAgB,IAA4B;CAC1C,OAAO,EAAM;AACf;AAGA,SAAgB,KAA+B;CAC7C,OAAO,EAAM,SAAS,SAAS;AACjC;AAOA,SAAgB,KAAsC;CACpD,OAAO,EAAM;AACf;AAMA,SAAS,KAAwB;CAE/B,OADK,EAAM,UACJ,KAAK,IAAI,KAAK,EAAM,QAAQ,YAAY,KADpB;AAE7B;AAUA,SAAgB,GAAc,GAGrB;CACP,IAAI,CAAC,EAAQ,SAAS,OAAO,EAAQ,SAAU,UAAU;CACzD,IAAM,IAAY,KAAK,MAAM,EAAQ,UAAU;CAC3C,YAAO,MAAM,CAAS,MAE1B,EAAM,UAAU;EAAE,OAAO,EAAQ;EAAO;CAAU,GAG9C,EAAM,UAAS;EACjB,IAAM,IAAU,EAAM;EAGtB,AAFA,EAAM,UAAU,MAChB,aAAa,EAAQ,KAAK,GAC1B,EAAQ,QAAQ,EAAQ,KAAK;CAC/B;AACF;AAMA,SAAgB,IAAwB;CACtC,EAAM,UAAU;AAClB;AAeA,SAAgB,IAAmC;CAEjD,IAAI,CAAC,GAAa,KAAK,EAAM,SAC3B,OAAO,QAAQ,QAAQ,EAAM,QAAQ,KAAK;CAK5C,IAAI,EAAM,SACR,OAAO,EAAM,QAAQ;CAIvB,IAAI,GACA,GACE,IAAU,IAAI,SAAiB,GAAK,MAAQ;EAEhD,AADA,IAAU,GACV,IAAS;CACX,CAAC,GAEK,IAAQ,iBAAiB;EAM7B,AAHI,EAAM,WAAW,EAAM,QAAQ,YAAY,MAC7C,EAAM,UAAU,OAElB,EACE,gBAAI,MACF,oCAAoC,GAAsB,GAC5D,CACF;CACF,GAAG,EAAqB;CAQxB,OANA,EAAM,UAAU;EAAE;EAAS;EAAS;EAAQ;CAAM,GAIlD,EAAa,EAAM,cAAc,EAAoB,cAAc,CAAC,CAAC,GAE9D;AACT;;;AC/JA,IAAM,KAAc,uBAQd,KAAoB;AAW1B,SAAS,KAAuC;CAC9C,IAAI,OAAO,mBAAqB,KAAa,OAAO;CACpD,IAAI;EACF,OAAO,IAAI,iBAAiB,EAAiB;CAC/C,QAAQ;EACN,OAAO;CACT;AACF;AAIA,IAAM,IAAuB;CAC3B,WAAW;CACX,iBAAiB;CACjB,kBAAkB;CAClB,2BAAW,IAAI,IAAI;CACnB,SAAS;AACX;AAIA,SAAS,IAAsB;CACzB,EAAM,WAAW,OAAO,mBAAqB,QACjD,EAAM,UAAU,GAAY,GACvB,EAAM,WACX,EAAM,QAAQ,iBAAiB,YAAY,MAAO;EAChD,IAAM,IAAO,EAAG;EACZ,OAAC,KAAQ,EAAK,SAAS,WAAW,CAAC,EAAK,UAGxC,GAAM,kBACV;KAAM,mBAAmB;GACzB,KAAK,IAAM,KAAM,EAAM,WAAW,EAAG,EAAK,KAAK;EADtB;CAE3B,CAAC;AACH;AAQA,SAAgB,GAAQ,GAA2C;CAEjE,IADA,EAAc,GACV,CAAC,GAAS;CACd,IAAM,IAAQ,EAAQ,IAAI,EAAW;CAChC,OACL;MAAI,CAAC,EAAM,WAAW;GACpB,EAAM,YAAY;GAClB;EACF;EACI,MAAU,EAAM,cAChB,EAAM,oBAIV,EAAW,EAAE,YAAY;GAGvB,EAAU;IAAE,MAAM;IAAW,UAAU;GAAM,CAAC;EAChD,CAAC;CAVD;AAWF;AAMA,eAAsB,IAA4B;CAChD,EAAc;CACd,IAAI;CACJ,IAAI;EACF,IAAM,IAAkC,CAAC;EAQzC,AAPI,EAAM,cACR,EAAQ,mBAAmB,IAAI,EAAM,UAAU,KAMjD,IAAM,MAAM,MAAM,gBAAgB;GAChC,QAAQ;GACR,aAAa,EAAiB,IAAI,SAAS;GAC3C;EACF,CAAC;CACH,QAAQ;EAEN,MAAU,MAAM,6BAA6B;CAC/C;CAEA,IAAI,EAAI,WAAW,KAIjB;CAEF,IAAI,CAAC,EAAI,IAGP,MAAU,MAAM,iBAAiB,EAAI,QAAQ;CAE/C,IAAI;CAMJ,IAAI;EACF,IAAQ,MAAM,EAAI,KAAK;CACzB,QAAQ;EACN,MAAU,MAAM,8BAA8B;CAChD;CAEA,IAAM,IAAW,EAAK;CACtB,IAAI,OAAO,KAAa,YAAY,MAAa,IAC/C,MAAU,MAAM,mCAAmC;CAGrD,IAAM,IACJ,OAAO,EAAK,sBAAuB,YACnC,OAAO,EAAK,qBAAsB,YAClC,OAAO,EAAK,sBAAuB,WAC/B;EACE,QAAQ,EAAK;EACb,OAAO,EAAK;EACZ,QAAQ,EAAK;CACf,IACA,KAAA;CAUN,AACE,EAAM,cAAY,GAEhB,CAAC,EAAM,mBAAmB,KAAc,MAAa,EAAM,cAC7D,EAAM,kBAAkB,IAGtB,MAAa,EAAM,aAGvB,EAAU;EAAE,MADC,GAAc,EAAM,iBAAiB,CACtC;EAAM;EAAU;CAAW,CAAC;AAC1C;AAOA,SAAS,GACP,GACA,GACsB;CACtB,IAAI,CAAC,KAAY,CAAC,GAAS,OAAO;CAClC,IAAM,IAAgB,EAAS,WAAW,EAAQ,QAC5C,IAAe,EAAS,UAAU,EAAQ,OAC1C,IAAgB,EAAS,WAAW,EAAQ;CAKlD,OAJI,KAAiB,CAAC,KAAgB,CAAC,IAAsB,gBACzD,KAAgB,IAAsB,oBAGnC;AACT;AAEA,SAAS,EAAU,GAA2B;CACxC,OAAM,kBACV;IAAM,mBAAmB;EACzB,KAAK,IAAM,KAAM,EAAM,WAAW,EAAG,CAAK;EAC1C,IAAI,EAAM,SACR,IAAI;GACF,EAAM,QAAQ,YAAY;IAAE,MAAM;IAAS;GAAM,CAAC;EACpD,QAAQ,CAER;EASF,AAAI,EAAiB,KAAK,EAAM,SAAS,iBACvC,EAAa,GAAqB,GAAG,EAAoB,gBAAgB,EACvE,QAAQ,kBAAkB,EAAM,OAClC,CAAC;CAnBsB;AAqB3B;AAMA,SAAgB,GAAU,GAAsC;CAE9D,OADA,EAAM,UAAU,IAAI,CAAE,SACT;EACX,EAAM,UAAU,OAAO,CAAE;CAC3B;AACF;;;AC1PA,IAAM,MAAkB,MACtB,KAAK,UAAU,KAAQ,CAAC,CAAC,GAEvB,IAAuC,MAU9B,IAAb,cAAsC,MAAM;CACvB;CAAnB,YAAY,GAAuB;EAEjC,AADA,MAAM,8BAA8B,EAAO,EAAE,GAD5B,KAAA,SAAA,GAEjB,KAAK,OAAO;CACd;AACF,GAUI,IAA8D,MAC9D,IAAqC,MAEnC,IAA8B,KAG9B,KAAiC;AAKvC,SAAS,IAAgC;CACvC,EAAmB,EAAE,OAAO,MAAM;EAChC,AAAI,aAAa,KAAkB,EAAwB;CAC7D,CAAC;AACH;AAMA,SAAgB,EAAyB,GAAqC;CAM5E,IALA,AAEE,OADA,aAAa,CAAqB,GACV,OAE1B,IAAsB,MAClB,CAAC,KAAa,OAAO,SAAW,KAAa;CACjD,IAAM,IAAS,KAAK,MAAM,CAAS;CACnC,IAAI,OAAO,MAAM,CAAM,GAAG;CAC1B,IAAsB;CACtB,IAAM,IAAQ,KAAK,IACjB,IAAS,KAAK,IAAI,IAAI,GACtB,EACF;CACA,IAAwB,iBAAiB;EAEvC,AADA,IAAwB,MACxB,EAAwB;CAC1B,GAAG,CAAK;AACV;AAGA,SAAgB,KAA+B;CAK7C,AAJA,AAEE,OADA,aAAa,CAAqB,GACV,OAE1B,IAAsB;AACxB;AAMI,OAAO,WAAa,OACtB,SAAS,iBAAiB,0BAA0B;CAC9C,SAAS,oBAAoB,aAE/B,MAAwB,QACxB,KAAK,IAAI,KAAK,IAAsB,KAEpC,EAAwB;AAE5B,CAAC;AAKH,eAAe,EAAuB,GAA8B;CAClE,IAAI;EACF,IAAM,IAAQ,MAAM,EAAI,KAAK;EAI7B,EAAyB,GAAM,QAAQ,cAAc,GAAM,UAAU;CACvE,QAAQ,CAER;AACF;AAEA,eAAe,KAAoC;CACjD,IAAM,IAAoB;EACxB,QAAQ;EACR,aAAa;EACb,SAAS,EAAE,gBAAgB,mBAAmB;EAC9C,MAAM,GAAe,CAAC,CAAC;CACzB,GACM,IAAM,MAAM,MAAM,6BAA6B,CAAI;CAIzD,IAAI,EAAI,WAAW,KAAK;EACtB,IAAM,IAAa,SAAS,EAAI,QAAQ,IAAI,aAAa,KAAK,KAAK,EAAE;EACrE,MAAM,IAAI,SAAS,MAAM,WAAW,GAAG,IAAa,GAAI,CAAC;EACzD,IAAM,IAAW,MAAM,MAAM,6BAA6B,CAAI;EAC9D,IAAI,CAAC,EAAS,IACZ,MAAM,IAAI,EAAiB,EAAS,MAAM;EAE5C,MAAM,EAAuB,CAAQ;EACrC;CACF;CAEA,IAAI,CAAC,EAAI,IACP,MAAM,IAAI,EAAiB,EAAI,MAAM;CAEvC,MAAM,EAAuB,CAAG;AAClC;AAEA,eAAsB,IAA+B;CAKnD,OAJI,MACJ,IAAiB,GAAmB,EAAE,cAAc;EAClD,IAAiB;CACnB,CAAC,GACM;AACT;AAuBA,IAAI,UAd4E;CAC9E,IAAI,OAAO,SAAW,KAAa;CACnC,IAAM,EAAE,gBAAa,OAAO;CAE1B,EAAS,WAAW,QAAQ,KAC5B,EAAS,WAAW,SAAS,KAC7B,MAAa,YACb,MAAa,cAIf,OAAO,SAAS,OAAO,GAA4B;AACrD;AAQA,SAAgB,GACd,GAC8B;CAC9B,IAAM,IAAO;CAEb,OADA,IAA0B,GACnB;AACT;AAKA,SAAgB,IAAgE;CAC9E,OAAO;AACT;AAmBA,IAAI,MAJ+C,EAAE,WAAQ,SAAM,iBAAc;CAC/E,QAAQ,MAAM,QAAQ,EAAO,IAAI,EAAK,IAAI,GAAS;AACrD;AAoCA,SAAS,EACP,GACA,GACA,GACA,GACS;CACT,IAAM,IAAU,IAAI,QAAQ,CAAa;CAUzC,OATI,CAAC,EAAQ,IAAI,cAAc,KAAK,KAClC,EAAQ,IAAI,gBAAgB,kBAAkB,GAE5C,EAAQ,aAAa,CAAC,EAAQ,IAAI,QAAQ,KAC5C,EAAQ,IAAI,UAAU,KAAK,GAEzB,KACF,EAAQ,IAAI,iBAAiB,UAAU,GAAa,GAE/C;AACT;AAEA,eAAe,EACb,GACA,GACA,GACA,GACY;CAQZ,IAAM,IAAQ,EAAiB,GACzB,IAAyC,IAAQ,SAAS,WAE1D,IAAU,EACd,EAAQ,SACR,CAAC,CAAC,EAAQ,MACV,GACA,IAAQ,GAAc,IAAI,IAC5B,GAOM,IAAM,MAAM,MAAM,GAAM;EAC5B,GAAG;EACH;EACA,aAAa;CACf,CAAC;CAUD,IAFA,GAAe,EAAI,OAAO,GAEtB,CAAC,EAAI,IAAI;EACX,IAAI,IAAU,kBACV,IAAO,WACP,GACA;EAEJ,IAAI;GACF,IAAM,MAAM,EAAI,KAAK;GACrB,IAAM,IAAO;GACb,AAAI,KAAQ,EAAK,UACf,IAAU,EAAK,MAAM,SACrB,IAAO,EAAK,MAAM,MACd,EAAK,SAAS,SAAM,IAAQ,EAAK;EAEzC,QAAQ,CAER;EASA,IAAI,EAAI,WAAW,QAAQ,MAAS,0BAA0B,MAAS,iBACrE,IAAI;GACF,IAAI,IAA6B;GACjC,AAAI,KAKF,EAAgB,GAChB,IAAc,MAAM,EAAgB,KAEpC,MAAM,EAAc;GAEtB,IAAM,IAAe,EACnB,EAAQ,SACR,CAAC,CAAC,EAAQ,MACV,GACA,CACF,GACM,IAAW,MAAM,MAAM,GAAM;IACjC,GAAG;IACH,SAAS;IACT,aAAa;GACf,CAAC;GACD,IAAI,CAAC,EAAS,IAAI;IAChB,EAAwB;IACxB,IAAM,IAAa,MAAM,EACtB,KAAK,EACL,aAAa,EAAE,OAAO;KAAE,SAAS;KAAkB,MAAM;IAAU,EAAE,EAAE;IAC1E,MAAM,IAAI,EAAS,EAAS,QAAQ,EAAU,MAAM,SAAS,EAAU,MAAM,MAAM,EAAU,KAAK;GACpG;GACA,OAAO,EAAU,CAAQ;EAC3B,SAAS,GAAY;GAKnB,MAJI,aAAsB,IAClB,KAER,EAAwB,GAClB,IAAI,EAAS,KAAK,GAAS,GAAM,CAAK;EAC9C;EASF,MALI,EAAI,WAAW,OACjB,EAAwB,GAG1B,GAAiB;GAAE,QAAQ,EAAI;GAAQ;GAAM;EAAQ,CAAC,GAChD,IAAI,EAAS,EAAI,QAAQ,GAAS,GAAM,GAAO,CAAG;CAC1D;CAEA,OAAO,EAAU,CAAG;AACtB;AAMA,SAAS,GAAiB,GAA2B;CAInD,OAHI,EAAI,WAAW,MACV,QAAQ,QAAQ,KAAA,CAAc,IAEhC,EAAI,KAAK;AAClB;AAEA,eAAsB,EACpB,GACA,IAAuB,CAAC,GACZ;CACZ,OAAO,EAAW,GAAM,GAAS,EAAE,WAAW,GAAM,GAAG,EAAa;AACtE;AAsCA,eAAe,GAAc,GAAsC;CAEjE,OAAO;EAAE,MAAA,MADU,EAAI,KAAK;EACb,UAAU,GAAgC,EAAI,OAAO;EAAG,SAAS,EAAI;CAAQ;AAC9F;AAEA,eAAsB,GACpB,GACA,IAAuB,CAAC,GACD;CACvB,OAAO,EAAsB,GAAM,GAAS,EAAE,WAAW,GAAK,GAAG,EAAa;AAChF;AAMA,SAAgB,GAAgC,GAA0B;CACxE,IAAM,IAAK,EAAQ,IAAI,qBAAqB;CAC5C,IAAI,CAAC,GAAI,OAAO;CAEhB,IAAM,IAAO,EAAG,MAAM,2CAA2C;CACjE,IAAI,GACF,IAAI;EACF,OAAO,mBAAmB,EAAK,GAAG,KAAK,CAAC;CAC1C,QAAQ,CAER;CAGF,IAAM,IAAS,EAAG,MAAM,qCAAqC;CAI7D,OAHI,KACM,EAAO,MAAM,EAAO,MAAM,IAAI,KAAK,IAEtC;AACT;AAWA,eAAsB,EACpB,GACA,IAAgB,CAAC,GACL;CAQZ,QAAO,MAPW,EAChB,gBAAgB,mBAAmB,CAAI,KACvC;EACE,QAAQ;EACR,MAAM,GAAe,CAAI;CAC3B,CACF,GACW;AACb;;;AC1dA,IAAM,IAAqB;AA4C3B,SAAS,GAAkB,GAA0C;CACnE,IAAI,OAAO,KAAU,aAAY,GAAgB,OAAO;CACxD,IAAM,IAAI;CACV,OAAO,OAAO,EAAE,OAAQ,YAAY,OAAO,EAAE,SAAU;AACzD;AAGA,SAAS,GAAgB,GAAgB,GAAmC;CAC1E,IAAM,IAAK,mBAAmB,CAAM;CAIpC,OAHI,OAAO,KAAY,WACd,GAAG,EAAmB,GAAG,EAAG,WAAW,mBAAmB,EAAQ,UAAU,EAAE,QAEhF,GAAG,EAAmB,GAAG,EAAG;AACrC;AAOA,IAAa,IAAb,cAA4C,MAAM;CACpB;CAAgC;CAA5D,YAAY,GAAgC,GAA6B;EAEvE,AADA,MAAM,eAAe,EAAI,YAAY,EAAO,qBAAqB,GADvC,KAAA,SAAA,GAAgC,KAAA,MAAA,GAE1D,KAAK,OAAO;CACd;AACF;AASA,SAAS,EAAiB,GAAe,GAAmC;CAC1E,OAAO;EACL,QAAQ;EACR,aAAa;EACb,SAAS,EAAE,eAAe,UAAU,IAAQ;EAC5C;CACF;AACF;AAOA,SAAS,GAA0B,GAAqB;CAEtD,OAAO,GAAG,IADQ,EAAI,SAAS,GAAG,IAAI,MAAM,IAClB;AAC5B;AAcA,eAAe,EACb,GACA,GACmB;CACnB,IAAM,IAAQ,MAAM,EAAgB,GAC9B,IAAM,MAAM,MAAM,GAAK,EAAiB,GAAO,CAAM,CAAC;CAC5D,IAAI,EAAI,WAAW,KAAK,OAAO;CAI/B,EAAgB;CAChB,IAAM,IAAa,MAAM,EAAgB;CACzC,OAAO,MAAM,GAAK,EAAiB,GAAY,CAAM,CAAC;AACxD;AAwBA,eAAsB,EACpB,GACA,GACA,GACuB;CACvB,IAAI,CAAC,EAAiB,GACpB,MAAU,MAAM,qDAAqD;CAMvE,IAAM,IAAU,MAAM,EAAiB,GAAgB,GAAQ,CAAO,GAAG,CAAM;CAG/E,IAAI,EAAQ,WAAW,OAAO,OAAO,KAAY,UAC/C,MAAM,IAAI,EAAuB,GAAQ,EAAQ,UAAU;CAE7D,IAAI,CAAC,EAAQ,IACX,MAAU,MACR,kCAAkC,EAAQ,OAAO,GAAG,EAAQ,YAC9D;CAGF,IAAM,IAAgB,MAAM,EAAQ,KAAK;CACzC,IAAI,CAAC,GAAkB,CAAI,GACzB,MAAU,MAAM,sCAAsC;CAKxD,IAAI,CAAC,EAAK,OACR,OAAO;EAAE,KAAK,EAAK;EAAK,MAAM;CAAS;CAazC,IAAI,CAAC,EAAK,IAAI,WAAW,aAAwB,GAC/C,MAAU,MACR,kDAAkD,KAAK,UAAU,EAAK,GAAG,GAC3E;CAIF,IAAM,IAAW,MAAM,EADrB,MAAY,aAAa,GAA0B,EAAK,GAAG,IAAI,EAAK,KACpB,CAAM;CACxD,IAAI,CAAC,EAAS,IACZ,MAAU,MACR,kCAAkC,EAAS,OAAO,GAAG,EAAS,YAChE;CAEF,IAAM,IAAO,MAAM,EAAS,KAAK;CACjC,OAAO;EAAE,KAAK,IAAI,gBAAgB,CAAI;EAAG,MAAM;CAAO;AACxD;;;AC7NA,IAAa,IAAb,cAAiC,MAAM;CACrC;CACA;CACA,YAAY,GAAgB,GAAc,GAAiB;EAGzD,AAFA,MAAM,CAAO,GACb,KAAK,OAAO,GACZ,KAAK,SAAS;CAChB;AACF;AAmBA,eAAsB,GAAW,GAA4C;CAC3E,IAAM,IAAQ,EAAiB;CAC/B,IAAI;EAEF,OAAO,MAAM,EAAW,GADF,IAAQ,MAAM,EAAgB,IAAI,IACb;CAC7C,SAAS,GAAK;EAKZ,IAAI,EAHF,aAAe,KACf,EAAI,WAAW,QACd,EAAI,SAAS,0BAA0B,EAAI,SAAS,kBACnC,MAAM;EAE1B,IAAI,IAA6B;EACjC,IAAI;GACF,AAAI,KAKF,EAAgB,GAChB,IAAc,MAAM,EAAgB,KAEpC,MAAM,EAAc;EAExB,QAAQ;GAMN,MADA,EAAgC,EAAE,GAC5B;EACR;EAEA,IAAI;GACF,OAAO,MAAM,EAAW,GAAM,CAAW;EAC3C,SAAS,GAAU;GAIjB,MAHI,aAAoB,KAAe,EAAS,WAAW,OACzD,EAAgC,EAAE,GAE9B;EACR;CACF;AACF;AAYA,SAAS,EACP,GACA,GACuB;CACvB,IAAM,EAAE,SAAM,eAAY,cAAW,aAAU,eAAY,cAAW;CACtE,OAAO,IAAI,SAAuB,GAAS,MAAW;EACpD,IAAM,IAAM,IAAI,eAAe;EA4C/B,IA3CA,EAAI,KAAK,QAAQ,GAAU,GAAY,GAAW,CAAQ,CAAC,GAC3D,EAAI,kBAAkB,MAAgB,MAClC,KACF,EAAI,iBAAiB,iBAAiB,UAAU,GAAa,GAG/D,EAAI,OAAO,cAAc,MAAO;GAC9B,IAAI,CAAC,GAAY;GACjB,IAAM,IAAQ,EAAG,mBAAmB,EAAG,QAAQ,EAAK,MAC9C,IAAM,IAAQ,IAAI,KAAK,IAAI,KAAK,KAAK,MAAO,EAAG,SAAS,IAAS,GAAG,CAAC,IAAI;GAC/E,EAAW;IAAE,QAAQ,EAAG;IAAQ;IAAO;GAAI,CAAC;EAC9C,GAEA,EAAI,eAAe;GACjB,IAAM,IAAM,EAAI,gBAAgB,IAC5B,IAAkB;GACtB,IAAI;IACF,IAAS,IAAM,KAAK,MAAM,CAAG,IAAI;GACnC,QAAQ,CAER;GACA,IAAI,EAAI,UAAU,OAAO,EAAI,SAAS,KAAK;IACzC,IAAM,IAAM,EAAsB,CAAM;IACxC,IAAI,CAAC,KAAO,CAAC,EAAI,IAAI;KACnB,EAAO,IAAI,EAAY,EAAI,QAAQ,gBAAgB,4BAA4B,CAAC;KAChF;IACF;IACA,EAAQ,CAAG;IACX;GACF;GACA,IAAM,IAAO,GACP,IAAO,GAAM,OAAO,QAAQ,iBAC5B,IAAU,GAAM,OAAO,WAAW,kBAAkB,EAAI,OAAO;GACrE,EAAO,IAAI,EAAY,EAAI,QAAQ,GAAM,CAAO,CAAC;EACnD,GAEA,EAAI,gBAAgB;GAClB,EAAO,IAAI,EAAY,GAAG,iBAAiB,6BAA6B,CAAC;EAC3E,GACA,EAAI,gBAAgB;GAClB,EAAO,IAAI,EAAY,GAAG,WAAW,gBAAgB,CAAC;EACxD,GAEI,GAAQ;GACV,IAAI,EAAO,SAAS;IAElB,AADA,EAAI,MAAM,GACV,EAAO,IAAI,EAAY,GAAG,WAAW,gBAAgB,CAAC;IACtD;GACF;GACA,EAAO,iBAAiB,eAAe,EAAI,MAAM,GAAG,EAAE,MAAM,GAAK,CAAC;EACpE;EAEA,IAAM,IAAK,IAAI,SAAS;EAExB,AADA,EAAG,OAAO,QAAQ,GAAM,EAAK,IAAI,GACjC,EAAI,KAAK,CAAE;CACb,CAAC;AACH;AAEA,SAAS,GAAU,GAAqB,GAAoB,GAA2B;CACrF,IAAM,IAAK,IAAI,gBAAgB;CAC/B,AAAI,KAAc,MAChB,EAAG,IAAI,eAAe,CAAU,GAChC,EAAG,IAAI,cAAc,CAAS,GAC1B,KAAU,EAAG,IAAI,aAAa,CAAQ;CAE5C,IAAM,IAAS,EAAG,SAAS;CAC3B,OAAO,IAAS,qBAAqB,MAAW;AAClD;AAEA,SAAS,EAAsB,GAAuC;CAWpE,OAVK,EAAS,CAAO,IACjB,EAAS,EAAQ,MAAM,IAClB,EAAsB,EAAQ,MAAM,IAEzC,EAAS,EAAQ,IAAI,IAChB,GAAqB,EAAQ,IAAI,IAEtC,OAAO,EAAQ,MAAO,WACjB,IAEF,OAVwB;AAWjC;AAEA,SAAS,GAAqB,GAAuD;CACnF,IAAM,IAAK,EAAS,EAAQ,OAAO;CAGnC,OAFK,IAEE;EACL;EACA,UAHe,EAAS,EAAQ,iBAAiB,KAAK,EAAS,EAAQ,mBAAmB,KAAK;EAI/F,cAAc,EAAS,EAAQ,YAAY,KAAK;EAChD,YAAY,GAAS,EAAQ,UAAU,KAAK;EAC5C,QAAQ,EAAS,EAAQ,MAAM,KAAK;EACpC,OAAO,GAAU,EAAQ,KAAK;CAChC,IATgB;AAUlB;AAEA,SAAS,EAAS,GAAkD;CAClE,OAAO,OAAO,KAAU,cAAY;AACtC;AAEA,SAAS,EAAS,GAAoC;CACpD,OAAO,OAAO,KAAU,WAAW,IAAQ,KAAA;AAC7C;AAEA,SAAS,GAAS,GAAoC;CACpD,OAAO,OAAO,KAAU,WAAW,IAAQ,KAAA;AAC7C;AAEA,SAAS,GAAU,GAAqC;CACtD,OAAO,OAAO,KAAU,YAAY,IAAQ,KAAA;AAC9C;AAMA,eAAsB,GAAY,GAA+B;CAC/D,MAAM,EAAY,iBAAiB,EAAE,SAAS,EAAO,CAAC;AACxD;AAMA,SAAgB,GACd,GACA,GACA,GAC0C;CAC1C,IAAI,KAAY,EAAK,OAAO,GAC1B,OAAO;EACL,MAAM;EACN,SAAS,WAAW,EAAY,EAAK,IAAI,EAAE,QAAQ,EAAY,CAAQ;CACzE;CAEF,IAAI,KAAW,EAAQ,SAAS,GAAG;EACjC,IAAM,IAAK,EAAK,QAAQ;EAExB,IAAI,CADO,EAAQ,MAAM,MAAQ,GAAY,GAAK,CAAE,CAC/C,GACH,OAAO;GACL,MAAM;GACN,SAAS,aAAa,EAAG;EAC3B;CAEJ;CACA,OAAO;AACT;AAEA,SAAS,GAAY,GAAiB,GAAqB;CACzD,IAAI,MAAY,SAAS,MAAY,KAAK,OAAO;CACjD,IAAI,EAAQ,SAAS,IAAI,GAAG;EAC1B,IAAM,IAAS,EAAQ,MAAM,GAAG,EAAQ,SAAS,CAAC;EAClD,OAAO,EAAG,WAAW,CAAM;CAC7B;CACA,OAAO,MAAY;AACrB;AAEA,SAAgB,EAAY,GAAmB;CAC7C,IAAI,IAAI,MAAM,OAAO,GAAG,EAAE;CAC1B,IAAM,IAAQ;EAAC;EAAM;EAAM;CAAI,GAC3B,IAAI,IAAI,MACR,IAAI;CACR,OAAO,KAAK,QAAQ,IAAI,EAAM,SAAS,IAErC,AADA,KAAK,MACL;CAEF,OAAO,GAAG,EAAE,QAAQ,KAAK,KAAK,IAAI,CAAC,EAAE,GAAG,EAAM;AAChD;;;AC1QA,IAAM,KAAyB,CAAC,aAAa,WAAW;AAExD,SAAS,KAA6B;CAGpC,OAFI,OAAO,YAAc,MAAoB,CAAC,IAAI,KAClC,UAAU,WAAW,SAAS,UAAU,YAAY,CAAC,UAAU,QAAQ,GACxE,QAAQ,MAA2B,OAAO,KAAU,YAAY,EAAM,SAAS,CAAC;AACjG;AAEA,SAAS,GAAqB,GAA+B,GAAuC;CAClG,KAAK,IAAM,KAAU,GAAS;EAC5B,IAAI,EAAM,IAAS,OAAO,EAAM;EAChC,IAAM,IAAO,EAAO,MAAM,GAAG,EAAE;EAC/B,IAAI,KAAQ,EAAM,IAAO,OAAO,EAAM;CACxC;CACA,KAAK,IAAM,KAAS,OAAO,OAAO,CAAK,GACrC,IAAI,OAAO,KAAU,YAAY,EAAM,SAAS,GAAG,OAAO;AAG9D;AAEA,SAAgB,GACd,GACA,GACA,GACQ;CACR,IAAI,GAAK,OAAO;CAEhB,IAAM,IADO,GAAM,UACD;CAClB,IAAI,OAAO,KAAQ,YAAY,EAAI,SAAS,GAAG,OAAO;CACtD,IAAI,KAAO,OAAO,KAAQ,YAAY,CAAC,MAAM,QAAQ,CAAG,GAAG;EAMzD,IAAM,IAAW,GALC,OAAO,YACvB,OAAO,QAAQ,CAAG,EAAE,QACjB,MAAqC,EAAM,GAAG,SAAS,KAAK,OAAO,EAAM,MAAO,QACnF,CAEoC,GAAW,GAAiB,CAAC;EACnE,IAAI,GAAU,OAAO;CACvB;CACA,OAAO;AACT;AAQA,IAAM,KAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,SAAgB,GAAe,GAA6B;CAC1D,IAAM,IAAK,EAAK,cAAc,YAAY,EAAE,MAAM,GAAG,EAAE,IAAI,KAAK,KAAK;CACrE,OAAO,GAAkB,IAAI,CAAE;AACjC;AAEA,SAAgB,GAAe,GAAwC;CACrE,OAAO,EAAK,OAAO,EAAK;AAC1B;AAEA,SAAgB,GAAgB,GAAwC;CAEtE,OADI,EAAK,MAAY,GAAgB,EAAK,KAAK,YAAY,IACpD,EAAK;AACd;AAEA,SAAgB,GACd,GACA,GACoB;CACpB,IAAI,CAAC,GAAM;CACX,IAAM,IAAc,EAAK,eAAe,CAAC;CACzC,IAAI,KAA0B,EAAY,IACxC,OAAO,EAAY;CAErB,KAAK,IAAM,KAAO,IAChB,IAAI,EAAY,IAAM,OAAO,EAAY;CAE3C,OAAO,EAAK,OAAO,EAAK;AAC1B;AAQA,SAAgB,GACd,GACA,GACkB;CAClB,IAAM,IAAc,GAAM,eAAe,CAAC;CAC1C,IAAI,KAA0B,EAAY,IACxC,OAAO,EAAE,YAAY,EAAuB;CAE9C,KAAK,IAAM,KAAO,IAChB,IAAI,EAAY,IAAM,OAAO,EAAE,YAAY,EAAI;CAEjD,OAAO;AACT;AAEA,SAAS,GAAgB,GAAa,GAAmC;CACvE,IAAI;EACF,IAAM,IAAS,IAAI,IAAI,GAAK,OAAO,SAAW,MAAc,OAAO,SAAS,SAAS,kBAAkB;EAGvG,OAFA,EAAO,aAAa,IAAI,eAAe,CAAW,GAC9C,gBAAgB,KAAK,CAAG,IAAU,EAAO,SAAS,IAC/C,GAAG,EAAO,WAAW,EAAO,SAAS,EAAO;CACrD,QAAQ;EAEN,OAAO,GAAG,IADQ,EAAI,SAAS,GAAG,IAAI,MAAM,IAClB,cAAc;CAC1C;AACF;AAEA,SAAgB,GAAe,GAA8C;CAC3E,IAAM,IAAO,GAAM,UAAkD;CACrE,IAAI,CAAC,KAAO,OAAO,KAAQ,YAAY,MAAM,QAAQ,CAAG,GAAG,OAAO;CAClE,IAAM,IAAY,GACZ,IAAI,OAAO,EAAU,KAAM,WAAW,EAAU,IAAI,KACpD,IAAI,OAAO,EAAU,KAAM,WAAW,EAAU,IAAI;CAG1D,OAFI,CAAC,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,KACzC,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,IAAU,OACtC;EAAE;EAAG;CAAE;AAChB;AAEA,SAAgB,GAAyB,GAA8C;CAChF,OACL,OAAO,GAAG,EAAM,IAAI,IAAI,IAAI,EAAM,IAAI,IAAI;AAC5C;AAEA,SAAgB,GAAe,GAAoB,GAAiC;CAClF,OAAO;EACL,GAAG;EACH,UAAU;GACR,GAAI,EAAK,YAAY,CAAC;GACtB,aAAa;EACf;CACF;AACF;;;ACzGA,IAAM,KAAwB,EAAiC;CAP7D,gBAAgB,CAAC;CACjB,kBAAkB,CAAC;CACnB,WAAW;EAAE,OAAO;CAAO;CAC3B,gBAAgB,YAAY,CAAC;CAC7B,gBAAgB,CAAC;AAG4C,CAAO;AAEtE,SAAgB,GAAe,EAAE,eAAqC;CACpE,IAAM,IAAY,kBAAgC,IAAI,IAAI,CAAC,GAErD,IAAM,SAAkC;EAC5C,cAAc,GAAO;GACnB,EAAU,QAAQ,IAAI,EAAM,QAAQ,CAAK;EAC3C;EACA,gBAAgB,GAAQ;GACtB,EAAU,QAAQ,OAAO,CAAM;EACjC;EACA,SAAS,GAAQ;GACf,OAAO,EAAU,QAAQ,IAAI,CAAM;EACrC;EACA,cAAc,GAAQ;GACpB,EAAU,QAAQ,OAAO,CAAM;EACjC;EACA,MAAM,iBAAiB;GACrB,IAAM,IAAU,MAAM,KAAK,EAAU,QAAQ,OAAO,CAAC;GAIrD,AAHA,EAAU,QAAQ,MAAM,GAGxB,MAAM,QAAQ,WAAW,EAAQ,KAAK,MAAM,QAAQ,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;EAC3E;CACF,IAAI,CAAC,CAAC;CAEN,OACE,kBAAC,GAAsB,UAAvB;EAAgC,OAAO;EACpC;CAC6B,CAAA;AAEpC;AAEA,SAAgB,KAAuC;CACrD,OAAO,EAAW,EAAqB;AACzC;AAIA,SAAgB,KAAuB;CACrC,IAAM,IAAQ,GAAkB;CAgBhC,OAAO;EAAE,UAfQ,GACd,GAAgB,MAAwC;GACvD,EAAM,cAAc;IAAE;IAAQ;GAAQ,CAAC;EACzC,GACA,CAAC,CAAK,CAWC;EAAU,YATA,GAAa,MAAmB;GACjD,EAAM,gBAAgB,CAAM;EAC9B,GAAG,CAAC,CAAK,CAOU;EAAY,eANT,GAAa,MAAmB;GACpD,EAAM,cAAc,CAAM;EAC5B,GAAG,CAAC,CAAK,CAIsB;EAAe,SAH9B,GAAa,MACpB,EAAM,SAAS,CAAM,GAC3B,CAAC,CAAK,CACqC;CAAQ;AACxD;;;ACvEA,SAAS,GAAa,GAAmC;CACvD,OAAO,OAAO,KAAY,WACtB,cAAc,EAAQ,eACtB;AACN;AAGA,SAAS,GAAa,GAA+B;CAKnD,OAJI,EAAI,WAAW,aAAa,IACvB,EAAE,YAAY,EAAI,MAAM,EAAoB,EAAE,IAGhD,MAAQ,aAAa,aAAa;AAC3C;AAsBA,SAAgB,GACd,GACA,GACA,GACoB;CACpB,IAAM,IAAa,GAAa,CAAO,GAEjC,CAAC,GAAU,KAAe,QAC9B,EAAiB,IAAI,KAAA,IAAY,CACnC;CAkEA,OAzDA,QAAgB;EACV,EAAiB,KACrB,EAAY,CAAS;CACvB,GAAG,CAAC,CAAS,CAAC,GAEd,QAAgB;EACd,IAAI,CAAC,EAAiB,GAAG;EAGzB,IAAI,CAAC,GAAQ;GACX,EAAY,KAAA,CAAS;GACrB;EACF;EAEA,IAAI,IAA2B,MAC3B,IAAY,IACV,IAAa,IAAI,gBAAgB;EAgCvC,OA7BA,EAAY,KAAA,CAAS,GAErB,EAAoB,GAAQ,GAAa,CAAU,GAAG,EAAW,MAAM,EACpE,MAAM,MAAsB;GAC3B,IAAI,GAAW;IAGb,AAAI,EAAI,SAAS,UAAQ,IAAI,gBAAgB,EAAI,GAAG;IACpD;GACF;GAEA,AADI,EAAI,SAAS,WAAQ,IAAY,EAAI,MACzC,EAAY,EAAI,GAAG;EACrB,CAAC,EACA,OAAO,MAAiB;GACnB,QAGJ;QAAI,aAAe,GAAwB;KACzC,EAAY,KAAA,CAAS;KACrB;IACF;IAII,aAAe,gBAAgB,EAAI,SAAS,iBAChD,QAAQ,MAAM,iDAAiD,CAAG,GAClE,EAAY,KAAA,CAAS;GANrB;EAOF,CAAC,SAEU;GAKX,AAJA,IAAY,IACZ,EAAW,MAAM,GAGb,KAAW,IAAI,gBAAgB,CAAS;EAC9C;CACF,GAAG,CAAC,GAAQ,CAAU,CAAC,GAEhB;AACT;AAmBA,SAAgB,KAMd;CAGA,IAAM,IAAmB,EAAiB,CAAC,CAAC;CAmC5C,OAjCA,cACe;EACX,KAAK,IAAM,KAAO,EAAiB,SAAS,IAAI,gBAAgB,CAAG;EACnE,EAAiB,UAAU,CAAC;CAC9B,GACC,CAAC,CAAC,GA4BE,EAAE,MA1BI,GAET,GACA,GACA,MACS;EAET,IAAI,CAAC,EAAiB,GAAG;GACvB,AAAI,KAAW,OAAO,KAAK,GAAW,UAAU,UAAU;GAC1D;EACF;EACK,KAEL,EAAoB,GAAQ,CAAO,EAChC,MAAM,MAAsB;GAE3B,AADI,EAAI,SAAS,UAAQ,EAAiB,QAAQ,KAAK,EAAI,GAAG,GAC9D,OAAO,KAAK,EAAI,KAAK,UAAU,UAAU;EAC3C,CAAC,EACA,OAAO,MAAiB;GACnB,aAAe,KACnB,QAAQ,MAAM,uCAAuC,CAAG;EAC1D,CAAC;CACL,GACA,CAAC,CAGM,EAAK;AAChB;;;AChNA,SAAgB,EAAe,GAAuC;CACpE,OAAO,OAAO,KAAU,cAAY,KAAkB,OAAQ,EAAuB,MAAO;AAC9F;AAEA,SAAgB,GAAoB,GAA4C;CAG9E,OAFK,IACD,MAAM,QAAQ,CAAK,IAAU,EAAM,KAAK,CAAc,KAAK,OACxD,EAAe,CAAK,IAAI,IAAQ,OAFpB;AAGrB;AAEA,SAAgB,GAAmB,GAAuC;CAGxE,OAFK,IACD,MAAM,QAAQ,CAAK,IAAU,EAAM,OAAO,CAAc,IACrD,EAAe,CAAK,IAAI,CAAC,CAAK,IAAI,CAAC,IAFvB,CAAC;AAGtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@declarion/react",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "React SDK for Declarion, the schema-driven business apps platform.",