{"_id":"@autolabz/z-supabase-sdk","_rev":"2-acc933b09ad4cb4960fca4f71035c9aa","name":"@autolabz/z-supabase-sdk","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@autolabz/z-supabase-sdk","version":"0.1.0","_id":"@autolabz/z-supabase-sdk@0.1.0","maintainers":[{"name":"autolabz","email":"mzhh@mzhh.xyz"}],"dist":{"shasum":"e6ee0fd372cb8335d3578c2e19c8b2df13797d0d","tarball":"https://registry.npmjs.org/@autolabz/z-supabase-sdk/-/z-supabase-sdk-0.1.0.tgz","fileCount":23,"integrity":"sha512-RQ8N2k8v6XTzPcBN+K9QddaRk7/tssEveBN7x+sVw9KOlT0SVyBBXHSdQwsX/Q5xwFOoCk3/5wvLDcAZ7iZG+Q==","signatures":[{"sig":"MEUCIQCxuQNgfNLaXeQORjk07BIciFI8wxi+hCmEXUQFY6JiTQIgAVXIDPhfwKrCD7nAJ1iEEiM7quNVp2ZcAOUgYSZEwG0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":51170},"main":"dist/index.js","type":"module","types":"dist/index.d.ts","module":"dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"gitHead":"3ce9fadb84c2c2ebdbbe86895c843b16a86b19fa","private":false,"scripts":{"test":"tsx --test ./src/client.test.ts","build":"tsc -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"autolabz","email":"mzhh@mzhh.xyz"},"_npmVersion":"10.8.2","description":"Client SDK for z-supabase-service shadow sessions","directories":{},"_nodeVersion":"20.19.5","dependencies":{"tsx":"^4.7.1","zod":"^3.23.8","ts-node":"^10.9.2","typescript":"^5.6.3","@supabase/supabase-js":"^2.47.1"},"_hasShrinkwrap":false,"devDependencies":{},"peerDependencies":{"@supabase/supabase-js":"^2.47.1"},"_npmOperationalInternal":{"tmp":"tmp/z-supabase-sdk_0.1.0_1764338744611_0.6648288534794213","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@autolabz/z-supabase-sdk","version":"0.1.2","description":"Client SDK for z-supabase-service shadow sessions","private":false,"type":"module","main":"dist/index.js","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":"./dist/index.js","require":"./dist/index.js","types":"./dist/index.d.ts"}},"scripts":{"build":"tsc -p tsconfig.json","test":"tsx --test ./src/client.test.ts","prepublishOnly":"npm run build"},"dependencies":{"@supabase/supabase-js":"^2.47.1","ts-node":"^10.9.2","tsx":"^4.7.1","typescript":"^5.6.3","zod":"^3.23.8"},"devDependencies":{},"peerDependencies":{"@supabase/supabase-js":"^2.47.1"},"_id":"@autolabz/z-supabase-sdk@0.1.2","gitHead":"f1597a752ed7afb3db7156f0eb56f3c72f191283","_nodeVersion":"20.19.5","_npmVersion":"10.8.2","dist":{"integrity":"sha512-BSymRlA3lzMhiVGJX7obGhJM+Lr/9M6KOA+WSsf6Yp5GfyY/Lftt8IkFrnREKxM+qgVOwArLV9irWT6glg3DUg==","shasum":"2e203988b6dd705e58a06e1a14b87abdeba8eb94","tarball":"https://registry.npmjs.org/@autolabz/z-supabase-sdk/-/z-supabase-sdk-0.1.2.tgz","fileCount":23,"unpackedSize":51599,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCuYDknQmZMgzzoZoV7MN2/egRn/rj5z4D773fyXb6krgIgR5UA/1QSFxJeTgtIY3bqKI7a5gTZ8JP+k+fl4hWcLY8="}]},"_npmUser":{"name":"autolabz","email":"mzhh@mzhh.xyz"},"directories":{},"maintainers":[{"name":"autolabz","email":"mzhh@mzhh.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/z-supabase-sdk_0.1.2_1768279503940_0.7165297883108639"},"_hasShrinkwrap":false}},"time":{"created":"2025-11-28T14:05:44.373Z","modified":"2026-01-13T04:45:04.275Z","0.1.0":"2025-11-28T14:05:44.799Z","0.1.2":"2026-01-13T04:45:04.074Z"},"description":"Client SDK for z-supabase-service shadow sessions","maintainers":[{"name":"autolabz","email":"mzhh@mzhh.xyz"}],"readme":"# @autolabz/z-supabase-sdk\n\n面向前端应用的 Supabase 工具集，聚焦自托管 `z-supabase-service` 的三件事：会话托管、Shadow Session 同步、Storage 限流。\n\n## 功能总览\n\n- **Supabase Client**：`createSupabaseClient` 默认关闭 `autoRefreshToken` / `persistSession`，将 session 生命周期交给业务决定。\n- **Shadow Session 管理**：`syncSupabaseSession` / `refreshShadowSession` 通过服务端 API 下发 shadow 用户的 access/refresh token，并支持内存或 Web Storage 缓存。\n- **Storage 工具**：`createSignedUploadUrl` 通过服务端统一校验配额；`getStorageQuota` 独立查询配额；`listStorageObjects` 复用 shadow session 查询 `resources` 表；`deleteStorageResource` 触发服务端删除并具备幂等自愈能力。所有方法在异常时都会抛出可枚举的 `StorageUploadError`。\n\n## 1. Supabase 客户端创建策略\n\nSDK 会在创建客户端时合并默认 auth 选项，避免 Supabase JS 自动持久化 session，从而确保 shadow token 只来自受控流程。\n\n```ts\nimport { createSupabaseClient } from '@autolabz/z-supabase-sdk';\n\nconst supabase = createSupabaseClient({\n  supabaseUrl: import.meta.env.VITE_SUPABASE_URL!,\n  supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY!,\n  clientOptions: {\n    global: { headers: { 'X-App-Version': 'paper-admin@1.2.0' } },\n  },\n});\n```\n\n如需特殊场景（例如 SSR）可继续覆写 `clientOptions.auth`，SDK 仅负责提供安全的默认值。\n\n## 2. Shadow Session 生命周期\n\n`syncSupabaseSession` 调用 `/auth/sync` 生成 shadow 用户并向 Supabase client 写入 access / refresh token；`refreshShadowSession` 命中 `/session/refresh`，在 token 即将过期时续期。两个方法都会在失败时抛出 `ShadowSessionError`，并支持自定义 fetcher、clientId 以及各类回调。\n\n```ts\nimport {\n  syncSupabaseSession,\n  refreshShadowSession,\n  WebStorageSessionStorage,\n} from '@autolabz/z-supabase-sdk';\n\nconst storage = new WebStorageSessionStorage('zsupa_session');\n\nawait syncSupabaseSession({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: oauthAccessToken,\n  supabaseClient: supabase,\n  storage,\n  clientId: 'paper-admin',\n  onBeforeSync: () => startLoading(),\n  onSyncSuccess: (payload) => console.log('shadow user:', payload.shadowUserId),\n});\n\nawait refreshShadowSession({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  supabaseClient: supabase,\n  storage,\n  clientId: 'paper-admin',\n});\n```\n\n默认提供三种存储策略：\n\n- `MemorySessionStorage`：测试或短生命周期页面；\n- `WebStorageSessionStorage`：使用 `localStorage`，支持自定义 key；\n- `createDefaultStorage()`：在浏览器中落地到 `localStorage`，否则回退内存。\n\n## 3. Storage 工具\n\n所有 Storage API 都会先进行参数校验（大小、路径、分页等）再请求服务端，当配额不足 / 参数非法时抛出带有 `code` 与 `meta` 的 `StorageUploadError`，便于将 `ZS501` 之类的错误码直接映射到 UI。\n\n### 上传流程（Signed URL 代上传）\n\n1. 前端调用 `createSignedUploadUrl`。SDK 会校验 `authToken`、`fileName` 与文件大小后，向 `z-supabase-service` 的 `/storage/uploads/sign` 发起请求。\n2. 服务端收到请求后，会：\n   - 根据 OAuth 用户 ID 找到对应 shadow user；\n   - 创建一条 `resources` 记录并生成 `resourceId`，将真实存储文件名固定为 `<resourceId>(.ext)`，并只允许写入 `<shadowUserId>/<resourceId>.ext`；\n   - 通过 Supabase Service Role 调用 `createSignedUploadUrl`，把存储配额（`userQuotaBytes`、`maxFileBytes`）和允许的 bucket 统一限制住。\n3. 服务端返回包含 `resourceId` 的 signed URL 响应；客户端可把 `resourceId` 用于后续轮询处理状态，并直接对该 URL 发起 `PUT`，整个过程无需接触 Service Role Key。\n\n这样即使用户在浏览器 F12 中看到请求，也只能把 `signedUrl` 用于当前文件上传，无法在桶内任意写入其他路径，实现“系统代用户上传”的受控链路。\n\n### 资源列表（Supabase resources 表）\n\n`listStorageObjects` 需要一个 shadow session 已经连线的 `supabaseClient`，它会直接查询 `resources` 表并返回规范化字段（`fileName`, `storagePath`, `size`, `status`, `createdAt` 等），默认按 `created_at desc` 排序，同时返回 `count` 和 `hasMore` 方便分页。如果当前 Supabase Client 里没有 session，方法会尝试从提供的 `storage`（或默认的 `createDefaultStorage()`）恢复 shadow access/refresh token，确保真正发送的 `Authorization` 为 `Bearer eyJ...`；若缓存缺失则抛出 `SHADOW_SESSION_MISSING` 提醒先执行 `syncSupabaseSession`。\n\n```ts\nconst listed = await listStorageObjects({\n  supabaseClient,\n  shadowUserId: session.shadowUserId,\n  storage, // 传入与 sync 阶段相同的 storage，便于页面刷新后自动恢复 JWT\n  limit: 20,\n  orderBy: { column: 'created_at', order: 'desc' },\n});\n\nlisted.objects.forEach((resource) => {\n  console.log(resource.fileName, resource.size, resource.storagePath);\n});\n```\n\n### 配额查询（Quota API）\n\n当页面需要展示“已用/剩余空间”而不想触发签名流程时，可直接调用 `getStorageQuota`。SDK 仅需 `serviceBaseUrl` 与 `authToken`，会向 `/storage/quota` 发送 `GET` 请求，并返回 `shadowUserId`, `usedBytes`, `remainingBytes`, `quotaBytes`, `maxFileBytes`。\n\n```ts\nimport { getStorageQuota } from '@autolabz/z-supabase-sdk';\n\nconst quota = await getStorageQuota({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: shadowAccessToken,\n  clientId: 'paper-admin',\n});\n\nconsole.log(`已用 ${(quota.usedBytes / 1024 / 1024).toFixed(1)} MB / 总额度 ${(quota.quotaBytes / 1024 / 1024).toFixed(1)} MB`);\n```\n\n### 资源删除（Delete API）\n\n`deleteStorageResource` 会向 `/storage/resources/:resourceId` 发送 `DELETE` 请求。服务端会验证影子账号归属、删除 Supabase Storage 对象，并在对象缺失时自动忽略错误、仅清理数据库记录，实现幂等、“自愈”的删除逻辑。SDK 只需提供 `authToken`, `serviceBaseUrl`, `resourceId`：\n\n```ts\nimport { deleteStorageResource } from '@autolabz/z-supabase-sdk';\n\nawait deleteStorageResource({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: shadowAccessToken,\n  resourceId: selectedResourceId,\n  clientId: 'paper-admin',\n});\n```\n\n如返回 `success: true` 即表示顺利清理，无论 Supabase Storage 中对象是否已经不存在。\n\n### 查看 / 下载方式\n\n- **生成可访问链接**：拿到资源记录后，可用 shadow session 驱动的 Supabase JS 客户端直接调用 `supabase.storage.from(bucket).createSignedUrl(resource.storagePath!, ttl)` 或 `download(resource.storagePath!)` 获取一次性访问链接 / Blob 数据。RLS 只允许访问 `<shadowUserId>/**`，因此天然满足“只看自己的文件”。\n- **对外分享**（可选）：需要向第三方暴露文件时，可在自建后端使用 Service Role Key 生成一次性签名，再下发给终端；SDK 专注在“用户查看自己的资源”场景，推荐优先走 shadow session。\n\n```ts\nimport { createSignedUploadUrl, getStorageQuota, listStorageObjects } from '@autolabz/z-supabase-sdk';\n\nconst signed = await createSignedUploadUrl({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: supabase.auth.session()?.access_token ?? '',\n  fileName: file.name,\n  bytes: file.size,\n  mimeType: file.type,\n  clientId: 'paper-admin',\n});\n\nconsole.log('resourceId for follow-up polling:', signed.resourceId);\n\nawait fetch(signed.uploadUrl, { method: 'PUT', body: file, headers: signed.headers });\n\nconst quota = await getStorageQuota({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: supabase.auth.session()?.access_token ?? '',\n});\n\nconst listed = await listStorageObjects({\n  supabaseClient,\n  shadowUserId: session.shadowUserId,\n  storage,\n  limit: 20,\n});\n```\n\n## 安装\n\n```bash\nnpm install @autolabz/z-supabase-sdk\n```\n\n## 快速上手\n\n```ts\nimport { createSupabaseClient, syncSupabaseSession, createSignedUploadUrl } from '@autolabz/z-supabase-sdk';\n\nconst supabase = createSupabaseClient({\n  supabaseUrl: import.meta.env.VITE_SUPABASE_URL!,\n  supabaseAnonKey: import.meta.env.VITE_SUPABASE_ANON_KEY!,\n});\n\nawait syncSupabaseSession({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: oauthAccessToken,\n  supabaseClient: supabase,\n  clientId: 'paper-admin',\n});\n\nconst signed = await createSignedUploadUrl({\n  serviceBaseUrl: import.meta.env.VITE_ZSUPA_SERVICE_URL!,\n  authToken: supabase.auth.session()?.access_token ?? '',\n  fileName: file.name,\n  bytes: file.size,\n  mimeType: file.type,\n});\n\nawait fetch(signed.uploadUrl, {\n  method: 'PUT',\n  body: file,\n  headers: signed.headers,\n});\n```\n\n## API 速览\n\n| 方法 / 类型 | 描述 |\n| --- | --- |\n| `createSupabaseClient` | 生成禁用自动 refresh/persist 的 Supabase client。 |\n| `syncSupabaseSession` | 请求 `/auth/sync`，拉取 shadow session 并写入 Supabase client。 |\n| `refreshShadowSession` | 请求 `/session/refresh`，在 access token 即将过期时续期。 |\n| `createSignedUploadUrl` | 请求 `/storage/uploads/sign`，校验配额并拿到 PUT 签名链接。 |\n| `getStorageQuota` | 请求 `/storage/quota`，查询影子用户的已用/剩余额度。 |\n| `deleteStorageResource` | 请求 `/storage/resources/:resourceId`，触发服务端删除并自愈缺失对象。 |\n| `listStorageObjects` | 使用 shadow session 的 Supabase client 查询 `resources` 表并返回分页结果。 |\n| `MemorySessionStorage` / `WebStorageSessionStorage` | shadow session 的可插拔持久化策略。 |\n| `ShadowSessionError` / `StorageUploadError` | 携带 `code`、`meta` 的错误类型，便于 UI 映射。 |\n\n`createSignedUploadUrl` 会在参数阶段阻止 `authToken` 缺失、`bytes <= 0` 等问题；服务端错误会透出 `StorageUploadError`，可直接针对 `ZS501`（容量用尽）、`ZS401`（鉴权失败）等错误码给出提示。\n\n\n","readmeFilename":"README.md"}