@ai-matrx/media 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @ai-matrx/media
2
2
 
3
+ ## 0.9.2
4
+
5
+ `filesDb(client)` and the engine's `iamDb(client)` take any supabase-js client: a host's client is
6
+ typed by the host's own generated Database, which never has to match the package's snapshot (0.9.0's
7
+ `C extends SupabaseClient<Database>` refused the app's client as soon as its types were regenerated).
8
+ The result is typed by the package's `files` / `iam` schema. Engine DB types regenerated. No consumer
9
+ action required.
10
+
11
+ ## 0.9.1
12
+
13
+ `configureFilesHost` keeps the host on a `globalThis` `Symbol.for("ai-matrx.media.files-host")`
14
+ slot instead of module scope, so a test runner's module reset (or a second loader graph) still
15
+ sees the one configured host. No consumer action required.
16
+
3
17
  ## 0.9.0
4
18
 
5
19
  New subpath `@ai-matrx/media/files/engine` plus one subpath per engine module
@@ -38393,7 +38393,7 @@ export type FilesDatabase = {
38393
38393
  };
38394
38394
  Returns: Json;
38395
38395
  };
38396
- get_structured_list_for_selection: {
38396
+ get_pick_list_for_selection: {
38397
38397
  Args: {
38398
38398
  p_list_id: string;
38399
38399
  };
@@ -38556,7 +38556,7 @@ export type FilesDatabase = {
38556
38556
  };
38557
38557
  Returns: Json;
38558
38558
  };
38559
- get_user_lists_summary: {
38559
+ get_pick_lists_summary: {
38560
38560
  Args: {
38561
38561
  p_user_id: string;
38562
38562
  };
@@ -26,8 +26,7 @@
26
26
  */
27
27
  import type { SupabaseClient } from "@supabase/supabase-js";
28
28
  import type { Database } from "./db-types.js";
29
- /** A supabase client scoped to the `files` schema. */
30
- export declare function filesDb<C extends SupabaseClient<Database>>(client: C): import("@supabase/postgrest-js").PostgrestClient<import("./db-types.js").FilesDatabase, {
29
+ declare const filesSchemaOf: (client: SupabaseClient<Database>) => import("@supabase/postgrest-js").PostgrestClient<import("./db-types.js").FilesDatabase, {
31
30
  PostgrestVersion: "14.5";
32
31
  }, "files", {
33
32
  Tables: {
@@ -1848,6 +1847,14 @@ export declare function filesDb<C extends SupabaseClient<Database>>(client: C):
1848
1847
  Enums: { [_ in never]: never; };
1849
1848
  CompositeTypes: { [_ in never]: never; };
1850
1849
  }>;
1850
+ /** The `files` schema client, typed by this package's own generated types. */
1851
+ export type FilesSchemaClient = ReturnType<typeof filesSchemaOf>;
1852
+ /**
1853
+ * A supabase client scoped to the `files` schema. Takes ANY supabase-js client — a host's
1854
+ * client is typed by the host's own (newer or older) generated Database, which never has to
1855
+ * match this package's snapshot; the result is typed by the package's `files` schema.
1856
+ */
1857
+ export declare function filesDb(client: SupabaseClient<any, any, any>): FilesSchemaClient;
1851
1858
  /**
1852
1859
  * The ONLY legal column list for reading `files.files` from the client.
1853
1860
  *
@@ -1911,3 +1918,4 @@ export declare function readFileRowById<C extends SupabaseClient<Database>>(clie
1911
1918
  * this cheap enough to call for a whole pile at once.
1912
1919
  */
1913
1920
  export declare function readFileSizesByIds<C extends SupabaseClient<Database>>(client: C, fileIds: string[]): Promise<Map<string, number | null>>;
1921
+ export {};
@@ -1,5 +1,6 @@
1
+ const filesSchemaOf = (client) => client.schema("files");
1
2
  function filesDb(client) {
2
- return client.schema("files");
3
+ return filesSchemaOf(client);
3
4
  }
4
5
  const FILES_TABLE_COLUMNS = "id, created_by, updated_by, version, file_path, file_name, mime_type, size_bytes, checksum, visibility, current_version, parent_folder_id, metadata, created_at, updated_at, deleted_at, organization_id, parent_file_id, derivation_kind, derivation_metadata, duplicate_of_file_id, canonical_processed_document_id, width, height, duration_ms, origin_device_id, artifact_kind";
5
6
  const FILE_VERSIONS_TABLE_COLUMNS = "id, file_id, version_number, size_bytes, checksum, created_by, created_at, change_summary, organization_id, metadata";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/files/engine/filesDb.ts"],"sourcesContent":["/**\n * features/files/filesDb.ts\n *\n * The cloud-files tables live in the dedicated `files` Postgres schema (moved\n * out of `public` in the 2026 DB restructure; the `cld_` prefix was dropped).\n * supabase-js reaches a non-public schema via `.schema(...)`, so every table\n * read/write for these tables must go through `client.schema('files')`.\n *\n * Use this helper instead of inlining `.schema('files')` everywhere:\n *\n * const db = filesDb(supabase);\n * const { data } = await db.from('files').select(FILES_TABLE_COLUMNS);\n * await db.from('folders').upsert(...); // files.folders\n *\n * NEVER `select('*')` on `files.files` / `files.file_versions` — the\n * server-only `storage_uri` column grant is REVOKEd and `*` errors.\n *\n * Works with the browser client, the SSR server client, and the admin client —\n * they all expose `.schema()`.\n *\n * NOTE: file-permission grants are NOT in this schema — they live in the\n * canonical `iam.permissions` store (resource_type='file'/'folder'). Reach\n * those via `iamDb(supabase).rpc('fn_list_resource_permissions', ...)` (see\n * utils/supabase/iamDb.ts) — list/grant/revoke are admin-gated inside the\n * RPC (iam.has_access), never a plain `.from('permissions')` select.\n */\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { Database } from \"./db-types\";\n\n/** A supabase client scoped to the `files` schema. */\nexport function filesDb<C extends SupabaseClient<Database>>(client: C) {\n return client.schema(\"files\");\n}\n\n/**\n * The ONLY legal column list for reading `files.files` from the client.\n *\n * `storage_uri` (the native S3 location) is server-only: the column grant is\n * REVOKEd from `authenticated`, so `select(\"*\")` on `files.files` ERRORS with\n * \"permission denied for column storage_uri\". Never select `*` on this table\n * and never add `storage_uri` here. Renderable URLs come from the server's\n * FileRecord/FileRef URL contract (`url` / `cdn_url` / `download_url`),\n * never assembled client-side from a storage location.\n */\nexport const FILES_TABLE_COLUMNS =\n \"id, created_by, updated_by, version, file_path, file_name, mime_type, size_bytes, checksum, visibility, current_version, parent_folder_id, metadata, created_at, updated_at, deleted_at, organization_id, parent_file_id, derivation_kind, derivation_metadata, duplicate_of_file_id, canonical_processed_document_id, width, height, duration_ms, origin_device_id, artifact_kind\";\n\n/**\n * Same rule for `files.file_versions` — its `storage_uri` is server-only.\n * Version bytes download via the server (`/files/{id}/versions/{n}/download`).\n */\nexport const FILE_VERSIONS_TABLE_COLUMNS =\n \"id, file_id, version_number, size_bytes, checksum, created_by, created_at, change_summary, organization_id, metadata\";\n\n/**\n * Read one complete, RLS-authorized file row by durable identity.\n *\n * REST `FileRecord` is a render/navigation envelope and historically omitted\n * the owning organization. Mutation preparation must use this canonical DB\n * row so a path-targeted version write can preserve ownership exactly.\n */\nexport async function readFileRowById<C extends SupabaseClient<Database>>(\n client: C,\n fileId: string,\n) {\n const { data, error } = await filesDb(client)\n .from(\"files\")\n .select(FILES_TABLE_COLUMNS)\n .eq(\"id\", fileId)\n .is(\"deleted_at\", null)\n .maybeSingle();\n if (error) throw error;\n return data;\n}\n\n/**\n * Batch-read just the byte size for a set of file ids, RLS-authorized like\n * every other read here.\n *\n * Built for a source list that names files by id (a Rulebook's attached\n * sources, a resource pile) and needs to show — before any run touches\n * them — which of those files are genuinely empty. `size_bytes` alone (no\n * `storage_uri`, no need for the single-row reader's full column list) keeps\n * this cheap enough to call for a whole pile at once.\n */\nexport async function readFileSizesByIds<C extends SupabaseClient<Database>>(\n client: C,\n fileIds: string[],\n): Promise<Map<string, number | null>> {\n const out = new Map<string, number | null>();\n if (fileIds.length === 0) return out;\n const { data, error } = await filesDb(client)\n .from(\"files\")\n .select(\"id, size_bytes\")\n .in(\"id\", fileIds)\n .is(\"deleted_at\", null);\n if (error) throw error;\n for (const row of data ?? []) {\n out.set(row.id, row.size_bytes ?? null);\n }\n return out;\n}\n"],"mappings":"AA8BO,SAAS,QAA4C,QAAW;AACrE,SAAO,OAAO,OAAO,OAAO;AAC9B;AAYO,MAAM,sBACX;AAMK,MAAM,8BACX;AASF,eAAsB,gBACpB,QACA,QACA;AACA,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,QAAQ,MAAM,EACzC,KAAK,OAAO,EACZ,OAAO,mBAAmB,EAC1B,GAAG,MAAM,MAAM,EACf,GAAG,cAAc,IAAI,EACrB,YAAY;AACf,MAAI,MAAO,OAAM;AACjB,SAAO;AACT;AAYA,eAAsB,mBACpB,QACA,SACqC;AACrC,QAAM,MAAM,oBAAI,IAA2B;AAC3C,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,QAAQ,MAAM,EACzC,KAAK,OAAO,EACZ,OAAO,gBAAgB,EACvB,GAAG,MAAM,OAAO,EAChB,GAAG,cAAc,IAAI;AACxB,MAAI,MAAO,OAAM;AACjB,aAAW,OAAO,QAAQ,CAAC,GAAG;AAC5B,QAAI,IAAI,IAAI,IAAI,IAAI,cAAc,IAAI;AAAA,EACxC;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/files/engine/filesDb.ts"],"sourcesContent":["/**\n * features/files/filesDb.ts\n *\n * The cloud-files tables live in the dedicated `files` Postgres schema (moved\n * out of `public` in the 2026 DB restructure; the `cld_` prefix was dropped).\n * supabase-js reaches a non-public schema via `.schema(...)`, so every table\n * read/write for these tables must go through `client.schema('files')`.\n *\n * Use this helper instead of inlining `.schema('files')` everywhere:\n *\n * const db = filesDb(supabase);\n * const { data } = await db.from('files').select(FILES_TABLE_COLUMNS);\n * await db.from('folders').upsert(...); // files.folders\n *\n * NEVER `select('*')` on `files.files` / `files.file_versions` — the\n * server-only `storage_uri` column grant is REVOKEd and `*` errors.\n *\n * Works with the browser client, the SSR server client, and the admin client —\n * they all expose `.schema()`.\n *\n * NOTE: file-permission grants are NOT in this schema — they live in the\n * canonical `iam.permissions` store (resource_type='file'/'folder'). Reach\n * those via `iamDb(supabase).rpc('fn_list_resource_permissions', ...)` (see\n * utils/supabase/iamDb.ts) — list/grant/revoke are admin-gated inside the\n * RPC (iam.has_access), never a plain `.from('permissions')` select.\n */\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { Database } from \"./db-types\";\n\nconst filesSchemaOf = (client: SupabaseClient<Database>) => client.schema(\"files\");\n\n/** The `files` schema client, typed by this package's own generated types. */\nexport type FilesSchemaClient = ReturnType<typeof filesSchemaOf>;\n\n/**\n * A supabase client scoped to the `files` schema. Takes ANY supabase-js client — a host's\n * client is typed by the host's own (newer or older) generated Database, which never has to\n * match this package's snapshot; the result is typed by the package's `files` schema.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function filesDb(client: SupabaseClient<any, any, any>): FilesSchemaClient {\n return filesSchemaOf(client as unknown as SupabaseClient<Database>);\n}\n\n/**\n * The ONLY legal column list for reading `files.files` from the client.\n *\n * `storage_uri` (the native S3 location) is server-only: the column grant is\n * REVOKEd from `authenticated`, so `select(\"*\")` on `files.files` ERRORS with\n * \"permission denied for column storage_uri\". Never select `*` on this table\n * and never add `storage_uri` here. Renderable URLs come from the server's\n * FileRecord/FileRef URL contract (`url` / `cdn_url` / `download_url`),\n * never assembled client-side from a storage location.\n */\nexport const FILES_TABLE_COLUMNS =\n \"id, created_by, updated_by, version, file_path, file_name, mime_type, size_bytes, checksum, visibility, current_version, parent_folder_id, metadata, created_at, updated_at, deleted_at, organization_id, parent_file_id, derivation_kind, derivation_metadata, duplicate_of_file_id, canonical_processed_document_id, width, height, duration_ms, origin_device_id, artifact_kind\";\n\n/**\n * Same rule for `files.file_versions` — its `storage_uri` is server-only.\n * Version bytes download via the server (`/files/{id}/versions/{n}/download`).\n */\nexport const FILE_VERSIONS_TABLE_COLUMNS =\n \"id, file_id, version_number, size_bytes, checksum, created_by, created_at, change_summary, organization_id, metadata\";\n\n/**\n * Read one complete, RLS-authorized file row by durable identity.\n *\n * REST `FileRecord` is a render/navigation envelope and historically omitted\n * the owning organization. Mutation preparation must use this canonical DB\n * row so a path-targeted version write can preserve ownership exactly.\n */\nexport async function readFileRowById<C extends SupabaseClient<Database>>(\n client: C,\n fileId: string,\n) {\n const { data, error } = await filesDb(client)\n .from(\"files\")\n .select(FILES_TABLE_COLUMNS)\n .eq(\"id\", fileId)\n .is(\"deleted_at\", null)\n .maybeSingle();\n if (error) throw error;\n return data;\n}\n\n/**\n * Batch-read just the byte size for a set of file ids, RLS-authorized like\n * every other read here.\n *\n * Built for a source list that names files by id (a Rulebook's attached\n * sources, a resource pile) and needs to show — before any run touches\n * them — which of those files are genuinely empty. `size_bytes` alone (no\n * `storage_uri`, no need for the single-row reader's full column list) keeps\n * this cheap enough to call for a whole pile at once.\n */\nexport async function readFileSizesByIds<C extends SupabaseClient<Database>>(\n client: C,\n fileIds: string[],\n): Promise<Map<string, number | null>> {\n const out = new Map<string, number | null>();\n if (fileIds.length === 0) return out;\n const { data, error } = await filesDb(client)\n .from(\"files\")\n .select(\"id, size_bytes\")\n .in(\"id\", fileIds)\n .is(\"deleted_at\", null);\n if (error) throw error;\n for (const row of data ?? []) {\n out.set(row.id, row.size_bytes ?? null);\n }\n return out;\n}\n"],"mappings":"AA6BA,MAAM,gBAAgB,CAAC,WAAqC,OAAO,OAAO,OAAO;AAW1E,SAAS,QAAQ,QAA0D;AAChF,SAAO,cAAc,MAA6C;AACpE;AAYO,MAAM,sBACX;AAMK,MAAM,8BACX;AASF,eAAsB,gBACpB,QACA,QACA;AACA,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,QAAQ,MAAM,EACzC,KAAK,OAAO,EACZ,OAAO,mBAAmB,EAC1B,GAAG,MAAM,MAAM,EACf,GAAG,cAAc,IAAI,EACrB,YAAY;AACf,MAAI,MAAO,OAAM;AACjB,SAAO;AACT;AAYA,eAAsB,mBACpB,QACA,SACqC;AACrC,QAAM,MAAM,oBAAI,IAA2B;AAC3C,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,QAAQ,MAAM,EACzC,KAAK,OAAO,EACZ,OAAO,gBAAgB,EACvB,GAAG,MAAM,OAAO,EAChB,GAAG,cAAc,IAAI;AACxB,MAAI,MAAO,OAAM;AACjB,aAAW,OAAO,QAAQ,CAAC,GAAG;AAC5B,QAAI,IAAI,IAAI,IAAI,IAAI,cAAc,IAAI;AAAA,EACxC;AACA,SAAO;AACT;","names":[]}
@@ -6,19 +6,20 @@ class FilesHostNotConfiguredError extends Error {
6
6
  this.name = "FilesHostNotConfiguredError";
7
7
  }
8
8
  }
9
- let host = null;
9
+ const SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.media.files-host");
10
10
  function configureFilesHost(next) {
11
- host = next;
11
+ globalThis[SLOT] = next;
12
12
  }
13
13
  function isFilesHostConfigured() {
14
- return host !== null;
14
+ return Boolean(globalThis[SLOT]);
15
15
  }
16
16
  function getFilesHost(member) {
17
+ const host = globalThis[SLOT];
17
18
  if (!host) throw new FilesHostNotConfiguredError(member);
18
19
  return host;
19
20
  }
20
21
  function __resetFilesHostForTests() {
21
- host = null;
22
+ globalThis[SLOT] = null;
22
23
  }
23
24
  export {
24
25
  FilesHostNotConfiguredError,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/files/engine/host/configure.ts"],"sourcesContent":["/**\n * The files engine's host port — the ONE place a client hands the engine what\n * only it can own: its database client, its AI Matrx server client, its Redux\n * store, its share-link calls, its organization chooser and its notifications.\n *\n * Everything else the engine needs (the slice, thunks, middlewares, uploads,\n * the resolver, the typed REST verbs, session retry, claims) is package code.\n *\n * Configure once, before the store is created:\n *\n * configureFilesHost({ db, server, store: () => store, ... });\n *\n * Every seam module reads the host at CALL time, never at import time, so the\n * order of module loading never matters. Used before configuration, a call\n * throws `FilesHostNotConfiguredError`, which names the remedy.\n */\n\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nexport interface FilesRequestOptions {\n requestId?: string;\n signal?: AbortSignal;\n baseUrlOverride?: string;\n timeoutMs?: number;\n captureErrors?: boolean;\n guestFingerprint?: string;\n idempotencyKey?: string;\n organizationId?: string;\n bodyCarriedRead?: boolean;\n cloudFilesBypass?: string;\n}\n\nexport interface FilesResponseMeta {\n requestId: string;\n status: number;\n serverRequestId: string | null;\n}\n\nexport interface FilesUploadProgressEvent {\n loaded: number;\n total: number;\n}\n\nexport interface FilesDownloadProgressEvent {\n loaded: number;\n total: number | null;\n}\n\ntype Envelope<T> = Promise<{ data: T; meta: FilesResponseMeta }>;\ntype BlobEnvelope = Promise<{ blob: Blob; meta: FilesResponseMeta; filename: string | null }>;\n\n/** The host's AI Matrx server client (matrx-frontend: `lib/python-client`). */\nexport interface FilesServerClient {\n resolveFilesBaseUrl(override?: string): string;\n resolveBaseUrlForPath(path: string, override?: string, method?: string): string;\n buildHeaders(\n opts: FilesRequestOptions,\n includeContentType: boolean,\n method?: string,\n ): Promise<{ headers: Record<string, string>; requestId: string }>;\n getJson<T>(path: string, opts?: FilesRequestOptions): Envelope<T>;\n postJson<T, B = unknown>(path: string, body: B, opts?: FilesRequestOptions): Envelope<T>;\n patchJson<T, B = unknown>(path: string, body: B, opts?: FilesRequestOptions): Envelope<T>;\n putJson<T, B = unknown>(path: string, body: B, opts?: FilesRequestOptions): Envelope<T>;\n del<T = null>(path: string, opts?: FilesRequestOptions): Promise<{ data: T | null; meta: FilesResponseMeta }>;\n delJson<T = null, B = unknown>(\n path: string,\n body: B,\n opts?: FilesRequestOptions,\n ): Promise<{ data: T | null; meta: FilesResponseMeta }>;\n postMultipart<T>(path: string, form: FormData, opts?: FilesRequestOptions): Envelope<T>;\n uploadWithProgress<T>(\n path: string,\n form: FormData,\n onProgress: (event: FilesUploadProgressEvent) => void,\n opts?: FilesRequestOptions,\n ): Envelope<T>;\n downloadBlob(path: string, opts?: FilesRequestOptions): BlobEnvelope;\n downloadBlobWithProgress(\n path: string,\n onProgress: (event: FilesDownloadProgressEvent) => void,\n opts?: FilesRequestOptions,\n ): BlobEnvelope;\n}\n\nexport interface FilesShareLink {\n id: string;\n token: string;\n shortToken: string | null;\n permissionLevel: string;\n label: string | null;\n expiresAt: string | null;\n maxUses: number | null;\n useCount: number;\n isActive: boolean;\n createdAt: string | null;\n lastUsedAt: string | null;\n}\n\nexport interface FilesCreateShareLinkOptions {\n resourceType: string;\n resourceId: string;\n permissionLevel?: string;\n expiresAt?: string | null;\n maxUses?: number | null;\n label?: string | null;\n}\n\n/** The host's share-link store (matrx-frontend: `utils/permissions/shareLinks`). */\nexport interface FilesShareLinks {\n shareLinkUrl(token: string): string;\n createShareLink(options: FilesCreateShareLinkOptions): Promise<{\n success: boolean;\n token?: string;\n shortToken?: string | null;\n url?: string;\n error?: string;\n }>;\n listShareLinks(resourceType: string, resourceId: string): Promise<FilesShareLink[]>;\n revokeShareLink(linkId: string): Promise<{ success: boolean; error?: string }>;\n}\n\n/** The store the engine's slice is mounted in (under `cloudFiles`). */\nexport interface FilesStore {\n getState(): unknown;\n dispatch(action: unknown): unknown;\n}\n\n/** Where new uploads land by default: the person's current organization / project / task. */\nexport interface FilesScope {\n /** The signed-in person (ownership checks). */\n userId?: string | null;\n organizationId: string | null;\n projectId: string | null;\n taskId: string | null;\n}\n\nexport interface FilesNotifyPort {\n success(message: string, options?: { description?: string; id?: string | number; duration?: number }): void;\n error(message: string, options?: { description?: string; id?: string | number; duration?: number }): void;\n}\n\nexport interface FilesHost {\n /** A supabase-js client for the signed-in person (the engine reads the `files` and `iam` schemas). */\n db: SupabaseClient<any, any, any>;\n server: FilesServerClient;\n /** The store the `cloudFiles` reducer is mounted in; null until it exists. */\n store(): FilesStore | null;\n /** The person's current organization / project / task, read from `state`. Default: none. */\n scope?(state: unknown): FilesScope;\n /**\n * Resolve the organization a write lands in. Given a declared id, return it;\n * otherwise ask the person (the host's chooser). Default: the declared id,\n * else the scope's organization, else a named error.\n */\n ensureOrganizationContext?(options: { organizationId?: string | null }): Promise<string>;\n shareLinks?: FilesShareLinks;\n notify?: FilesNotifyPort;\n}\n\nexport class FilesHostNotConfiguredError extends Error {\n constructor(member: string) {\n super(\n `@ai-matrx/media/files-engine: \"${member}\" was used before configureFilesHost(...) ran. ` +\n \"Call configureFilesHost({ db, server, store }) once at startup, before the store is created.\",\n );\n this.name = \"FilesHostNotConfiguredError\";\n }\n}\n\nlet host: FilesHost | null = null;\n\nexport function configureFilesHost(next: FilesHost): void {\n host = next;\n}\n\nexport function isFilesHostConfigured(): boolean {\n return host !== null;\n}\n\nexport function getFilesHost(member: string): FilesHost {\n if (!host) throw new FilesHostNotConfiguredError(member);\n return host;\n}\n\n/** For tests: drop the configured host. */\nexport function __resetFilesHostForTests(): void {\n host = null;\n}\n"],"mappings":"AAgKO,MAAM,oCAAoC,MAAM;AAAA,EACrD,YAAY,QAAgB;AAC1B;AAAA,MACE,kCAAkC,MAAM;AAAA,IAE1C;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAI,OAAyB;AAEtB,SAAS,mBAAmB,MAAuB;AACxD,SAAO;AACT;AAEO,SAAS,wBAAiC;AAC/C,SAAO,SAAS;AAClB;AAEO,SAAS,aAAa,QAA2B;AACtD,MAAI,CAAC,KAAM,OAAM,IAAI,4BAA4B,MAAM;AACvD,SAAO;AACT;AAGO,SAAS,2BAAiC;AAC/C,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../../src/files/engine/host/configure.ts"],"sourcesContent":["/**\n * The files engine's host port — the ONE place a client hands the engine what\n * only it can own: its database client, its AI Matrx server client, its Redux\n * store, its share-link calls, its organization chooser and its notifications.\n *\n * Everything else the engine needs (the slice, thunks, middlewares, uploads,\n * the resolver, the typed REST verbs, session retry, claims) is package code.\n *\n * Configure once, before the store is created:\n *\n * configureFilesHost({ db, server, store: () => store, ... });\n *\n * Every seam module reads the host at CALL time, never at import time, so the\n * order of module loading never matters. Used before configuration, a call\n * throws `FilesHostNotConfiguredError`, which names the remedy.\n */\n\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\n\nexport interface FilesRequestOptions {\n requestId?: string;\n signal?: AbortSignal;\n baseUrlOverride?: string;\n timeoutMs?: number;\n captureErrors?: boolean;\n guestFingerprint?: string;\n idempotencyKey?: string;\n organizationId?: string;\n bodyCarriedRead?: boolean;\n cloudFilesBypass?: string;\n}\n\nexport interface FilesResponseMeta {\n requestId: string;\n status: number;\n serverRequestId: string | null;\n}\n\nexport interface FilesUploadProgressEvent {\n loaded: number;\n total: number;\n}\n\nexport interface FilesDownloadProgressEvent {\n loaded: number;\n total: number | null;\n}\n\ntype Envelope<T> = Promise<{ data: T; meta: FilesResponseMeta }>;\ntype BlobEnvelope = Promise<{ blob: Blob; meta: FilesResponseMeta; filename: string | null }>;\n\n/** The host's AI Matrx server client (matrx-frontend: `lib/python-client`). */\nexport interface FilesServerClient {\n resolveFilesBaseUrl(override?: string): string;\n resolveBaseUrlForPath(path: string, override?: string, method?: string): string;\n buildHeaders(\n opts: FilesRequestOptions,\n includeContentType: boolean,\n method?: string,\n ): Promise<{ headers: Record<string, string>; requestId: string }>;\n getJson<T>(path: string, opts?: FilesRequestOptions): Envelope<T>;\n postJson<T, B = unknown>(path: string, body: B, opts?: FilesRequestOptions): Envelope<T>;\n patchJson<T, B = unknown>(path: string, body: B, opts?: FilesRequestOptions): Envelope<T>;\n putJson<T, B = unknown>(path: string, body: B, opts?: FilesRequestOptions): Envelope<T>;\n del<T = null>(path: string, opts?: FilesRequestOptions): Promise<{ data: T | null; meta: FilesResponseMeta }>;\n delJson<T = null, B = unknown>(\n path: string,\n body: B,\n opts?: FilesRequestOptions,\n ): Promise<{ data: T | null; meta: FilesResponseMeta }>;\n postMultipart<T>(path: string, form: FormData, opts?: FilesRequestOptions): Envelope<T>;\n uploadWithProgress<T>(\n path: string,\n form: FormData,\n onProgress: (event: FilesUploadProgressEvent) => void,\n opts?: FilesRequestOptions,\n ): Envelope<T>;\n downloadBlob(path: string, opts?: FilesRequestOptions): BlobEnvelope;\n downloadBlobWithProgress(\n path: string,\n onProgress: (event: FilesDownloadProgressEvent) => void,\n opts?: FilesRequestOptions,\n ): BlobEnvelope;\n}\n\nexport interface FilesShareLink {\n id: string;\n token: string;\n shortToken: string | null;\n permissionLevel: string;\n label: string | null;\n expiresAt: string | null;\n maxUses: number | null;\n useCount: number;\n isActive: boolean;\n createdAt: string | null;\n lastUsedAt: string | null;\n}\n\nexport interface FilesCreateShareLinkOptions {\n resourceType: string;\n resourceId: string;\n permissionLevel?: string;\n expiresAt?: string | null;\n maxUses?: number | null;\n label?: string | null;\n}\n\n/** The host's share-link store (matrx-frontend: `utils/permissions/shareLinks`). */\nexport interface FilesShareLinks {\n shareLinkUrl(token: string): string;\n createShareLink(options: FilesCreateShareLinkOptions): Promise<{\n success: boolean;\n token?: string;\n shortToken?: string | null;\n url?: string;\n error?: string;\n }>;\n listShareLinks(resourceType: string, resourceId: string): Promise<FilesShareLink[]>;\n revokeShareLink(linkId: string): Promise<{ success: boolean; error?: string }>;\n}\n\n/** The store the engine's slice is mounted in (under `cloudFiles`). */\nexport interface FilesStore {\n getState(): unknown;\n dispatch(action: unknown): unknown;\n}\n\n/** Where new uploads land by default: the person's current organization / project / task. */\nexport interface FilesScope {\n /** The signed-in person (ownership checks). */\n userId?: string | null;\n organizationId: string | null;\n projectId: string | null;\n taskId: string | null;\n}\n\nexport interface FilesNotifyPort {\n success(message: string, options?: { description?: string; id?: string | number; duration?: number }): void;\n error(message: string, options?: { description?: string; id?: string | number; duration?: number }): void;\n}\n\nexport interface FilesHost {\n /** A supabase-js client for the signed-in person (the engine reads the `files` and `iam` schemas). */\n db: SupabaseClient<any, any, any>;\n server: FilesServerClient;\n /** The store the `cloudFiles` reducer is mounted in; null until it exists. */\n store(): FilesStore | null;\n /** The person's current organization / project / task, read from `state`. Default: none. */\n scope?(state: unknown): FilesScope;\n /**\n * Resolve the organization a write lands in. Given a declared id, return it;\n * otherwise ask the person (the host's chooser). Default: the declared id,\n * else the scope's organization, else a named error.\n */\n ensureOrganizationContext?(options: { organizationId?: string | null }): Promise<string>;\n shareLinks?: FilesShareLinks;\n notify?: FilesNotifyPort;\n}\n\nexport class FilesHostNotConfiguredError extends Error {\n constructor(member: string) {\n super(\n `@ai-matrx/media/files-engine: \"${member}\" was used before configureFilesHost(...) ran. ` +\n \"Call configureFilesHost({ db, server, store }) once at startup, before the store is created.\",\n );\n this.name = \"FilesHostNotConfiguredError\";\n }\n}\n\n// The host lives on a globalThis Symbol.for slot, not module scope: a test\n// runner's module reset, or a second copy of this module in another loader\n// graph, must still see the ONE host the app configured.\nconst SLOT = Symbol.for(\"ai-matrx.media.files-host\");\ntype Slot = { [SLOT]?: FilesHost | null };\n\nexport function configureFilesHost(next: FilesHost): void {\n (globalThis as Slot)[SLOT] = next;\n}\n\nexport function isFilesHostConfigured(): boolean {\n return Boolean((globalThis as Slot)[SLOT]);\n}\n\nexport function getFilesHost(member: string): FilesHost {\n const host = (globalThis as Slot)[SLOT];\n if (!host) throw new FilesHostNotConfiguredError(member);\n return host;\n}\n\n/** For tests: drop the configured host. */\nexport function __resetFilesHostForTests(): void {\n (globalThis as Slot)[SLOT] = null;\n}\n"],"mappings":"AAgKO,MAAM,oCAAoC,MAAM;AAAA,EACrD,YAAY,QAAgB;AAC1B;AAAA,MACE,kCAAkC,MAAM;AAAA,IAE1C;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAKA,MAAM,OAAO,uBAAO,IAAI,2BAA2B;AAG5C,SAAS,mBAAmB,MAAuB;AACxD,EAAC,WAAoB,IAAI,IAAI;AAC/B;AAEO,SAAS,wBAAiC;AAC/C,SAAO,QAAS,WAAoB,IAAI,CAAC;AAC3C;AAEO,SAAS,aAAa,QAA2B;AACtD,QAAM,OAAQ,WAAoB,IAAI;AACtC,MAAI,CAAC,KAAM,OAAM,IAAI,4BAA4B,MAAM;AACvD,SAAO;AACT;AAGO,SAAS,2BAAiC;AAC/C,EAAC,WAAoB,IAAI,IAAI;AAC/B;","names":[]}
@@ -15,7 +15,7 @@ export declare function createClient(): FilesDb;
15
15
  * configured host's client at that moment (methods stay bound to it).
16
16
  */
17
17
  export declare const supabase: FilesDb;
18
- export declare function iamDb<C extends FilesDb>(client: C): import("@supabase/postgrest-js").PostgrestClient<FilesDatabase, {
18
+ declare const iamSchemaOf: (client: FilesDb) => import("@supabase/postgrest-js").PostgrestClient<FilesDatabase, {
19
19
  PostgrestVersion: "14.5";
20
20
  }, "iam", {
21
21
  Tables: {
@@ -3802,6 +3802,8 @@ export declare function iamDb<C extends FilesDb>(client: C): import("@supabase/p
3802
3802
  Enums: { [_ in never]: never; };
3803
3803
  CompositeTypes: { [_ in never]: never; };
3804
3804
  }>;
3805
+ /** The `iam` schema client for any supabase-js client (see `filesDb`). */
3806
+ export declare function iamDb(client: SupabaseClient<any, any, any>): ReturnType<typeof iamSchemaOf>;
3805
3807
  export declare const runWithSessionRetry: <T>(run: () => PromiseLike<import("@ai-matrx/data/db").AuthRetryableResult<T>>) => Promise<import("@ai-matrx/data/db").AuthRetryableResult<T> & {
3806
3808
  retried: boolean;
3807
3809
  sessionRecovered: boolean;
@@ -15,8 +15,9 @@ const supabase = new Proxy({}, {
15
15
  return typeof value === "function" ? value.bind(db) : value;
16
16
  }
17
17
  });
18
+ const iamSchemaOf = (client) => client.schema("iam");
18
19
  function iamDb(client) {
19
- return client.schema("iam");
20
+ return iamSchemaOf(client);
20
21
  }
21
22
  const runWithSessionRetry = createSessionRetry({
22
23
  auth: { getSession: () => createClient().auth.getSession() }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/files/engine/host/supabase.ts"],"sourcesContent":["/**\n * Database seam — the host's supabase-js client, read at CALL time. Same\n * names the call sites imported from matrx-frontend's `utils/supabase/*` and\n * `lib/supabase/authRetry`.\n */\n\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport { createSessionRetry } from \"@ai-matrx/data/db\";\nimport type { FilesDatabase } from \"../db-types\";\nimport { getFilesHost } from \"./configure\";\n\nexport {\n isMissingSessionError,\n SessionUnavailableError,\n type AuthRetryableResult,\n} from \"@ai-matrx/data/db\";\nexport { getClaimsUser } from \"../support/claimsUser\";\n\nexport type FilesDb = SupabaseClient<FilesDatabase>;\n\n/** The configured host's database client. */\nexport function createClient(): FilesDb {\n return getFilesHost(\"db\").db as unknown as FilesDb;\n}\n\n/**\n * `supabase` as the call sites knew it: every property read goes to the\n * configured host's client at that moment (methods stay bound to it).\n */\nexport const supabase: FilesDb = new Proxy({} as FilesDb, {\n get(_target, key) {\n const db = createClient() as unknown as Record<PropertyKey, unknown>;\n const value = db[key];\n return typeof value === \"function\" ? (value as (...a: unknown[]) => unknown).bind(db) : value;\n },\n});\n\nexport function iamDb<C extends FilesDb>(client: C) {\n return client.schema(\"iam\");\n}\n\nexport const runWithSessionRetry = createSessionRetry({\n auth: { getSession: () => createClient().auth.getSession() },\n});\n"],"mappings":"AAOA,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB;AAE7B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,qBAAqB;AAKvB,SAAS,eAAwB;AACtC,SAAO,aAAa,IAAI,EAAE;AAC5B;AAMO,MAAM,WAAoB,IAAI,MAAM,CAAC,GAAc;AAAA,EACxD,IAAI,SAAS,KAAK;AAChB,UAAM,KAAK,aAAa;AACxB,UAAM,QAAQ,GAAG,GAAG;AACpB,WAAO,OAAO,UAAU,aAAc,MAAuC,KAAK,EAAE,IAAI;AAAA,EAC1F;AACF,CAAC;AAEM,SAAS,MAAyB,QAAW;AAClD,SAAO,OAAO,OAAO,KAAK;AAC5B;AAEO,MAAM,sBAAsB,mBAAmB;AAAA,EACpD,MAAM,EAAE,YAAY,MAAM,aAAa,EAAE,KAAK,WAAW,EAAE;AAC7D,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../../src/files/engine/host/supabase.ts"],"sourcesContent":["/**\n * Database seam — the host's supabase-js client, read at CALL time. Same\n * names the call sites imported from matrx-frontend's `utils/supabase/*` and\n * `lib/supabase/authRetry`.\n */\n\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport { createSessionRetry } from \"@ai-matrx/data/db\";\nimport type { FilesDatabase } from \"../db-types\";\nimport { getFilesHost } from \"./configure\";\n\nexport {\n isMissingSessionError,\n SessionUnavailableError,\n type AuthRetryableResult,\n} from \"@ai-matrx/data/db\";\nexport { getClaimsUser } from \"../support/claimsUser\";\n\nexport type FilesDb = SupabaseClient<FilesDatabase>;\n\n/** The configured host's database client. */\nexport function createClient(): FilesDb {\n return getFilesHost(\"db\").db as unknown as FilesDb;\n}\n\n/**\n * `supabase` as the call sites knew it: every property read goes to the\n * configured host's client at that moment (methods stay bound to it).\n */\nexport const supabase: FilesDb = new Proxy({} as FilesDb, {\n get(_target, key) {\n const db = createClient() as unknown as Record<PropertyKey, unknown>;\n const value = db[key];\n return typeof value === \"function\" ? (value as (...a: unknown[]) => unknown).bind(db) : value;\n },\n});\n\nconst iamSchemaOf = (client: FilesDb) => client.schema(\"iam\");\n\n/** The `iam` schema client for any supabase-js client (see `filesDb`). */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function iamDb(client: SupabaseClient<any, any, any>): ReturnType<typeof iamSchemaOf> {\n return iamSchemaOf(client as unknown as FilesDb);\n}\n\nexport const runWithSessionRetry = createSessionRetry({\n auth: { getSession: () => createClient().auth.getSession() },\n});\n"],"mappings":"AAOA,SAAS,0BAA0B;AAEnC,SAAS,oBAAoB;AAE7B;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,qBAAqB;AAKvB,SAAS,eAAwB;AACtC,SAAO,aAAa,IAAI,EAAE;AAC5B;AAMO,MAAM,WAAoB,IAAI,MAAM,CAAC,GAAc;AAAA,EACxD,IAAI,SAAS,KAAK;AAChB,UAAM,KAAK,aAAa;AACxB,UAAM,QAAQ,GAAG,GAAG;AACpB,WAAO,OAAO,UAAU,aAAc,MAAuC,KAAK,EAAE,IAAI;AAAA,EAC1F;AACF,CAAC;AAED,MAAM,cAAc,CAAC,WAAoB,OAAO,OAAO,KAAK;AAIrD,SAAS,MAAM,QAAuE;AAC3F,SAAO,YAAY,MAA4B;AACjD;AAEO,MAAM,sBAAsB,mBAAmB;AAAA,EACpD,MAAM,EAAE,YAAY,MAAM,aAAa,EAAE,KAAK,WAAW,EAAE;AAC7D,CAAC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/media",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "The AI Matrx media components kit: durable-ref-only renderers for images, video, audio and file thumbnails plus live TTS playback 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",
@@ -505,11 +505,11 @@
505
505
  "tailwind-merge": "^3.7.0",
506
506
  "tus-js-client": "^4.3.1",
507
507
  "@ai-matrx/associations": "latest",
508
- "@ai-matrx/agents": "latest",
509
- "@ai-matrx/design-system": "latest",
508
+ "@ai-matrx/data": "latest",
510
509
  "@ai-matrx/kit": "latest",
511
510
  "@ai-matrx/realtime": "latest",
512
- "@ai-matrx/data": "latest"
511
+ "@ai-matrx/design-system": "latest",
512
+ "@ai-matrx/agents": "latest"
513
513
  },
514
514
  "peerDependencies": {
515
515
  "@reduxjs/toolkit": ">=2.0.0",