@ai-matrx/media 0.5.11 → 0.6.0

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/types.ts"],"sourcesContent":["/**\n * @ai-matrx/media — root entry: the port types only.\n *\n * Pure module (no \"use client\" banner, no React import at runtime): a\n * MediaClient implementation (e.g. `@ai-matrx/data/files`, or a host's\n * strangler wiring over its existing file handler) depends on this entry\n * alone. Hooks live in `./core`, DOM components in `./react`.\n */\n\nexport * from \"./types\";\n","/**\n * @ai-matrx/media — the root types: the MediaClient port and the host ports.\n *\n * The package renders Matrx durable media; it NEVER talks to a network.\n * Everything data-shaped arrives through the injected {@link MediaClient}\n * (canonically implemented by `@ai-matrx/data/files`; any structural\n * implementation works — the strangler step wires it over an app's existing\n * file handler). The five file-handling laws are enforced here at the type\n * level:\n *\n * 1. Never fetch a media URL yourself — the package contains zero network\n * primitives; bytes exist only via {@link MediaClient.getBlob}.\n * 2. Off-boundary code resolves media itself — every component takes a\n * {@link MediaRefLike} and calls `resolve()` unconditionally. There is\n * NO `src` prop on any component in this package.\n * 3. A signed URL is a handoff, never an identity — the only string that\n * reaches a media element is the branded {@link DurableSrc}, which only\n * a MediaClient implementation can mint (via {@link mintDurableSrc}).\n * 4. Public-facing media is persisted public, never signed —\n * {@link MediaClient.shareableUrl} fails closed (`null` → the action\n * renders disabled).\n * 5. One URL contract; the engine builds URLs — zero URL construction in\n * this package; `resolve()` is the only door.\n */\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/** The canonical durable media reference. `file_id` is the identity. */\nexport interface MediaRef {\n file_id?: string | null;\n url?: string | null;\n mime_type?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n/**\n * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a\n * signed URL (the client's `resolve()` classifies and screams on one).\n */\nexport type MediaRefLike = MediaRef | string;\n\ndeclare const durable: unique symbol;\n\n/**\n * A URL that has passed through `MediaClient.resolve()` — the only string\n * type any media element in this package will bind. A caller cannot pass an\n * expiring URL through the package's types without an explicit unsafe cast.\n */\nexport type DurableSrc = string & { readonly [durable]: true };\n\n/**\n * Brand a URL as durable. FOR MediaClient IMPLEMENTATIONS ONLY — the\n * implementation must classify the URL first (the ONE signed-URL classifier\n * lives with the platform's files layer; never re-implement it here or in a\n * host). Calling this on an unclassified string defeats law 3.\n */\nexport function mintDurableSrc(url: string): DurableSrc {\n return url as DurableSrc;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";\n\nexport interface MediaResolution {\n src: DurableSrc;\n /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */\n headers?: Record<string, string> | undefined;\n mimeType?: string | undefined;\n kind: MediaKind;\n /**\n * How pixels must travel. `\"element\"` (default): bind `src` directly to the\n * media element. `\"blob\"`: the bytes require bearer auth the element cannot\n * attach — consumers go through {@link MediaClient.getBlob} and render the\n * returned object URL (the private-image lane).\n */\n transport?: \"element\" | \"blob\" | undefined;\n /**\n * A server-rendered thumbnail for this file, when the client knows one\n * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the\n * thumbnail tier machine.\n */\n thumbnailSrc?: DurableSrc | undefined;\n /**\n * True when this is a permanent public CDN URL — lets a host-supplied\n * `ImageComponent` (e.g. a next/image adapter) take the render, and marks\n * the URL CORS-safe for element-level `crossOrigin`.\n */\n isCdn?: boolean | undefined;\n /**\n * Whether a load error can be recovered by the client's ONE retry policy\n * (session-refresh → same-URL retry). False for foreign/external URLs —\n * a session refresh can't resurrect someone else's link rot. Default true.\n */\n recoverable?: boolean | undefined;\n}\n\n/** Typed unavailable states — render-as-state, never thrown as incidents. */\nexport type MediaUnavailableReason =\n | \"access_denied\"\n | \"not_found\"\n | \"deleted\"\n | \"unknown\";\n\n// ---------------------------------------------------------------------------\n// Diagnostics — the terminal-failure capture contract (the 2026-08-30 outage\n// class: every private image failed, the honest unavailable panel rendered,\n// and NOTHING reached the host's error system because the package had no\n// diagnostics port). Per C22 the capture logic lives IN the package; a host\n// binds only a sink. Absent sink → the package console.errors the same\n// structured payload once per failure — a media failure is NEVER silent.\n// ---------------------------------------------------------------------------\n\n/** Where in the pipeline a media failure surfaced. */\nexport type MediaFailurePhase =\n /** `MediaClient.resolve()` refused the ref (threw). */\n | \"resolve\"\n /** The client's session bootstrap/mint failed (`recoverLoadError` rejected),\n * or the server reported an ephemeral cookie secret (warning). */\n | \"session\"\n /** `MediaClient.getBlob()` rejected — private-pixels bytes never arrived. */\n | \"blob\"\n /** The media element errored and recovery was exhausted or impossible. */\n | \"element-load\"\n /** The client's recovery machinery declined to retry on the first attempt. */\n | \"recovery\";\n\n/** What the ONE retry policy did before the failure went terminal. */\nexport type MediaRetryOutcome =\n /** Recovery was impossible (unrecoverable source) or was never reached. */\n | \"not-attempted\"\n /** The client granted a session-refresh retry and the retry ALSO failed. */\n | \"retried\"\n /** The recovery call itself threw/rejected. */\n | \"recovery-failed\";\n\n/**\n * The strictly-typed structural payload every terminal media failure emits.\n * `mediaRef` is DURABLE IDENTITY ONLY — a `file:<id>` / query-stripped URL\n * key, NEVER a signed URL (signatures live in query strings, which\n * {@link failureMediaIdentity} removes).\n */\nexport interface MediaFailureInfo {\n source: \"media\";\n phase: MediaFailurePhase;\n /** File id / ref key identity — never a signed URL. */\n mediaRef: string;\n /** HTTP status when known (e.g. from a structured client error). */\n status?: number | undefined;\n /** Outcome of the client's ONE retry policy, when the phase reached it. */\n retryOutcome?: MediaRetryOutcome | undefined;\n /** True for a dead render; false for a captured warning (ephemeral secret). */\n terminal: boolean;\n message: string;\n}\n\n/**\n * The host's error-capture sink. Bind `capture` to the app's error system\n * (matrx-frontend: `captureError` in lib/diagnostics/errorCaptureStore).\n * When absent, the package console.errors the same payload once per failure\n * (the loud default) — it never downgrades to silence.\n */\nexport interface MediaDiagnosticsPort {\n capture?: ((info: MediaFailureInfo) => void) | undefined;\n}\n\n/** String verdicts of the ONE retry policy. */\nexport type MediaRecoveryVerdict = \"retry\" | \"terminal\";\n\n/**\n * Structured `recoverLoadError` result — optional; the bare string verdict\n * remains fully supported. Lets the client surface what it learned during the\n * session bootstrap without a second channel:\n *\n * - `status` — HTTP status of the failed mint/probe, forwarded into the\n * terminal {@link MediaFailureInfo};\n * - `secretSource` — the `/files/session` mint response's `secret_source`\n * when the server sends one (a matrx-connect field). `\"ephemeral\"` means\n * the server signs cookies with a boot-generated secret: private renders\n * WILL break across restarts/instances, and the package captures a\n * non-terminal warning immediately instead of waiting for the outage.\n */\nexport interface MediaRecoveryReport {\n verdict: MediaRecoveryVerdict;\n status?: number | undefined;\n secretSource?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Bytes / upload\n// ---------------------------------------------------------------------------\n\nexport interface MediaBlobHandle {\n /** `blob:` object URL safe for any browser API. */\n url: string;\n blob: Blob;\n /** Release the handle. Implementations backed by a shared cache may no-op. */\n release(): void;\n}\n\nexport interface MediaUploadOptions {\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n fileName?: string | undefined;\n mimeType?: string | undefined;\n /** Host visibility vocabulary passes through structurally. */\n visibility?: string | undefined;\n parentFolderId?: string | null | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/** Upload → durable identity. `fileId` is required at compile time and runtime. */\nexport interface MediaUploadResult {\n fileId: string;\n ref: MediaRef;\n}\n\nexport interface MediaMultiUploadResult {\n /** File ids for every successful upload. */\n uploaded: string[];\n /** Per-file failure with the real backend error message. */\n failed: Array<{ name: string; error: string }>;\n /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */\n cancelled: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// THE port — one structural interface, owned by this package, implemented by\n// the data layer (canonically `@ai-matrx/data/files`).\n// ---------------------------------------------------------------------------\n\nexport interface MediaClient {\n /**\n * Synchronous durable-URL build + classification. Returns `null` when the\n * ref carries no usable identity (components render their fallback).\n * THROWS (and screams via its own diagnostics sink) on a signed/expiring\n * URL — the D108 guard lives behind this door.\n */\n resolve(ref: MediaRefLike): MediaResolution | null;\n /** Bytes for canvas/crossOrigin/private-pixel consumers. */\n getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;\n /**\n * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,\n * second failure terminal. Called by the package's onError path, never\n * re-implemented.\n */\n recoverLoadError(\n src: DurableSrc,\n attempt: number,\n ): Promise<MediaRecoveryVerdict | MediaRecoveryReport>;\n /**\n * Optional object-URL recovery policy for private blob-backed media.\n * The client decides whether the durable ref should be re-read; older\n * clients may omit this and fail terminally.\n */\n recoverBlobLoadError?(\n ref: MediaRefLike,\n attempt: number,\n ): Promise<MediaRecoveryVerdict>;\n /** Upload → durable identity. */\n upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;\n /**\n * Optional batch upload with host pre-flight (dedup dialogs etc.). When\n * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.\n */\n uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;\n /** Share affordances — fails closed (null), never emits a signed URL. */\n shareableUrl(ref: MediaRefLike): Promise<string | null>;\n /** Typed unavailable states — render-as-state, never thrown as incidents. */\n classifyError(err: unknown): MediaUnavailableReason;\n}\n\n// ---------------------------------------------------------------------------\n// Host ports — all optional, all defaulting to inert.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural component slot. `(props) => unknown` keeps the root entry free\n * of React type imports; any React function component satisfies it.\n */\nexport type MediaSlotComponent<P> = (props: P) => unknown;\n\nexport interface MediaImageComponentProps {\n src: string;\n alt: string;\n width: number;\n height: number;\n className?: string | undefined;\n onError?: ((event: unknown) => void) | undefined;\n onClick?: ((event: unknown) => void) | undefined;\n onKeyDown?: ((event: unknown) => void) | undefined;\n role?: string | undefined;\n tabIndex?: number | undefined;\n}\n\nexport interface PlaybackSessionRegistration {\n isPlaying: boolean;\n source: string;\n label: string;\n trackKey?: string | undefined;\n}\n\n/**\n * The host's audio system (exclusive-playback lock, output-sink routing).\n * Both members are HOOKS — the package calls them unconditionally on every\n * render of a media element, so implementations must obey the Rules of Hooks\n * and the port object must be referentially stable.\n */\nexport interface PlaybackSessionPort {\n /**\n * Returns a ref callback for the underlying media element (routes audio\n * output / setSinkId on hosts that support it) and forwards to the caller's\n * own element ref.\n */\n useMediaElementSink(\n forward:\n | ((node: unknown) => void)\n | { current: unknown }\n | null\n | undefined,\n ): (node: unknown) => void;\n /** Joins the host's unified playback session for the element. */\n usePlaybackSession(\n registration: PlaybackSessionRegistration,\n elementRef: { current: unknown },\n ): void;\n}\n\n// ---------------------------------------------------------------------------\n// THE SHARE MANDATE (C19) — the complete headless media-actions contract.\n//\n// v0.1 ships this contract plus toolbar/lightbox shells wired to it, typed so\n// the associations/sharing package can plug in WITHOUT a media release: it\n// implements MediaActionsPort (handlers and/or the SharePopover body) and the\n// host injects it through MediaHostPorts. Nothing here may change shape in a\n// breaking way for that wave.\n// ---------------------------------------------------------------------------\n\nexport type MediaActionKind = \"share\" | \"download\" | \"copy\" | \"open\";\n\nexport interface MediaActionContext {\n ref: MediaRefLike;\n resolution: MediaResolution | null;\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaSharePopoverProps {\n context: MediaActionContext;\n onClose: () => void;\n}\n\nexport type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;\n\nexport interface MediaActionsPort {\n /**\n * Quick-share handler (copy a share link, open the native share sheet…).\n * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /\n * durable identities — never a signed URL (law 4).\n */\n share?: MediaActionHandler | undefined;\n download?: MediaActionHandler | undefined;\n copy?: MediaActionHandler | undefined;\n open?: MediaActionHandler | undefined;\n /**\n * Rich share UI body — the slot the associations package fills. When\n * present, the shells open it instead of (or on top of) the `share`\n * handler. Receives {@link MediaSharePopoverProps}.\n */\n SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;\n}\n\nexport interface MediaHostPorts {\n playbackSession?: PlaybackSessionPort | undefined;\n actions?: MediaActionsPort | undefined;\n /** Web host may pass a next/image adapter; default is a plain `<img>`. */\n ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;\n /**\n * Error-capture sink for terminal media failures (and the ephemeral-secret\n * warning). Bind to the host's error system; absent → loud console.error\n * default. See {@link MediaDiagnosticsPort}.\n */\n diagnostics?: MediaDiagnosticsPort | undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC0DO,SAAS,eAAe,KAAyB;AACtD,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts"],"sourcesContent":["/**\n * @ai-matrx/media — root entry: the port types only.\n *\n * Pure module (no \"use client\" banner, no React import at runtime): a\n * MediaClient implementation (e.g. `@ai-matrx/data/files`, or a host's\n * strangler wiring over its existing file handler) depends on this entry\n * alone. Hooks live in `./core`, DOM components in `./react`.\n */\n\nexport * from \"./types\";\n","/**\n * @ai-matrx/media — the root types: the MediaClient port and the host ports.\n *\n * The package renders Matrx durable media; it NEVER talks to a network.\n * Everything data-shaped arrives through the injected {@link MediaClient}\n * (canonically implemented by `@ai-matrx/data/files`; any structural\n * implementation works — the strangler step wires it over an app's existing\n * file handler). The five file-handling laws are enforced here at the type\n * level:\n *\n * 1. Never fetch a media URL yourself — the package contains zero network\n * primitives; bytes exist only via {@link MediaClient.getBlob}.\n * 2. Off-boundary code resolves media itself — every component takes a\n * {@link MediaRefLike} and calls `resolve()` unconditionally. There is\n * NO `src` prop on any component in this package.\n * 3. A signed URL is a handoff, never an identity — the only string that\n * reaches a media element is the branded {@link DurableSrc}, which only\n * a MediaClient implementation can mint (via {@link mintDurableSrc}).\n * 4. Public-facing media is persisted public, never signed —\n * {@link MediaClient.shareableUrl} fails closed (`null` → the action\n * renders disabled).\n * 5. One URL contract; the engine builds URLs — zero URL construction in\n * this package; `resolve()` is the only door.\n */\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/** The canonical durable media reference. `file_id` is the identity. */\nexport interface MediaRef {\n file_id?: string | null;\n url?: string | null;\n mime_type?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n/**\n * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a\n * signed URL (the client's `resolve()` classifies and screams on one).\n */\nexport type MediaRefLike = MediaRef | string;\n\ndeclare const durable: unique symbol;\n\n/**\n * A URL that has passed through `MediaClient.resolve()` — the only string\n * type any media element in this package will bind. A caller cannot pass an\n * expiring URL through the package's types without an explicit unsafe cast.\n */\nexport type DurableSrc = string & { readonly [durable]: true };\n\n/**\n * Brand a URL as durable. FOR MediaClient IMPLEMENTATIONS ONLY — the\n * implementation must classify the URL first (the ONE signed-URL classifier\n * lives with the platform's files layer; never re-implement it here or in a\n * host). Calling this on an unclassified string defeats law 3.\n */\nexport function mintDurableSrc(url: string): DurableSrc {\n return url as DurableSrc;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";\n\nexport interface MediaResolution {\n src: DurableSrc;\n /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */\n headers?: Record<string, string> | undefined;\n mimeType?: string | undefined;\n kind: MediaKind;\n /**\n * How pixels must travel. `\"element\"` (default): bind `src` directly to the\n * media element. `\"blob\"`: the bytes require bearer auth the element cannot\n * attach — consumers go through {@link MediaClient.getBlob} and render the\n * returned object URL (the private-image lane).\n */\n transport?: \"element\" | \"blob\" | undefined;\n /**\n * A server-rendered thumbnail for this file, when the client knows one\n * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the\n * thumbnail tier machine.\n */\n thumbnailSrc?: DurableSrc | undefined;\n /**\n * True when this is a permanent public CDN URL — lets a host-supplied\n * `ImageComponent` (e.g. a next/image adapter) take the render, and marks\n * the URL CORS-safe for element-level `crossOrigin`.\n */\n isCdn?: boolean | undefined;\n /**\n * Whether a load error can be recovered by the client's ONE retry policy\n * (session-refresh → same-URL retry). False for foreign/external URLs —\n * a session refresh can't resurrect someone else's link rot. Default true.\n */\n recoverable?: boolean | undefined;\n}\n\n/** Typed unavailable states — render-as-state, never thrown as incidents. */\nexport type MediaUnavailableReason =\n | \"access_denied\"\n | \"not_found\"\n | \"deleted\"\n | \"unknown\";\n\n// ---------------------------------------------------------------------------\n// Diagnostics — the terminal-failure capture contract (the 2026-08-30 outage\n// class: every private image failed, the honest unavailable panel rendered,\n// and NOTHING reached the host's error system because the package had no\n// diagnostics port). Per C22 the capture logic lives IN the package; a host\n// binds only a sink. Absent sink → the package console.errors the same\n// structured payload once per failure — a media failure is NEVER silent.\n// ---------------------------------------------------------------------------\n\n/** Where in the pipeline a media failure surfaced. */\nexport type MediaFailurePhase =\n /** `MediaClient.resolve()` refused the ref (threw). */\n | \"resolve\"\n /** The client's session bootstrap/mint failed (`recoverLoadError` rejected),\n * or the server reported an ephemeral cookie secret (warning). */\n | \"session\"\n /** `MediaClient.getBlob()` rejected — private-pixels bytes never arrived. */\n | \"blob\"\n /** The media element errored and recovery was exhausted or impossible. */\n | \"element-load\"\n /** The client's recovery machinery declined to retry on the first attempt. */\n | \"recovery\"\n /**\n * The heal ladder ran after the ONE retry failed. Non-terminal when a lane\n * healed the render (the user saw the media); terminal when every lane\n * died. Either way the row carries `attempts` + `diagnosis`.\n */\n | \"heal\";\n\n/** What the ONE retry policy did before the failure went terminal. */\nexport type MediaRetryOutcome =\n /** Recovery was impossible (unrecoverable source) or was never reached. */\n | \"not-attempted\"\n /** The client granted a session-refresh retry and the retry ALSO failed. */\n | \"retried\"\n /** The recovery call itself threw/rejected. */\n | \"recovery-failed\";\n\n/**\n * The strictly-typed structural payload every terminal media failure emits.\n * `mediaRef` is DURABLE IDENTITY ONLY — a `file:<id>` / query-stripped URL\n * key, NEVER a signed URL (signatures live in query strings, which\n * {@link failureMediaIdentity} removes).\n */\nexport interface MediaFailureInfo {\n source: \"media\";\n phase: MediaFailurePhase;\n /** File id / ref key identity — never a signed URL. */\n mediaRef: string;\n /** HTTP status when known (e.g. from a structured client error). */\n status?: number | undefined;\n /** Outcome of the client's ONE retry policy, when the phase reached it. */\n retryOutcome?: MediaRetryOutcome | undefined;\n /** True for a dead render; false for a captured warning (ephemeral secret)\n * or a HEALED render (`healed: true`). */\n terminal: boolean;\n message: string;\n /**\n * True when the heal ladder produced a renderable source after the primary\n * lane died. A healed render is NEVER silent: hosts persist it exactly like\n * a dead one so \"errors that fix themselves\" stay visible and countable.\n */\n healed?: boolean | undefined;\n /** The lane that healed the render (`MediaHealLane`). */\n healedBy?: MediaHealLane | undefined;\n /** Every lane the ladder tried, in order, with its outcome. */\n attempts?: MediaHealAttempt[] | undefined;\n /** The client's root-cause reading of the attempt pattern. */\n diagnosis?: MediaHealDiagnosis | undefined;\n}\n\n/** The lanes a render can be healed through, in ladder order. */\nexport type MediaHealLane =\n /** Re-mint the files-session cookie, retry the SAME durable URL in the element. */\n | \"session-refresh-retry\"\n /** Bearer-authenticated byte fetch on the primary files host → object URL. */\n | \"bearer-blob\"\n /** Bearer-authenticated byte fetch on the OTHER configured files host. */\n | \"bearer-blob-alternate-host\";\n\nexport interface MediaHealAttempt {\n lane: MediaHealLane;\n /** \"ok\" healed it; anything else is why it did not. */\n outcome: \"ok\" | \"http-error\" | \"network-error\" | \"unavailable\" | \"skipped\";\n /** HTTP status when the lane got an answer. */\n status?: number | undefined;\n /** Wall-clock the lane took. */\n ms: number;\n /** Which host answered (origin only — never a signed URL). */\n host?: string | undefined;\n /** Free-text detail for the row (error class, refusal reason). */\n detail?: string | undefined;\n}\n\n/**\n * What the attempt pattern says about the ROOT CAUSE. Named so a row in the\n * error catcher reads as a diagnosis, not a symptom.\n */\nexport type MediaHealDiagnosis =\n /** Cookie mint answered 200, the element still 401'd, the bearer lane\n * worked: the browser is not sending the files-session cookie cross-site\n * (third-party cookies blocked — Chrome setting / Incognito / Safari / Brave). */\n | \"cookie-not-sent-by-browser\"\n /** The cookie mint itself failed (status carried on the attempt). */\n | \"cookie-mint-failed\"\n /** The bearer token was refused (401): the sign-in is stale on this tab. */\n | \"bearer-rejected\"\n /** 403 on the bytes: this identity may not read this file. */\n | \"access-denied\"\n /** 404 on the bytes: the file row/object is gone. */\n | \"file-missing\"\n /** 5xx from the files host. */\n | \"files-host-error\"\n /** No host answered at all (DNS/network/CORS). */\n | \"files-host-unreachable\"\n /** Primary host dead, the alternate host served the bytes. */\n | \"primary-host-down-alternate-served\"\n /** Nothing in the pattern matches a known cause. */\n | \"unknown\";\n\n/** The client's heal verdict — the lanes it ran and what it concluded. */\nexport interface MediaHealResult {\n /** Renderable `blob:`/durable source when healed; null when every lane died. */\n src: string | null;\n healedBy?: MediaHealLane | undefined;\n attempts: MediaHealAttempt[];\n diagnosis: MediaHealDiagnosis;\n /** Plain-English root cause + what served the user — the log line. */\n summary: string;\n}\n\n/**\n * The host's error-capture sink. Bind `capture` to the app's error system\n * (matrx-frontend: `captureError` in lib/diagnostics/errorCaptureStore).\n * When absent, the package console.errors the same payload once per failure\n * (the loud default) — it never downgrades to silence.\n */\nexport interface MediaDiagnosticsPort {\n capture?: ((info: MediaFailureInfo) => void) | undefined;\n}\n\n/** String verdicts of the ONE retry policy. */\nexport type MediaRecoveryVerdict = \"retry\" | \"terminal\";\n\n/**\n * Structured `recoverLoadError` result — optional; the bare string verdict\n * remains fully supported. Lets the client surface what it learned during the\n * session bootstrap without a second channel:\n *\n * - `status` — HTTP status of the failed mint/probe, forwarded into the\n * terminal {@link MediaFailureInfo};\n * - `secretSource` — the `/files/session` mint response's `secret_source`\n * when the server sends one (a matrx-connect field). `\"ephemeral\"` means\n * the server signs cookies with a boot-generated secret: private renders\n * WILL break across restarts/instances, and the package captures a\n * non-terminal warning immediately instead of waiting for the outage.\n */\nexport interface MediaRecoveryReport {\n verdict: MediaRecoveryVerdict;\n status?: number | undefined;\n secretSource?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Bytes / upload\n// ---------------------------------------------------------------------------\n\nexport interface MediaBlobHandle {\n /** `blob:` object URL safe for any browser API. */\n url: string;\n blob: Blob;\n /** Release the handle. Implementations backed by a shared cache may no-op. */\n release(): void;\n}\n\nexport interface MediaUploadOptions {\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n fileName?: string | undefined;\n mimeType?: string | undefined;\n /** Host visibility vocabulary passes through structurally. */\n visibility?: string | undefined;\n parentFolderId?: string | null | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/** Upload → durable identity. `fileId` is required at compile time and runtime. */\nexport interface MediaUploadResult {\n fileId: string;\n ref: MediaRef;\n}\n\nexport interface MediaMultiUploadResult {\n /** File ids for every successful upload. */\n uploaded: string[];\n /** Per-file failure with the real backend error message. */\n failed: Array<{ name: string; error: string }>;\n /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */\n cancelled: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// THE port — one structural interface, owned by this package, implemented by\n// the data layer (canonically `@ai-matrx/data/files`).\n// ---------------------------------------------------------------------------\n\nexport interface MediaClient {\n /**\n * Synchronous durable-URL build + classification. Returns `null` when the\n * ref carries no usable identity (components render their fallback).\n * THROWS (and screams via its own diagnostics sink) on a signed/expiring\n * URL — the D108 guard lives behind this door.\n */\n resolve(ref: MediaRefLike): MediaResolution | null;\n /** Bytes for canvas/crossOrigin/private-pixel consumers. */\n getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;\n /**\n * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,\n * second failure terminal. Called by the package's onError path, never\n * re-implemented.\n */\n recoverLoadError(\n src: DurableSrc,\n attempt: number,\n ): Promise<MediaRecoveryVerdict | MediaRecoveryReport>;\n /**\n * Optional object-URL recovery policy for private blob-backed media.\n * The client decides whether the durable ref should be re-read; older\n * clients may omit this and fail terminally.\n */\n recoverBlobLoadError?(\n ref: MediaRefLike,\n attempt: number,\n ): Promise<MediaRecoveryVerdict>;\n /**\n * The heal ladder — runs AFTER the ONE retry policy is exhausted. Tries\n * every lane the client owns (bearer byte fetch on the primary host, then\n * the alternate host), records each attempt, and names the root cause. The\n * package renders `src` when present and captures the result either way:\n * a healed render is logged as loudly as a dead one. Clients that omit it\n * fall back to the terminal verdict.\n */\n healLoadError?(\n ref: MediaRefLike,\n context: { src: DurableSrc; attempts: MediaHealAttempt[] },\n ): Promise<MediaHealResult>;\n /** Upload → durable identity. */\n upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;\n /**\n * Optional batch upload with host pre-flight (dedup dialogs etc.). When\n * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.\n */\n uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;\n /** Share affordances — fails closed (null), never emits a signed URL. */\n shareableUrl(ref: MediaRefLike): Promise<string | null>;\n /** Typed unavailable states — render-as-state, never thrown as incidents. */\n classifyError(err: unknown): MediaUnavailableReason;\n}\n\n// ---------------------------------------------------------------------------\n// Host ports — all optional, all defaulting to inert.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural component slot. `(props) => unknown` keeps the root entry free\n * of React type imports; any React function component satisfies it.\n */\nexport type MediaSlotComponent<P> = (props: P) => unknown;\n\nexport interface MediaImageComponentProps {\n src: string;\n alt: string;\n width: number;\n height: number;\n className?: string | undefined;\n onError?: ((event: unknown) => void) | undefined;\n onClick?: ((event: unknown) => void) | undefined;\n onKeyDown?: ((event: unknown) => void) | undefined;\n role?: string | undefined;\n tabIndex?: number | undefined;\n}\n\nexport interface PlaybackSessionRegistration {\n isPlaying: boolean;\n source: string;\n label: string;\n trackKey?: string | undefined;\n}\n\n/**\n * The host's audio system (exclusive-playback lock, output-sink routing).\n * Both members are HOOKS — the package calls them unconditionally on every\n * render of a media element, so implementations must obey the Rules of Hooks\n * and the port object must be referentially stable.\n */\nexport interface PlaybackSessionPort {\n /**\n * Returns a ref callback for the underlying media element (routes audio\n * output / setSinkId on hosts that support it) and forwards to the caller's\n * own element ref.\n */\n useMediaElementSink(\n forward:\n | ((node: unknown) => void)\n | { current: unknown }\n | null\n | undefined,\n ): (node: unknown) => void;\n /** Joins the host's unified playback session for the element. */\n usePlaybackSession(\n registration: PlaybackSessionRegistration,\n elementRef: { current: unknown },\n ): void;\n}\n\n// ---------------------------------------------------------------------------\n// THE SHARE MANDATE (C19) — the complete headless media-actions contract.\n//\n// v0.1 ships this contract plus toolbar/lightbox shells wired to it, typed so\n// the associations/sharing package can plug in WITHOUT a media release: it\n// implements MediaActionsPort (handlers and/or the SharePopover body) and the\n// host injects it through MediaHostPorts. Nothing here may change shape in a\n// breaking way for that wave.\n// ---------------------------------------------------------------------------\n\nexport type MediaActionKind = \"share\" | \"download\" | \"copy\" | \"open\";\n\nexport interface MediaActionContext {\n ref: MediaRefLike;\n resolution: MediaResolution | null;\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaSharePopoverProps {\n context: MediaActionContext;\n onClose: () => void;\n}\n\nexport type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;\n\nexport interface MediaActionsPort {\n /**\n * Quick-share handler (copy a share link, open the native share sheet…).\n * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /\n * durable identities — never a signed URL (law 4).\n */\n share?: MediaActionHandler | undefined;\n download?: MediaActionHandler | undefined;\n copy?: MediaActionHandler | undefined;\n open?: MediaActionHandler | undefined;\n /**\n * Rich share UI body — the slot the associations package fills. When\n * present, the shells open it instead of (or on top of) the `share`\n * handler. Receives {@link MediaSharePopoverProps}.\n */\n SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;\n}\n\nexport interface MediaHostPorts {\n playbackSession?: PlaybackSessionPort | undefined;\n actions?: MediaActionsPort | undefined;\n /** Web host may pass a next/image adapter; default is a plain `<img>`. */\n ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;\n /**\n * Error-capture sink for terminal media failures (and the ephemeral-secret\n * warning). Bind to the host's error system; absent → loud console.error\n * default. See {@link MediaDiagnosticsPort}.\n */\n diagnostics?: MediaDiagnosticsPort | undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC0DO,SAAS,eAAe,KAAyB;AACtD,SAAO;AACT;","names":[]}
package/dist/index.d.cts CHANGED
@@ -97,7 +97,13 @@ type MediaFailurePhase =
97
97
  /** The media element errored and recovery was exhausted or impossible. */
98
98
  | "element-load"
99
99
  /** The client's recovery machinery declined to retry on the first attempt. */
100
- | "recovery";
100
+ | "recovery"
101
+ /**
102
+ * The heal ladder ran after the ONE retry failed. Non-terminal when a lane
103
+ * healed the render (the user saw the media); terminal when every lane
104
+ * died. Either way the row carries `attempts` + `diagnosis`.
105
+ */
106
+ | "heal";
101
107
  /** What the ONE retry policy did before the failure went terminal. */
102
108
  type MediaRetryOutcome =
103
109
  /** Recovery was impossible (unrecoverable source) or was never reached. */
@@ -121,9 +127,78 @@ interface MediaFailureInfo {
121
127
  status?: number | undefined;
122
128
  /** Outcome of the client's ONE retry policy, when the phase reached it. */
123
129
  retryOutcome?: MediaRetryOutcome | undefined;
124
- /** True for a dead render; false for a captured warning (ephemeral secret). */
130
+ /** True for a dead render; false for a captured warning (ephemeral secret)
131
+ * or a HEALED render (`healed: true`). */
125
132
  terminal: boolean;
126
133
  message: string;
134
+ /**
135
+ * True when the heal ladder produced a renderable source after the primary
136
+ * lane died. A healed render is NEVER silent: hosts persist it exactly like
137
+ * a dead one so "errors that fix themselves" stay visible and countable.
138
+ */
139
+ healed?: boolean | undefined;
140
+ /** The lane that healed the render (`MediaHealLane`). */
141
+ healedBy?: MediaHealLane | undefined;
142
+ /** Every lane the ladder tried, in order, with its outcome. */
143
+ attempts?: MediaHealAttempt[] | undefined;
144
+ /** The client's root-cause reading of the attempt pattern. */
145
+ diagnosis?: MediaHealDiagnosis | undefined;
146
+ }
147
+ /** The lanes a render can be healed through, in ladder order. */
148
+ type MediaHealLane =
149
+ /** Re-mint the files-session cookie, retry the SAME durable URL in the element. */
150
+ "session-refresh-retry"
151
+ /** Bearer-authenticated byte fetch on the primary files host → object URL. */
152
+ | "bearer-blob"
153
+ /** Bearer-authenticated byte fetch on the OTHER configured files host. */
154
+ | "bearer-blob-alternate-host";
155
+ interface MediaHealAttempt {
156
+ lane: MediaHealLane;
157
+ /** "ok" healed it; anything else is why it did not. */
158
+ outcome: "ok" | "http-error" | "network-error" | "unavailable" | "skipped";
159
+ /** HTTP status when the lane got an answer. */
160
+ status?: number | undefined;
161
+ /** Wall-clock the lane took. */
162
+ ms: number;
163
+ /** Which host answered (origin only — never a signed URL). */
164
+ host?: string | undefined;
165
+ /** Free-text detail for the row (error class, refusal reason). */
166
+ detail?: string | undefined;
167
+ }
168
+ /**
169
+ * What the attempt pattern says about the ROOT CAUSE. Named so a row in the
170
+ * error catcher reads as a diagnosis, not a symptom.
171
+ */
172
+ type MediaHealDiagnosis =
173
+ /** Cookie mint answered 200, the element still 401'd, the bearer lane
174
+ * worked: the browser is not sending the files-session cookie cross-site
175
+ * (third-party cookies blocked — Chrome setting / Incognito / Safari / Brave). */
176
+ "cookie-not-sent-by-browser"
177
+ /** The cookie mint itself failed (status carried on the attempt). */
178
+ | "cookie-mint-failed"
179
+ /** The bearer token was refused (401): the sign-in is stale on this tab. */
180
+ | "bearer-rejected"
181
+ /** 403 on the bytes: this identity may not read this file. */
182
+ | "access-denied"
183
+ /** 404 on the bytes: the file row/object is gone. */
184
+ | "file-missing"
185
+ /** 5xx from the files host. */
186
+ | "files-host-error"
187
+ /** No host answered at all (DNS/network/CORS). */
188
+ | "files-host-unreachable"
189
+ /** Primary host dead, the alternate host served the bytes. */
190
+ | "primary-host-down-alternate-served"
191
+ /** Nothing in the pattern matches a known cause. */
192
+ | "unknown";
193
+ /** The client's heal verdict — the lanes it ran and what it concluded. */
194
+ interface MediaHealResult {
195
+ /** Renderable `blob:`/durable source when healed; null when every lane died. */
196
+ src: string | null;
197
+ healedBy?: MediaHealLane | undefined;
198
+ attempts: MediaHealAttempt[];
199
+ diagnosis: MediaHealDiagnosis;
200
+ /** Plain-English root cause + what served the user — the log line. */
201
+ summary: string;
127
202
  }
128
203
  /**
129
204
  * The host's error-capture sink. Bind `capture` to the app's error system
@@ -208,6 +283,18 @@ interface MediaClient {
208
283
  * clients may omit this and fail terminally.
209
284
  */
210
285
  recoverBlobLoadError?(ref: MediaRefLike, attempt: number): Promise<MediaRecoveryVerdict>;
286
+ /**
287
+ * The heal ladder — runs AFTER the ONE retry policy is exhausted. Tries
288
+ * every lane the client owns (bearer byte fetch on the primary host, then
289
+ * the alternate host), records each attempt, and names the root cause. The
290
+ * package renders `src` when present and captures the result either way:
291
+ * a healed render is logged as loudly as a dead one. Clients that omit it
292
+ * fall back to the terminal verdict.
293
+ */
294
+ healLoadError?(ref: MediaRefLike, context: {
295
+ src: DurableSrc;
296
+ attempts: MediaHealAttempt[];
297
+ }): Promise<MediaHealResult>;
211
298
  /** Upload → durable identity. */
212
299
  upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;
213
300
  /**
@@ -305,4 +392,4 @@ interface MediaHostPorts {
305
392
  diagnostics?: MediaDiagnosticsPort | undefined;
306
393
  }
307
394
 
308
- export { type DurableSrc, type MediaActionContext, type MediaActionHandler, type MediaActionKind, type MediaActionsPort, type MediaBlobHandle, type MediaClient, type MediaDiagnosticsPort, type MediaFailureInfo, type MediaFailurePhase, type MediaHostPorts, type MediaImageComponentProps, type MediaKind, type MediaMultiUploadResult, type MediaRecoveryReport, type MediaRecoveryVerdict, type MediaRef, type MediaRefLike, type MediaResolution, type MediaRetryOutcome, type MediaSharePopoverProps, type MediaSlotComponent, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type PlaybackSessionPort, type PlaybackSessionRegistration, mintDurableSrc };
395
+ export { type DurableSrc, type MediaActionContext, type MediaActionHandler, type MediaActionKind, type MediaActionsPort, type MediaBlobHandle, type MediaClient, type MediaDiagnosticsPort, type MediaFailureInfo, type MediaFailurePhase, type MediaHealAttempt, type MediaHealDiagnosis, type MediaHealLane, type MediaHealResult, type MediaHostPorts, type MediaImageComponentProps, type MediaKind, type MediaMultiUploadResult, type MediaRecoveryReport, type MediaRecoveryVerdict, type MediaRef, type MediaRefLike, type MediaResolution, type MediaRetryOutcome, type MediaSharePopoverProps, type MediaSlotComponent, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type PlaybackSessionPort, type PlaybackSessionRegistration, mintDurableSrc };
package/dist/index.d.ts CHANGED
@@ -97,7 +97,13 @@ type MediaFailurePhase =
97
97
  /** The media element errored and recovery was exhausted or impossible. */
98
98
  | "element-load"
99
99
  /** The client's recovery machinery declined to retry on the first attempt. */
100
- | "recovery";
100
+ | "recovery"
101
+ /**
102
+ * The heal ladder ran after the ONE retry failed. Non-terminal when a lane
103
+ * healed the render (the user saw the media); terminal when every lane
104
+ * died. Either way the row carries `attempts` + `diagnosis`.
105
+ */
106
+ | "heal";
101
107
  /** What the ONE retry policy did before the failure went terminal. */
102
108
  type MediaRetryOutcome =
103
109
  /** Recovery was impossible (unrecoverable source) or was never reached. */
@@ -121,9 +127,78 @@ interface MediaFailureInfo {
121
127
  status?: number | undefined;
122
128
  /** Outcome of the client's ONE retry policy, when the phase reached it. */
123
129
  retryOutcome?: MediaRetryOutcome | undefined;
124
- /** True for a dead render; false for a captured warning (ephemeral secret). */
130
+ /** True for a dead render; false for a captured warning (ephemeral secret)
131
+ * or a HEALED render (`healed: true`). */
125
132
  terminal: boolean;
126
133
  message: string;
134
+ /**
135
+ * True when the heal ladder produced a renderable source after the primary
136
+ * lane died. A healed render is NEVER silent: hosts persist it exactly like
137
+ * a dead one so "errors that fix themselves" stay visible and countable.
138
+ */
139
+ healed?: boolean | undefined;
140
+ /** The lane that healed the render (`MediaHealLane`). */
141
+ healedBy?: MediaHealLane | undefined;
142
+ /** Every lane the ladder tried, in order, with its outcome. */
143
+ attempts?: MediaHealAttempt[] | undefined;
144
+ /** The client's root-cause reading of the attempt pattern. */
145
+ diagnosis?: MediaHealDiagnosis | undefined;
146
+ }
147
+ /** The lanes a render can be healed through, in ladder order. */
148
+ type MediaHealLane =
149
+ /** Re-mint the files-session cookie, retry the SAME durable URL in the element. */
150
+ "session-refresh-retry"
151
+ /** Bearer-authenticated byte fetch on the primary files host → object URL. */
152
+ | "bearer-blob"
153
+ /** Bearer-authenticated byte fetch on the OTHER configured files host. */
154
+ | "bearer-blob-alternate-host";
155
+ interface MediaHealAttempt {
156
+ lane: MediaHealLane;
157
+ /** "ok" healed it; anything else is why it did not. */
158
+ outcome: "ok" | "http-error" | "network-error" | "unavailable" | "skipped";
159
+ /** HTTP status when the lane got an answer. */
160
+ status?: number | undefined;
161
+ /** Wall-clock the lane took. */
162
+ ms: number;
163
+ /** Which host answered (origin only — never a signed URL). */
164
+ host?: string | undefined;
165
+ /** Free-text detail for the row (error class, refusal reason). */
166
+ detail?: string | undefined;
167
+ }
168
+ /**
169
+ * What the attempt pattern says about the ROOT CAUSE. Named so a row in the
170
+ * error catcher reads as a diagnosis, not a symptom.
171
+ */
172
+ type MediaHealDiagnosis =
173
+ /** Cookie mint answered 200, the element still 401'd, the bearer lane
174
+ * worked: the browser is not sending the files-session cookie cross-site
175
+ * (third-party cookies blocked — Chrome setting / Incognito / Safari / Brave). */
176
+ "cookie-not-sent-by-browser"
177
+ /** The cookie mint itself failed (status carried on the attempt). */
178
+ | "cookie-mint-failed"
179
+ /** The bearer token was refused (401): the sign-in is stale on this tab. */
180
+ | "bearer-rejected"
181
+ /** 403 on the bytes: this identity may not read this file. */
182
+ | "access-denied"
183
+ /** 404 on the bytes: the file row/object is gone. */
184
+ | "file-missing"
185
+ /** 5xx from the files host. */
186
+ | "files-host-error"
187
+ /** No host answered at all (DNS/network/CORS). */
188
+ | "files-host-unreachable"
189
+ /** Primary host dead, the alternate host served the bytes. */
190
+ | "primary-host-down-alternate-served"
191
+ /** Nothing in the pattern matches a known cause. */
192
+ | "unknown";
193
+ /** The client's heal verdict — the lanes it ran and what it concluded. */
194
+ interface MediaHealResult {
195
+ /** Renderable `blob:`/durable source when healed; null when every lane died. */
196
+ src: string | null;
197
+ healedBy?: MediaHealLane | undefined;
198
+ attempts: MediaHealAttempt[];
199
+ diagnosis: MediaHealDiagnosis;
200
+ /** Plain-English root cause + what served the user — the log line. */
201
+ summary: string;
127
202
  }
128
203
  /**
129
204
  * The host's error-capture sink. Bind `capture` to the app's error system
@@ -208,6 +283,18 @@ interface MediaClient {
208
283
  * clients may omit this and fail terminally.
209
284
  */
210
285
  recoverBlobLoadError?(ref: MediaRefLike, attempt: number): Promise<MediaRecoveryVerdict>;
286
+ /**
287
+ * The heal ladder — runs AFTER the ONE retry policy is exhausted. Tries
288
+ * every lane the client owns (bearer byte fetch on the primary host, then
289
+ * the alternate host), records each attempt, and names the root cause. The
290
+ * package renders `src` when present and captures the result either way:
291
+ * a healed render is logged as loudly as a dead one. Clients that omit it
292
+ * fall back to the terminal verdict.
293
+ */
294
+ healLoadError?(ref: MediaRefLike, context: {
295
+ src: DurableSrc;
296
+ attempts: MediaHealAttempt[];
297
+ }): Promise<MediaHealResult>;
211
298
  /** Upload → durable identity. */
212
299
  upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;
213
300
  /**
@@ -305,4 +392,4 @@ interface MediaHostPorts {
305
392
  diagnostics?: MediaDiagnosticsPort | undefined;
306
393
  }
307
394
 
308
- export { type DurableSrc, type MediaActionContext, type MediaActionHandler, type MediaActionKind, type MediaActionsPort, type MediaBlobHandle, type MediaClient, type MediaDiagnosticsPort, type MediaFailureInfo, type MediaFailurePhase, type MediaHostPorts, type MediaImageComponentProps, type MediaKind, type MediaMultiUploadResult, type MediaRecoveryReport, type MediaRecoveryVerdict, type MediaRef, type MediaRefLike, type MediaResolution, type MediaRetryOutcome, type MediaSharePopoverProps, type MediaSlotComponent, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type PlaybackSessionPort, type PlaybackSessionRegistration, mintDurableSrc };
395
+ export { type DurableSrc, type MediaActionContext, type MediaActionHandler, type MediaActionKind, type MediaActionsPort, type MediaBlobHandle, type MediaClient, type MediaDiagnosticsPort, type MediaFailureInfo, type MediaFailurePhase, type MediaHealAttempt, type MediaHealDiagnosis, type MediaHealLane, type MediaHealResult, type MediaHostPorts, type MediaImageComponentProps, type MediaKind, type MediaMultiUploadResult, type MediaRecoveryReport, type MediaRecoveryVerdict, type MediaRef, type MediaRefLike, type MediaResolution, type MediaRetryOutcome, type MediaSharePopoverProps, type MediaSlotComponent, type MediaUnavailableReason, type MediaUploadOptions, type MediaUploadResult, type PlaybackSessionPort, type PlaybackSessionRegistration, mintDurableSrc };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * @ai-matrx/media — the root types: the MediaClient port and the host ports.\n *\n * The package renders Matrx durable media; it NEVER talks to a network.\n * Everything data-shaped arrives through the injected {@link MediaClient}\n * (canonically implemented by `@ai-matrx/data/files`; any structural\n * implementation works — the strangler step wires it over an app's existing\n * file handler). The five file-handling laws are enforced here at the type\n * level:\n *\n * 1. Never fetch a media URL yourself — the package contains zero network\n * primitives; bytes exist only via {@link MediaClient.getBlob}.\n * 2. Off-boundary code resolves media itself — every component takes a\n * {@link MediaRefLike} and calls `resolve()` unconditionally. There is\n * NO `src` prop on any component in this package.\n * 3. A signed URL is a handoff, never an identity — the only string that\n * reaches a media element is the branded {@link DurableSrc}, which only\n * a MediaClient implementation can mint (via {@link mintDurableSrc}).\n * 4. Public-facing media is persisted public, never signed —\n * {@link MediaClient.shareableUrl} fails closed (`null` → the action\n * renders disabled).\n * 5. One URL contract; the engine builds URLs — zero URL construction in\n * this package; `resolve()` is the only door.\n */\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/** The canonical durable media reference. `file_id` is the identity. */\nexport interface MediaRef {\n file_id?: string | null;\n url?: string | null;\n mime_type?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n/**\n * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a\n * signed URL (the client's `resolve()` classifies and screams on one).\n */\nexport type MediaRefLike = MediaRef | string;\n\ndeclare const durable: unique symbol;\n\n/**\n * A URL that has passed through `MediaClient.resolve()` — the only string\n * type any media element in this package will bind. A caller cannot pass an\n * expiring URL through the package's types without an explicit unsafe cast.\n */\nexport type DurableSrc = string & { readonly [durable]: true };\n\n/**\n * Brand a URL as durable. FOR MediaClient IMPLEMENTATIONS ONLY — the\n * implementation must classify the URL first (the ONE signed-URL classifier\n * lives with the platform's files layer; never re-implement it here or in a\n * host). Calling this on an unclassified string defeats law 3.\n */\nexport function mintDurableSrc(url: string): DurableSrc {\n return url as DurableSrc;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";\n\nexport interface MediaResolution {\n src: DurableSrc;\n /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */\n headers?: Record<string, string> | undefined;\n mimeType?: string | undefined;\n kind: MediaKind;\n /**\n * How pixels must travel. `\"element\"` (default): bind `src` directly to the\n * media element. `\"blob\"`: the bytes require bearer auth the element cannot\n * attach — consumers go through {@link MediaClient.getBlob} and render the\n * returned object URL (the private-image lane).\n */\n transport?: \"element\" | \"blob\" | undefined;\n /**\n * A server-rendered thumbnail for this file, when the client knows one\n * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the\n * thumbnail tier machine.\n */\n thumbnailSrc?: DurableSrc | undefined;\n /**\n * True when this is a permanent public CDN URL — lets a host-supplied\n * `ImageComponent` (e.g. a next/image adapter) take the render, and marks\n * the URL CORS-safe for element-level `crossOrigin`.\n */\n isCdn?: boolean | undefined;\n /**\n * Whether a load error can be recovered by the client's ONE retry policy\n * (session-refresh → same-URL retry). False for foreign/external URLs —\n * a session refresh can't resurrect someone else's link rot. Default true.\n */\n recoverable?: boolean | undefined;\n}\n\n/** Typed unavailable states — render-as-state, never thrown as incidents. */\nexport type MediaUnavailableReason =\n | \"access_denied\"\n | \"not_found\"\n | \"deleted\"\n | \"unknown\";\n\n// ---------------------------------------------------------------------------\n// Diagnostics — the terminal-failure capture contract (the 2026-08-30 outage\n// class: every private image failed, the honest unavailable panel rendered,\n// and NOTHING reached the host's error system because the package had no\n// diagnostics port). Per C22 the capture logic lives IN the package; a host\n// binds only a sink. Absent sink → the package console.errors the same\n// structured payload once per failure — a media failure is NEVER silent.\n// ---------------------------------------------------------------------------\n\n/** Where in the pipeline a media failure surfaced. */\nexport type MediaFailurePhase =\n /** `MediaClient.resolve()` refused the ref (threw). */\n | \"resolve\"\n /** The client's session bootstrap/mint failed (`recoverLoadError` rejected),\n * or the server reported an ephemeral cookie secret (warning). */\n | \"session\"\n /** `MediaClient.getBlob()` rejected — private-pixels bytes never arrived. */\n | \"blob\"\n /** The media element errored and recovery was exhausted or impossible. */\n | \"element-load\"\n /** The client's recovery machinery declined to retry on the first attempt. */\n | \"recovery\";\n\n/** What the ONE retry policy did before the failure went terminal. */\nexport type MediaRetryOutcome =\n /** Recovery was impossible (unrecoverable source) or was never reached. */\n | \"not-attempted\"\n /** The client granted a session-refresh retry and the retry ALSO failed. */\n | \"retried\"\n /** The recovery call itself threw/rejected. */\n | \"recovery-failed\";\n\n/**\n * The strictly-typed structural payload every terminal media failure emits.\n * `mediaRef` is DURABLE IDENTITY ONLY — a `file:<id>` / query-stripped URL\n * key, NEVER a signed URL (signatures live in query strings, which\n * {@link failureMediaIdentity} removes).\n */\nexport interface MediaFailureInfo {\n source: \"media\";\n phase: MediaFailurePhase;\n /** File id / ref key identity — never a signed URL. */\n mediaRef: string;\n /** HTTP status when known (e.g. from a structured client error). */\n status?: number | undefined;\n /** Outcome of the client's ONE retry policy, when the phase reached it. */\n retryOutcome?: MediaRetryOutcome | undefined;\n /** True for a dead render; false for a captured warning (ephemeral secret). */\n terminal: boolean;\n message: string;\n}\n\n/**\n * The host's error-capture sink. Bind `capture` to the app's error system\n * (matrx-frontend: `captureError` in lib/diagnostics/errorCaptureStore).\n * When absent, the package console.errors the same payload once per failure\n * (the loud default) — it never downgrades to silence.\n */\nexport interface MediaDiagnosticsPort {\n capture?: ((info: MediaFailureInfo) => void) | undefined;\n}\n\n/** String verdicts of the ONE retry policy. */\nexport type MediaRecoveryVerdict = \"retry\" | \"terminal\";\n\n/**\n * Structured `recoverLoadError` result — optional; the bare string verdict\n * remains fully supported. Lets the client surface what it learned during the\n * session bootstrap without a second channel:\n *\n * - `status` — HTTP status of the failed mint/probe, forwarded into the\n * terminal {@link MediaFailureInfo};\n * - `secretSource` — the `/files/session` mint response's `secret_source`\n * when the server sends one (a matrx-connect field). `\"ephemeral\"` means\n * the server signs cookies with a boot-generated secret: private renders\n * WILL break across restarts/instances, and the package captures a\n * non-terminal warning immediately instead of waiting for the outage.\n */\nexport interface MediaRecoveryReport {\n verdict: MediaRecoveryVerdict;\n status?: number | undefined;\n secretSource?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Bytes / upload\n// ---------------------------------------------------------------------------\n\nexport interface MediaBlobHandle {\n /** `blob:` object URL safe for any browser API. */\n url: string;\n blob: Blob;\n /** Release the handle. Implementations backed by a shared cache may no-op. */\n release(): void;\n}\n\nexport interface MediaUploadOptions {\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n fileName?: string | undefined;\n mimeType?: string | undefined;\n /** Host visibility vocabulary passes through structurally. */\n visibility?: string | undefined;\n parentFolderId?: string | null | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/** Upload → durable identity. `fileId` is required at compile time and runtime. */\nexport interface MediaUploadResult {\n fileId: string;\n ref: MediaRef;\n}\n\nexport interface MediaMultiUploadResult {\n /** File ids for every successful upload. */\n uploaded: string[];\n /** Per-file failure with the real backend error message. */\n failed: Array<{ name: string; error: string }>;\n /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */\n cancelled: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// THE port — one structural interface, owned by this package, implemented by\n// the data layer (canonically `@ai-matrx/data/files`).\n// ---------------------------------------------------------------------------\n\nexport interface MediaClient {\n /**\n * Synchronous durable-URL build + classification. Returns `null` when the\n * ref carries no usable identity (components render their fallback).\n * THROWS (and screams via its own diagnostics sink) on a signed/expiring\n * URL — the D108 guard lives behind this door.\n */\n resolve(ref: MediaRefLike): MediaResolution | null;\n /** Bytes for canvas/crossOrigin/private-pixel consumers. */\n getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;\n /**\n * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,\n * second failure terminal. Called by the package's onError path, never\n * re-implemented.\n */\n recoverLoadError(\n src: DurableSrc,\n attempt: number,\n ): Promise<MediaRecoveryVerdict | MediaRecoveryReport>;\n /**\n * Optional object-URL recovery policy for private blob-backed media.\n * The client decides whether the durable ref should be re-read; older\n * clients may omit this and fail terminally.\n */\n recoverBlobLoadError?(\n ref: MediaRefLike,\n attempt: number,\n ): Promise<MediaRecoveryVerdict>;\n /** Upload → durable identity. */\n upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;\n /**\n * Optional batch upload with host pre-flight (dedup dialogs etc.). When\n * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.\n */\n uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;\n /** Share affordances — fails closed (null), never emits a signed URL. */\n shareableUrl(ref: MediaRefLike): Promise<string | null>;\n /** Typed unavailable states — render-as-state, never thrown as incidents. */\n classifyError(err: unknown): MediaUnavailableReason;\n}\n\n// ---------------------------------------------------------------------------\n// Host ports — all optional, all defaulting to inert.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural component slot. `(props) => unknown` keeps the root entry free\n * of React type imports; any React function component satisfies it.\n */\nexport type MediaSlotComponent<P> = (props: P) => unknown;\n\nexport interface MediaImageComponentProps {\n src: string;\n alt: string;\n width: number;\n height: number;\n className?: string | undefined;\n onError?: ((event: unknown) => void) | undefined;\n onClick?: ((event: unknown) => void) | undefined;\n onKeyDown?: ((event: unknown) => void) | undefined;\n role?: string | undefined;\n tabIndex?: number | undefined;\n}\n\nexport interface PlaybackSessionRegistration {\n isPlaying: boolean;\n source: string;\n label: string;\n trackKey?: string | undefined;\n}\n\n/**\n * The host's audio system (exclusive-playback lock, output-sink routing).\n * Both members are HOOKS — the package calls them unconditionally on every\n * render of a media element, so implementations must obey the Rules of Hooks\n * and the port object must be referentially stable.\n */\nexport interface PlaybackSessionPort {\n /**\n * Returns a ref callback for the underlying media element (routes audio\n * output / setSinkId on hosts that support it) and forwards to the caller's\n * own element ref.\n */\n useMediaElementSink(\n forward:\n | ((node: unknown) => void)\n | { current: unknown }\n | null\n | undefined,\n ): (node: unknown) => void;\n /** Joins the host's unified playback session for the element. */\n usePlaybackSession(\n registration: PlaybackSessionRegistration,\n elementRef: { current: unknown },\n ): void;\n}\n\n// ---------------------------------------------------------------------------\n// THE SHARE MANDATE (C19) — the complete headless media-actions contract.\n//\n// v0.1 ships this contract plus toolbar/lightbox shells wired to it, typed so\n// the associations/sharing package can plug in WITHOUT a media release: it\n// implements MediaActionsPort (handlers and/or the SharePopover body) and the\n// host injects it through MediaHostPorts. Nothing here may change shape in a\n// breaking way for that wave.\n// ---------------------------------------------------------------------------\n\nexport type MediaActionKind = \"share\" | \"download\" | \"copy\" | \"open\";\n\nexport interface MediaActionContext {\n ref: MediaRefLike;\n resolution: MediaResolution | null;\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaSharePopoverProps {\n context: MediaActionContext;\n onClose: () => void;\n}\n\nexport type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;\n\nexport interface MediaActionsPort {\n /**\n * Quick-share handler (copy a share link, open the native share sheet…).\n * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /\n * durable identities — never a signed URL (law 4).\n */\n share?: MediaActionHandler | undefined;\n download?: MediaActionHandler | undefined;\n copy?: MediaActionHandler | undefined;\n open?: MediaActionHandler | undefined;\n /**\n * Rich share UI body — the slot the associations package fills. When\n * present, the shells open it instead of (or on top of) the `share`\n * handler. Receives {@link MediaSharePopoverProps}.\n */\n SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;\n}\n\nexport interface MediaHostPorts {\n playbackSession?: PlaybackSessionPort | undefined;\n actions?: MediaActionsPort | undefined;\n /** Web host may pass a next/image adapter; default is a plain `<img>`. */\n ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;\n /**\n * Error-capture sink for terminal media failures (and the ephemeral-secret\n * warning). Bind to the host's error system; absent → loud console.error\n * default. See {@link MediaDiagnosticsPort}.\n */\n diagnostics?: MediaDiagnosticsPort | undefined;\n}\n"],"mappings":";AA0DO,SAAS,eAAe,KAAyB;AACtD,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * @ai-matrx/media — the root types: the MediaClient port and the host ports.\n *\n * The package renders Matrx durable media; it NEVER talks to a network.\n * Everything data-shaped arrives through the injected {@link MediaClient}\n * (canonically implemented by `@ai-matrx/data/files`; any structural\n * implementation works — the strangler step wires it over an app's existing\n * file handler). The five file-handling laws are enforced here at the type\n * level:\n *\n * 1. Never fetch a media URL yourself — the package contains zero network\n * primitives; bytes exist only via {@link MediaClient.getBlob}.\n * 2. Off-boundary code resolves media itself — every component takes a\n * {@link MediaRefLike} and calls `resolve()` unconditionally. There is\n * NO `src` prop on any component in this package.\n * 3. A signed URL is a handoff, never an identity — the only string that\n * reaches a media element is the branded {@link DurableSrc}, which only\n * a MediaClient implementation can mint (via {@link mintDurableSrc}).\n * 4. Public-facing media is persisted public, never signed —\n * {@link MediaClient.shareableUrl} fails closed (`null` → the action\n * renders disabled).\n * 5. One URL contract; the engine builds URLs — zero URL construction in\n * this package; `resolve()` is the only door.\n */\n\n// ---------------------------------------------------------------------------\n// Identity\n// ---------------------------------------------------------------------------\n\n/** The canonical durable media reference. `file_id` is the identity. */\nexport interface MediaRef {\n file_id?: string | null;\n url?: string | null;\n mime_type?: string | null;\n metadata?: Record<string, unknown> | null;\n}\n\n/**\n * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a\n * signed URL (the client's `resolve()` classifies and screams on one).\n */\nexport type MediaRefLike = MediaRef | string;\n\ndeclare const durable: unique symbol;\n\n/**\n * A URL that has passed through `MediaClient.resolve()` — the only string\n * type any media element in this package will bind. A caller cannot pass an\n * expiring URL through the package's types without an explicit unsafe cast.\n */\nexport type DurableSrc = string & { readonly [durable]: true };\n\n/**\n * Brand a URL as durable. FOR MediaClient IMPLEMENTATIONS ONLY — the\n * implementation must classify the URL first (the ONE signed-URL classifier\n * lives with the platform's files layer; never re-implement it here or in a\n * host). Calling this on an unclassified string defeats law 3.\n */\nexport function mintDurableSrc(url: string): DurableSrc {\n return url as DurableSrc;\n}\n\n// ---------------------------------------------------------------------------\n// Resolution\n// ---------------------------------------------------------------------------\n\nexport type MediaKind = \"image\" | \"video\" | \"audio\" | \"file\";\n\nexport interface MediaResolution {\n src: DurableSrc;\n /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */\n headers?: Record<string, string> | undefined;\n mimeType?: string | undefined;\n kind: MediaKind;\n /**\n * How pixels must travel. `\"element\"` (default): bind `src` directly to the\n * media element. `\"blob\"`: the bytes require bearer auth the element cannot\n * attach — consumers go through {@link MediaClient.getBlob} and render the\n * returned object URL (the private-image lane).\n */\n transport?: \"element\" | \"blob\" | undefined;\n /**\n * A server-rendered thumbnail for this file, when the client knows one\n * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the\n * thumbnail tier machine.\n */\n thumbnailSrc?: DurableSrc | undefined;\n /**\n * True when this is a permanent public CDN URL — lets a host-supplied\n * `ImageComponent` (e.g. a next/image adapter) take the render, and marks\n * the URL CORS-safe for element-level `crossOrigin`.\n */\n isCdn?: boolean | undefined;\n /**\n * Whether a load error can be recovered by the client's ONE retry policy\n * (session-refresh → same-URL retry). False for foreign/external URLs —\n * a session refresh can't resurrect someone else's link rot. Default true.\n */\n recoverable?: boolean | undefined;\n}\n\n/** Typed unavailable states — render-as-state, never thrown as incidents. */\nexport type MediaUnavailableReason =\n | \"access_denied\"\n | \"not_found\"\n | \"deleted\"\n | \"unknown\";\n\n// ---------------------------------------------------------------------------\n// Diagnostics — the terminal-failure capture contract (the 2026-08-30 outage\n// class: every private image failed, the honest unavailable panel rendered,\n// and NOTHING reached the host's error system because the package had no\n// diagnostics port). Per C22 the capture logic lives IN the package; a host\n// binds only a sink. Absent sink → the package console.errors the same\n// structured payload once per failure — a media failure is NEVER silent.\n// ---------------------------------------------------------------------------\n\n/** Where in the pipeline a media failure surfaced. */\nexport type MediaFailurePhase =\n /** `MediaClient.resolve()` refused the ref (threw). */\n | \"resolve\"\n /** The client's session bootstrap/mint failed (`recoverLoadError` rejected),\n * or the server reported an ephemeral cookie secret (warning). */\n | \"session\"\n /** `MediaClient.getBlob()` rejected — private-pixels bytes never arrived. */\n | \"blob\"\n /** The media element errored and recovery was exhausted or impossible. */\n | \"element-load\"\n /** The client's recovery machinery declined to retry on the first attempt. */\n | \"recovery\"\n /**\n * The heal ladder ran after the ONE retry failed. Non-terminal when a lane\n * healed the render (the user saw the media); terminal when every lane\n * died. Either way the row carries `attempts` + `diagnosis`.\n */\n | \"heal\";\n\n/** What the ONE retry policy did before the failure went terminal. */\nexport type MediaRetryOutcome =\n /** Recovery was impossible (unrecoverable source) or was never reached. */\n | \"not-attempted\"\n /** The client granted a session-refresh retry and the retry ALSO failed. */\n | \"retried\"\n /** The recovery call itself threw/rejected. */\n | \"recovery-failed\";\n\n/**\n * The strictly-typed structural payload every terminal media failure emits.\n * `mediaRef` is DURABLE IDENTITY ONLY — a `file:<id>` / query-stripped URL\n * key, NEVER a signed URL (signatures live in query strings, which\n * {@link failureMediaIdentity} removes).\n */\nexport interface MediaFailureInfo {\n source: \"media\";\n phase: MediaFailurePhase;\n /** File id / ref key identity — never a signed URL. */\n mediaRef: string;\n /** HTTP status when known (e.g. from a structured client error). */\n status?: number | undefined;\n /** Outcome of the client's ONE retry policy, when the phase reached it. */\n retryOutcome?: MediaRetryOutcome | undefined;\n /** True for a dead render; false for a captured warning (ephemeral secret)\n * or a HEALED render (`healed: true`). */\n terminal: boolean;\n message: string;\n /**\n * True when the heal ladder produced a renderable source after the primary\n * lane died. A healed render is NEVER silent: hosts persist it exactly like\n * a dead one so \"errors that fix themselves\" stay visible and countable.\n */\n healed?: boolean | undefined;\n /** The lane that healed the render (`MediaHealLane`). */\n healedBy?: MediaHealLane | undefined;\n /** Every lane the ladder tried, in order, with its outcome. */\n attempts?: MediaHealAttempt[] | undefined;\n /** The client's root-cause reading of the attempt pattern. */\n diagnosis?: MediaHealDiagnosis | undefined;\n}\n\n/** The lanes a render can be healed through, in ladder order. */\nexport type MediaHealLane =\n /** Re-mint the files-session cookie, retry the SAME durable URL in the element. */\n | \"session-refresh-retry\"\n /** Bearer-authenticated byte fetch on the primary files host → object URL. */\n | \"bearer-blob\"\n /** Bearer-authenticated byte fetch on the OTHER configured files host. */\n | \"bearer-blob-alternate-host\";\n\nexport interface MediaHealAttempt {\n lane: MediaHealLane;\n /** \"ok\" healed it; anything else is why it did not. */\n outcome: \"ok\" | \"http-error\" | \"network-error\" | \"unavailable\" | \"skipped\";\n /** HTTP status when the lane got an answer. */\n status?: number | undefined;\n /** Wall-clock the lane took. */\n ms: number;\n /** Which host answered (origin only — never a signed URL). */\n host?: string | undefined;\n /** Free-text detail for the row (error class, refusal reason). */\n detail?: string | undefined;\n}\n\n/**\n * What the attempt pattern says about the ROOT CAUSE. Named so a row in the\n * error catcher reads as a diagnosis, not a symptom.\n */\nexport type MediaHealDiagnosis =\n /** Cookie mint answered 200, the element still 401'd, the bearer lane\n * worked: the browser is not sending the files-session cookie cross-site\n * (third-party cookies blocked — Chrome setting / Incognito / Safari / Brave). */\n | \"cookie-not-sent-by-browser\"\n /** The cookie mint itself failed (status carried on the attempt). */\n | \"cookie-mint-failed\"\n /** The bearer token was refused (401): the sign-in is stale on this tab. */\n | \"bearer-rejected\"\n /** 403 on the bytes: this identity may not read this file. */\n | \"access-denied\"\n /** 404 on the bytes: the file row/object is gone. */\n | \"file-missing\"\n /** 5xx from the files host. */\n | \"files-host-error\"\n /** No host answered at all (DNS/network/CORS). */\n | \"files-host-unreachable\"\n /** Primary host dead, the alternate host served the bytes. */\n | \"primary-host-down-alternate-served\"\n /** Nothing in the pattern matches a known cause. */\n | \"unknown\";\n\n/** The client's heal verdict — the lanes it ran and what it concluded. */\nexport interface MediaHealResult {\n /** Renderable `blob:`/durable source when healed; null when every lane died. */\n src: string | null;\n healedBy?: MediaHealLane | undefined;\n attempts: MediaHealAttempt[];\n diagnosis: MediaHealDiagnosis;\n /** Plain-English root cause + what served the user — the log line. */\n summary: string;\n}\n\n/**\n * The host's error-capture sink. Bind `capture` to the app's error system\n * (matrx-frontend: `captureError` in lib/diagnostics/errorCaptureStore).\n * When absent, the package console.errors the same payload once per failure\n * (the loud default) — it never downgrades to silence.\n */\nexport interface MediaDiagnosticsPort {\n capture?: ((info: MediaFailureInfo) => void) | undefined;\n}\n\n/** String verdicts of the ONE retry policy. */\nexport type MediaRecoveryVerdict = \"retry\" | \"terminal\";\n\n/**\n * Structured `recoverLoadError` result — optional; the bare string verdict\n * remains fully supported. Lets the client surface what it learned during the\n * session bootstrap without a second channel:\n *\n * - `status` — HTTP status of the failed mint/probe, forwarded into the\n * terminal {@link MediaFailureInfo};\n * - `secretSource` — the `/files/session` mint response's `secret_source`\n * when the server sends one (a matrx-connect field). `\"ephemeral\"` means\n * the server signs cookies with a boot-generated secret: private renders\n * WILL break across restarts/instances, and the package captures a\n * non-terminal warning immediately instead of waiting for the outage.\n */\nexport interface MediaRecoveryReport {\n verdict: MediaRecoveryVerdict;\n status?: number | undefined;\n secretSource?: string | undefined;\n}\n\n// ---------------------------------------------------------------------------\n// Bytes / upload\n// ---------------------------------------------------------------------------\n\nexport interface MediaBlobHandle {\n /** `blob:` object URL safe for any browser API. */\n url: string;\n blob: Blob;\n /** Release the handle. Implementations backed by a shared cache may no-op. */\n release(): void;\n}\n\nexport interface MediaUploadOptions {\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n fileName?: string | undefined;\n mimeType?: string | undefined;\n /** Host visibility vocabulary passes through structurally. */\n visibility?: string | undefined;\n parentFolderId?: string | null | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/** Upload → durable identity. `fileId` is required at compile time and runtime. */\nexport interface MediaUploadResult {\n fileId: string;\n ref: MediaRef;\n}\n\nexport interface MediaMultiUploadResult {\n /** File ids for every successful upload. */\n uploaded: string[];\n /** Per-file failure with the real backend error message. */\n failed: Array<{ name: string; error: string }>;\n /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */\n cancelled: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// THE port — one structural interface, owned by this package, implemented by\n// the data layer (canonically `@ai-matrx/data/files`).\n// ---------------------------------------------------------------------------\n\nexport interface MediaClient {\n /**\n * Synchronous durable-URL build + classification. Returns `null` when the\n * ref carries no usable identity (components render their fallback).\n * THROWS (and screams via its own diagnostics sink) on a signed/expiring\n * URL — the D108 guard lives behind this door.\n */\n resolve(ref: MediaRefLike): MediaResolution | null;\n /** Bytes for canvas/crossOrigin/private-pixel consumers. */\n getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;\n /**\n * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,\n * second failure terminal. Called by the package's onError path, never\n * re-implemented.\n */\n recoverLoadError(\n src: DurableSrc,\n attempt: number,\n ): Promise<MediaRecoveryVerdict | MediaRecoveryReport>;\n /**\n * Optional object-URL recovery policy for private blob-backed media.\n * The client decides whether the durable ref should be re-read; older\n * clients may omit this and fail terminally.\n */\n recoverBlobLoadError?(\n ref: MediaRefLike,\n attempt: number,\n ): Promise<MediaRecoveryVerdict>;\n /**\n * The heal ladder — runs AFTER the ONE retry policy is exhausted. Tries\n * every lane the client owns (bearer byte fetch on the primary host, then\n * the alternate host), records each attempt, and names the root cause. The\n * package renders `src` when present and captures the result either way:\n * a healed render is logged as loudly as a dead one. Clients that omit it\n * fall back to the terminal verdict.\n */\n healLoadError?(\n ref: MediaRefLike,\n context: { src: DurableSrc; attempts: MediaHealAttempt[] },\n ): Promise<MediaHealResult>;\n /** Upload → durable identity. */\n upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;\n /**\n * Optional batch upload with host pre-flight (dedup dialogs etc.). When\n * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.\n */\n uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;\n /** Share affordances — fails closed (null), never emits a signed URL. */\n shareableUrl(ref: MediaRefLike): Promise<string | null>;\n /** Typed unavailable states — render-as-state, never thrown as incidents. */\n classifyError(err: unknown): MediaUnavailableReason;\n}\n\n// ---------------------------------------------------------------------------\n// Host ports — all optional, all defaulting to inert.\n// ---------------------------------------------------------------------------\n\n/**\n * Structural component slot. `(props) => unknown` keeps the root entry free\n * of React type imports; any React function component satisfies it.\n */\nexport type MediaSlotComponent<P> = (props: P) => unknown;\n\nexport interface MediaImageComponentProps {\n src: string;\n alt: string;\n width: number;\n height: number;\n className?: string | undefined;\n onError?: ((event: unknown) => void) | undefined;\n onClick?: ((event: unknown) => void) | undefined;\n onKeyDown?: ((event: unknown) => void) | undefined;\n role?: string | undefined;\n tabIndex?: number | undefined;\n}\n\nexport interface PlaybackSessionRegistration {\n isPlaying: boolean;\n source: string;\n label: string;\n trackKey?: string | undefined;\n}\n\n/**\n * The host's audio system (exclusive-playback lock, output-sink routing).\n * Both members are HOOKS — the package calls them unconditionally on every\n * render of a media element, so implementations must obey the Rules of Hooks\n * and the port object must be referentially stable.\n */\nexport interface PlaybackSessionPort {\n /**\n * Returns a ref callback for the underlying media element (routes audio\n * output / setSinkId on hosts that support it) and forwards to the caller's\n * own element ref.\n */\n useMediaElementSink(\n forward:\n | ((node: unknown) => void)\n | { current: unknown }\n | null\n | undefined,\n ): (node: unknown) => void;\n /** Joins the host's unified playback session for the element. */\n usePlaybackSession(\n registration: PlaybackSessionRegistration,\n elementRef: { current: unknown },\n ): void;\n}\n\n// ---------------------------------------------------------------------------\n// THE SHARE MANDATE (C19) — the complete headless media-actions contract.\n//\n// v0.1 ships this contract plus toolbar/lightbox shells wired to it, typed so\n// the associations/sharing package can plug in WITHOUT a media release: it\n// implements MediaActionsPort (handlers and/or the SharePopover body) and the\n// host injects it through MediaHostPorts. Nothing here may change shape in a\n// breaking way for that wave.\n// ---------------------------------------------------------------------------\n\nexport type MediaActionKind = \"share\" | \"download\" | \"copy\" | \"open\";\n\nexport interface MediaActionContext {\n ref: MediaRefLike;\n resolution: MediaResolution | null;\n fileName?: string | undefined;\n alt?: string | undefined;\n}\n\nexport interface MediaSharePopoverProps {\n context: MediaActionContext;\n onClose: () => void;\n}\n\nexport type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;\n\nexport interface MediaActionsPort {\n /**\n * Quick-share handler (copy a share link, open the native share sheet…).\n * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /\n * durable identities — never a signed URL (law 4).\n */\n share?: MediaActionHandler | undefined;\n download?: MediaActionHandler | undefined;\n copy?: MediaActionHandler | undefined;\n open?: MediaActionHandler | undefined;\n /**\n * Rich share UI body — the slot the associations package fills. When\n * present, the shells open it instead of (or on top of) the `share`\n * handler. Receives {@link MediaSharePopoverProps}.\n */\n SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;\n}\n\nexport interface MediaHostPorts {\n playbackSession?: PlaybackSessionPort | undefined;\n actions?: MediaActionsPort | undefined;\n /** Web host may pass a next/image adapter; default is a plain `<img>`. */\n ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;\n /**\n * Error-capture sink for terminal media failures (and the ephemeral-secret\n * warning). Bind to the host's error system; absent → loud console.error\n * default. See {@link MediaDiagnosticsPort}.\n */\n diagnostics?: MediaDiagnosticsPort | undefined;\n}\n"],"mappings":";AA0DO,SAAS,eAAe,KAAyB;AACtD,SAAO;AACT;","names":[]}
package/dist/react.cjs CHANGED
@@ -785,7 +785,7 @@ function useMediaLoadRecovery(src, options) {
785
785
  const recoverable = options?.recoverable !== false;
786
786
  const onTerminal = options?.onTerminal;
787
787
  const failureRef = options?.failureRef;
788
- const [result, setResult] = (0, import_react5.useState)({ source: src, retryKey: 0, failed: false });
788
+ const [result, setResult] = (0, import_react5.useState)({ source: src, retryKey: 0, failed: false, healedSrc: null });
789
789
  const attemptRef = (0, import_react5.useRef)(0);
790
790
  const recoveryInFlightRef = (0, import_react5.useRef)(false);
791
791
  (0, import_react5.useEffect)(() => {
@@ -799,7 +799,7 @@ function useMediaLoadRecovery(src, options) {
799
799
  const onLoadError = (event) => {
800
800
  if (recoveryInFlightRef.current) return;
801
801
  if (!src || !recoverable) {
802
- setResult({ source: src, retryKey: 0, failed: true });
802
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
803
803
  if (src) {
804
804
  capture({
805
805
  phase: "element-load",
@@ -830,23 +830,90 @@ function useMediaLoadRecovery(src, options) {
830
830
  setResult((current) => ({
831
831
  source: src,
832
832
  retryKey: current.source === src ? current.retryKey + 1 : 1,
833
- failed: false
833
+ failed: false,
834
+ healedSrc: null
834
835
  }));
835
- } else {
836
- setResult({ source: src, retryKey: 0, failed: true });
836
+ return;
837
+ }
838
+ const lane1 = {
839
+ lane: "session-refresh-retry",
840
+ outcome: attempt > 1 ? "http-error" : "unavailable",
841
+ ms: 0,
842
+ ...report.status !== void 0 ? { status: report.status } : {},
843
+ detail: attempt > 1 ? "cookie re-minted, same durable URL retried in the element, element errored again" : "recovery declined on the first attempt (no usable session)"
844
+ };
845
+ if (!failureRef || !client.healLoadError) {
846
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
837
847
  capture({
838
848
  phase: attempt > 1 ? "element-load" : "recovery",
839
849
  mediaRef: identity(),
840
850
  terminal: true,
841
851
  retryOutcome: attempt > 1 ? "retried" : "not-attempted",
842
- message: attempt > 1 ? "media element failed to load after the session-refresh retry" : "media load recovery declined on the first attempt (session refresh did not produce a usable session)",
852
+ message: (attempt > 1 ? "media element failed to load after the session-refresh retry" : "media load recovery declined on the first attempt (session refresh did not produce a usable session)") + (failureRef ? " \u2014 no heal ladder on this client (healLoadError not implemented)" : " \u2014 no durable ref supplied, so no heal lane could run"),
843
853
  ...report.status !== void 0 ? { status: report.status } : {}
844
854
  });
845
855
  onTerminal?.(event);
856
+ return;
846
857
  }
858
+ const heal = client.healLoadError(failureRef, {
859
+ src: mintDurableSrc(src),
860
+ attempts: [lane1]
861
+ });
862
+ heal.then(
863
+ (verdict) => {
864
+ if (verdict.src) {
865
+ setResult({
866
+ source: src,
867
+ retryKey: 0,
868
+ failed: false,
869
+ healedSrc: verdict.src
870
+ });
871
+ capture({
872
+ phase: "heal",
873
+ mediaRef: identity(),
874
+ terminal: false,
875
+ healed: true,
876
+ healedBy: verdict.healedBy,
877
+ attempts: verdict.attempts,
878
+ diagnosis: verdict.diagnosis,
879
+ retryOutcome: "retried",
880
+ message: `HEALED (${verdict.healedBy ?? "unknown lane"}): ${verdict.summary}`,
881
+ ...report.status !== void 0 ? { status: report.status } : {}
882
+ });
883
+ return;
884
+ }
885
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
886
+ capture({
887
+ phase: "heal",
888
+ mediaRef: identity(),
889
+ terminal: true,
890
+ healed: false,
891
+ attempts: verdict.attempts,
892
+ diagnosis: verdict.diagnosis,
893
+ retryOutcome: "retried",
894
+ message: `DEAD after ${verdict.attempts.length} lane(s): ${verdict.summary}`,
895
+ ...report.status !== void 0 ? { status: report.status } : {}
896
+ });
897
+ onTerminal?.(event);
898
+ },
899
+ (err) => {
900
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
901
+ capture({
902
+ phase: "heal",
903
+ mediaRef: identity(),
904
+ terminal: true,
905
+ healed: false,
906
+ attempts: [lane1],
907
+ diagnosis: "unknown",
908
+ retryOutcome: "recovery-failed",
909
+ message: `heal ladder threw: ${err instanceof Error ? err.message : String(err ?? "unknown error")}`
910
+ });
911
+ onTerminal?.(event);
912
+ }
913
+ );
847
914
  },
848
915
  (err) => {
849
- setResult({ source: src, retryKey: 0, failed: true });
916
+ setResult({ source: src, retryKey: 0, failed: true, healedSrc: null });
850
917
  capture({
851
918
  phase: "session",
852
919
  mediaRef: identity(),
@@ -861,7 +928,8 @@ function useMediaLoadRecovery(src, options) {
861
928
  return {
862
929
  retryKey: result.source === src ? result.retryKey : 0,
863
930
  onLoadError,
864
- failed: result.source === src ? result.failed : false
931
+ failed: result.source === src ? result.failed : false,
932
+ healedSrc: result.source === src ? result.healedSrc : null
865
933
  };
866
934
  }
867
935
 
@@ -1190,15 +1258,16 @@ function InlineMediaRef({
1190
1258
  resolution && (resolution.transport === "blob" || crossOrigin && resolution.isCdn !== true)
1191
1259
  );
1192
1260
  const blob = useMediaBlob(needsBlob ? mediaRef : null);
1193
- const url = needsBlob ? blob.url : resolution?.src ?? null;
1261
+ const resolvedUrl = needsBlob ? blob.url : resolution?.src ?? null;
1194
1262
  const terminalCallback = (event) => onError?.(
1195
1263
  event
1196
1264
  );
1197
- const recovery = useMediaLoadRecovery(url, {
1265
+ const recovery = useMediaLoadRecovery(resolvedUrl, {
1198
1266
  recoverable: resolution?.recoverable !== false,
1199
1267
  failureRef: mediaRef,
1200
1268
  onTerminal: terminalCallback
1201
1269
  });
1270
+ const url = recovery.healedSrc ?? resolvedUrl;
1202
1271
  const blobRecovery = useMediaBlobLoadRecovery(
1203
1272
  needsBlob ? mediaRef : null,
1204
1273
  needsBlob ? url : null,
@@ -1593,18 +1662,19 @@ function ImageThumb({
1593
1662
  blobRecovery,
1594
1663
  fallback
1595
1664
  }) {
1596
- const { retryKey, onLoadError, failed } = useMediaLoadRecovery(url, {
1665
+ const { retryKey, onLoadError, failed, healedSrc } = useMediaLoadRecovery(url, {
1597
1666
  recoverable,
1598
1667
  failureRef: mediaRef
1599
1668
  });
1600
1669
  const activeRecovery = blobBacked ? { retryKey: 0, ...blobRecovery } : { retryKey, onLoadError, failed };
1670
+ const renderUrl = !blobBacked && healedSrc || url;
1601
1671
  if (activeRecovery.failed) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: fallback });
1602
1672
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1603
1673
  "img",
1604
1674
  {
1605
- src: url,
1675
+ src: renderUrl,
1606
1676
  alt,
1607
- loading: url.startsWith("blob:") ? "eager" : "lazy",
1677
+ loading: renderUrl.startsWith("blob:") ? "eager" : "lazy",
1608
1678
  className: "h-full w-full object-cover",
1609
1679
  onError: activeRecovery.onLoadError
1610
1680
  },
@@ -1619,11 +1689,12 @@ function VideoPosterThumb({
1619
1689
  blobRecovery,
1620
1690
  fallback
1621
1691
  }) {
1622
- const { retryKey, onLoadError, failed } = useMediaLoadRecovery(url, {
1692
+ const { retryKey, onLoadError, failed, healedSrc } = useMediaLoadRecovery(url, {
1623
1693
  recoverable,
1624
1694
  failureRef: mediaRef
1625
1695
  });
1626
1696
  const activeRecovery = blobBacked ? { retryKey: 0, ...blobRecovery } : { retryKey, onLoadError, failed };
1697
+ const renderUrl = !blobBacked && healedSrc || url;
1627
1698
  const ref = (0, import_react9.useRef)(null);
1628
1699
  (0, import_react9.useEffect)(() => {
1629
1700
  const v = ref.current;
@@ -1642,7 +1713,7 @@ function VideoPosterThumb({
1642
1713
  "video",
1643
1714
  {
1644
1715
  ref,
1645
- src: url,
1716
+ src: renderUrl,
1646
1717
  className: "h-full w-full object-cover pointer-events-none",
1647
1718
  preload: "metadata",
1648
1719
  muted: true,