@pixpilot/supabase-backup 0.0.0 → 1.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/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # supabase-backup
1
+ # @pixpilot/supabase-backup
2
2
 
3
- ## Add usage instructions here.
3
+ Encrypted backups of application schemas plus `auth.users` and `auth.identities`.
4
+ It is deliberately not full Supabase-project disaster recovery. Requires Node
5
+ 22+, `pg_dump`, `pg_restore`, and `age`; direct PostgreSQL or the Supabase
6
+ Session Pooler on port 5432 is supported, while Transaction Pooler port 6543 is rejected.
7
+
8
+ ## Backup
9
+
10
+ ```bash
11
+ SOURCE_DATABASE_URL='postgresql://…' BACKUP_AGE_RECIPIENT='age1…' \
12
+ R2_ACCESS_KEY_ID=… R2_SECRET_ACCESS_KEY=… R2_ENDPOINT='https://<account>.r2.cloudflarestorage.com' \
13
+ R2_BUCKET='myapp-production-backups' BACKUP_PREFIX='production/database' \
14
+ npx @pixpilot/supabase-backup@1 backup
15
+ ```
16
+
17
+ `APP_SCHEMAS` is comma-separated and defaults to `public`; `auth` is never an
18
+ application schema. Backups fail if non-empty `auth.mfa_factors`,
19
+ `auth.sso_providers`, or `auth.saml_providers` would be omitted. R2 must be a
20
+ private bucket with bucket-scoped credentials; configure retention/bucket lock
21
+ in R2, not the CLI.
22
+
23
+ ```bash
24
+ npx @pixpilot/supabase-backup@1 status --max-age-hours 36
25
+ ```
26
+
27
+ ## Restore drill
28
+
29
+ Keep `AGE_IDENTITY` outside GitHub Actions and R2 credentials. Restore verifies
30
+ hashes, decrypts in a private temporary directory, inspects both archives, and
31
+ does nothing unless `--apply` is present. Apply only to a fresh recovery project.
32
+
33
+ ```bash
34
+ AGE_IDENTITY='AGE-SECRET-KEY-…' R2_ACCESS_KEY_ID=… R2_SECRET_ACCESS_KEY=… \
35
+ R2_ENDPOINT=… R2_BUCKET=… TARGET_DATABASE_URL='postgresql://…' \
36
+ npx @pixpilot/supabase-backup@1 restore --key <manifest-key> --apply \
37
+ --confirm-target '<host>:5432/<database>'
38
+ ```
39
+
40
+ Apply restores Auth data before application data, never cleans `auth`, requires
41
+ empty target Auth tables, and rejects a target matching `SOURCE_DATABASE_URL`.
42
+ Verify user login and a representative application workflow manually afterward.
43
+
44
+ ## Reusable workflow
45
+
46
+ `.github/workflows/backup.yml` is `workflow_call`, has no schedule, and never
47
+ receives `AGE_IDENTITY`. Consumers own schedule, concurrency, configuration,
48
+ and notifications:
49
+
50
+ ```yaml
51
+ name: Database Backup
52
+ on:
53
+ workflow_dispatch:
54
+ schedule:
55
+ - cron: '17 3 * * *'
56
+ permissions:
57
+ contents: read
58
+ concurrency:
59
+ group: production-database-backup
60
+ cancel-in-progress: false
61
+ jobs:
62
+ backup:
63
+ uses: pixpilot/supabase-toolkit/.github/workflows/backup.yml@main
64
+ with:
65
+ backup-prefix: production/database
66
+ age-recipient: ${{ vars.BACKUP_AGE_RECIPIENT }}
67
+ r2-endpoint: ${{ vars.R2_ENDPOINT }}
68
+ r2-bucket: ${{ vars.R2_BUCKET }}
69
+ max-age-hours: 36
70
+ package-version: '1'
71
+ secrets:
72
+ database-url: ${{ secrets.SOURCE_DATABASE_URL }}
73
+ r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
74
+ r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
75
+ ```
76
+
77
+ Do not use `secrets: inherit`. Publish matching npm and `v1` workflow releases;
78
+ run a recovery drill immediately, quarterly, and after material Auth changes.
package/dist/cli.cjs ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ const require_status = require('./status-DgmkvmrB.cjs');
3
+
4
+ //#region src/cli.ts
5
+ function valueAfter(args, flag) {
6
+ const index = args.indexOf(flag);
7
+ return index === -1 ? void 0 : args[index + 1];
8
+ }
9
+ /** Executes the package CLI commands. */
10
+ async function runCli(args = process.argv.slice(2)) {
11
+ const [command] = args;
12
+ if (command === "backup") {
13
+ await require_status.backup();
14
+ return;
15
+ }
16
+ if (command === "status") {
17
+ const raw = valueAfter(args, "--max-age-hours");
18
+ await require_status.status(raw === void 0 ? void 0 : Number(raw));
19
+ return;
20
+ }
21
+ if (command === "restore") {
22
+ const key = valueAfter(args, "--key");
23
+ if (!key) throw new require_status.BackupError("restore requires --key <manifest-key>.");
24
+ const confirmTarget = valueAfter(args, "--confirm-target");
25
+ await require_status.restore({
26
+ key,
27
+ apply: args.includes("--apply"),
28
+ ...confirmTarget ? { confirmTarget } : {}
29
+ });
30
+ return;
31
+ }
32
+ throw new require_status.BackupError("Usage: supabase-backup <backup|status|restore> [options]");
33
+ }
34
+ runCli().catch((error) => {
35
+ process.stderr.write(`${require_status.redact(error instanceof Error ? error.message : "Unexpected backup failure.")}\n`);
36
+ process.exitCode = 1;
37
+ });
38
+
39
+ //#endregion
40
+ exports.runCli = runCli;
package/dist/cli.d.cts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ //#region src/cli.d.ts
3
+ /** Executes the package CLI commands. */
4
+ declare function runCli(args?: string[]): Promise<void>;
5
+ //#endregion
6
+ export { runCli };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ //#region src/cli.d.ts
3
+ /** Executes the package CLI commands. */
4
+ declare function runCli(args?: string[]): Promise<void>;
5
+ //#endregion
6
+ export { runCli };
package/dist/cli.js ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import { a as backup, g as BackupError, i as redact, n as status, r as restore } from "./status-Ba7xyZV8.js";
3
+
4
+ //#region src/cli.ts
5
+ function valueAfter(args, flag) {
6
+ const index = args.indexOf(flag);
7
+ return index === -1 ? void 0 : args[index + 1];
8
+ }
9
+ /** Executes the package CLI commands. */
10
+ async function runCli(args = process.argv.slice(2)) {
11
+ const [command] = args;
12
+ if (command === "backup") {
13
+ await backup();
14
+ return;
15
+ }
16
+ if (command === "status") {
17
+ const raw = valueAfter(args, "--max-age-hours");
18
+ await status(raw === void 0 ? void 0 : Number(raw));
19
+ return;
20
+ }
21
+ if (command === "restore") {
22
+ const key = valueAfter(args, "--key");
23
+ if (!key) throw new BackupError("restore requires --key <manifest-key>.");
24
+ const confirmTarget = valueAfter(args, "--confirm-target");
25
+ await restore({
26
+ key,
27
+ apply: args.includes("--apply"),
28
+ ...confirmTarget ? { confirmTarget } : {}
29
+ });
30
+ return;
31
+ }
32
+ throw new BackupError("Usage: supabase-backup <backup|status|restore> [options]");
33
+ }
34
+ runCli().catch((error) => {
35
+ process.stderr.write(`${redact(error instanceof Error ? error.message : "Unexpected backup failure.")}\n`);
36
+ process.exitCode = 1;
37
+ });
38
+
39
+ //#endregion
40
+ export { runCli };
package/dist/index.cjs CHANGED
@@ -1,6 +1,18 @@
1
+ const require_status = require('./status-DgmkvmrB.cjs');
1
2
 
2
- //#region src/index.ts
3
- const name = "supabase-backup";
4
-
5
- //#endregion
6
- exports.name = name;
3
+ exports.BackupError = require_status.BackupError;
4
+ exports.backup = require_status.backup;
5
+ exports.backupObjectKeys = require_status.backupObjectKeys;
6
+ exports.backupWithConfig = require_status.backupWithConfig;
7
+ exports.databaseLabel = require_status.databaseLabel;
8
+ exports.ensureDifferentDatabases = require_status.ensureDifferentDatabases;
9
+ exports.getBackupStatus = require_status.getBackupStatus;
10
+ exports.loadBackupConfig = require_status.loadBackupConfig;
11
+ exports.loadRestoreConfig = require_status.loadRestoreConfig;
12
+ exports.loadStatusConfig = require_status.loadStatusConfig;
13
+ exports.parseDatabaseUrl = require_status.parseDatabaseUrl;
14
+ exports.parseManifest = require_status.parseManifest;
15
+ exports.redact = require_status.redact;
16
+ exports.restore = require_status.restore;
17
+ exports.status = require_status.status;
18
+ exports.toLibpqEnvironment = require_status.toLibpqEnvironment;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,145 @@
1
- //#region src/index.d.ts
2
- declare const name = "supabase-backup";
1
+ //#region src/config.d.ts
2
+ interface R2Config {
3
+ accessKeyId: string;
4
+ bucket: string;
5
+ endpoint: string;
6
+ secretAccessKey: string;
7
+ }
8
+ interface BackupConfig extends R2Config {
9
+ ageRecipient: string;
10
+ appSchemas: string[];
11
+ prefix: string;
12
+ sourceDatabaseUrl: string;
13
+ }
14
+ interface RestoreConfig extends R2Config {
15
+ ageIdentity: string;
16
+ sourceDatabaseUrl?: string;
17
+ targetDatabaseUrl?: string;
18
+ }
19
+ interface StatusConfig extends R2Config {
20
+ prefix: string;
21
+ }
22
+ /** Loads and validates backup-only environment configuration. */
23
+ declare function loadBackupConfig(env?: NodeJS.ProcessEnv): BackupConfig;
24
+ /** Loads restore credentials while allowing dry-run archive checks without a target. */
25
+ declare function loadRestoreConfig(env?: NodeJS.ProcessEnv, requireTarget?: boolean): RestoreConfig;
26
+ /** Loads the R2 prefix and credentials required by the read-only health check. */
27
+ declare function loadStatusConfig(env?: NodeJS.ProcessEnv): StatusConfig;
3
28
  //#endregion
4
- export { name };
29
+ //#region src/manifest.d.ts
30
+ declare const authTables: readonly ["auth.users", "auth.identities"];
31
+ type AuthTable = (typeof authTables)[number];
32
+ interface Column {
33
+ dataType: string;
34
+ name: string;
35
+ }
36
+ interface TableCount {
37
+ table: string;
38
+ count: number;
39
+ }
40
+ interface BackupManifest {
41
+ appChecksumObjectKey: string;
42
+ appEncryptedBytes: number;
43
+ appObjectKey: string;
44
+ appSchemas: string[];
45
+ appSha256: string;
46
+ appTableCounts: TableCount[];
47
+ authColumns: Record<AuthTable, Column[]>;
48
+ authChecksumObjectKey: string;
49
+ authEncryptedBytes: number;
50
+ authObjectKey: string;
51
+ authRowCounts: TableCount[];
52
+ authSha256: string;
53
+ authTables: AuthTable[];
54
+ cliVersion: string;
55
+ createdAt: string;
56
+ environment: string;
57
+ pgDumpVersion: string;
58
+ postgresServerVersion: string;
59
+ }
60
+ /** Generates immutable object names for one UTC backup timestamp. */
61
+ declare function backupObjectKeys(prefix: string, createdAt: Date): Record<'app' | 'auth' | 'appChecksum' | 'authChecksum' | 'manifest', string>;
62
+ /** Rejects malformed manifests before they can guide a restore. */
63
+ declare function parseManifest(value: string): BackupManifest;
64
+ //#endregion
65
+ //#region src/process.d.ts
66
+ interface ProgramRunner {
67
+ run: (command: string, args: string[], options?: {
68
+ env?: NodeJS.ProcessEnv;
69
+ }) => Promise<string>;
70
+ }
71
+ //#endregion
72
+ //#region src/r2.d.ts
73
+ interface ObjectStore {
74
+ get: (key: string) => Promise<Uint8Array>;
75
+ has: (key: string) => Promise<boolean>;
76
+ list: (prefix: string) => Promise<string[]>;
77
+ putImmutable: (key: string, body: Uint8Array) => Promise<void>;
78
+ }
79
+ //#endregion
80
+ //#region src/backup.d.ts
81
+ /** Creates encrypted, immutable app/Auth archives and publishes their manifest last. */
82
+ declare function backup(env?: NodeJS.ProcessEnv, dependencies?: {
83
+ now?: Date;
84
+ runner?: ProgramRunner;
85
+ store?: ObjectStore;
86
+ }): Promise<BackupManifest>;
87
+ /** Implements backup with injected dependencies for deterministic tests. */
88
+ declare function backupWithConfig(config: BackupConfig, dependencies?: {
89
+ now?: Date;
90
+ runner?: ProgramRunner;
91
+ store?: ObjectStore;
92
+ }): Promise<BackupManifest>;
93
+ //#endregion
94
+ //#region src/database-url.d.ts
95
+ interface DatabaseConnection {
96
+ database: string;
97
+ host: string;
98
+ password: string;
99
+ port: string;
100
+ sslmode: string;
101
+ user: string;
102
+ }
103
+ /** Parses a PostgreSQL URL without retaining it in process arguments. */
104
+ declare function parseDatabaseUrl(value: string): DatabaseConnection;
105
+ /** Converts a parsed URL to the libpq environment passed to PostgreSQL tools. */
106
+ declare function toLibpqEnvironment(connection: DatabaseConnection): NodeJS.ProcessEnv;
107
+ /** Produces a non-secret target label used for typed restore confirmation. */
108
+ declare function databaseLabel(connection: DatabaseConnection): string;
109
+ /** Prevents a configured source database from also being used as a restore target. */
110
+ declare function ensureDifferentDatabases(source: DatabaseConnection | undefined, target: DatabaseConnection): void;
111
+ //#endregion
112
+ //#region src/errors.d.ts
113
+ /** Error whose message is safe to display from the CLI. */
114
+ declare class BackupError extends Error {
115
+ constructor(message: string);
116
+ }
117
+ //#endregion
118
+ //#region src/redact.d.ts
119
+ /** Removes connection strings, age identities, and common credential values from text. */
120
+ declare function redact(value: string): string;
121
+ //#endregion
122
+ //#region src/restore.d.ts
123
+ interface RestoreOptions {
124
+ apply: boolean;
125
+ confirmTarget?: string;
126
+ key: string;
127
+ }
128
+ /** Downloads, verifies, decrypts, and optionally restores a single manifest. */
129
+ declare function restore(options: RestoreOptions, env?: NodeJS.ProcessEnv, dependencies?: {
130
+ runner?: ProgramRunner;
131
+ store?: ObjectStore;
132
+ }): Promise<BackupManifest>;
133
+ //#endregion
134
+ //#region src/status.d.ts
135
+ interface BackupStatus {
136
+ ageHours: number;
137
+ manifest: BackupManifest;
138
+ manifestKey: string;
139
+ }
140
+ /** Finds the latest complete manifest and checks its referenced archives remain available. */
141
+ declare function getBackupStatus(prefix: string, store: ObjectStore, now?: Date): Promise<BackupStatus>;
142
+ /** Prints a machine-safe health result and fails when no recent valid backup exists. */
143
+ declare function status(maxAgeHours: number | undefined, env?: NodeJS.ProcessEnv): Promise<BackupStatus>;
144
+ //#endregion
145
+ export { BackupError, type BackupManifest, backup, backupObjectKeys, backupWithConfig, databaseLabel, ensureDifferentDatabases, getBackupStatus, loadBackupConfig, loadRestoreConfig, loadStatusConfig, parseDatabaseUrl, parseManifest, redact, restore, status, toLibpqEnvironment };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,145 @@
1
- //#region src/index.d.ts
2
- declare const name = "supabase-backup";
1
+ //#region src/config.d.ts
2
+ interface R2Config {
3
+ accessKeyId: string;
4
+ bucket: string;
5
+ endpoint: string;
6
+ secretAccessKey: string;
7
+ }
8
+ interface BackupConfig extends R2Config {
9
+ ageRecipient: string;
10
+ appSchemas: string[];
11
+ prefix: string;
12
+ sourceDatabaseUrl: string;
13
+ }
14
+ interface RestoreConfig extends R2Config {
15
+ ageIdentity: string;
16
+ sourceDatabaseUrl?: string;
17
+ targetDatabaseUrl?: string;
18
+ }
19
+ interface StatusConfig extends R2Config {
20
+ prefix: string;
21
+ }
22
+ /** Loads and validates backup-only environment configuration. */
23
+ declare function loadBackupConfig(env?: NodeJS.ProcessEnv): BackupConfig;
24
+ /** Loads restore credentials while allowing dry-run archive checks without a target. */
25
+ declare function loadRestoreConfig(env?: NodeJS.ProcessEnv, requireTarget?: boolean): RestoreConfig;
26
+ /** Loads the R2 prefix and credentials required by the read-only health check. */
27
+ declare function loadStatusConfig(env?: NodeJS.ProcessEnv): StatusConfig;
3
28
  //#endregion
4
- export { name };
29
+ //#region src/manifest.d.ts
30
+ declare const authTables: readonly ["auth.users", "auth.identities"];
31
+ type AuthTable = (typeof authTables)[number];
32
+ interface Column {
33
+ dataType: string;
34
+ name: string;
35
+ }
36
+ interface TableCount {
37
+ table: string;
38
+ count: number;
39
+ }
40
+ interface BackupManifest {
41
+ appChecksumObjectKey: string;
42
+ appEncryptedBytes: number;
43
+ appObjectKey: string;
44
+ appSchemas: string[];
45
+ appSha256: string;
46
+ appTableCounts: TableCount[];
47
+ authColumns: Record<AuthTable, Column[]>;
48
+ authChecksumObjectKey: string;
49
+ authEncryptedBytes: number;
50
+ authObjectKey: string;
51
+ authRowCounts: TableCount[];
52
+ authSha256: string;
53
+ authTables: AuthTable[];
54
+ cliVersion: string;
55
+ createdAt: string;
56
+ environment: string;
57
+ pgDumpVersion: string;
58
+ postgresServerVersion: string;
59
+ }
60
+ /** Generates immutable object names for one UTC backup timestamp. */
61
+ declare function backupObjectKeys(prefix: string, createdAt: Date): Record<'app' | 'auth' | 'appChecksum' | 'authChecksum' | 'manifest', string>;
62
+ /** Rejects malformed manifests before they can guide a restore. */
63
+ declare function parseManifest(value: string): BackupManifest;
64
+ //#endregion
65
+ //#region src/process.d.ts
66
+ interface ProgramRunner {
67
+ run: (command: string, args: string[], options?: {
68
+ env?: NodeJS.ProcessEnv;
69
+ }) => Promise<string>;
70
+ }
71
+ //#endregion
72
+ //#region src/r2.d.ts
73
+ interface ObjectStore {
74
+ get: (key: string) => Promise<Uint8Array>;
75
+ has: (key: string) => Promise<boolean>;
76
+ list: (prefix: string) => Promise<string[]>;
77
+ putImmutable: (key: string, body: Uint8Array) => Promise<void>;
78
+ }
79
+ //#endregion
80
+ //#region src/backup.d.ts
81
+ /** Creates encrypted, immutable app/Auth archives and publishes their manifest last. */
82
+ declare function backup(env?: NodeJS.ProcessEnv, dependencies?: {
83
+ now?: Date;
84
+ runner?: ProgramRunner;
85
+ store?: ObjectStore;
86
+ }): Promise<BackupManifest>;
87
+ /** Implements backup with injected dependencies for deterministic tests. */
88
+ declare function backupWithConfig(config: BackupConfig, dependencies?: {
89
+ now?: Date;
90
+ runner?: ProgramRunner;
91
+ store?: ObjectStore;
92
+ }): Promise<BackupManifest>;
93
+ //#endregion
94
+ //#region src/database-url.d.ts
95
+ interface DatabaseConnection {
96
+ database: string;
97
+ host: string;
98
+ password: string;
99
+ port: string;
100
+ sslmode: string;
101
+ user: string;
102
+ }
103
+ /** Parses a PostgreSQL URL without retaining it in process arguments. */
104
+ declare function parseDatabaseUrl(value: string): DatabaseConnection;
105
+ /** Converts a parsed URL to the libpq environment passed to PostgreSQL tools. */
106
+ declare function toLibpqEnvironment(connection: DatabaseConnection): NodeJS.ProcessEnv;
107
+ /** Produces a non-secret target label used for typed restore confirmation. */
108
+ declare function databaseLabel(connection: DatabaseConnection): string;
109
+ /** Prevents a configured source database from also being used as a restore target. */
110
+ declare function ensureDifferentDatabases(source: DatabaseConnection | undefined, target: DatabaseConnection): void;
111
+ //#endregion
112
+ //#region src/errors.d.ts
113
+ /** Error whose message is safe to display from the CLI. */
114
+ declare class BackupError extends Error {
115
+ constructor(message: string);
116
+ }
117
+ //#endregion
118
+ //#region src/redact.d.ts
119
+ /** Removes connection strings, age identities, and common credential values from text. */
120
+ declare function redact(value: string): string;
121
+ //#endregion
122
+ //#region src/restore.d.ts
123
+ interface RestoreOptions {
124
+ apply: boolean;
125
+ confirmTarget?: string;
126
+ key: string;
127
+ }
128
+ /** Downloads, verifies, decrypts, and optionally restores a single manifest. */
129
+ declare function restore(options: RestoreOptions, env?: NodeJS.ProcessEnv, dependencies?: {
130
+ runner?: ProgramRunner;
131
+ store?: ObjectStore;
132
+ }): Promise<BackupManifest>;
133
+ //#endregion
134
+ //#region src/status.d.ts
135
+ interface BackupStatus {
136
+ ageHours: number;
137
+ manifest: BackupManifest;
138
+ manifestKey: string;
139
+ }
140
+ /** Finds the latest complete manifest and checks its referenced archives remain available. */
141
+ declare function getBackupStatus(prefix: string, store: ObjectStore, now?: Date): Promise<BackupStatus>;
142
+ /** Prints a machine-safe health result and fails when no recent valid backup exists. */
143
+ declare function status(maxAgeHours: number | undefined, env?: NodeJS.ProcessEnv): Promise<BackupStatus>;
144
+ //#endregion
145
+ export { BackupError, type BackupManifest, backup, backupObjectKeys, backupWithConfig, databaseLabel, ensureDifferentDatabases, getBackupStatus, loadBackupConfig, loadRestoreConfig, loadStatusConfig, parseDatabaseUrl, parseManifest, redact, restore, status, toLibpqEnvironment };
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- //#region src/index.ts
2
- const name = "supabase-backup";
1
+ import { a as backup, c as parseManifest, d as parseDatabaseUrl, f as toLibpqEnvironment, g as BackupError, h as loadStatusConfig, i as redact, l as databaseLabel, m as loadRestoreConfig, n as status, o as backupWithConfig, p as loadBackupConfig, r as restore, s as backupObjectKeys, t as getBackupStatus, u as ensureDifferentDatabases } from "./status-Ba7xyZV8.js";
3
2
 
4
- //#endregion
5
- export { name };
3
+ export { BackupError, backup, backupObjectKeys, backupWithConfig, databaseLabel, ensureDifferentDatabases, getBackupStatus, loadBackupConfig, loadRestoreConfig, loadStatusConfig, parseDatabaseUrl, parseManifest, redact, restore, status, toLibpqEnvironment };