supabase-worker-previews 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +152 -0
- package/dist/cloudflare.d.ts +37 -0
- package/dist/cloudflare.js +95 -0
- package/dist/commands/branches.d.ts +62 -0
- package/dist/commands/branches.js +273 -0
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +289 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.js +132 -0
- package/dist/commands/pr.d.ts +37 -0
- package/dist/commands/pr.js +78 -0
- package/dist/commands/prune.d.ts +32 -0
- package/dist/commands/prune.js +110 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +105 -0
- package/dist/feedback.d.ts +64 -0
- package/dist/feedback.js +238 -0
- package/dist/github.d.ts +33 -0
- package/dist/github.js +78 -0
- package/dist/http.d.ts +7 -0
- package/dist/http.js +44 -0
- package/dist/jsonc.d.ts +2 -0
- package/dist/jsonc.js +56 -0
- package/dist/preview-name.d.ts +3 -0
- package/dist/preview-name.js +15 -0
- package/dist/run.d.ts +21 -0
- package/dist/run.js +59 -0
- package/dist/runtime.d.ts +56 -0
- package/dist/runtime.js +121 -0
- package/dist/style.d.ts +8 -0
- package/dist/style.js +21 -0
- package/dist/suggest.d.ts +2 -0
- package/dist/suggest.js +22 -0
- package/dist/supabase.d.ts +78 -0
- package/dist/supabase.js +168 -0
- package/dist/usage.d.ts +46 -0
- package/dist/usage.js +297 -0
- package/package.json +84 -9
- package/templates/default-privileges.sql +9 -0
- package/templates/supabase-previews-action.yml +29 -0
- package/templates/supabase-previews.yml +38 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-side half: one build serves any Supabase database. The Worker reads
|
|
3
|
+
* its Supabase values at request time (from wrangler vars, or a Preview's
|
|
4
|
+
* SUPABASE_OVERRIDE secret) and hands the public ones to the browser, so a
|
|
5
|
+
* Preview never needs its own build.
|
|
6
|
+
*/
|
|
7
|
+
export const OVERRIDE_SECRET = "SUPABASE_OVERRIDE";
|
|
8
|
+
export const DEFAULT_GLOBAL = "__SUPABASE_PUBLIC__";
|
|
9
|
+
export const IDENTITY_PATH = "/.well-known/supabase-preview";
|
|
10
|
+
export const SUPABASE_VARS = [
|
|
11
|
+
"SUPABASE_URL",
|
|
12
|
+
"SUPABASE_PUBLISHABLE_KEY",
|
|
13
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
14
|
+
"SUPABASE_PROJECT_REF",
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* The override replaces all four values. It is one secret, not four, because a
|
|
18
|
+
* Worker var and secret must never share a name.
|
|
19
|
+
*/
|
|
20
|
+
export function parseOverride(raw) {
|
|
21
|
+
if (typeof raw !== "string" || !raw)
|
|
22
|
+
return null;
|
|
23
|
+
const values = JSON.parse(raw);
|
|
24
|
+
for (const name of SUPABASE_VARS) {
|
|
25
|
+
if (typeof values[name] !== "string" || !values[name])
|
|
26
|
+
throw new Error(`${OVERRIDE_SECRET} is missing ${name}`);
|
|
27
|
+
}
|
|
28
|
+
return values;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* `env` with any SUPABASE_OVERRIDE applied. Also patches `process.env` when
|
|
32
|
+
* nodejs_compat provides it, for code that reads Supabase settings from there.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveEnv(env) {
|
|
35
|
+
const override = parseOverride(env[OVERRIDE_SECRET]);
|
|
36
|
+
if (!override)
|
|
37
|
+
return env;
|
|
38
|
+
const proc = globalThis.process;
|
|
39
|
+
if (proc?.env)
|
|
40
|
+
Object.assign(proc.env, override);
|
|
41
|
+
return new Proxy(env, {
|
|
42
|
+
get: (target, key, receiver) => typeof key === "string" && key in override
|
|
43
|
+
? override[key]
|
|
44
|
+
: Reflect.get(target, key, receiver),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function publicConfigFromEnv(env) {
|
|
48
|
+
const { SUPABASE_URL: supabaseUrl, SUPABASE_PUBLISHABLE_KEY: supabaseKey } = env;
|
|
49
|
+
return typeof supabaseUrl === "string" && typeof supabaseKey === "string" && supabaseUrl && supabaseKey
|
|
50
|
+
? { supabaseUrl, supabaseKey }
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
export function publicConfigScript(config, globalName = DEFAULT_GLOBAL) {
|
|
54
|
+
const json = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
55
|
+
return `<script>window.${globalName}=${json}</script>`;
|
|
56
|
+
}
|
|
57
|
+
/** Browser side: the values the Worker injected, or null under a dev server that did not. */
|
|
58
|
+
export function readPublicConfig(globalName = DEFAULT_GLOBAL, scope = globalThis) {
|
|
59
|
+
const value = scope[globalName];
|
|
60
|
+
return value?.supabaseUrl && value.supabaseKey
|
|
61
|
+
? { supabaseUrl: value.supabaseUrl, supabaseKey: value.supabaseKey }
|
|
62
|
+
: null;
|
|
63
|
+
}
|
|
64
|
+
/** `https://<ref>.supabase.co` gives `<ref>`; a custom API domain gives null. */
|
|
65
|
+
export function projectRefOf(url) {
|
|
66
|
+
return url?.match(/^https:\/\/([a-z0-9]+)\.supabase\.co\/?$/)?.[1] ?? null;
|
|
67
|
+
}
|
|
68
|
+
export async function injectIntoHtml(response, html) {
|
|
69
|
+
if (!(response.headers.get("content-type") ?? "").includes("text/html"))
|
|
70
|
+
return response;
|
|
71
|
+
const Rewriter = globalThis.HTMLRewriter;
|
|
72
|
+
if (Rewriter) {
|
|
73
|
+
return new Rewriter()
|
|
74
|
+
.on("head", { element: (el) => el.prepend(html, { html: true }) })
|
|
75
|
+
.transform(response);
|
|
76
|
+
}
|
|
77
|
+
const body = (await response.text()).replace(/<head(\s[^>]*)?>/i, (head) => head + html);
|
|
78
|
+
const headers = new Headers(response.headers);
|
|
79
|
+
headers.delete("content-length");
|
|
80
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
81
|
+
}
|
|
82
|
+
export function identityResponse(env) {
|
|
83
|
+
const url = env.SUPABASE_URL;
|
|
84
|
+
const ref = env.SUPABASE_PROJECT_REF ?? projectRefOf(url);
|
|
85
|
+
return Response.json({ projectRef: ref ?? null, supabaseUrl: url ?? null }, { headers: { "cache-control": "no-store" } });
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Frameworks that call the entry without `env` (TanStack Start on nitro calls
|
|
89
|
+
* `fetch(request)`) still expose vars and secrets on process.env under nodejs_compat.
|
|
90
|
+
*/
|
|
91
|
+
function envOrProcessEnv(env) {
|
|
92
|
+
if (env && typeof env === "object")
|
|
93
|
+
return env;
|
|
94
|
+
return (globalThis.process?.env ?? {});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Wraps a Worker's default export. Every handler sees the override applied;
|
|
98
|
+
* `fetch` also injects the public config and answers the identity route that
|
|
99
|
+
* `swp check` reads.
|
|
100
|
+
*/
|
|
101
|
+
export function withSupabasePreviews(handler, options = {}) {
|
|
102
|
+
const { globalName = DEFAULT_GLOBAL, inject = true, identity = true } = options;
|
|
103
|
+
const wrapped = { ...handler };
|
|
104
|
+
for (const [name, fn] of Object.entries(handler)) {
|
|
105
|
+
if (typeof fn !== "function" || name === "fetch")
|
|
106
|
+
continue;
|
|
107
|
+
wrapped[name] = (event, env, ctx) => fn.call(handler, event, resolveEnv(envOrProcessEnv(env)), ctx);
|
|
108
|
+
}
|
|
109
|
+
if (handler.fetch) {
|
|
110
|
+
const fetchFn = handler.fetch;
|
|
111
|
+
wrapped.fetch = async (request, rawEnv, ctx) => {
|
|
112
|
+
const env = resolveEnv(envOrProcessEnv(rawEnv));
|
|
113
|
+
if (identity && new URL(request.url).pathname === IDENTITY_PATH)
|
|
114
|
+
return identityResponse(env);
|
|
115
|
+
const response = await fetchFn.call(handler, request, env, ctx);
|
|
116
|
+
const config = inject ? publicConfigFromEnv(env) : null;
|
|
117
|
+
return config ? injectIntoHtml(response, publicConfigScript(config, globalName)) : response;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return wrapped;
|
|
121
|
+
}
|
package/dist/style.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type Style = Record<"bold" | "dim" | "red" | "yellow" | "green" | "cyan", (text: string) => string>;
|
|
2
|
+
/** Color only on a terminal, and never when NO_COLOR is set (https://no-color.org). */
|
|
3
|
+
export declare function colorEnabled(stream?: {
|
|
4
|
+
isTTY?: boolean;
|
|
5
|
+
}, env?: Record<string, string | undefined>): boolean;
|
|
6
|
+
export declare function makeStyle(enabled: boolean): Style;
|
|
7
|
+
export declare const out: Style;
|
|
8
|
+
export declare const err: Style;
|
package/dist/style.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const CODES = {
|
|
2
|
+
bold: [1, 22],
|
|
3
|
+
dim: [2, 22],
|
|
4
|
+
red: [31, 39],
|
|
5
|
+
yellow: [33, 39],
|
|
6
|
+
green: [32, 39],
|
|
7
|
+
cyan: [36, 39],
|
|
8
|
+
};
|
|
9
|
+
/** Color only on a terminal, and never when NO_COLOR is set (https://no-color.org). */
|
|
10
|
+
export function colorEnabled(stream = process.stdout, env = process.env) {
|
|
11
|
+
return !!stream.isTTY && !env.NO_COLOR && env.TERM !== "dumb";
|
|
12
|
+
}
|
|
13
|
+
export function makeStyle(enabled) {
|
|
14
|
+
const entries = Object.entries(CODES).map(([name, [open, close]]) => [
|
|
15
|
+
name,
|
|
16
|
+
(text) => (enabled ? `\x1b[${open}m${text}\x1b[${close}m` : text),
|
|
17
|
+
]);
|
|
18
|
+
return Object.fromEntries(entries);
|
|
19
|
+
}
|
|
20
|
+
export const out = makeStyle(colorEnabled(process.stdout));
|
|
21
|
+
export const err = makeStyle(colorEnabled(process.stderr));
|
package/dist/suggest.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** The candidate closest to `input` by edit distance, if it is close enough to be a typo. */
|
|
2
|
+
export function didYouMean(input, candidates) {
|
|
3
|
+
let best;
|
|
4
|
+
let bestDistance = Infinity;
|
|
5
|
+
for (const c of candidates) {
|
|
6
|
+
const [a, b] = [input.toLowerCase(), c.toLowerCase()];
|
|
7
|
+
const d = b.startsWith(a) && a.length >= 2 ? 0 : distance(a, b);
|
|
8
|
+
if (d < bestDistance)
|
|
9
|
+
[best, bestDistance] = [c, d];
|
|
10
|
+
}
|
|
11
|
+
return bestDistance <= Math.max(1, Math.floor((best?.length ?? 0) / 3)) ? best : undefined;
|
|
12
|
+
}
|
|
13
|
+
function distance(a, b) {
|
|
14
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
15
|
+
for (let i = 1; i <= a.length; i++) {
|
|
16
|
+
const row = [i];
|
|
17
|
+
for (let j = 1; j <= b.length; j++)
|
|
18
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
19
|
+
prev = row;
|
|
20
|
+
}
|
|
21
|
+
return prev[b.length];
|
|
22
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type Branch = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
project_ref: string;
|
|
5
|
+
parent_project_ref?: string;
|
|
6
|
+
is_default?: boolean;
|
|
7
|
+
git_branch?: string | null;
|
|
8
|
+
persistent?: boolean;
|
|
9
|
+
status: string;
|
|
10
|
+
};
|
|
11
|
+
export type BranchDetail = {
|
|
12
|
+
ref: string;
|
|
13
|
+
status: string;
|
|
14
|
+
};
|
|
15
|
+
export type ProjectKeys = {
|
|
16
|
+
publishable: string;
|
|
17
|
+
secret: string;
|
|
18
|
+
};
|
|
19
|
+
export type KeyKind = "legacy" | "new";
|
|
20
|
+
export type ApiKey = {
|
|
21
|
+
name: string;
|
|
22
|
+
type?: "legacy" | "publishable" | "secret" | null;
|
|
23
|
+
api_key?: string | null;
|
|
24
|
+
};
|
|
25
|
+
export type ActionRun = {
|
|
26
|
+
id: string;
|
|
27
|
+
git_config?: {
|
|
28
|
+
owner?: string;
|
|
29
|
+
repo?: string;
|
|
30
|
+
ref?: string;
|
|
31
|
+
} | null;
|
|
32
|
+
created_at: string;
|
|
33
|
+
};
|
|
34
|
+
export declare class SupabaseApiError extends Error {
|
|
35
|
+
readonly method: string;
|
|
36
|
+
readonly path: string;
|
|
37
|
+
readonly status: number;
|
|
38
|
+
readonly body: string;
|
|
39
|
+
constructor(method: string, path: string, status: number, body: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class SupabaseApi {
|
|
42
|
+
private token;
|
|
43
|
+
private fetchImpl;
|
|
44
|
+
constructor(token: string, fetchImpl?: typeof fetch);
|
|
45
|
+
private call;
|
|
46
|
+
getProject(ref: string): Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
status: string;
|
|
50
|
+
}>;
|
|
51
|
+
listBranches(ref: string): Promise<Branch[]>;
|
|
52
|
+
createBranch(ref: string, args: {
|
|
53
|
+
name: string;
|
|
54
|
+
gitBranch?: string;
|
|
55
|
+
persistent?: boolean;
|
|
56
|
+
}): Promise<Branch>;
|
|
57
|
+
/** 404s for a few seconds after the branch is created. */
|
|
58
|
+
getBranch(branchRef: string): Promise<BranchDetail>;
|
|
59
|
+
updateBranch(branchRef: string, args: {
|
|
60
|
+
gitBranch?: string;
|
|
61
|
+
persistent?: boolean;
|
|
62
|
+
}): Promise<Branch>;
|
|
63
|
+
deleteBranch(branchRef: string): Promise<unknown>;
|
|
64
|
+
query<T>(ref: string, sql: string): Promise<T[]>;
|
|
65
|
+
/** The preferred kind of key pair when the project has it (and legacy keys are enabled), else the other. */
|
|
66
|
+
keys(ref: string, prefer?: KeyKind): Promise<ProjectKeys>;
|
|
67
|
+
private legacyKeysEnabled;
|
|
68
|
+
/** Newest first. Runs the GitHub integration made carry `git_config`. */
|
|
69
|
+
actionRuns(branchRef: string): Promise<ActionRun[]>;
|
|
70
|
+
getAuthConfig(ref: string): Promise<{
|
|
71
|
+
site_url?: string;
|
|
72
|
+
uri_allow_list?: string;
|
|
73
|
+
}>;
|
|
74
|
+
/** Sets the site URL and adds redirect URLs, keeping the ones already allowed. */
|
|
75
|
+
allowRedirects(ref: string, siteUrl: string, redirects: string[]): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export declare function explainCreateBranchError(ref: string, name: string, err: SupabaseApiError): string;
|
|
78
|
+
export declare function projectUrl(ref: string): string;
|
package/dist/supabase.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { apiErrorMessage } from "./http.js";
|
|
2
|
+
const API = "https://api.supabase.com/v1";
|
|
3
|
+
export class SupabaseApiError extends Error {
|
|
4
|
+
method;
|
|
5
|
+
path;
|
|
6
|
+
status;
|
|
7
|
+
body;
|
|
8
|
+
constructor(method, path, status, body) {
|
|
9
|
+
super(apiErrorMessage(`Supabase API ${method} ${path}`, status, body, "SUPABASE_ACCESS_TOKEN"));
|
|
10
|
+
this.method = method;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.body = body;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class SupabaseApi {
|
|
17
|
+
token;
|
|
18
|
+
fetchImpl;
|
|
19
|
+
constructor(token, fetchImpl = fetch) {
|
|
20
|
+
this.token = token;
|
|
21
|
+
this.fetchImpl = fetchImpl;
|
|
22
|
+
}
|
|
23
|
+
async call(method, path, body) {
|
|
24
|
+
const res = await this.fetchImpl(`${API}${path}`, {
|
|
25
|
+
method,
|
|
26
|
+
headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" },
|
|
27
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok)
|
|
30
|
+
throw new SupabaseApiError(method, path, res.status, await res.text());
|
|
31
|
+
const text = await res.text();
|
|
32
|
+
return (text ? JSON.parse(text) : undefined);
|
|
33
|
+
}
|
|
34
|
+
getProject(ref) {
|
|
35
|
+
return this.call("GET", `/projects/${ref}`);
|
|
36
|
+
}
|
|
37
|
+
async listBranches(ref) {
|
|
38
|
+
return (await this.call("GET", `/projects/${ref}/branches`)) ?? [];
|
|
39
|
+
}
|
|
40
|
+
async createBranch(ref, args) {
|
|
41
|
+
try {
|
|
42
|
+
return await this.call("POST", `/projects/${ref}/branches`, {
|
|
43
|
+
branch_name: args.name,
|
|
44
|
+
...(args.gitBranch ? { git_branch: args.gitBranch } : {}),
|
|
45
|
+
persistent: args.persistent ?? false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
throw err instanceof SupabaseApiError ? new Error(explainCreateBranchError(ref, args.name, err)) : err;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** 404s for a few seconds after the branch is created. */
|
|
53
|
+
getBranch(branchRef) {
|
|
54
|
+
return this.call("GET", `/branches/${branchRef}`);
|
|
55
|
+
}
|
|
56
|
+
updateBranch(branchRef, args) {
|
|
57
|
+
return this.call("PATCH", `/branches/${branchRef}`, {
|
|
58
|
+
...(args.gitBranch ? { git_branch: args.gitBranch } : {}),
|
|
59
|
+
...(args.persistent !== undefined ? { persistent: args.persistent } : {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
deleteBranch(branchRef) {
|
|
63
|
+
return this.call("DELETE", `/branches/${branchRef}`);
|
|
64
|
+
}
|
|
65
|
+
query(ref, sql) {
|
|
66
|
+
return this.call("POST", `/projects/${ref}/database/query`, { query: sql });
|
|
67
|
+
}
|
|
68
|
+
/** The preferred kind of key pair when the project has it (and legacy keys are enabled), else the other. */
|
|
69
|
+
async keys(ref, prefer = "legacy") {
|
|
70
|
+
const keys = await this.call("GET", `/projects/${ref}/api-keys?reveal=true`);
|
|
71
|
+
const pairs = { legacy: legacyPair(keys), new: newPair(keys) };
|
|
72
|
+
if (pairs.legacy && !(await this.legacyKeysEnabled(ref)))
|
|
73
|
+
pairs.legacy = null;
|
|
74
|
+
const pair = pairs[prefer] ?? pairs[prefer === "legacy" ? "new" : "legacy"];
|
|
75
|
+
if (!pair) {
|
|
76
|
+
const masked = keys.some((k) => k.api_key && !revealed(k.api_key));
|
|
77
|
+
throw new Error(masked
|
|
78
|
+
? `Project ${ref}: the access token cannot reveal secret API keys; use a token with the project's secrets permission`
|
|
79
|
+
: `Project ${ref} has no publishable and secret API key pair`);
|
|
80
|
+
}
|
|
81
|
+
return pair;
|
|
82
|
+
}
|
|
83
|
+
async legacyKeysEnabled(ref) {
|
|
84
|
+
const status = await this.call("GET", `/projects/${ref}/api-keys/legacy`).catch(() => null);
|
|
85
|
+
return status?.enabled !== false;
|
|
86
|
+
}
|
|
87
|
+
/** Newest first. Runs the GitHub integration made carry `git_config`. */
|
|
88
|
+
async actionRuns(branchRef) {
|
|
89
|
+
return (await this.call("GET", `/projects/${branchRef}/actions`)) ?? [];
|
|
90
|
+
}
|
|
91
|
+
getAuthConfig(ref) {
|
|
92
|
+
return this.call("GET", `/projects/${ref}/config/auth`);
|
|
93
|
+
}
|
|
94
|
+
/** Sets the site URL and adds redirect URLs, keeping the ones already allowed. */
|
|
95
|
+
async allowRedirects(ref, siteUrl, redirects) {
|
|
96
|
+
const current = (await this.getAuthConfig(ref)).uri_allow_list ?? "";
|
|
97
|
+
const list = [
|
|
98
|
+
...new Set([
|
|
99
|
+
...current
|
|
100
|
+
.split(",")
|
|
101
|
+
.map((s) => s.trim())
|
|
102
|
+
.filter(Boolean),
|
|
103
|
+
...redirects,
|
|
104
|
+
]),
|
|
105
|
+
];
|
|
106
|
+
await this.call("PATCH", `/projects/${ref}/config/auth`, {
|
|
107
|
+
site_url: siteUrl,
|
|
108
|
+
uri_allow_list: list.join(","),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Without `reveal`, or without permission to reveal, secret keys come back with `·` in place of characters. */
|
|
113
|
+
function revealed(key) {
|
|
114
|
+
return !key.includes("·");
|
|
115
|
+
}
|
|
116
|
+
function legacyPair(keys) {
|
|
117
|
+
const find = (name) => keys.find((k) => k.name === name && (k.type ?? "legacy") === "legacy" && k.api_key && revealed(k.api_key))
|
|
118
|
+
?.api_key;
|
|
119
|
+
const publishable = find("anon");
|
|
120
|
+
const secret = find("service_role");
|
|
121
|
+
return publishable && secret ? { publishable, secret } : null;
|
|
122
|
+
}
|
|
123
|
+
function newPair(keys) {
|
|
124
|
+
const find = (type) => {
|
|
125
|
+
const usable = keys.filter((k) => k.type === type && k.api_key && revealed(k.api_key));
|
|
126
|
+
return (usable.find((k) => k.name === "default") ?? usable[0])?.api_key;
|
|
127
|
+
};
|
|
128
|
+
const publishable = find("publishable");
|
|
129
|
+
const secret = find("secret");
|
|
130
|
+
return publishable && secret ? { publishable, secret } : null;
|
|
131
|
+
}
|
|
132
|
+
/** The plan-gate envelope: `{"error":{"code":"entitlement_required","feature":"branching_limit","upgrade_url":...}}`. */
|
|
133
|
+
function planGate(body) {
|
|
134
|
+
try {
|
|
135
|
+
const error = JSON.parse(body)
|
|
136
|
+
.error;
|
|
137
|
+
return error?.code === "entitlement_required" ? error : null;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function explainCreateBranchError(ref, name, err) {
|
|
144
|
+
const gate = planGate(err.body);
|
|
145
|
+
const upgrade = gate?.upgrade_url ? ` (${gate.upgrade_url})` : "";
|
|
146
|
+
let advice;
|
|
147
|
+
if (gate?.feature === "branching_persistent")
|
|
148
|
+
advice = `The organization's plan does not allow persistent branches${upgrade}.`;
|
|
149
|
+
else if (gate?.feature === "branching_limit" ||
|
|
150
|
+
/branch(es|ing)?[ _]limit|(maximum|max|too many)[\w ]* branches/i.test(err.body))
|
|
151
|
+
advice = `Project ${ref} has reached its branch limit. Delete branches you no longer need (\`swp prune\` lists leftovers) or raise the limit${upgrade}.`;
|
|
152
|
+
else if (gate)
|
|
153
|
+
advice = `The organization's plan does not include ${gate.feature ?? "this feature"}${upgrade}.`;
|
|
154
|
+
else if (err.status === 401)
|
|
155
|
+
advice = "The Supabase access token is invalid or expired.";
|
|
156
|
+
else if (err.status === 403)
|
|
157
|
+
advice = `The Supabase access token cannot create branches on ${ref}; use an organization-scoped token with access to it.`;
|
|
158
|
+
else if (err.status === 409 || /already exists/i.test(err.body))
|
|
159
|
+
advice = `A Supabase branch named "${name}" already exists on ${ref}.`;
|
|
160
|
+
else if (err.status === 429)
|
|
161
|
+
advice = "The Supabase API is rate limiting this token; run again in a minute.";
|
|
162
|
+
return advice
|
|
163
|
+
? `Cannot create Supabase branch "${name}": ${advice} Supabase said: ${err.status} ${err.body}`
|
|
164
|
+
: err.message;
|
|
165
|
+
}
|
|
166
|
+
export function projectUrl(ref) {
|
|
167
|
+
return `https://${ref}.supabase.co`;
|
|
168
|
+
}
|
package/dist/usage.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Style } from "./style.js";
|
|
2
|
+
type Flag = {
|
|
3
|
+
name: string;
|
|
4
|
+
value?: string;
|
|
5
|
+
help: string;
|
|
6
|
+
};
|
|
7
|
+
export type CommandName = "init" | "doctor" | "shared" | "up" | "check" | "down" | "pr" | "prune";
|
|
8
|
+
type Command = {
|
|
9
|
+
name: CommandName;
|
|
10
|
+
group: "Set up" | "Per branch" | "In CI";
|
|
11
|
+
args: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
details: string;
|
|
14
|
+
flags: Flag[];
|
|
15
|
+
env: string[];
|
|
16
|
+
/** The variables `--dry-run` still needs, because the dry run reads live state. */
|
|
17
|
+
dryRunEnv: string[];
|
|
18
|
+
examples: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare const COMMANDS: Command[];
|
|
21
|
+
export declare const BOOLEAN_FLAGS: ReadonlySet<string>;
|
|
22
|
+
export declare function command(name: string): Command | undefined;
|
|
23
|
+
export declare function version(): string;
|
|
24
|
+
export declare function mainHelp(s?: Style): string;
|
|
25
|
+
export declare function commandHelp(c: Command, s?: Style): string;
|
|
26
|
+
export type Invocation = {
|
|
27
|
+
kind: "help";
|
|
28
|
+
text: string;
|
|
29
|
+
code: 0 | 2;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "version";
|
|
32
|
+
} | {
|
|
33
|
+
kind: "error";
|
|
34
|
+
message: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "run";
|
|
37
|
+
command: CommandName;
|
|
38
|
+
flags: Record<string, string | true>;
|
|
39
|
+
};
|
|
40
|
+
/** What the arguments ask for, or a usage error that says how to fix them. */
|
|
41
|
+
export declare function resolveInvocation(argv: string[], s?: Style): Invocation;
|
|
42
|
+
/** Names every variable the command needs and lacks, with where to get each; undefined when none is missing. */
|
|
43
|
+
export declare function missingEnvMessage(name: CommandName, dryRun: boolean, env?: Record<string, string | undefined>): string | undefined;
|
|
44
|
+
/** One line for the user: the message, and for a network failure the host and the system error. */
|
|
45
|
+
export declare function formatError(err: unknown, debug?: boolean): string;
|
|
46
|
+
export {};
|