@ai-matrx/media 0.1.0 → 0.1.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.1.1 — 2026-08-29
4
+
5
+ Types-only fix; runtime behavior unchanged.
6
+
7
+ - **Fixed: the `DurableSrc` unique-symbol brand was duplicated in the built
8
+ declarations.** The dts was built in two passes (mirroring the two-config
9
+ "use client" banner split), so the root `index.d.ts` declared its own copy
10
+ of the brand while `/core` and `/react` used a shared-chunk copy — two
11
+ distinct `unique symbol`s. Result: a `MediaClient` typed via the root entry
12
+ was nominally incompatible with `/core`'s `MediaProvider`, forcing a
13
+ documented cast in the host. The dts now builds in ONE pass over all three
14
+ entries (the JS banner split is unchanged), so the brand is declared once
15
+ in `index.d.ts` and the other entries' declarations import it.
16
+ - **Canary added:** `verify:tarball` now compiles a consumer against the
17
+ packed tarball that assigns a root-typed `MediaClient` into
18
+ `/core`'s `MediaProvider` props with **no cast** — the brand-duplication
19
+ regression class fails `check:package` from now on.
20
+
3
21
  ## 0.1.0 — 2026-08-29
4
22
 
5
23
  Initial release (campaign ruling C19). Root types + `/core` headless hooks +
package/dist/core.d.cts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { M as MediaClient, a as MediaHostPorts, b as MediaRefLike, c as MediaResolution, d as MediaUnavailableReason, D as DurableSrc, e as MediaActionContext, f as MediaActionKind, g as MediaSlotComponent, h as MediaSharePopoverProps } from './use-media-upload-VjUhqbyY.cjs';
4
- export { i as MediaUploadEntry, j as MediaUploadEntryStatus, k as MediaUploadProgress, U as UseMediaUploadResult, u as useMediaUpload } from './use-media-upload-VjUhqbyY.cjs';
3
+ import { MediaClient, MediaHostPorts, MediaRefLike, MediaResolution, MediaUnavailableReason, DurableSrc, MediaActionContext, MediaActionKind, MediaSlotComponent, MediaSharePopoverProps } from './index.cjs';
4
+ export { M as MediaUploadEntry, a as MediaUploadEntryStatus, b as MediaUploadProgress, U as UseMediaUploadResult, u as useMediaUpload } from './use-media-upload-v5Uq9Ruq.cjs';
5
5
 
6
6
  interface MediaProviderProps {
7
7
  client: MediaClient;
package/dist/core.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { M as MediaClient, a as MediaHostPorts, b as MediaRefLike, c as MediaResolution, d as MediaUnavailableReason, D as DurableSrc, e as MediaActionContext, f as MediaActionKind, g as MediaSlotComponent, h as MediaSharePopoverProps } from './use-media-upload-VjUhqbyY.js';
4
- export { i as MediaUploadEntry, j as MediaUploadEntryStatus, k as MediaUploadProgress, U as UseMediaUploadResult, u as useMediaUpload } from './use-media-upload-VjUhqbyY.js';
3
+ import { MediaClient, MediaHostPorts, MediaRefLike, MediaResolution, MediaUnavailableReason, DurableSrc, MediaActionContext, MediaActionKind, MediaSlotComponent, MediaSharePopoverProps } from './index.js';
4
+ export { M as MediaUploadEntry, a as MediaUploadEntryStatus, b as MediaUploadProgress, U as UseMediaUploadResult, u as useMediaUpload } from './use-media-upload-Dg2HSvTq.js';
5
5
 
6
6
  interface MediaProviderProps {
7
7
  client: MediaClient;
package/dist/react.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import react__default, { SVGProps, HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
3
- import { b as MediaRefLike, l as MediaUploadOptions, i as MediaUploadEntry, f as MediaActionKind } from './use-media-upload-VjUhqbyY.cjs';
3
+ import { MediaRefLike, MediaUploadOptions, MediaActionKind } from './index.cjs';
4
+ import { M as MediaUploadEntry } from './use-media-upload-v5Uq9Ruq.cjs';
4
5
 
5
6
  /**
6
7
  * src/react/cn.ts — conditional class join with last-wins Tailwind conflict
package/dist/react.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import react__default, { SVGProps, HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
3
- import { b as MediaRefLike, l as MediaUploadOptions, i as MediaUploadEntry, f as MediaActionKind } from './use-media-upload-VjUhqbyY.js';
3
+ import { MediaRefLike, MediaUploadOptions, MediaActionKind } from './index.js';
4
+ import { M as MediaUploadEntry } from './use-media-upload-Dg2HSvTq.js';
4
5
 
5
6
  /**
6
7
  * src/react/cn.ts — conditional class join with last-wins Tailwind conflict
@@ -0,0 +1,53 @@
1
+ import { MediaUploadOptions, MediaUploadResult, MediaMultiUploadResult } from './index.js';
2
+
3
+ /**
4
+ * src/core/use-media-upload.ts — the React-side upload primitive.
5
+ *
6
+ * Ported from matrx-frontend `handler/hooks/useFileUpload.ts` with two seam
7
+ * inversions: the transport is `MediaClient.upload` (never touched directly),
8
+ * and the multi-file pre-flight (SHA-256 dedup dialog) is the client's
9
+ * optional `uploadMany` — when absent, the batch runs sequentially through
10
+ * `upload` with per-file results.
11
+ *
12
+ * Additionally exposes a per-file `entries` tray state (name, size, bytes,
13
+ * status) so the dropzone's UploadProgressList works without any app store —
14
+ * the Redux `selectVisibleUploads` coupling of the original, inverted.
15
+ */
16
+
17
+ interface MediaUploadProgress {
18
+ loaded: number;
19
+ total: number;
20
+ ratio: number;
21
+ }
22
+ type MediaUploadEntryStatus = "uploading" | "success" | "error";
23
+ interface MediaUploadEntry {
24
+ id: string;
25
+ fileName: string;
26
+ fileSize: number;
27
+ bytesUploaded: number;
28
+ status: MediaUploadEntryStatus;
29
+ error: string | null;
30
+ fileId: string | null;
31
+ completedAt: number | null;
32
+ }
33
+ interface UseMediaUploadResult {
34
+ /** Upload one file/blob. Returns the durable identity. */
35
+ upload: (file: File | Blob, opts?: MediaUploadOptions) => Promise<MediaUploadResult>;
36
+ /**
37
+ * Upload many files. Uses the client's `uploadMany` (host pre-flight /
38
+ * dedup dialog) when implemented; otherwise a sequential per-file loop with
39
+ * real per-file error messages.
40
+ */
41
+ uploadMany: (files: File[], opts?: MediaUploadOptions) => Promise<MediaMultiUploadResult>;
42
+ uploading: boolean;
43
+ progress: MediaUploadProgress | null;
44
+ result: MediaUploadResult | null;
45
+ error: Error | null;
46
+ /** Live per-file tray entries for progress UI. */
47
+ entries: MediaUploadEntry[];
48
+ clearEntry: (id: string) => void;
49
+ reset: () => void;
50
+ }
51
+ declare function useMediaUpload(): UseMediaUploadResult;
52
+
53
+ export { type MediaUploadEntry as M, type UseMediaUploadResult as U, type MediaUploadEntryStatus as a, type MediaUploadProgress as b, useMediaUpload as u };
@@ -0,0 +1,53 @@
1
+ import { MediaUploadOptions, MediaUploadResult, MediaMultiUploadResult } from './index.cjs';
2
+
3
+ /**
4
+ * src/core/use-media-upload.ts — the React-side upload primitive.
5
+ *
6
+ * Ported from matrx-frontend `handler/hooks/useFileUpload.ts` with two seam
7
+ * inversions: the transport is `MediaClient.upload` (never touched directly),
8
+ * and the multi-file pre-flight (SHA-256 dedup dialog) is the client's
9
+ * optional `uploadMany` — when absent, the batch runs sequentially through
10
+ * `upload` with per-file results.
11
+ *
12
+ * Additionally exposes a per-file `entries` tray state (name, size, bytes,
13
+ * status) so the dropzone's UploadProgressList works without any app store —
14
+ * the Redux `selectVisibleUploads` coupling of the original, inverted.
15
+ */
16
+
17
+ interface MediaUploadProgress {
18
+ loaded: number;
19
+ total: number;
20
+ ratio: number;
21
+ }
22
+ type MediaUploadEntryStatus = "uploading" | "success" | "error";
23
+ interface MediaUploadEntry {
24
+ id: string;
25
+ fileName: string;
26
+ fileSize: number;
27
+ bytesUploaded: number;
28
+ status: MediaUploadEntryStatus;
29
+ error: string | null;
30
+ fileId: string | null;
31
+ completedAt: number | null;
32
+ }
33
+ interface UseMediaUploadResult {
34
+ /** Upload one file/blob. Returns the durable identity. */
35
+ upload: (file: File | Blob, opts?: MediaUploadOptions) => Promise<MediaUploadResult>;
36
+ /**
37
+ * Upload many files. Uses the client's `uploadMany` (host pre-flight /
38
+ * dedup dialog) when implemented; otherwise a sequential per-file loop with
39
+ * real per-file error messages.
40
+ */
41
+ uploadMany: (files: File[], opts?: MediaUploadOptions) => Promise<MediaMultiUploadResult>;
42
+ uploading: boolean;
43
+ progress: MediaUploadProgress | null;
44
+ result: MediaUploadResult | null;
45
+ error: Error | null;
46
+ /** Live per-file tray entries for progress UI. */
47
+ entries: MediaUploadEntry[];
48
+ clearEntry: (id: string) => void;
49
+ reset: () => void;
50
+ }
51
+ declare function useMediaUpload(): UseMediaUploadResult;
52
+
53
+ export { type MediaUploadEntry as M, type UseMediaUploadResult as U, type MediaUploadEntryStatus as a, type MediaUploadProgress as b, useMediaUpload as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/media",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The AI Matrx media components kit: durable-ref-only renderers for images, video, audio and file thumbnails that just work with the Matrx file system — headless core hooks plus DOM bindings, wired to an injected MediaClient. A signed URL is a handoff, never an identity.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -1,270 +0,0 @@
1
- /**
2
- * @ai-matrx/media — the root types: the MediaClient port and the host ports.
3
- *
4
- * The package renders Matrx durable media; it NEVER talks to a network.
5
- * Everything data-shaped arrives through the injected {@link MediaClient}
6
- * (canonically implemented by `@ai-matrx/data/files`; any structural
7
- * implementation works — the strangler step wires it over an app's existing
8
- * file handler). The five file-handling laws are enforced here at the type
9
- * level:
10
- *
11
- * 1. Never fetch a media URL yourself — the package contains zero network
12
- * primitives; bytes exist only via {@link MediaClient.getBlob}.
13
- * 2. Off-boundary code resolves media itself — every component takes a
14
- * {@link MediaRefLike} and calls `resolve()` unconditionally. There is
15
- * NO `src` prop on any component in this package.
16
- * 3. A signed URL is a handoff, never an identity — the only string that
17
- * reaches a media element is the branded {@link DurableSrc}, which only
18
- * a MediaClient implementation can mint (via {@link mintDurableSrc}).
19
- * 4. Public-facing media is persisted public, never signed —
20
- * {@link MediaClient.shareableUrl} fails closed (`null` → the action
21
- * renders disabled).
22
- * 5. One URL contract; the engine builds URLs — zero URL construction in
23
- * this package; `resolve()` is the only door.
24
- */
25
- /** The canonical durable media reference. `file_id` is the identity. */
26
- interface MediaRef {
27
- file_id?: string | null;
28
- url?: string | null;
29
- mime_type?: string | null;
30
- metadata?: Record<string, unknown> | null;
31
- }
32
- /**
33
- * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a
34
- * signed URL (the client's `resolve()` classifies and screams on one).
35
- */
36
- type MediaRefLike = MediaRef | string;
37
- declare const durable: unique symbol;
38
- /**
39
- * A URL that has passed through `MediaClient.resolve()` — the only string
40
- * type any media element in this package will bind. A caller cannot pass an
41
- * expiring URL through the package's types without an explicit unsafe cast.
42
- */
43
- type DurableSrc = string & {
44
- readonly [durable]: true;
45
- };
46
- type MediaKind = "image" | "video" | "audio" | "file";
47
- interface MediaResolution {
48
- src: DurableSrc;
49
- /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */
50
- headers?: Record<string, string> | undefined;
51
- mimeType?: string | undefined;
52
- kind: MediaKind;
53
- /**
54
- * How pixels must travel. `"element"` (default): bind `src` directly to the
55
- * media element. `"blob"`: the bytes require bearer auth the element cannot
56
- * attach — consumers go through {@link MediaClient.getBlob} and render the
57
- * returned object URL (the private-image lane).
58
- */
59
- transport?: "element" | "blob" | undefined;
60
- /**
61
- * A server-rendered thumbnail for this file, when the client knows one
62
- * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the
63
- * thumbnail tier machine.
64
- */
65
- thumbnailSrc?: DurableSrc | undefined;
66
- /**
67
- * True when this is a permanent public CDN URL — lets a host-supplied
68
- * `ImageComponent` (e.g. a next/image adapter) take the render, and marks
69
- * the URL CORS-safe for element-level `crossOrigin`.
70
- */
71
- isCdn?: boolean | undefined;
72
- /**
73
- * Whether a load error can be recovered by the client's ONE retry policy
74
- * (session-refresh → same-URL retry). False for foreign/external URLs —
75
- * a session refresh can't resurrect someone else's link rot. Default true.
76
- */
77
- recoverable?: boolean | undefined;
78
- }
79
- /** Typed unavailable states — render-as-state, never thrown as incidents. */
80
- type MediaUnavailableReason = "access_denied" | "not_found" | "deleted" | "unknown";
81
- interface MediaBlobHandle {
82
- /** `blob:` object URL safe for any browser API. */
83
- url: string;
84
- blob: Blob;
85
- /** Release the handle. Implementations backed by a shared cache may no-op. */
86
- release(): void;
87
- }
88
- interface MediaUploadOptions {
89
- onProgress?: ((loaded: number, total: number) => void) | undefined;
90
- fileName?: string | undefined;
91
- mimeType?: string | undefined;
92
- /** Host visibility vocabulary passes through structurally. */
93
- visibility?: string | undefined;
94
- parentFolderId?: string | null | undefined;
95
- metadata?: Record<string, unknown> | undefined;
96
- }
97
- /** Upload → durable identity. `fileId` is required at compile time and runtime. */
98
- interface MediaUploadResult {
99
- fileId: string;
100
- ref: MediaRef;
101
- }
102
- interface MediaMultiUploadResult {
103
- /** File ids for every successful upload. */
104
- uploaded: string[];
105
- /** Per-file failure with the real backend error message. */
106
- failed: Array<{
107
- name: string;
108
- error: string;
109
- }>;
110
- /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */
111
- cancelled: boolean;
112
- }
113
- interface MediaClient {
114
- /**
115
- * Synchronous durable-URL build + classification. Returns `null` when the
116
- * ref carries no usable identity (components render their fallback).
117
- * THROWS (and screams via its own diagnostics sink) on a signed/expiring
118
- * URL — the D108 guard lives behind this door.
119
- */
120
- resolve(ref: MediaRefLike): MediaResolution | null;
121
- /** Bytes for canvas/crossOrigin/private-pixel consumers. */
122
- getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;
123
- /**
124
- * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,
125
- * second failure terminal. Called by the package's onError path, never
126
- * re-implemented.
127
- */
128
- recoverLoadError(src: DurableSrc, attempt: number): Promise<"retry" | "terminal">;
129
- /** Upload → durable identity. */
130
- upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;
131
- /**
132
- * Optional batch upload with host pre-flight (dedup dialogs etc.). When
133
- * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.
134
- */
135
- uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;
136
- /** Share affordances — fails closed (null), never emits a signed URL. */
137
- shareableUrl(ref: MediaRefLike): Promise<string | null>;
138
- /** Typed unavailable states — render-as-state, never thrown as incidents. */
139
- classifyError(err: unknown): MediaUnavailableReason;
140
- }
141
- /**
142
- * Structural component slot. `(props) => unknown` keeps the root entry free
143
- * of React type imports; any React function component satisfies it.
144
- */
145
- type MediaSlotComponent<P> = (props: P) => unknown;
146
- interface MediaImageComponentProps {
147
- src: string;
148
- alt: string;
149
- width: number;
150
- height: number;
151
- className?: string | undefined;
152
- onError?: ((event: unknown) => void) | undefined;
153
- onClick?: ((event: unknown) => void) | undefined;
154
- onKeyDown?: ((event: unknown) => void) | undefined;
155
- role?: string | undefined;
156
- tabIndex?: number | undefined;
157
- }
158
- interface PlaybackSessionRegistration {
159
- isPlaying: boolean;
160
- source: string;
161
- label: string;
162
- trackKey?: string | undefined;
163
- }
164
- /**
165
- * The host's audio system (exclusive-playback lock, output-sink routing).
166
- * Both members are HOOKS — the package calls them unconditionally on every
167
- * render of a media element, so implementations must obey the Rules of Hooks
168
- * and the port object must be referentially stable.
169
- */
170
- interface PlaybackSessionPort {
171
- /**
172
- * Returns a ref callback for the underlying media element (routes audio
173
- * output / setSinkId on hosts that support it) and forwards to the caller's
174
- * own element ref.
175
- */
176
- useMediaElementSink(forward: ((node: unknown) => void) | {
177
- current: unknown;
178
- } | null | undefined): (node: unknown) => void;
179
- /** Joins the host's unified playback session for the element. */
180
- usePlaybackSession(registration: PlaybackSessionRegistration, elementRef: {
181
- current: unknown;
182
- }): void;
183
- }
184
- type MediaActionKind = "share" | "download" | "copy" | "open";
185
- interface MediaActionContext {
186
- ref: MediaRefLike;
187
- resolution: MediaResolution | null;
188
- fileName?: string | undefined;
189
- alt?: string | undefined;
190
- }
191
- interface MediaSharePopoverProps {
192
- context: MediaActionContext;
193
- onClose: () => void;
194
- }
195
- type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;
196
- interface MediaActionsPort {
197
- /**
198
- * Quick-share handler (copy a share link, open the native share sheet…).
199
- * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /
200
- * durable identities — never a signed URL (law 4).
201
- */
202
- share?: MediaActionHandler | undefined;
203
- download?: MediaActionHandler | undefined;
204
- copy?: MediaActionHandler | undefined;
205
- open?: MediaActionHandler | undefined;
206
- /**
207
- * Rich share UI body — the slot the associations package fills. When
208
- * present, the shells open it instead of (or on top of) the `share`
209
- * handler. Receives {@link MediaSharePopoverProps}.
210
- */
211
- SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;
212
- }
213
- interface MediaHostPorts {
214
- playbackSession?: PlaybackSessionPort | undefined;
215
- actions?: MediaActionsPort | undefined;
216
- /** Web host may pass a next/image adapter; default is a plain `<img>`. */
217
- ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;
218
- }
219
-
220
- /**
221
- * src/core/use-media-upload.ts — the React-side upload primitive.
222
- *
223
- * Ported from matrx-frontend `handler/hooks/useFileUpload.ts` with two seam
224
- * inversions: the transport is `MediaClient.upload` (never touched directly),
225
- * and the multi-file pre-flight (SHA-256 dedup dialog) is the client's
226
- * optional `uploadMany` — when absent, the batch runs sequentially through
227
- * `upload` with per-file results.
228
- *
229
- * Additionally exposes a per-file `entries` tray state (name, size, bytes,
230
- * status) so the dropzone's UploadProgressList works without any app store —
231
- * the Redux `selectVisibleUploads` coupling of the original, inverted.
232
- */
233
-
234
- interface MediaUploadProgress {
235
- loaded: number;
236
- total: number;
237
- ratio: number;
238
- }
239
- type MediaUploadEntryStatus = "uploading" | "success" | "error";
240
- interface MediaUploadEntry {
241
- id: string;
242
- fileName: string;
243
- fileSize: number;
244
- bytesUploaded: number;
245
- status: MediaUploadEntryStatus;
246
- error: string | null;
247
- fileId: string | null;
248
- completedAt: number | null;
249
- }
250
- interface UseMediaUploadResult {
251
- /** Upload one file/blob. Returns the durable identity. */
252
- upload: (file: File | Blob, opts?: MediaUploadOptions) => Promise<MediaUploadResult>;
253
- /**
254
- * Upload many files. Uses the client's `uploadMany` (host pre-flight /
255
- * dedup dialog) when implemented; otherwise a sequential per-file loop with
256
- * real per-file error messages.
257
- */
258
- uploadMany: (files: File[], opts?: MediaUploadOptions) => Promise<MediaMultiUploadResult>;
259
- uploading: boolean;
260
- progress: MediaUploadProgress | null;
261
- result: MediaUploadResult | null;
262
- error: Error | null;
263
- /** Live per-file tray entries for progress UI. */
264
- entries: MediaUploadEntry[];
265
- clearEntry: (id: string) => void;
266
- reset: () => void;
267
- }
268
- declare function useMediaUpload(): UseMediaUploadResult;
269
-
270
- export { type DurableSrc as D, type MediaClient as M, type UseMediaUploadResult as U, type MediaHostPorts as a, type MediaRefLike as b, type MediaResolution as c, type MediaUnavailableReason as d, type MediaActionContext as e, type MediaActionKind as f, type MediaSlotComponent as g, type MediaSharePopoverProps as h, type MediaUploadEntry as i, type MediaUploadEntryStatus as j, type MediaUploadProgress as k, type MediaUploadOptions as l, useMediaUpload as u };
@@ -1,270 +0,0 @@
1
- /**
2
- * @ai-matrx/media — the root types: the MediaClient port and the host ports.
3
- *
4
- * The package renders Matrx durable media; it NEVER talks to a network.
5
- * Everything data-shaped arrives through the injected {@link MediaClient}
6
- * (canonically implemented by `@ai-matrx/data/files`; any structural
7
- * implementation works — the strangler step wires it over an app's existing
8
- * file handler). The five file-handling laws are enforced here at the type
9
- * level:
10
- *
11
- * 1. Never fetch a media URL yourself — the package contains zero network
12
- * primitives; bytes exist only via {@link MediaClient.getBlob}.
13
- * 2. Off-boundary code resolves media itself — every component takes a
14
- * {@link MediaRefLike} and calls `resolve()` unconditionally. There is
15
- * NO `src` prop on any component in this package.
16
- * 3. A signed URL is a handoff, never an identity — the only string that
17
- * reaches a media element is the branded {@link DurableSrc}, which only
18
- * a MediaClient implementation can mint (via {@link mintDurableSrc}).
19
- * 4. Public-facing media is persisted public, never signed —
20
- * {@link MediaClient.shareableUrl} fails closed (`null` → the action
21
- * renders disabled).
22
- * 5. One URL contract; the engine builds URLs — zero URL construction in
23
- * this package; `resolve()` is the only door.
24
- */
25
- /** The canonical durable media reference. `file_id` is the identity. */
26
- interface MediaRef {
27
- file_id?: string | null;
28
- url?: string | null;
29
- mime_type?: string | null;
30
- metadata?: Record<string, unknown> | null;
31
- }
32
- /**
33
- * Identity in. A raw string is a fileId or an EXTERNAL public URL — never a
34
- * signed URL (the client's `resolve()` classifies and screams on one).
35
- */
36
- type MediaRefLike = MediaRef | string;
37
- declare const durable: unique symbol;
38
- /**
39
- * A URL that has passed through `MediaClient.resolve()` — the only string
40
- * type any media element in this package will bind. A caller cannot pass an
41
- * expiring URL through the package's types without an explicit unsafe cast.
42
- */
43
- type DurableSrc = string & {
44
- readonly [durable]: true;
45
- };
46
- type MediaKind = "image" | "video" | "audio" | "file";
47
- interface MediaResolution {
48
- src: DurableSrc;
49
- /** RN auth lane (`<Image source={{uri, headers}}>`); absent on web (cookie session). */
50
- headers?: Record<string, string> | undefined;
51
- mimeType?: string | undefined;
52
- kind: MediaKind;
53
- /**
54
- * How pixels must travel. `"element"` (default): bind `src` directly to the
55
- * media element. `"blob"`: the bytes require bearer auth the element cannot
56
- * attach — consumers go through {@link MediaClient.getBlob} and render the
57
- * returned object URL (the private-image lane).
58
- */
59
- transport?: "element" | "blob" | undefined;
60
- /**
61
- * A server-rendered thumbnail for this file, when the client knows one
62
- * (Matrx: `FileRecord.thumbnail_url` / asset variants). Consumed by the
63
- * thumbnail tier machine.
64
- */
65
- thumbnailSrc?: DurableSrc | undefined;
66
- /**
67
- * True when this is a permanent public CDN URL — lets a host-supplied
68
- * `ImageComponent` (e.g. a next/image adapter) take the render, and marks
69
- * the URL CORS-safe for element-level `crossOrigin`.
70
- */
71
- isCdn?: boolean | undefined;
72
- /**
73
- * Whether a load error can be recovered by the client's ONE retry policy
74
- * (session-refresh → same-URL retry). False for foreign/external URLs —
75
- * a session refresh can't resurrect someone else's link rot. Default true.
76
- */
77
- recoverable?: boolean | undefined;
78
- }
79
- /** Typed unavailable states — render-as-state, never thrown as incidents. */
80
- type MediaUnavailableReason = "access_denied" | "not_found" | "deleted" | "unknown";
81
- interface MediaBlobHandle {
82
- /** `blob:` object URL safe for any browser API. */
83
- url: string;
84
- blob: Blob;
85
- /** Release the handle. Implementations backed by a shared cache may no-op. */
86
- release(): void;
87
- }
88
- interface MediaUploadOptions {
89
- onProgress?: ((loaded: number, total: number) => void) | undefined;
90
- fileName?: string | undefined;
91
- mimeType?: string | undefined;
92
- /** Host visibility vocabulary passes through structurally. */
93
- visibility?: string | undefined;
94
- parentFolderId?: string | null | undefined;
95
- metadata?: Record<string, unknown> | undefined;
96
- }
97
- /** Upload → durable identity. `fileId` is required at compile time and runtime. */
98
- interface MediaUploadResult {
99
- fileId: string;
100
- ref: MediaRef;
101
- }
102
- interface MediaMultiUploadResult {
103
- /** File ids for every successful upload. */
104
- uploaded: string[];
105
- /** Per-file failure with the real backend error message. */
106
- failed: Array<{
107
- name: string;
108
- error: string;
109
- }>;
110
- /** True when the host's pre-flight (e.g. a duplicate dialog) cancelled the batch. */
111
- cancelled: boolean;
112
- }
113
- interface MediaClient {
114
- /**
115
- * Synchronous durable-URL build + classification. Returns `null` when the
116
- * ref carries no usable identity (components render their fallback).
117
- * THROWS (and screams via its own diagnostics sink) on a signed/expiring
118
- * URL — the D108 guard lives behind this door.
119
- */
120
- resolve(ref: MediaRefLike): MediaResolution | null;
121
- /** Bytes for canvas/crossOrigin/private-pixel consumers. */
122
- getBlob(ref: MediaRefLike): Promise<MediaBlobHandle>;
123
- /**
124
- * Session bootstrap + the ONE retry policy: refresh-then-same-URL-retry,
125
- * second failure terminal. Called by the package's onError path, never
126
- * re-implemented.
127
- */
128
- recoverLoadError(src: DurableSrc, attempt: number): Promise<"retry" | "terminal">;
129
- /** Upload → durable identity. */
130
- upload(file: File | Blob, opts?: MediaUploadOptions): Promise<MediaUploadResult>;
131
- /**
132
- * Optional batch upload with host pre-flight (dedup dialogs etc.). When
133
- * absent, `useMediaUpload().uploadMany` falls back to sequential `upload`.
134
- */
135
- uploadMany?(files: File[], opts?: MediaUploadOptions): Promise<MediaMultiUploadResult>;
136
- /** Share affordances — fails closed (null), never emits a signed URL. */
137
- shareableUrl(ref: MediaRefLike): Promise<string | null>;
138
- /** Typed unavailable states — render-as-state, never thrown as incidents. */
139
- classifyError(err: unknown): MediaUnavailableReason;
140
- }
141
- /**
142
- * Structural component slot. `(props) => unknown` keeps the root entry free
143
- * of React type imports; any React function component satisfies it.
144
- */
145
- type MediaSlotComponent<P> = (props: P) => unknown;
146
- interface MediaImageComponentProps {
147
- src: string;
148
- alt: string;
149
- width: number;
150
- height: number;
151
- className?: string | undefined;
152
- onError?: ((event: unknown) => void) | undefined;
153
- onClick?: ((event: unknown) => void) | undefined;
154
- onKeyDown?: ((event: unknown) => void) | undefined;
155
- role?: string | undefined;
156
- tabIndex?: number | undefined;
157
- }
158
- interface PlaybackSessionRegistration {
159
- isPlaying: boolean;
160
- source: string;
161
- label: string;
162
- trackKey?: string | undefined;
163
- }
164
- /**
165
- * The host's audio system (exclusive-playback lock, output-sink routing).
166
- * Both members are HOOKS — the package calls them unconditionally on every
167
- * render of a media element, so implementations must obey the Rules of Hooks
168
- * and the port object must be referentially stable.
169
- */
170
- interface PlaybackSessionPort {
171
- /**
172
- * Returns a ref callback for the underlying media element (routes audio
173
- * output / setSinkId on hosts that support it) and forwards to the caller's
174
- * own element ref.
175
- */
176
- useMediaElementSink(forward: ((node: unknown) => void) | {
177
- current: unknown;
178
- } | null | undefined): (node: unknown) => void;
179
- /** Joins the host's unified playback session for the element. */
180
- usePlaybackSession(registration: PlaybackSessionRegistration, elementRef: {
181
- current: unknown;
182
- }): void;
183
- }
184
- type MediaActionKind = "share" | "download" | "copy" | "open";
185
- interface MediaActionContext {
186
- ref: MediaRefLike;
187
- resolution: MediaResolution | null;
188
- fileName?: string | undefined;
189
- alt?: string | undefined;
190
- }
191
- interface MediaSharePopoverProps {
192
- context: MediaActionContext;
193
- onClose: () => void;
194
- }
195
- type MediaActionHandler = (ctx: MediaActionContext) => Promise<void>;
196
- interface MediaActionsPort {
197
- /**
198
- * Quick-share handler (copy a share link, open the native share sheet…).
199
- * Implementations obtain URLs ONLY via `MediaClient.shareableUrl` /
200
- * durable identities — never a signed URL (law 4).
201
- */
202
- share?: MediaActionHandler | undefined;
203
- download?: MediaActionHandler | undefined;
204
- copy?: MediaActionHandler | undefined;
205
- open?: MediaActionHandler | undefined;
206
- /**
207
- * Rich share UI body — the slot the associations package fills. When
208
- * present, the shells open it instead of (or on top of) the `share`
209
- * handler. Receives {@link MediaSharePopoverProps}.
210
- */
211
- SharePopover?: MediaSlotComponent<MediaSharePopoverProps> | undefined;
212
- }
213
- interface MediaHostPorts {
214
- playbackSession?: PlaybackSessionPort | undefined;
215
- actions?: MediaActionsPort | undefined;
216
- /** Web host may pass a next/image adapter; default is a plain `<img>`. */
217
- ImageComponent?: MediaSlotComponent<MediaImageComponentProps> | undefined;
218
- }
219
-
220
- /**
221
- * src/core/use-media-upload.ts — the React-side upload primitive.
222
- *
223
- * Ported from matrx-frontend `handler/hooks/useFileUpload.ts` with two seam
224
- * inversions: the transport is `MediaClient.upload` (never touched directly),
225
- * and the multi-file pre-flight (SHA-256 dedup dialog) is the client's
226
- * optional `uploadMany` — when absent, the batch runs sequentially through
227
- * `upload` with per-file results.
228
- *
229
- * Additionally exposes a per-file `entries` tray state (name, size, bytes,
230
- * status) so the dropzone's UploadProgressList works without any app store —
231
- * the Redux `selectVisibleUploads` coupling of the original, inverted.
232
- */
233
-
234
- interface MediaUploadProgress {
235
- loaded: number;
236
- total: number;
237
- ratio: number;
238
- }
239
- type MediaUploadEntryStatus = "uploading" | "success" | "error";
240
- interface MediaUploadEntry {
241
- id: string;
242
- fileName: string;
243
- fileSize: number;
244
- bytesUploaded: number;
245
- status: MediaUploadEntryStatus;
246
- error: string | null;
247
- fileId: string | null;
248
- completedAt: number | null;
249
- }
250
- interface UseMediaUploadResult {
251
- /** Upload one file/blob. Returns the durable identity. */
252
- upload: (file: File | Blob, opts?: MediaUploadOptions) => Promise<MediaUploadResult>;
253
- /**
254
- * Upload many files. Uses the client's `uploadMany` (host pre-flight /
255
- * dedup dialog) when implemented; otherwise a sequential per-file loop with
256
- * real per-file error messages.
257
- */
258
- uploadMany: (files: File[], opts?: MediaUploadOptions) => Promise<MediaMultiUploadResult>;
259
- uploading: boolean;
260
- progress: MediaUploadProgress | null;
261
- result: MediaUploadResult | null;
262
- error: Error | null;
263
- /** Live per-file tray entries for progress UI. */
264
- entries: MediaUploadEntry[];
265
- clearEntry: (id: string) => void;
266
- reset: () => void;
267
- }
268
- declare function useMediaUpload(): UseMediaUploadResult;
269
-
270
- export { type DurableSrc as D, type MediaClient as M, type UseMediaUploadResult as U, type MediaHostPorts as a, type MediaRefLike as b, type MediaResolution as c, type MediaUnavailableReason as d, type MediaActionContext as e, type MediaActionKind as f, type MediaSlotComponent as g, type MediaSharePopoverProps as h, type MediaUploadEntry as i, type MediaUploadEntryStatus as j, type MediaUploadProgress as k, type MediaUploadOptions as l, useMediaUpload as u };