@agentpond/supabase 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marcus Schiesser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ import { type AgentPondEnvironmentContext, type AgentPondProviderProject } from "@agentpond/core";
2
+ import { type SupabaseProcessRunner } from "./supabase-project.js";
3
+ export type SupabaseEnvironmentContextOptions = {
4
+ cwd?: string;
5
+ envName?: string;
6
+ };
7
+ export declare function supabaseEnvironmentContextFromCwdIfAvailable(options?: SupabaseEnvironmentContextOptions, dependencies?: {
8
+ run?: SupabaseProcessRunner;
9
+ }): AgentPondEnvironmentContext | undefined;
10
+ export declare const supabaseProvider: {
11
+ readonly kind: "supabase";
12
+ readonly displayName: "Supabase";
13
+ readonly instrumentationPrompt: "Use $agentpond-instrumentation to inspect this Supabase project and add\nOpenInference tracing to its trusted AI application.\n\nUse the dedicated private agentpond Storage bucket and export spans directly\nwith createSupabaseSpanExporter() from @agentpond/supabase.\n\nBuild the application, exercise one real AI request, then use $agentpond to:\n\n npx agentpond sync\n npx agentpond traces list --limit 10";
14
+ readonly openProject: (options?: {
15
+ cwd?: string;
16
+ allowUnlinked?: boolean;
17
+ } | undefined) => AgentPondProviderProject | undefined;
18
+ };
@@ -0,0 +1,78 @@
1
+ import { agentPondWorkspaceRoot, resolveAgentPondEnvironment, } from "@agentpond/core";
2
+ import { SUPABASE_INSTRUMENTATION_PROMPT, selectSupabaseEnvironment, supabaseCliProjectConfigFromCwdIfAvailable, supabaseLinkedProjectRef, supabaseProjectDirectory, validateSupabaseProjectRef, } from "./supabase-project.js";
3
+ import { SupabaseStorageObjectStore } from "./supabase-storage.js";
4
+ export function supabaseEnvironmentContextFromCwdIfAvailable(options = {}, dependencies = {}) {
5
+ const root = supabaseProjectDirectory(options.cwd);
6
+ if (!root)
7
+ return undefined;
8
+ const projectRef = options.envName
9
+ ? validateSupabaseProjectRef(options.envName)
10
+ : supabaseLinkedProjectRef(root);
11
+ if (!projectRef) {
12
+ throw new Error("Run supabase link --project-ref <project-ref> before using AgentPond with this Supabase project");
13
+ }
14
+ return supabaseEnvironmentContext({ projectRef, root }, dependencies);
15
+ }
16
+ function supabaseEnvironmentContext(project, dependencies = {}) {
17
+ const environment = resolveAgentPondEnvironment({
18
+ cwd: project.root,
19
+ name: project.projectRef,
20
+ });
21
+ const config = {
22
+ projectId: project.projectRef,
23
+ dbPath: environment.dbPath,
24
+ prefix: "",
25
+ auth: {
26
+ projectId: project.projectRef,
27
+ publicKey: "pk-agentpond",
28
+ secretKey: "sk-agentpond",
29
+ },
30
+ environment,
31
+ };
32
+ return {
33
+ kind: "supabase",
34
+ rootDir: project.root,
35
+ config,
36
+ usesAgentPondDevServer: false,
37
+ async resolveStorage() {
38
+ return {
39
+ store: await SupabaseStorageObjectStore.fromCliProject(project, dependencies),
40
+ projectId: project.projectRef,
41
+ prefix: "",
42
+ };
43
+ },
44
+ };
45
+ }
46
+ export const supabaseProvider = {
47
+ kind: "supabase",
48
+ displayName: "Supabase",
49
+ instrumentationPrompt: SUPABASE_INSTRUMENTATION_PROMPT,
50
+ openProject(options = {}) {
51
+ const candidateRoot = supabaseProjectDirectory(options.cwd);
52
+ if (!candidateRoot && !options.allowUnlinked)
53
+ return undefined;
54
+ const root = candidateRoot ?? agentPondWorkspaceRoot(options.cwd);
55
+ return supabaseProviderProject(root);
56
+ },
57
+ };
58
+ function supabaseProviderProject(root) {
59
+ return {
60
+ get projectLabel() {
61
+ return (supabaseCliProjectConfigFromCwdIfAvailable(root)?.projectRef ??
62
+ "unlinked");
63
+ },
64
+ rootDir: root,
65
+ selectEnvironment(name) {
66
+ return selectSupabaseEnvironment(name, { cwd: root });
67
+ },
68
+ resolveEnvironment(envName) {
69
+ const projectRef = envName
70
+ ? validateSupabaseProjectRef(envName)
71
+ : supabaseLinkedProjectRef(root);
72
+ if (!projectRef) {
73
+ throw new Error("Run supabase link --project-ref <project-ref> before using AgentPond with this Supabase project");
74
+ }
75
+ return supabaseEnvironmentContext({ projectRef, root });
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./environment-context.js";
2
+ export * from "./span-exporter.js";
3
+ export * from "./supabase-project.js";
4
+ export * from "./supabase-storage.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./environment-context.js";
2
+ export * from "./span-exporter.js";
3
+ export * from "./supabase-project.js";
4
+ export * from "./supabase-storage.js";
@@ -0,0 +1,13 @@
1
+ import { AgentPondSpanExporter } from "@agentpond/otel";
2
+ import type { SupabaseClient } from "@supabase/supabase-js";
3
+ import { type SupabaseEnvironment } from "./supabase-storage.js";
4
+ export type SupabaseSpanExporterOptions = {
5
+ bucket?: string;
6
+ client?: SupabaseClient;
7
+ env?: SupabaseEnvironment;
8
+ prefix?: string;
9
+ projectId?: string;
10
+ secretKey?: string;
11
+ url?: string;
12
+ };
13
+ export declare function createSupabaseSpanExporter(options?: SupabaseSpanExporterOptions): AgentPondSpanExporter;
@@ -0,0 +1,37 @@
1
+ import { AgentPondSpanExporter } from "@agentpond/otel";
2
+ import { supabaseProjectRefFromUrl, validateSupabaseProjectRef, } from "./supabase-project.js";
3
+ import { defaultSupabaseStorageBucket, defaultSupabaseStoragePrefix, SupabaseStorageObjectStore, supabaseRuntimeEnvironment, supabaseStorageConfigFromEnv, } from "./supabase-storage.js";
4
+ export function createSupabaseSpanExporter(options = {}) {
5
+ if (options.client) {
6
+ const projectId = options.projectId
7
+ ? validateSupabaseProjectRef(options.projectId)
8
+ : supabaseProjectRefFromUrl(supabaseClientUrl(options.client));
9
+ const store = SupabaseStorageObjectStore.fromClient(options.client, {
10
+ bucket: options.bucket ?? defaultSupabaseStorageBucket,
11
+ prefix: options.prefix ?? defaultSupabaseStoragePrefix,
12
+ });
13
+ return new AgentPondSpanExporter({ store, projectId });
14
+ }
15
+ const env = options.env ??
16
+ (options.url && options.secretKey ? {} : supabaseRuntimeEnvironment());
17
+ const storageConfig = supabaseStorageConfigFromEnv({
18
+ ...env,
19
+ ...(options.url ? { SUPABASE_URL: options.url } : {}),
20
+ ...(options.secretKey ? { SUPABASE_SECRET_KEY: options.secretKey } : {}),
21
+ }, {
22
+ bucket: options.bucket ?? defaultSupabaseStorageBucket,
23
+ prefix: options.prefix ?? defaultSupabaseStoragePrefix,
24
+ });
25
+ const projectId = options.projectId
26
+ ? validateSupabaseProjectRef(options.projectId)
27
+ : supabaseProjectRefFromUrl(storageConfig.url);
28
+ const store = SupabaseStorageObjectStore.fromConfig(storageConfig);
29
+ return new AgentPondSpanExporter({ store, projectId });
30
+ }
31
+ function supabaseClientUrl(client) {
32
+ const url = client.supabaseUrl;
33
+ if (typeof url !== "string") {
34
+ throw new Error("createSupabaseSpanExporter() requires an explicit projectId when the Supabase client URL is unavailable");
35
+ }
36
+ return url;
37
+ }
@@ -0,0 +1,34 @@
1
+ export declare const SUPABASE_PROJECT_REF_PATTERN: RegExp;
2
+ export type SupabaseCliProjectConfig = {
3
+ projectRef: string;
4
+ root: string;
5
+ };
6
+ export type SupabaseProcessRequest = {
7
+ args: readonly string[];
8
+ cwd: string;
9
+ stdio: "capture" | "inherit";
10
+ };
11
+ export type SupabaseProcessResult = {
12
+ exitCode: number;
13
+ stderr: string;
14
+ stdout: string;
15
+ };
16
+ export type SupabaseProcessRunner = (request: SupabaseProcessRequest) => Promise<SupabaseProcessResult>;
17
+ export declare const SUPABASE_INSTRUMENTATION_PROMPT = "Use $agentpond-instrumentation to inspect this Supabase project and add\nOpenInference tracing to its trusted AI application.\n\nUse the dedicated private agentpond Storage bucket and export spans directly\nwith createSupabaseSpanExporter() from @agentpond/supabase.\n\nBuild the application, exercise one real AI request, then use $agentpond to:\n\n npx agentpond sync\n npx agentpond traces list --limit 10";
18
+ export declare function isSupabaseProjectDirectory(cwd?: string): boolean;
19
+ export declare function supabaseProjectDirectory(cwd?: string): string | undefined;
20
+ export declare function supabaseCliProjectConfigFromCwd(cwd?: string): SupabaseCliProjectConfig;
21
+ export declare function supabaseCliProjectConfigFromCwdIfAvailable(cwd?: string): SupabaseCliProjectConfig | undefined;
22
+ export declare function supabaseLinkedProjectRef(root: string): string | undefined;
23
+ export declare function validateSupabaseProjectRef(projectRef: string): string;
24
+ export declare function supabaseHostedUrl(projectRef: string): string;
25
+ export declare function supabaseProjectRefFromUrl(urlValue: string): string;
26
+ export declare function selectSupabaseEnvironment(name: string, options?: {
27
+ cwd?: string;
28
+ }, dependencies?: {
29
+ run?: SupabaseProcessRunner;
30
+ }): Promise<string>;
31
+ export declare function supabaseSecretKeyForProject(project: SupabaseCliProjectConfig, dependencies?: {
32
+ run?: SupabaseProcessRunner;
33
+ }): Promise<string>;
34
+ export declare function runSupabaseProcess(request: SupabaseProcessRequest): Promise<SupabaseProcessResult>;
@@ -0,0 +1,192 @@
1
+ import { spawn } from "node:child_process";
2
+ import { once } from "node:events";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { text } from "node:stream/consumers";
6
+ import { findAncestorDirectory, nonEmpty } from "@agentpond/core";
7
+ import { validateSupabaseSecretKey } from "./supabase-storage.js";
8
+ export const SUPABASE_PROJECT_REF_PATTERN = /^[a-z]{20}$/;
9
+ export const SUPABASE_INSTRUMENTATION_PROMPT = `Use $agentpond-instrumentation to inspect this Supabase project and add
10
+ OpenInference tracing to its trusted AI application.
11
+
12
+ Use the dedicated private agentpond Storage bucket and export spans directly
13
+ with createSupabaseSpanExporter() from @agentpond/supabase.
14
+
15
+ Build the application, exercise one real AI request, then use $agentpond to:
16
+
17
+ npx agentpond sync
18
+ npx agentpond traces list --limit 10`;
19
+ export function isSupabaseProjectDirectory(cwd = process.cwd()) {
20
+ return supabaseProjectDirectory(cwd) !== undefined;
21
+ }
22
+ export function supabaseProjectDirectory(cwd = process.cwd()) {
23
+ return findAncestorDirectory(cwd, (directory) => existsSync(join(directory, "supabase", "config.toml")));
24
+ }
25
+ export function supabaseCliProjectConfigFromCwd(cwd = process.cwd()) {
26
+ const root = supabaseProjectDirectory(cwd);
27
+ if (!root) {
28
+ throw new Error("Run AgentPond inside an initialized Supabase project with supabase/config.toml");
29
+ }
30
+ const projectRef = supabaseLinkedProjectRef(root);
31
+ if (!projectRef) {
32
+ throw new Error("Run supabase link --project-ref <project-ref> before using AgentPond with this Supabase project");
33
+ }
34
+ return { projectRef, root };
35
+ }
36
+ export function supabaseCliProjectConfigFromCwdIfAvailable(cwd = process.cwd()) {
37
+ const root = supabaseProjectDirectory(cwd);
38
+ if (!root)
39
+ return undefined;
40
+ const projectRef = supabaseLinkedProjectRef(root);
41
+ return projectRef ? { projectRef, root } : undefined;
42
+ }
43
+ export function supabaseLinkedProjectRef(root) {
44
+ const path = join(root, "supabase", ".temp", "project-ref");
45
+ if (!existsSync(path))
46
+ return undefined;
47
+ let projectRef;
48
+ try {
49
+ projectRef = readFileSync(path, "utf8").trim();
50
+ }
51
+ catch {
52
+ throw new Error(`Could not read Supabase project link state at ${path}`);
53
+ }
54
+ if (!projectRef)
55
+ return undefined;
56
+ return validateSupabaseProjectRef(projectRef);
57
+ }
58
+ export function validateSupabaseProjectRef(projectRef) {
59
+ if (!SUPABASE_PROJECT_REF_PATTERN.test(projectRef)) {
60
+ throw new Error("Supabase project ref must contain exactly 20 lowercase letters");
61
+ }
62
+ return projectRef;
63
+ }
64
+ export function supabaseHostedUrl(projectRef) {
65
+ return `https://${validateSupabaseProjectRef(projectRef)}.supabase.co`;
66
+ }
67
+ export function supabaseProjectRefFromUrl(urlValue) {
68
+ let url;
69
+ try {
70
+ url = new URL(urlValue);
71
+ }
72
+ catch {
73
+ throw new Error("Could not derive a Supabase project ref from SUPABASE_URL");
74
+ }
75
+ const match = /^([a-z]{20})\.supabase\.co$/.exec(url.hostname);
76
+ if (url.protocol !== "https:" || url.port || !match) {
77
+ throw new Error("Could not derive a hosted Supabase project ref from SUPABASE_URL; provide projectId explicitly");
78
+ }
79
+ return validateSupabaseProjectRef(match[1]);
80
+ }
81
+ export async function selectSupabaseEnvironment(name, options = {}, dependencies = {}) {
82
+ const projectRef = validateSupabaseProjectRef(name);
83
+ const root = supabaseProjectDirectory(options.cwd);
84
+ if (!root) {
85
+ throw new Error("Run AgentPond inside an initialized Supabase project with supabase/config.toml");
86
+ }
87
+ const result = await (dependencies.run ?? runSupabaseProcess)({
88
+ args: ["link", "--project-ref", projectRef],
89
+ cwd: root,
90
+ stdio: "inherit",
91
+ });
92
+ if (result.exitCode !== 0) {
93
+ throw new Error(`Could not link Supabase project "${projectRef}". Run supabase login and try again.`);
94
+ }
95
+ return projectRef;
96
+ }
97
+ export async function supabaseSecretKeyForProject(project, dependencies = {}) {
98
+ const result = await (dependencies.run ?? runSupabaseProcess)({
99
+ args: [
100
+ "projects",
101
+ "api-keys",
102
+ "--project-ref",
103
+ project.projectRef,
104
+ "--output",
105
+ "json",
106
+ "--reveal",
107
+ ],
108
+ cwd: project.root,
109
+ stdio: "capture",
110
+ });
111
+ if (result.exitCode !== 0) {
112
+ throw new Error(`Could not load Supabase API keys for project "${project.projectRef}". Run supabase login and verify project access.`);
113
+ }
114
+ let value;
115
+ try {
116
+ value = JSON.parse(result.stdout);
117
+ }
118
+ catch {
119
+ throw new Error(`Supabase CLI returned invalid API-key metadata for project "${project.projectRef}"`);
120
+ }
121
+ const entries = supabaseApiKeyEntries(value);
122
+ const modern = entries.find((entry) => entry.name === "default" && entry.type === "secret");
123
+ const legacy = entries.find((entry) => entry.name === "service_role");
124
+ const key = nonEmpty(modern?.api_key) ?? nonEmpty(legacy?.api_key);
125
+ if (!key) {
126
+ throw new Error(`No default secret or legacy service-role key is available for Supabase project "${project.projectRef}"`);
127
+ }
128
+ try {
129
+ return validateSupabaseSecretKey(key);
130
+ }
131
+ catch {
132
+ throw new Error(`Supabase CLI did not reveal a usable secret or service-role key for project "${project.projectRef}"`);
133
+ }
134
+ }
135
+ export async function runSupabaseProcess(request) {
136
+ const child = spawn("supabase", request.args, {
137
+ cwd: request.cwd,
138
+ stdio: request.stdio === "inherit"
139
+ ? "inherit"
140
+ : ["inherit", "pipe", "pipe"],
141
+ });
142
+ try {
143
+ if (request.stdio === "inherit") {
144
+ const [code, signal] = (await once(child, "exit"));
145
+ if (code !== null)
146
+ return { exitCode: code, stderr: "", stdout: "" };
147
+ throw new Error(`Supabase CLI stopped by signal ${signal ?? "unknown"}`);
148
+ }
149
+ if (!child.stdout || !child.stderr) {
150
+ throw new Error("Could not capture Supabase CLI output");
151
+ }
152
+ const [[code, signal], stdout, stderr] = await Promise.all([
153
+ once(child, "exit"),
154
+ text(child.stdout),
155
+ text(child.stderr),
156
+ ]);
157
+ if (code !== null)
158
+ return { exitCode: code, stderr, stdout };
159
+ throw new Error(`Supabase CLI stopped by signal ${signal ?? "unknown"}`);
160
+ }
161
+ catch (error) {
162
+ if (error.code === "ENOENT") {
163
+ throw new Error("Supabase CLI is required. Install it, run supabase login, and try again.");
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+ function supabaseApiKeyEntries(value) {
169
+ const values = Array.isArray(value)
170
+ ? value
171
+ : value &&
172
+ typeof value === "object" &&
173
+ Array.isArray(value.apiKeys)
174
+ ? value.apiKeys
175
+ : [];
176
+ return values.flatMap((entry) => {
177
+ if (!entry || typeof entry !== "object")
178
+ return [];
179
+ const candidate = entry;
180
+ if (typeof candidate.name !== "string" ||
181
+ typeof candidate.api_key !== "string") {
182
+ return [];
183
+ }
184
+ return [
185
+ {
186
+ name: candidate.name,
187
+ api_key: candidate.api_key,
188
+ ...(typeof candidate.type === "string" ? { type: candidate.type } : {}),
189
+ },
190
+ ];
191
+ });
192
+ }
@@ -0,0 +1,48 @@
1
+ import { type IngestionSink, type ObjectStore, type ObjectStoreIngestionSinkOptions } from "@agentpond/core";
2
+ import { type SupabaseClient } from "@supabase/supabase-js";
3
+ import type { SupabaseCliProjectConfig, SupabaseProcessRunner } from "./supabase-project.js";
4
+ export declare const defaultSupabaseStorageBucket = "agentpond";
5
+ export declare const defaultSupabaseStoragePrefix = "";
6
+ export type SupabaseEnvironment = Record<string, string | undefined>;
7
+ export type SupabaseStorageObjectStoreConfig = {
8
+ url: string;
9
+ secretKey: string;
10
+ bucket?: string;
11
+ prefix?: string;
12
+ };
13
+ export type SupabaseStorageRuntimeOptions = {
14
+ bucket?: string;
15
+ env?: SupabaseEnvironment;
16
+ prefix?: string;
17
+ };
18
+ export type SupabaseStorageClientOptions = {
19
+ bucket?: string;
20
+ prefix?: string;
21
+ };
22
+ export type SupabaseStorageConfig = {
23
+ bucket: string;
24
+ prefix: string;
25
+ };
26
+ export declare function supabaseStorageConfigFromEnv(env: SupabaseEnvironment, options?: Pick<SupabaseStorageObjectStoreConfig, "bucket" | "prefix">): SupabaseStorageObjectStoreConfig;
27
+ export declare function supabaseSecretKeyFromEnv(env: SupabaseEnvironment): string;
28
+ export declare function validateSupabaseSecretKey(secretKey: string): string;
29
+ export declare class SupabaseStorageObjectStore implements ObjectStore {
30
+ readonly config: SupabaseStorageConfig;
31
+ private readonly storage;
32
+ private bucketValidation?;
33
+ static fromConfig(options: SupabaseStorageObjectStoreConfig): SupabaseStorageObjectStore;
34
+ static fromRuntimeEnv(options?: SupabaseStorageRuntimeOptions): SupabaseStorageObjectStore;
35
+ static fromClient(client: SupabaseClient, options?: SupabaseStorageClientOptions): SupabaseStorageObjectStore;
36
+ static fromCliProject(project: SupabaseCliProjectConfig, dependencies?: {
37
+ run?: SupabaseProcessRunner;
38
+ }): Promise<SupabaseStorageObjectStore>;
39
+ private constructor();
40
+ toSink(options?: ObjectStoreIngestionSinkOptions): IngestionSink;
41
+ putJson(key: string, value: unknown): Promise<void>;
42
+ getJson<T>(key: string): Promise<T>;
43
+ listKeys(prefix: string): Promise<string[]>;
44
+ private ensurePrivateBucket;
45
+ private validatePrivateBucket;
46
+ private listDirectory;
47
+ }
48
+ export declare function supabaseRuntimeEnvironment(): SupabaseEnvironment;
@@ -0,0 +1,276 @@
1
+ import { nonEmpty, normalizePrefix, sinkFromStore, } from "@agentpond/core";
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { supabaseHostedUrl, supabaseSecretKeyForProject, } from "./supabase-project.js";
4
+ export const defaultSupabaseStorageBucket = "agentpond";
5
+ export const defaultSupabaseStoragePrefix = "";
6
+ export function supabaseStorageConfigFromEnv(env, options = {}) {
7
+ const url = nonEmpty(env.SUPABASE_URL);
8
+ if (!url) {
9
+ throw new Error("Supabase storage requires SUPABASE_URL or an explicit url");
10
+ }
11
+ const secretKey = supabaseSecretKeyFromEnv(env);
12
+ return {
13
+ url: validatedSupabaseUrl(url),
14
+ secretKey,
15
+ bucket: options.bucket ?? defaultSupabaseStorageBucket,
16
+ prefix: options.prefix ?? defaultSupabaseStoragePrefix,
17
+ };
18
+ }
19
+ export function supabaseSecretKeyFromEnv(env) {
20
+ const direct = nonEmpty(env.SUPABASE_SECRET_KEY);
21
+ if (direct)
22
+ return validateSupabaseSecretKey(direct);
23
+ const dictionary = nonEmpty(env.SUPABASE_SECRET_KEYS);
24
+ if (dictionary) {
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(dictionary);
28
+ }
29
+ catch {
30
+ throw new Error("SUPABASE_SECRET_KEYS must contain a JSON object");
31
+ }
32
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
33
+ throw new Error("SUPABASE_SECRET_KEYS must contain a JSON object");
34
+ }
35
+ const value = parsed.default;
36
+ if (value !== undefined && (typeof value !== "string" || !value)) {
37
+ throw new Error("SUPABASE_SECRET_KEYS.default must be a non-empty string");
38
+ }
39
+ if (typeof value === "string" && value) {
40
+ return validateSupabaseSecretKey(value);
41
+ }
42
+ }
43
+ const legacy = nonEmpty(env.SUPABASE_SERVICE_ROLE_KEY);
44
+ if (legacy)
45
+ return validateSupabaseSecretKey(legacy);
46
+ throw new Error("Supabase storage requires SUPABASE_SECRET_KEY, SUPABASE_SECRET_KEYS.default, or SUPABASE_SERVICE_ROLE_KEY");
47
+ }
48
+ export function validateSupabaseSecretKey(secretKey) {
49
+ if (secretKey.includes("*") ||
50
+ secretKey.includes("·") ||
51
+ secretKey.includes("•") ||
52
+ secretKey.startsWith("sb_publishable_") ||
53
+ secretKey.startsWith("anon")) {
54
+ throw invalidSupabaseCredentialError();
55
+ }
56
+ if (secretKey.startsWith("sb_secret_") && secretKey.length > 10) {
57
+ return secretKey;
58
+ }
59
+ if (legacyJwtRole(secretKey) === "service_role")
60
+ return secretKey;
61
+ throw invalidSupabaseCredentialError();
62
+ }
63
+ export class SupabaseStorageObjectStore {
64
+ config;
65
+ storage;
66
+ bucketValidation;
67
+ static fromConfig(options) {
68
+ const url = validatedSupabaseUrl(options.url);
69
+ const secretKey = validateSupabaseSecretKey(options.secretKey);
70
+ const client = createClient(url, secretKey, {
71
+ auth: {
72
+ autoRefreshToken: false,
73
+ detectSessionInUrl: false,
74
+ persistSession: false,
75
+ },
76
+ });
77
+ return SupabaseStorageObjectStore.fromClient(client, options);
78
+ }
79
+ static fromRuntimeEnv(options = {}) {
80
+ return SupabaseStorageObjectStore.fromConfig(supabaseStorageConfigFromEnv(options.env ?? supabaseRuntimeEnvironment(), options));
81
+ }
82
+ static fromClient(client, options = {}) {
83
+ const key = client.supabaseKey;
84
+ if (typeof key !== "string") {
85
+ throw new Error("SupabaseStorageObjectStore.fromClient() requires a client initialized with a Supabase secret or service-role key");
86
+ }
87
+ validateSupabaseSecretKey(key);
88
+ return new SupabaseStorageObjectStore({
89
+ bucket: options.bucket ?? defaultSupabaseStorageBucket,
90
+ prefix: options.prefix ?? defaultSupabaseStoragePrefix,
91
+ }, client.storage);
92
+ }
93
+ static async fromCliProject(project, dependencies = {}) {
94
+ const secretKey = await supabaseSecretKeyForProject(project, dependencies);
95
+ return SupabaseStorageObjectStore.fromConfig({
96
+ url: supabaseHostedUrl(project.projectRef),
97
+ secretKey,
98
+ });
99
+ }
100
+ constructor(config, storage) {
101
+ this.config = config;
102
+ this.storage = storage;
103
+ }
104
+ toSink(options = {}) {
105
+ return sinkFromStore(this, {
106
+ prefix: normalizePrefix(options.prefix ?? this.config.prefix),
107
+ });
108
+ }
109
+ async putJson(key, value) {
110
+ await this.ensurePrivateBucket();
111
+ const { error } = await this.storage
112
+ .from(this.config.bucket)
113
+ .upload(key, JSON.stringify(value), {
114
+ cacheControl: "0",
115
+ contentType: "application/json",
116
+ upsert: true,
117
+ });
118
+ if (error) {
119
+ throw new Error(`Could not upload Supabase Storage object ${key}: ${storageErrorDetail(error)}`);
120
+ }
121
+ }
122
+ async getJson(key) {
123
+ await this.ensurePrivateBucket();
124
+ const { data, error } = await this.storage
125
+ .from(this.config.bucket)
126
+ .download(key);
127
+ if (error || !data) {
128
+ throw new Error(`Could not download Supabase Storage object ${key}: ${storageErrorDetail(error)}`);
129
+ }
130
+ const body = await data.text();
131
+ if (!body)
132
+ throw new Error(`Supabase Storage object is empty: ${key}`);
133
+ try {
134
+ return JSON.parse(body);
135
+ }
136
+ catch {
137
+ throw new Error(`Supabase Storage object is not valid JSON: ${key}`);
138
+ }
139
+ }
140
+ async listKeys(prefix) {
141
+ await this.ensurePrivateBucket();
142
+ const initialDirectory = listingDirectoryForPrefix(prefix);
143
+ const keys = new Set();
144
+ await this.listDirectory(initialDirectory, prefix, keys, new Set());
145
+ return [...keys].sort();
146
+ }
147
+ async ensurePrivateBucket() {
148
+ this.bucketValidation ??= this.validatePrivateBucket();
149
+ await this.bucketValidation;
150
+ }
151
+ async validatePrivateBucket() {
152
+ const { data, error } = await this.storage.getBucket(this.config.bucket);
153
+ if (error || !data) {
154
+ if (isMissingStorageResource(error)) {
155
+ throw new Error(`Supabase Storage bucket "${this.config.bucket}" does not exist`);
156
+ }
157
+ throw new Error(`Could not inspect Supabase Storage bucket "${this.config.bucket}": ${storageErrorDetail(error)}`);
158
+ }
159
+ if (data.public) {
160
+ throw new Error(`Supabase Storage bucket "${this.config.bucket}" must be private`);
161
+ }
162
+ }
163
+ async listDirectory(directory, prefix, keys, visited) {
164
+ if (visited.has(directory))
165
+ return;
166
+ visited.add(directory);
167
+ const entries = [];
168
+ const limit = 100;
169
+ let offset = 0;
170
+ while (true) {
171
+ const { data, error } = await this.storage
172
+ .from(this.config.bucket)
173
+ .list(directory, {
174
+ limit,
175
+ offset,
176
+ sortBy: { column: "name", order: "asc" },
177
+ });
178
+ if (error || !data) {
179
+ throw new Error(`Could not list Supabase Storage path ${directory || "/"}: ${storageErrorDetail(error)}`);
180
+ }
181
+ entries.push(...data);
182
+ if (data.length < limit)
183
+ break;
184
+ offset += data.length;
185
+ }
186
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
187
+ const key = directory ? `${directory}/${entry.name}` : entry.name;
188
+ if (isStorageDirectory(entry)) {
189
+ if (key.startsWith(prefix) || prefix.startsWith(`${key}/`)) {
190
+ await this.listDirectory(key, prefix, keys, visited);
191
+ }
192
+ continue;
193
+ }
194
+ if (key.startsWith(prefix))
195
+ keys.add(key);
196
+ }
197
+ }
198
+ }
199
+ export function supabaseRuntimeEnvironment() {
200
+ const names = [
201
+ "SUPABASE_URL",
202
+ "SUPABASE_SECRET_KEY",
203
+ "SUPABASE_SECRET_KEYS",
204
+ "SUPABASE_SERVICE_ROLE_KEY",
205
+ ];
206
+ const env = {};
207
+ if (typeof process !== "undefined") {
208
+ for (const name of names)
209
+ env[name] = process.env[name];
210
+ }
211
+ const deno = globalThis.Deno;
212
+ if (deno?.env) {
213
+ for (const name of names)
214
+ env[name] ??= deno.env.get(name);
215
+ }
216
+ return env;
217
+ }
218
+ function validatedSupabaseUrl(value) {
219
+ let url;
220
+ try {
221
+ url = new URL(value);
222
+ }
223
+ catch {
224
+ throw new Error("Supabase url must be a valid absolute URL");
225
+ }
226
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
227
+ throw new Error("Supabase url must use http or https");
228
+ }
229
+ return url.toString().replace(/\/$/, "");
230
+ }
231
+ function legacyJwtRole(value) {
232
+ const payload = value.split(".")[1];
233
+ if (!payload)
234
+ return undefined;
235
+ try {
236
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
237
+ const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
238
+ const parsed = JSON.parse(atob(padded));
239
+ return typeof parsed.role === "string" ? parsed.role : undefined;
240
+ }
241
+ catch {
242
+ return undefined;
243
+ }
244
+ }
245
+ function invalidSupabaseCredentialError() {
246
+ return new Error("Supabase storage requires a secret key or legacy service-role key; publishable and anonymous keys are not allowed");
247
+ }
248
+ function listingDirectoryForPrefix(prefix) {
249
+ let start = 0;
250
+ while (prefix[start] === "/")
251
+ start += 1;
252
+ let end = prefix.length;
253
+ while (end > start && prefix[end - 1] === "/")
254
+ end -= 1;
255
+ const trimmed = prefix.slice(start, end);
256
+ if (!trimmed)
257
+ return "";
258
+ if (prefix.endsWith("/"))
259
+ return trimmed;
260
+ const separator = trimmed.lastIndexOf("/");
261
+ return separator === -1 ? "" : trimmed.slice(0, separator);
262
+ }
263
+ function isStorageDirectory(entry) {
264
+ return entry.id == null && entry.metadata == null;
265
+ }
266
+ function isMissingStorageResource(error) {
267
+ if (!error)
268
+ return true;
269
+ const status = error.statusCode ?? error.status;
270
+ return (status === 404 ||
271
+ status === "404" ||
272
+ /not found|does not exist/i.test(error.message ?? ""));
273
+ }
274
+ function storageErrorDetail(error) {
275
+ return error?.message?.trim() || "unknown storage error";
276
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@agentpond/supabase",
3
+ "version": "0.6.0",
4
+ "description": "Supabase Storage adapter and direct OpenTelemetry exporter for AgentPond",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "types": "./dist/index.d.ts",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+ssh://git@github.com/marcusschiesser/agentpond.git",
14
+ "directory": "packages/supabase"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@supabase/supabase-js": "^2.110.6",
24
+ "@agentpond/core": "0.5.1",
25
+ "@agentpond/otel": "0.1.3"
26
+ },
27
+ "devDependencies": {
28
+ "@opentelemetry/sdk-trace-base": "^2.9.0"
29
+ },
30
+ "scripts": {
31
+ "test": "NODE_ENV=test node --test --import tsx tests/*.test.ts",
32
+ "test:deno": "deno run tests/deno-smoke.ts"
33
+ }
34
+ }