target-supabase-sdk 0.2.16 → 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +20 -0
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.d.ts.map +1 -1
  4. package/dist/browser.js +1 -1
  5. package/dist/chunks/{task-queue-Gi4DSq74.js → task-queue-DufG7PlX.js} +162 -2
  6. package/dist/chunks/task-queue-DufG7PlX.js.map +1 -0
  7. package/dist/node.d.ts +2 -0
  8. package/dist/node.d.ts.map +1 -1
  9. package/dist/node.js +211 -6
  10. package/dist/node.js.map +1 -1
  11. package/dist/shared/media/index.d.ts +14 -0
  12. package/dist/shared/media/index.d.ts.map +1 -0
  13. package/dist/shared/media/media.build.d.ts +20 -0
  14. package/dist/shared/media/media.build.d.ts.map +1 -0
  15. package/dist/shared/media/media.interface.d.ts +38 -0
  16. package/dist/shared/media/media.interface.d.ts.map +1 -0
  17. package/dist/shared/media/media.mime.d.ts +4 -0
  18. package/dist/shared/media/media.mime.d.ts.map +1 -0
  19. package/dist/shared/media/media.name.d.ts +6 -0
  20. package/dist/shared/media/media.name.d.ts.map +1 -0
  21. package/dist/shared/media/media.query.d.ts +8 -0
  22. package/dist/shared/media/media.query.d.ts.map +1 -0
  23. package/dist/shared/media/node/index.d.ts +9 -0
  24. package/dist/shared/media/node/index.d.ts.map +1 -0
  25. package/dist/shared/media/node/media.access.d.ts +40 -0
  26. package/dist/shared/media/node/media.access.d.ts.map +1 -0
  27. package/dist/shared/media/node/media.service.d.ts +30 -0
  28. package/dist/shared/media/node/media.service.d.ts.map +1 -0
  29. package/package.json +1 -1
  30. package/src/browser.ts +11 -0
  31. package/src/node.ts +13 -0
  32. package/src/shared/media/README.md +109 -0
  33. package/src/shared/media/index.ts +19 -0
  34. package/src/shared/media/media.build.ts +69 -0
  35. package/src/shared/media/media.interface.ts +45 -0
  36. package/src/shared/media/media.mime.ts +64 -0
  37. package/src/shared/media/media.name.ts +33 -0
  38. package/src/shared/media/media.query.ts +13 -0
  39. package/src/shared/media/node/index.ts +13 -0
  40. package/src/shared/media/node/media.access.ts +148 -0
  41. package/src/shared/media/node/media.service.ts +129 -0
  42. package/dist/chunks/task-queue-Gi4DSq74.js.map +0 -1
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Media Link — browser-safe barrel only.
3
+ *
4
+ * Node-only code lives in `./node/` and is exported from `src/node.ts`,
5
+ * never from this file (see README.md).
6
+ */
7
+ export type { BuildMediaLinkDraftInput } from "./media.build";
8
+ export { buildMediaLinkDraft } from "./media.build";
9
+ export type { MediaOriginal, MediaValue } from "./media.interface";
10
+ export { isMediaValue, MEDIA_CONTENT_HASH_ALGORITHM, MEDIA_CONTENT_HASH_PREFIX_LENGTH, MEDIA_VALUES, } from "./media.interface";
11
+ export { guessMediaMime } from "./media.mime";
12
+ export { buildMediaNameFromLocator } from "./media.name";
13
+ export { mediaLinkDedupFilters } from "./media.query";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/shared/media/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EACH,YAAY,EACZ,4BAA4B,EAC5B,gCAAgC,EAChC,YAAY,GACf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { TargetDraft } from "../../core.interface";
2
+ import type { Link } from "../../link/link.interface";
3
+ import { type MediaValue } from "./media.interface";
4
+ export interface BuildMediaLinkDraftInput {
5
+ value: MediaValue;
6
+ name: string;
7
+ storageProvider: string;
8
+ locator: string;
9
+ contentHash: string;
10
+ size: number;
11
+ mimeType: string;
12
+ description?: string;
13
+ tagList?: string[];
14
+ }
15
+ /**
16
+ * Assemble a media {@link Link} draft (`category=link`, `loaderKey === value`).
17
+ * Does not validate filesystem or write to Supabase.
18
+ */
19
+ export declare function buildMediaLinkDraft(input: BuildMediaLinkDraftInput): TargetDraft<Link>;
20
+ //# sourceMappingURL=media.build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.build.d.ts","sourceRoot":"","sources":["../../../src/shared/media/media.build.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAoC,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEtF,MAAM,WAAW,wBAAwB;IACrC,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,wBAAwB,GAAG,WAAW,CAAC,IAAI,CAAC,CA+CtF"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Media Link protocol — Link rows with value/loaderKey in {@link MEDIA_VALUES}.
3
+ * No separate Target category; category remains `link`.
4
+ */
5
+ /** Fixed digest algorithm for single-file media `contentHash`. */
6
+ export declare const MEDIA_CONTENT_HASH_ALGORITHM: "sha256";
7
+ /** Hex prefix length for {@link buildMediaNameFromLocator}. */
8
+ export declare const MEDIA_CONTENT_HASH_PREFIX_LENGTH = 6;
9
+ export declare const MEDIA_VALUES: readonly ["video", "audio", "image", "image-list"];
10
+ /** Discriminator stored in both `Link.value` and `details.loaderKey`. */
11
+ export type MediaValue = (typeof MEDIA_VALUES)[number];
12
+ /**
13
+ * `Link.details.original` for media Links.
14
+ *
15
+ * - `locator` — absolute filesystem path, or (future) network URL keyed by `storageProvider`.
16
+ * - `image-list`: `locator` is an absolute directory path; only the **immediate** children
17
+ * (one level, non-recursive) are counted. `contentHash` and `mimeType` are `""`;
18
+ * `size` is the number of files in that directory (not bytes).
19
+ * - other values: `size` is byte length; `contentHash` is SHA-256 hex; `mimeType` from the file.
20
+ */
21
+ export interface MediaOriginal {
22
+ storageProvider: string;
23
+ locator: string;
24
+ /**
25
+ * SHA-256 hex of file bytes, or `""` when `value === "image-list"`.
26
+ * Algorithm is always {@link MEDIA_CONTENT_HASH_ALGORITHM}.
27
+ */
28
+ contentHash: string;
29
+ /**
30
+ * Single-file media: byte length.
31
+ * `image-list`: number of files in the directory (one level).
32
+ */
33
+ size: number;
34
+ /** e.g. `video/mp4`, or `""` when `value === "image-list"`. */
35
+ mimeType: string;
36
+ }
37
+ export declare function isMediaValue(value: string): value is MediaValue;
38
+ //# sourceMappingURL=media.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.interface.d.ts","sourceRoot":"","sources":["../../../src/shared/media/media.interface.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,kEAAkE;AAClE,eAAO,MAAM,4BAA4B,EAAG,QAAiB,CAAC;AAE9D,+DAA+D;AAC/D,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,eAAO,MAAM,YAAY,oDAAqD,CAAC;AAE/E,yEAAyE;AACzE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,UAAU,CAE/D"}
@@ -0,0 +1,4 @@
1
+ import type { MediaValue } from "./media.interface";
2
+ /** Guess MIME from path extension; `null` when unknown. */
3
+ export declare function guessMediaMime(locator: string, value: Exclude<MediaValue, "image-list">): string | null;
4
+ //# sourceMappingURL=media.mime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.mime.d.ts","sourceRoot":"","sources":["../../../src/shared/media/media.mime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAkDpD,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,GAAG,MAAM,GAAG,IAAI,CAYvG"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `{basename}.{hash6}.{ext}` from an absolute (or path-like) locator and full content hash.
3
+ * Does not touch the filesystem.
4
+ */
5
+ export declare function buildMediaNameFromLocator(locator: string, contentHash: string): string;
6
+ //# sourceMappingURL=media.name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.name.d.ts","sourceRoot":"","sources":["../../../src/shared/media/media.name.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CA0BtF"}
@@ -0,0 +1,8 @@
1
+ import type { QueryFilter } from "../../core.interface";
2
+ import type { MediaValue } from "./media.interface";
3
+ /** Dedup key: `category=link` + `value` + `name` (matches register / find). */
4
+ export declare function mediaLinkDedupFilters(input: {
5
+ value: MediaValue;
6
+ name: string;
7
+ }): QueryFilter[];
8
+ //# sourceMappingURL=media.query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.query.d.ts","sourceRoot":"","sources":["../../../src/shared/media/media.query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,WAAW,EAAE,CAO/F"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Node-only media surface — for `src/node.ts` / `target-supabase-sdk/node` only.
3
+ * Never re-export from `../index.ts` or `browser.ts`.
4
+ */
5
+ export type { CheckMediaAvailabilityInput, MediaAvailabilityResult } from "./media.access";
6
+ export { checkMediaAvailability, probeMediaOriginal } from "./media.access";
7
+ export type { FindMediaLinkInput, RegisterMediaLinkInput, RegisterMediaLinkResult, } from "./media.service";
8
+ export { findMediaLink, registerMediaLink } from "./media.service";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/shared/media/node/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EAAE,2BAA2B,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC3F,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC5E,YAAY,EACR,kBAAkB,EAClB,sBAAsB,EACtB,uBAAuB,GAC1B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,40 @@
1
+ import { type MediaOriginal, type MediaValue } from "../media.interface";
2
+ export interface CheckMediaAvailabilityInput {
3
+ value: MediaValue;
4
+ storageProvider: string;
5
+ locator: string;
6
+ /**
7
+ * This host's storage id (e.g. from `LOCAL_STORAGE_PROVIDER`).
8
+ * Local path checks only run when it equals `storageProvider`.
9
+ */
10
+ localStorageProvider: string;
11
+ }
12
+ export interface MediaAvailabilityResult {
13
+ ok: boolean;
14
+ /** Absolute path when the locator was resolved on this host. */
15
+ absolutePath?: string;
16
+ reason?: string;
17
+ }
18
+ /**
19
+ * Fast local reachability check for a media locator.
20
+ *
21
+ * - Requires `storageProvider === localStorageProvider` (cross-host → not ok).
22
+ * - `locator` must be an absolute path.
23
+ * - `image-list`: directory exists; only immediate children are considered.
24
+ * - other values: path exists and is a non-empty file.
25
+ *
26
+ * **Node-only** — under `shared/media/node/`; do not import from the browser entry.
27
+ */
28
+ export declare function checkMediaAvailability(input: CheckMediaAvailabilityInput): Promise<MediaAvailabilityResult>;
29
+ /**
30
+ * Derive `contentHash` / `size` / `mimeType` for {@link MediaOriginal} after availability succeeds.
31
+ *
32
+ * **Node-only** — under `shared/media/node/`; do not import from the browser entry.
33
+ */
34
+ export declare function probeMediaOriginal(input: {
35
+ value: MediaValue;
36
+ storageProvider: string;
37
+ locator: string;
38
+ absolutePath: string;
39
+ }): Promise<MediaOriginal>;
40
+ //# sourceMappingURL=media.access.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.access.d.ts","sourceRoot":"","sources":["../../../../src/shared/media/node/media.access.ts"],"names":[],"mappings":"AAKA,OAAO,EAAgC,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGvG,MAAM,WAAW,2BAA2B;IACxC,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,OAAO,CAAC;IACZ,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AA+BD;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA0CjH;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,KAAK,EAAE;IAC5C,KAAK,EAAE,UAAU,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CA4BzB"}
@@ -0,0 +1,30 @@
1
+ import type { Link } from "../../../link/link.interface";
2
+ import { type MediaValue } from "../media.interface";
3
+ export interface FindMediaLinkInput {
4
+ value: MediaValue;
5
+ name: string;
6
+ }
7
+ /** Dedup lookup: same `category` + `value` + `name`. */
8
+ export declare function findMediaLink(input: FindMediaLinkInput): Promise<Link | null>;
9
+ export interface RegisterMediaLinkInput {
10
+ value: MediaValue;
11
+ name: string;
12
+ storageProvider: string;
13
+ locator: string;
14
+ /** This host id for availability (e.g. `process.env.LOCAL_STORAGE_PROVIDER`). */
15
+ localStorageProvider: string;
16
+ description?: string;
17
+ tagList?: string[];
18
+ }
19
+ export interface RegisterMediaLinkResult {
20
+ id: string;
21
+ created: boolean;
22
+ }
23
+ /**
24
+ * Register a media Link: dedup → availability → probe → `postTarget`.
25
+ * Idempotent when `category`+`value`+`name` already exist.
26
+ *
27
+ * **Node-only** — under `shared/media/node/`; do not import from the browser entry.
28
+ */
29
+ export declare function registerMediaLink(input: RegisterMediaLinkInput): Promise<RegisterMediaLinkResult>;
30
+ //# sourceMappingURL=media.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.service.d.ts","sourceRoot":"","sources":["../../../../src/shared/media/node/media.service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAGzD,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAMnE,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,wDAAwD;AACxD,wBAAsB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAYnF;AAED,MAAM,WAAW,sBAAsB;IACnC,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,oBAAoB,EAAE,MAAM,CAAC;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA4EvG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "target-supabase-sdk",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "description": "Supabase client initializer and data access APIs",
5
5
  "license": "MIT",
6
6
  "author": "KyLoc20",
package/src/browser.ts CHANGED
@@ -223,6 +223,17 @@ export {
223
223
  parseLogBatchMeta,
224
224
  resolveLogMinLevel,
225
225
  } from "./shared/log";
226
+ export type { BuildMediaLinkDraftInput, MediaOriginal, MediaValue } from "./shared/media";
227
+ export {
228
+ buildMediaLinkDraft,
229
+ buildMediaNameFromLocator,
230
+ guessMediaMime,
231
+ isMediaValue,
232
+ MEDIA_CONTENT_HASH_ALGORITHM,
233
+ MEDIA_CONTENT_HASH_PREFIX_LENGTH,
234
+ MEDIA_VALUES,
235
+ mediaLinkDedupFilters,
236
+ } from "./shared/media";
226
237
  export { isHttpUrl, resolveFetchUrl } from "./shared/utils/fetch-url";
227
238
  export { getValueAtPath } from "./shared/utils/get-value-at-path";
228
239
  export type {
package/src/node.ts CHANGED
@@ -165,6 +165,19 @@ export {
165
165
  shutdownLogSpool,
166
166
  } from "./shared/log/spool/service-lifecycle";
167
167
  export type { LogBatchMeta, LogPersistLane } from "./shared/log/upload/interface";
168
+ export type {
169
+ CheckMediaAvailabilityInput,
170
+ FindMediaLinkInput,
171
+ MediaAvailabilityResult,
172
+ RegisterMediaLinkInput,
173
+ RegisterMediaLinkResult,
174
+ } from "./shared/media/node";
175
+ export {
176
+ checkMediaAvailability,
177
+ findMediaLink,
178
+ probeMediaOriginal,
179
+ registerMediaLink,
180
+ } from "./shared/media/node";
168
181
  export type {
169
182
  ConfigSchema,
170
183
  LoadCachedJsConfigOptions,
@@ -0,0 +1,109 @@
1
+ # Media Links (`shared/media`)
2
+
3
+ Media is a **convention on `Link`**, not a new Target category (`category` stays `link`).
4
+
5
+ All media code lives under **`src/shared/media/`**. Node-only modules are kept in the **`node/`** subdirectory so the filesystem path matches the package entry split.
6
+
7
+ ## Package entry split (important)
8
+
9
+ | Consumer import | What you get | Source |
10
+ |-----------------|--------------|--------|
11
+ | `from "target-supabase-sdk"` | Browser-safe helpers only | `shared/media/index.ts` → `browser.ts` |
12
+ | `from "target-supabase-sdk/node"` | + register / availability / probe | `shared/media/node/` → `node.ts` |
13
+
14
+ There is **no** public subpath like `target-supabase-sdk/shared/media/node` — consumers always use the package `/node` entry.
15
+
16
+ **Do not** re-export anything from `node/` in `index.ts` / `browser.ts`. Those files import `node:fs` / `node:crypto` / `node:path`. Pulling them into the default entry breaks Chrome/Webpack bundles (`verify-browser-entry` will fail).
17
+
18
+ ```text
19
+ src/shared/media/
20
+ index.ts ← browser-safe barrel ONLY (never export ./node)
21
+ media.interface.ts
22
+ media.build.ts
23
+ media.mime.ts
24
+ media.name.ts
25
+ media.query.ts
26
+ README.md
27
+ node/ ← Node-only (fs / register)
28
+ index.ts ← barrel for src/node.ts only
29
+ media.access.ts
30
+ media.service.ts
31
+ ```
32
+
33
+ ### Correct consumer imports
34
+
35
+ ```typescript
36
+ // Default / browser — draft, mime, name, filters
37
+ import {
38
+ buildMediaLinkDraft,
39
+ buildMediaNameFromLocator,
40
+ MEDIA_VALUES,
41
+ mediaLinkDedupFilters,
42
+ } from "target-supabase-sdk";
43
+
44
+ // Node workers / CLI — dedup + availability + register
45
+ import {
46
+ findMediaLink,
47
+ checkMediaAvailability,
48
+ registerMediaLink,
49
+ } from "target-supabase-sdk/node";
50
+ ```
51
+
52
+ ### Incorrect
53
+
54
+ ```typescript
55
+ // BAD: Node-only APIs are not on the default entry
56
+ import { registerMediaLink } from "target-supabase-sdk";
57
+ ```
58
+
59
+ Internal SDK code:
60
+
61
+ - Browser-safe leaves: `from "../media.build"` (etc.)
62
+ - Node leaves: `from "./media.access"` inside `node/`, or `from "../shared/media/node"` from `src/node.ts`
63
+ - Never pull `./node` through the parent `index.ts`
64
+
65
+ ---
66
+
67
+ ## Link field rules
68
+
69
+ | Field | Rule |
70
+ |-------|------|
71
+ | `value` | `video` \| `audio` \| `image` \| `image-list` |
72
+ | `details.loaderKey` | Always equals `value` |
73
+ | `name` | Caller-provided unique key (or `buildMediaNameFromLocator`) |
74
+ | `tagList` / `description` | Caller-defined (e.g. producer / scene tags) |
75
+ | `details.manifestVersion` | `0` (via `buildLinkTargetDraft`) |
76
+ | `details.preview` | `""` |
77
+
78
+ ## `details.original`
79
+
80
+ | Key | Single file (`video`/`audio`/`image`) | `image-list` |
81
+ |-----|----------------------------------------|--------------|
82
+ | `storageProvider` | Host / backend id (e.g. `windows-storage:HOSTNAME`) | same |
83
+ | `locator` | Absolute file path | Absolute **directory** path |
84
+ | `contentHash` | SHA-256 hex (`MEDIA_CONTENT_HASH_ALGORITHM`) | `""` |
85
+ | `size` | Byte length | File count (immediate children only, non-recursive) |
86
+ | `mimeType` | e.g. `video/mp4` | `""` |
87
+
88
+ `image-list` rules:
89
+
90
+ 1. `locator` must be an **absolute** directory path.
91
+ 2. Only **one level** under that directory is considered (no recursion).
92
+ 3. `size` counts **files** in that level (directories among children are ignored).
93
+
94
+ ## Register flow (`registerMediaLink`)
95
+
96
+ 1. **Dedup** — `category` + `value` + `name` via `findMediaLink`. If present → `{ id, created: false }`.
97
+ 2. **Availability** — `storageProvider` must equal `localStorageProvider` (typically `LOCAL_STORAGE_PROVIDER` from the service `.env`); `locator` absolute and reachable.
98
+ 3. **Probe** — stream SHA-256 / `stat` / MIME (or directory file count for `image-list`).
99
+ 4. **Insert** — `buildMediaLinkDraft` → `postTarget` (race → re-find).
100
+
101
+ ## Env (Node hosts)
102
+
103
+ | Variable | Role |
104
+ |----------|------|
105
+ | `LOCAL_STORAGE_PROVIDER` | This machine’s storage id; pass as `localStorageProvider` into availability / register |
106
+
107
+ Cross-host `storageProvider` cannot be verified on the current process — availability returns `ok: false`.
108
+
109
+ See `.cursor/skills/media-link/SKILL.md`.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Media Link — browser-safe barrel only.
3
+ *
4
+ * Node-only code lives in `./node/` and is exported from `src/node.ts`,
5
+ * never from this file (see README.md).
6
+ */
7
+
8
+ export type { BuildMediaLinkDraftInput } from "./media.build";
9
+ export { buildMediaLinkDraft } from "./media.build";
10
+ export type { MediaOriginal, MediaValue } from "./media.interface";
11
+ export {
12
+ isMediaValue,
13
+ MEDIA_CONTENT_HASH_ALGORITHM,
14
+ MEDIA_CONTENT_HASH_PREFIX_LENGTH,
15
+ MEDIA_VALUES,
16
+ } from "./media.interface";
17
+ export { guessMediaMime } from "./media.mime";
18
+ export { buildMediaNameFromLocator } from "./media.name";
19
+ export { mediaLinkDedupFilters } from "./media.query";
@@ -0,0 +1,69 @@
1
+ import type { TargetDraft } from "../../core.interface";
2
+ import { buildLinkTargetDraft } from "../../link/link.build";
3
+ import type { Link } from "../../link/link.interface";
4
+ import { isMediaValue, type MediaOriginal, type MediaValue } from "./media.interface";
5
+
6
+ export interface BuildMediaLinkDraftInput {
7
+ value: MediaValue;
8
+ name: string;
9
+ storageProvider: string;
10
+ locator: string;
11
+ contentHash: string;
12
+ size: number;
13
+ mimeType: string;
14
+ description?: string;
15
+ tagList?: string[];
16
+ }
17
+
18
+ /**
19
+ * Assemble a media {@link Link} draft (`category=link`, `loaderKey === value`).
20
+ * Does not validate filesystem or write to Supabase.
21
+ */
22
+ export function buildMediaLinkDraft(input: BuildMediaLinkDraftInput): TargetDraft<Link> {
23
+ if (!isMediaValue(input.value)) {
24
+ throw new Error(`buildMediaLinkDraft: unsupported media value: ${String(input.value)}`);
25
+ }
26
+ const name = input.name.trim();
27
+ if (name === "") {
28
+ throw new Error("buildMediaLinkDraft: name is empty");
29
+ }
30
+ const storageProvider = input.storageProvider.trim();
31
+ if (storageProvider === "") {
32
+ throw new Error("buildMediaLinkDraft: storageProvider is empty");
33
+ }
34
+ const locator = input.locator.trim();
35
+ if (locator === "") {
36
+ throw new Error("buildMediaLinkDraft: locator is empty");
37
+ }
38
+ if (!Number.isFinite(input.size) || input.size < 0) {
39
+ throw new Error("buildMediaLinkDraft: size must be a non-negative number");
40
+ }
41
+
42
+ const isList = input.value === "image-list";
43
+ const contentHash = isList ? "" : input.contentHash.trim().toLowerCase();
44
+ const mimeType = isList ? "" : input.mimeType.trim();
45
+ if (!isList && contentHash === "") {
46
+ throw new Error("buildMediaLinkDraft: contentHash is required for single-file media");
47
+ }
48
+ if (!isList && mimeType === "") {
49
+ throw new Error("buildMediaLinkDraft: mimeType is required for single-file media");
50
+ }
51
+
52
+ const original: MediaOriginal = {
53
+ storageProvider,
54
+ locator,
55
+ contentHash,
56
+ size: Math.floor(input.size),
57
+ mimeType,
58
+ };
59
+
60
+ return buildLinkTargetDraft({
61
+ name,
62
+ value: input.value,
63
+ description: input.description ?? "",
64
+ preview: "",
65
+ loaderKey: input.value,
66
+ tagList: input.tagList ?? [],
67
+ original,
68
+ });
69
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Media Link protocol — Link rows with value/loaderKey in {@link MEDIA_VALUES}.
3
+ * No separate Target category; category remains `link`.
4
+ */
5
+
6
+ /** Fixed digest algorithm for single-file media `contentHash`. */
7
+ export const MEDIA_CONTENT_HASH_ALGORITHM = "sha256" as const;
8
+
9
+ /** Hex prefix length for {@link buildMediaNameFromLocator}. */
10
+ export const MEDIA_CONTENT_HASH_PREFIX_LENGTH = 6;
11
+
12
+ export const MEDIA_VALUES = ["video", "audio", "image", "image-list"] as const;
13
+
14
+ /** Discriminator stored in both `Link.value` and `details.loaderKey`. */
15
+ export type MediaValue = (typeof MEDIA_VALUES)[number];
16
+
17
+ /**
18
+ * `Link.details.original` for media Links.
19
+ *
20
+ * - `locator` — absolute filesystem path, or (future) network URL keyed by `storageProvider`.
21
+ * - `image-list`: `locator` is an absolute directory path; only the **immediate** children
22
+ * (one level, non-recursive) are counted. `contentHash` and `mimeType` are `""`;
23
+ * `size` is the number of files in that directory (not bytes).
24
+ * - other values: `size` is byte length; `contentHash` is SHA-256 hex; `mimeType` from the file.
25
+ */
26
+ export interface MediaOriginal {
27
+ storageProvider: string;
28
+ locator: string;
29
+ /**
30
+ * SHA-256 hex of file bytes, or `""` when `value === "image-list"`.
31
+ * Algorithm is always {@link MEDIA_CONTENT_HASH_ALGORITHM}.
32
+ */
33
+ contentHash: string;
34
+ /**
35
+ * Single-file media: byte length.
36
+ * `image-list`: number of files in the directory (one level).
37
+ */
38
+ size: number;
39
+ /** e.g. `video/mp4`, or `""` when `value === "image-list"`. */
40
+ mimeType: string;
41
+ }
42
+
43
+ export function isMediaValue(value: string): value is MediaValue {
44
+ return (MEDIA_VALUES as readonly string[]).includes(value);
45
+ }
@@ -0,0 +1,64 @@
1
+ import type { MediaValue } from "./media.interface";
2
+
3
+ const VIDEO_EXT_MIME: Record<string, string> = {
4
+ mp4: "video/mp4",
5
+ m4v: "video/mp4",
6
+ mov: "video/quicktime",
7
+ mkv: "video/x-matroska",
8
+ webm: "video/webm",
9
+ avi: "video/x-msvideo",
10
+ mpeg: "video/mpeg",
11
+ mpg: "video/mpeg",
12
+ wmv: "video/x-ms-wmv",
13
+ flv: "video/x-flv",
14
+ ts: "video/mp2t",
15
+ m2ts: "video/mp2t",
16
+ };
17
+
18
+ const AUDIO_EXT_MIME: Record<string, string> = {
19
+ wav: "audio/wav",
20
+ mp3: "audio/mpeg",
21
+ m4a: "audio/mp4",
22
+ aac: "audio/aac",
23
+ flac: "audio/flac",
24
+ ogg: "audio/ogg",
25
+ oga: "audio/ogg",
26
+ opus: "audio/opus",
27
+ wma: "audio/x-ms-wma",
28
+ };
29
+
30
+ const IMAGE_EXT_MIME: Record<string, string> = {
31
+ jpg: "image/jpeg",
32
+ jpeg: "image/jpeg",
33
+ png: "image/png",
34
+ webp: "image/webp",
35
+ gif: "image/gif",
36
+ bmp: "image/bmp",
37
+ tif: "image/tiff",
38
+ tiff: "image/tiff",
39
+ };
40
+
41
+ function extensionOf(pathOrUrl: string): string {
42
+ const cleaned = pathOrUrl.split(/[?#]/)[0] ?? pathOrUrl;
43
+ const base = cleaned.replace(/\\/g, "/").split("/").pop() ?? cleaned;
44
+ const dot = base.lastIndexOf(".");
45
+ if (dot <= 0 || dot === base.length - 1) {
46
+ return "";
47
+ }
48
+ return base.slice(dot + 1).toLowerCase();
49
+ }
50
+
51
+ /** Guess MIME from path extension; `null` when unknown. */
52
+ export function guessMediaMime(locator: string, value: Exclude<MediaValue, "image-list">): string | null {
53
+ const ext = extensionOf(locator);
54
+ if (ext === "") {
55
+ return null;
56
+ }
57
+ if (value === "video") {
58
+ return VIDEO_EXT_MIME[ext] ?? null;
59
+ }
60
+ if (value === "audio") {
61
+ return AUDIO_EXT_MIME[ext] ?? null;
62
+ }
63
+ return IMAGE_EXT_MIME[ext] ?? null;
64
+ }
@@ -0,0 +1,33 @@
1
+ import { MEDIA_CONTENT_HASH_PREFIX_LENGTH } from "./media.interface";
2
+
3
+ /**
4
+ * `{basename}.{hash6}.{ext}` from an absolute (or path-like) locator and full content hash.
5
+ * Does not touch the filesystem.
6
+ */
7
+ export function buildMediaNameFromLocator(locator: string, contentHash: string): string {
8
+ const trimmedLocator = locator.trim();
9
+ if (trimmedLocator === "") {
10
+ throw new Error("buildMediaNameFromLocator: locator is empty");
11
+ }
12
+ const hash = contentHash.trim().toLowerCase();
13
+ if (hash.length < MEDIA_CONTENT_HASH_PREFIX_LENGTH) {
14
+ throw new Error(
15
+ `buildMediaNameFromLocator: contentHash must be at least ${MEDIA_CONTENT_HASH_PREFIX_LENGTH} hex chars`,
16
+ );
17
+ }
18
+ const hash6 = hash.slice(0, MEDIA_CONTENT_HASH_PREFIX_LENGTH);
19
+
20
+ const normalized = trimmedLocator.replace(/\\/g, "/");
21
+ const base = normalized.split("/").pop() ?? normalized;
22
+ if (base === "" || base === "." || base === "..") {
23
+ throw new Error(`buildMediaNameFromLocator: invalid basename from locator: ${locator}`);
24
+ }
25
+
26
+ const dot = base.lastIndexOf(".");
27
+ if (dot <= 0 || dot === base.length - 1) {
28
+ return `${base}.${hash6}`;
29
+ }
30
+ const stem = base.slice(0, dot);
31
+ const ext = base.slice(dot + 1);
32
+ return `${stem}.${hash6}.${ext}`;
33
+ }
@@ -0,0 +1,13 @@
1
+ import type { QueryFilter } from "../../core.interface";
2
+ import { CategoryLink } from "../../link/link.interface";
3
+ import type { MediaValue } from "./media.interface";
4
+
5
+ /** Dedup key: `category=link` + `value` + `name` (matches register / find). */
6
+ export function mediaLinkDedupFilters(input: { value: MediaValue; name: string }): QueryFilter[] {
7
+ const name = input.name.trim();
8
+ return [
9
+ { field: "category", operator: "eq", value: CategoryLink.LINK },
10
+ { field: "value", operator: "eq", value: input.value },
11
+ { field: "name", operator: "eq", value: name },
12
+ ];
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Node-only media surface — for `src/node.ts` / `target-supabase-sdk/node` only.
3
+ * Never re-export from `../index.ts` or `browser.ts`.
4
+ */
5
+
6
+ export type { CheckMediaAvailabilityInput, MediaAvailabilityResult } from "./media.access";
7
+ export { checkMediaAvailability, probeMediaOriginal } from "./media.access";
8
+ export type {
9
+ FindMediaLinkInput,
10
+ RegisterMediaLinkInput,
11
+ RegisterMediaLinkResult,
12
+ } from "./media.service";
13
+ export { findMediaLink, registerMediaLink } from "./media.service";