supascan 0.0.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/README.md +165 -0
- package/commands/analyze.command.ts +187 -0
- package/commands/dump.command.ts +98 -0
- package/commands/rpc.command.ts +205 -0
- package/context.ts +84 -0
- package/index.ts +94 -0
- package/package.json +43 -0
- package/services/analyzer.service.test.ts +193 -0
- package/services/analyzer.service.ts +190 -0
- package/services/extractor.service.test.ts +194 -0
- package/services/extractor.service.ts +230 -0
- package/services/html-renderer.service.tsx +1246 -0
- package/services/supabase.service.test.ts +352 -0
- package/services/supabase.service.ts +316 -0
- package/utils.ts +127 -0
- package/version.ts +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# supascan
|
|
2
|
+
|
|
3
|
+
A security analysis CLI tool for Supabase databases that helps identify exposed data, analyze schemas, and test RPC functions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g supascan
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Analysis
|
|
14
|
+
|
|
15
|
+
Analyze your Supabase database for security issues:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
supascan --url https://your-project.supabase.co --key your-anon-key
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Available Commands
|
|
22
|
+
|
|
23
|
+
#### Database Analysis
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Analyze all schemas
|
|
27
|
+
supascan --url <url> --key <key>
|
|
28
|
+
|
|
29
|
+
# Analyze specific schema
|
|
30
|
+
supascan --url <url> --key <key> --schema public
|
|
31
|
+
|
|
32
|
+
# Generate HTML report
|
|
33
|
+
supascan --url <url> --key <key> --html
|
|
34
|
+
|
|
35
|
+
# JSON output
|
|
36
|
+
supascan --url <url> --key <key> --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Data Dumping
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Dump table data
|
|
43
|
+
supascan --url <url> --key <key> --dump public.users --limit 100
|
|
44
|
+
|
|
45
|
+
# Dump Swagger JSON for schema
|
|
46
|
+
supascan --url <url> --key <key> --dump public
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### RPC Testing
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Get RPC help
|
|
53
|
+
supascan --url <url> --key <key> --rpc public.get_user_stats
|
|
54
|
+
|
|
55
|
+
# Call RPC with parameters
|
|
56
|
+
supascan --url <url> --key <key> --rpc public.get_user_stats --args '{"user_id": "123"}'
|
|
57
|
+
|
|
58
|
+
# Show query execution plan
|
|
59
|
+
supascan --url <url> --key <key> --rpc public.get_user_stats --args '{"user_id": "123"}' --explain
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
#### Credential Extraction (Experimental)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Extract credentials from JS file
|
|
66
|
+
supascan --extract https://example.com/app.js --url <url> --key <key>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Options
|
|
70
|
+
|
|
71
|
+
| Option | Description |
|
|
72
|
+
| ---------------------------------- | ---------------------------------------------------------------- |
|
|
73
|
+
| `-u, --url <url>` | Supabase URL |
|
|
74
|
+
| `-k, --key <key>` | Supabase anon key |
|
|
75
|
+
| `-s, --schema <schema>` | Schema to analyze (default: all schemas) |
|
|
76
|
+
| `-x, --extract <url>` | Extract credentials from JS file URL (experimental) |
|
|
77
|
+
| `--dump <schema.table\|schema>` | Dump data from specific table or swagger JSON from schema |
|
|
78
|
+
| `--limit <number>` | Limit rows for dump or RPC results (default: 10) |
|
|
79
|
+
| `--rpc <schema.rpc_name>` | Call an RPC function (read-only operations only) |
|
|
80
|
+
| `--args <json>` | JSON arguments for RPC call (use $VAR for environment variables) |
|
|
81
|
+
| `--json` | Output as JSON |
|
|
82
|
+
| `--html` | Generate HTML report |
|
|
83
|
+
| `-d, --debug` | Enable debug mode |
|
|
84
|
+
| `--explain` | Show query execution plan |
|
|
85
|
+
| `--suppress-experimental-warnings` | Suppress experimental warnings |
|
|
86
|
+
|
|
87
|
+
## What supascan Analyzes
|
|
88
|
+
|
|
89
|
+
### Database Security Assessment
|
|
90
|
+
|
|
91
|
+
- **Schema Discovery**: Automatically discovers all available schemas
|
|
92
|
+
- **Table Access Analysis**: Identifies which tables are:
|
|
93
|
+
- ✅ **Readable** - Data is exposed and accessible
|
|
94
|
+
- ⚠️ **Empty/Protected** - No data or protected by RLS
|
|
95
|
+
- ❌ **Denied** - Access is explicitly denied
|
|
96
|
+
|
|
97
|
+
### JWT Token Analysis
|
|
98
|
+
|
|
99
|
+
- Parses and displays JWT token information
|
|
100
|
+
- Shows issuer, audience, expiration, and role information
|
|
101
|
+
|
|
102
|
+
### RPC Function Analysis
|
|
103
|
+
|
|
104
|
+
- Lists all available RPC functions
|
|
105
|
+
- Shows parameter requirements and types
|
|
106
|
+
- Validates parameters before execution
|
|
107
|
+
|
|
108
|
+
### Output Formats
|
|
109
|
+
|
|
110
|
+
- **Console**: Colorized terminal output with detailed analysis
|
|
111
|
+
- **JSON**: Machine-readable output for scripting
|
|
112
|
+
- **HTML**: Visual report that opens in your browser
|
|
113
|
+
|
|
114
|
+
## Examples
|
|
115
|
+
|
|
116
|
+
### Security Analysis Report
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
supascan --url https://abc123.supabase.co --key eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... --html
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Check Specific Table Access
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
supascan --url https://abc123.supabase.co --key eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... --dump public.users --limit 5
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Test RPC Function
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
supascan --url https://abc123.supabase.co --key eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... --rpc public.get_user_count --args '{"active": true}'
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Security Considerations
|
|
135
|
+
|
|
136
|
+
⚠️ **Important**: This tool is designed for security analysis and testing. Only use it on:
|
|
137
|
+
|
|
138
|
+
- Your own databases
|
|
139
|
+
- Databases you have explicit permission to test
|
|
140
|
+
- Staging/development environments
|
|
141
|
+
|
|
142
|
+
Never use this tool on production databases without proper authorization.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Install dependencies
|
|
148
|
+
bun install
|
|
149
|
+
|
|
150
|
+
# Run locally
|
|
151
|
+
bun run start
|
|
152
|
+
|
|
153
|
+
# Build
|
|
154
|
+
bun run build
|
|
155
|
+
|
|
156
|
+
# Test
|
|
157
|
+
bun test
|
|
158
|
+
|
|
159
|
+
# Lint
|
|
160
|
+
bun run lint
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
Private - All rights reserved.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import type { CLIContext } from "../context";
|
|
3
|
+
import {
|
|
4
|
+
AnalyzerService,
|
|
5
|
+
type AnalysisResult,
|
|
6
|
+
} from "../services/analyzer.service";
|
|
7
|
+
import { HtmlRendererService } from "../services/html-renderer.service";
|
|
8
|
+
import {
|
|
9
|
+
generateTempFilePath,
|
|
10
|
+
log,
|
|
11
|
+
openInBrowser,
|
|
12
|
+
writeHtmlFile,
|
|
13
|
+
} from "../utils";
|
|
14
|
+
|
|
15
|
+
export async function executeAnalyzeCommand(
|
|
16
|
+
ctx: CLIContext,
|
|
17
|
+
options: { schema?: string },
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const analysisResult = await AnalyzerService.analyze(ctx, options.schema);
|
|
20
|
+
|
|
21
|
+
if (!analysisResult.success) {
|
|
22
|
+
log.error("Analysis failed", analysisResult.error.message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (ctx.json) {
|
|
27
|
+
console.log(JSON.stringify(analysisResult.value, null, 2));
|
|
28
|
+
} else if (ctx.html) {
|
|
29
|
+
const htmlContent = HtmlRendererService.generateHtmlReport(
|
|
30
|
+
analysisResult.value,
|
|
31
|
+
ctx.url,
|
|
32
|
+
ctx.key,
|
|
33
|
+
);
|
|
34
|
+
const filePath = generateTempFilePath();
|
|
35
|
+
writeHtmlFile(filePath, htmlContent);
|
|
36
|
+
openInBrowser(filePath);
|
|
37
|
+
log.success(`HTML report generated: ${filePath}`);
|
|
38
|
+
} else {
|
|
39
|
+
displayAnalysisResult(analysisResult.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function displayAnalysisResult(result: AnalysisResult): void {
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
46
|
+
console.log(pc.bold(pc.cyan(" SUPABASE DATABASE ANALYSIS")));
|
|
47
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
48
|
+
console.log();
|
|
49
|
+
|
|
50
|
+
console.log(pc.bold(pc.yellow("TARGET SUMMARY")));
|
|
51
|
+
console.log(pc.dim("─".repeat(20)));
|
|
52
|
+
console.log(pc.bold("Domain:"), pc.white(result.summary.domain));
|
|
53
|
+
|
|
54
|
+
if (result.summary.metadata?.service) {
|
|
55
|
+
console.log(pc.bold("Service:"), pc.white(result.summary.metadata.service));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (result.summary.metadata?.region) {
|
|
59
|
+
console.log(
|
|
60
|
+
pc.bold("Project ID:"),
|
|
61
|
+
pc.white(result.summary.metadata.region),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (result.summary.metadata?.title) {
|
|
66
|
+
console.log(pc.bold("Title:"), pc.white(result.summary.metadata.title));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (result.summary.metadata?.version) {
|
|
70
|
+
console.log(pc.bold("Version:"), pc.white(result.summary.metadata.version));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (result.summary.jwtInfo) {
|
|
74
|
+
console.log();
|
|
75
|
+
console.log(pc.bold(pc.yellow("JWT TOKEN INFO")));
|
|
76
|
+
console.log(pc.dim("─".repeat(20)));
|
|
77
|
+
|
|
78
|
+
if (result.summary.jwtInfo.iss) {
|
|
79
|
+
console.log(pc.bold("Issuer:"), pc.white(result.summary.jwtInfo.iss));
|
|
80
|
+
}
|
|
81
|
+
if (result.summary.jwtInfo.aud) {
|
|
82
|
+
console.log(pc.bold("Audience:"), pc.white(result.summary.jwtInfo.aud));
|
|
83
|
+
}
|
|
84
|
+
if (result.summary.jwtInfo.role) {
|
|
85
|
+
console.log(pc.bold("Role:"), pc.white(result.summary.jwtInfo.role));
|
|
86
|
+
}
|
|
87
|
+
if (result.summary.jwtInfo.exp) {
|
|
88
|
+
const expDate = new Date(result.summary.jwtInfo.exp * 1000);
|
|
89
|
+
console.log(pc.bold("Expires:"), pc.white(expDate.toISOString()));
|
|
90
|
+
}
|
|
91
|
+
if (result.summary.jwtInfo.iat) {
|
|
92
|
+
const iatDate = new Date(result.summary.jwtInfo.iat * 1000);
|
|
93
|
+
console.log(pc.bold("Issued:"), pc.white(iatDate.toISOString()));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log();
|
|
98
|
+
console.log(pc.bold(pc.cyan("DATABASE ANALYSIS")));
|
|
99
|
+
console.log(pc.dim("─".repeat(20)));
|
|
100
|
+
console.log(
|
|
101
|
+
pc.bold("Schemas discovered:"),
|
|
102
|
+
pc.green(result.schemas.length.toString()),
|
|
103
|
+
);
|
|
104
|
+
console.log();
|
|
105
|
+
|
|
106
|
+
Object.entries(result.schemaDetails).forEach(([schema, analysis]) => {
|
|
107
|
+
console.log(pc.bold(pc.cyan(`Schema: ${schema}`)));
|
|
108
|
+
console.log();
|
|
109
|
+
|
|
110
|
+
const exposedCount = Object.values(analysis.tableAccess).filter(
|
|
111
|
+
(a) => a.status === "readable",
|
|
112
|
+
).length;
|
|
113
|
+
const deniedCount = Object.values(analysis.tableAccess).filter(
|
|
114
|
+
(a) => a.status === "denied",
|
|
115
|
+
).length;
|
|
116
|
+
const emptyCount = Object.values(analysis.tableAccess).filter(
|
|
117
|
+
(a) => a.status === "empty",
|
|
118
|
+
).length;
|
|
119
|
+
|
|
120
|
+
console.log(
|
|
121
|
+
pc.bold("Tables:"),
|
|
122
|
+
pc.green(analysis.tables.length.toString()),
|
|
123
|
+
);
|
|
124
|
+
console.log(
|
|
125
|
+
pc.dim(
|
|
126
|
+
` ${exposedCount} exposed • ${emptyCount} empty/protected • ${deniedCount} denied`,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
console.log();
|
|
130
|
+
|
|
131
|
+
if (analysis.tables.length > 0) {
|
|
132
|
+
analysis.tables.forEach((table) => {
|
|
133
|
+
const access = analysis.tableAccess[table];
|
|
134
|
+
let indicator = "";
|
|
135
|
+
let description = "";
|
|
136
|
+
|
|
137
|
+
switch (access?.status) {
|
|
138
|
+
case "readable":
|
|
139
|
+
indicator = pc.green("✓");
|
|
140
|
+
description = pc.dim("(data exposed)");
|
|
141
|
+
break;
|
|
142
|
+
case "empty":
|
|
143
|
+
indicator = pc.yellow("○");
|
|
144
|
+
description = pc.dim("(0 rows - empty or RLS)");
|
|
145
|
+
break;
|
|
146
|
+
case "denied":
|
|
147
|
+
indicator = pc.red("✗");
|
|
148
|
+
description = pc.dim("(access denied)");
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(` ${indicator} ${pc.white(table)} ${description}`);
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
console.log(pc.dim(" No tables found"));
|
|
156
|
+
}
|
|
157
|
+
console.log();
|
|
158
|
+
|
|
159
|
+
console.log(pc.bold("RPCs:"), pc.green(analysis.rpcs.length.toString()));
|
|
160
|
+
if (analysis.rpcFunctions.length > 0) {
|
|
161
|
+
analysis.rpcFunctions.forEach((rpc) => {
|
|
162
|
+
console.log(` • ${pc.white(rpc.name)}`);
|
|
163
|
+
if (rpc.parameters.length > 0) {
|
|
164
|
+
rpc.parameters.forEach((param) => {
|
|
165
|
+
const required = param.required
|
|
166
|
+
? pc.red("(required)")
|
|
167
|
+
: pc.dim("(optional)");
|
|
168
|
+
const type = param.format
|
|
169
|
+
? `${param.type} (${param.format})`
|
|
170
|
+
: param.type;
|
|
171
|
+
console.log(
|
|
172
|
+
` - ${pc.cyan(param.name)}: ${pc.yellow(type)} ${required}`,
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
console.log(pc.dim(" No parameters"));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
} else {
|
|
180
|
+
console.log(pc.dim(" No RPCs found"));
|
|
181
|
+
}
|
|
182
|
+
console.log();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
186
|
+
console.log();
|
|
187
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import type { CLIContext } from "../context";
|
|
3
|
+
import { SupabaseService } from "../services/supabase.service";
|
|
4
|
+
import { log } from "../utils";
|
|
5
|
+
|
|
6
|
+
export async function executeDumpCommand(
|
|
7
|
+
ctx: CLIContext,
|
|
8
|
+
options: {
|
|
9
|
+
dump: string;
|
|
10
|
+
limit: string;
|
|
11
|
+
},
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const parts = options.dump.split(".");
|
|
14
|
+
|
|
15
|
+
if (parts.length === 1 && parts[0]) {
|
|
16
|
+
const schema = parts[0];
|
|
17
|
+
|
|
18
|
+
const swaggerResult = await SupabaseService.getSwagger(ctx, schema);
|
|
19
|
+
|
|
20
|
+
if (!swaggerResult.success) {
|
|
21
|
+
log.error("Failed to get swagger", swaggerResult.error.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (ctx.json) {
|
|
26
|
+
console.log(JSON.stringify(swaggerResult.value, null, 2));
|
|
27
|
+
} else {
|
|
28
|
+
displaySwaggerResult(schema, swaggerResult.value);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
34
|
+
log.error("Invalid format. Use: schema.table or schema");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const schema = parts[0];
|
|
39
|
+
const table = parts[1];
|
|
40
|
+
const limit = parseInt(options.limit);
|
|
41
|
+
|
|
42
|
+
const dumpResult = await SupabaseService.dumpTable(ctx, schema, table, limit);
|
|
43
|
+
|
|
44
|
+
if (!dumpResult.success) {
|
|
45
|
+
log.error("Failed to dump table", dumpResult.error.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (ctx.json) {
|
|
50
|
+
console.log(JSON.stringify(dumpResult.value, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
displayTableDumpResult(schema, table, dumpResult.value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function displaySwaggerResult(schema: string, swagger: any): void {
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
59
|
+
console.log(pc.bold(pc.cyan(` SWAGGER DUMP: ${schema}`)));
|
|
60
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(JSON.stringify(swagger, null, 2));
|
|
63
|
+
console.log();
|
|
64
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function displayTableDumpResult(
|
|
69
|
+
schema: string,
|
|
70
|
+
table: string,
|
|
71
|
+
result: {
|
|
72
|
+
columns: string[];
|
|
73
|
+
rows: Record<string, unknown>[];
|
|
74
|
+
count: number;
|
|
75
|
+
},
|
|
76
|
+
): void {
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
79
|
+
console.log(pc.bold(pc.cyan(` TABLE DUMP: ${schema}.${table}`)));
|
|
80
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
81
|
+
console.log();
|
|
82
|
+
console.log(pc.bold("Total rows:"), pc.green(result.count.toString()));
|
|
83
|
+
console.log(pc.bold("Showing:"), pc.green(result.rows.length.toString()));
|
|
84
|
+
console.log(pc.bold("Columns:"), pc.green(result.columns.length.toString()));
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(pc.dim(result.columns.join(", ")));
|
|
87
|
+
console.log();
|
|
88
|
+
|
|
89
|
+
if (result.rows.length > 0) {
|
|
90
|
+
console.table(result.rows);
|
|
91
|
+
} else {
|
|
92
|
+
console.log(pc.dim("No rows found"));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log();
|
|
96
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
97
|
+
console.log();
|
|
98
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import type { CLIContext } from "../context";
|
|
3
|
+
import {
|
|
4
|
+
SupabaseService,
|
|
5
|
+
type RPCFunction,
|
|
6
|
+
type RPCParameter,
|
|
7
|
+
} from "../services/supabase.service";
|
|
8
|
+
import { log, parseRPCArgs } from "../utils";
|
|
9
|
+
|
|
10
|
+
export async function executeRPCCommand(
|
|
11
|
+
ctx: CLIContext,
|
|
12
|
+
options: {
|
|
13
|
+
rpc: string;
|
|
14
|
+
args?: string;
|
|
15
|
+
limit: string;
|
|
16
|
+
explain?: boolean;
|
|
17
|
+
},
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const parts = options.rpc.split(".");
|
|
20
|
+
|
|
21
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
22
|
+
log.error("Invalid RPC format. Use: schema.rpc_name");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const schema = parts[0];
|
|
27
|
+
const rpcName = parts[1];
|
|
28
|
+
|
|
29
|
+
const rpcFunctionsResult = await SupabaseService.getRPCsWithParameters(
|
|
30
|
+
ctx,
|
|
31
|
+
schema,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
let rpcFunction: RPCFunction | null = null;
|
|
35
|
+
|
|
36
|
+
if (rpcFunctionsResult.success) {
|
|
37
|
+
rpcFunction =
|
|
38
|
+
rpcFunctionsResult.value.find((rpc) => rpc.name === `rpc/${rpcName}`) ||
|
|
39
|
+
null;
|
|
40
|
+
} else {
|
|
41
|
+
log.warn(
|
|
42
|
+
"Failed to get RPC functions from schema, proceeding without validation",
|
|
43
|
+
rpcFunctionsResult.error.message,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!options.args) {
|
|
48
|
+
displayRPCHelp(schema, rpcName, rpcFunction);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let args: Record<string, any> = {};
|
|
53
|
+
try {
|
|
54
|
+
args = parseRPCArgs(options.args);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
log.error(
|
|
57
|
+
"Failed to parse RPC arguments",
|
|
58
|
+
error instanceof Error ? error.message : String(error),
|
|
59
|
+
);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (rpcFunction) {
|
|
64
|
+
const requiredParams = rpcFunction.parameters.filter(
|
|
65
|
+
(p: RPCParameter) => p.required,
|
|
66
|
+
);
|
|
67
|
+
const missingParams = requiredParams.filter(
|
|
68
|
+
(p: RPCParameter) => !(p.name in args),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (missingParams.length > 0) {
|
|
72
|
+
log.error(
|
|
73
|
+
`Missing required parameters: ${missingParams.map((p: RPCParameter) => p.name).join(", ")}`,
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
log.warn(
|
|
79
|
+
"Skipping parameter validation due to schema introspection failure",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const rpcResult = await SupabaseService.callRPC(ctx, schema, rpcName, args, {
|
|
84
|
+
get: true,
|
|
85
|
+
explain: options.explain,
|
|
86
|
+
limit: parseInt(options.limit),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!rpcResult.success) {
|
|
90
|
+
log.error("RPC call failed", rpcResult.error.message);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (ctx.json) {
|
|
95
|
+
console.log(JSON.stringify(rpcResult.value, null, 2));
|
|
96
|
+
} else {
|
|
97
|
+
displayRPCResult(schema, rpcName, rpcResult.value, options.explain);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function displayRPCHelp(
|
|
102
|
+
schema: string,
|
|
103
|
+
rpcName: string,
|
|
104
|
+
rpcFunction: RPCFunction | null,
|
|
105
|
+
): void {
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
108
|
+
console.log(pc.bold(pc.cyan(` RPC HELP: ${schema}.${rpcName}`)));
|
|
109
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
110
|
+
console.log();
|
|
111
|
+
|
|
112
|
+
if (rpcFunction && rpcFunction.parameters.length > 0) {
|
|
113
|
+
console.log(pc.bold("Parameters:"));
|
|
114
|
+
rpcFunction.parameters.forEach((param: RPCParameter) => {
|
|
115
|
+
const required = param.required
|
|
116
|
+
? pc.red("(required)")
|
|
117
|
+
: pc.dim("(optional)");
|
|
118
|
+
const type = param.format
|
|
119
|
+
? `${param.type} (${param.format})`
|
|
120
|
+
: param.type;
|
|
121
|
+
console.log(` • ${pc.cyan(param.name)}: ${pc.yellow(type)} ${required}`);
|
|
122
|
+
if (param.description) {
|
|
123
|
+
console.log(pc.dim(` ${param.description}`));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
console.log();
|
|
127
|
+
console.log(pc.bold("Usage:"));
|
|
128
|
+
console.log(
|
|
129
|
+
pc.dim(
|
|
130
|
+
`supascan --rpc "${schema}.${rpcName}" --args '{"param1": "value1", "param2": "value2"}'`,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
} else if (rpcFunction) {
|
|
134
|
+
console.log(pc.dim("No parameters required"));
|
|
135
|
+
console.log();
|
|
136
|
+
console.log(pc.bold("Usage:"));
|
|
137
|
+
console.log(pc.dim(`supascan --rpc "${schema}.${rpcName}"`));
|
|
138
|
+
} else {
|
|
139
|
+
console.log(
|
|
140
|
+
pc.yellow(
|
|
141
|
+
"⚠️ Schema introspection failed - parameter information unavailable",
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(pc.bold("Usage:"));
|
|
146
|
+
console.log(
|
|
147
|
+
pc.dim(
|
|
148
|
+
`supascan --rpc "${schema}.${rpcName}" --args '{"param1": "value1"}'`,
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(
|
|
153
|
+
pc.dim(
|
|
154
|
+
"Note: You can still call the RPC, but parameter validation is disabled",
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
160
|
+
console.log();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function displayRPCResult(
|
|
164
|
+
schema: string,
|
|
165
|
+
rpcName: string,
|
|
166
|
+
result: any,
|
|
167
|
+
explain?: boolean,
|
|
168
|
+
): void {
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
171
|
+
if (explain) {
|
|
172
|
+
console.log(pc.bold(pc.cyan(` QUERY PLAN: ${schema}.${rpcName}`)));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(pc.bold(pc.cyan(` RPC RESULT: ${schema}.${rpcName}`)));
|
|
175
|
+
}
|
|
176
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
177
|
+
console.log();
|
|
178
|
+
|
|
179
|
+
if (explain) {
|
|
180
|
+
console.log(pc.bold("Execution Plan:"));
|
|
181
|
+
console.log();
|
|
182
|
+
if (typeof result === "string") {
|
|
183
|
+
console.log(pc.yellow(result));
|
|
184
|
+
} else {
|
|
185
|
+
console.log(JSON.stringify(result, null, 2));
|
|
186
|
+
}
|
|
187
|
+
} else if (Array.isArray(result)) {
|
|
188
|
+
console.log(pc.bold("Results:"), pc.green(result.length.toString()));
|
|
189
|
+
console.log();
|
|
190
|
+
if (result.length > 0) {
|
|
191
|
+
console.table(result);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(pc.dim("No results returned"));
|
|
194
|
+
}
|
|
195
|
+
} else if (typeof result === "object" && result !== null) {
|
|
196
|
+
console.log(pc.bold("Result:"));
|
|
197
|
+
console.table([result]);
|
|
198
|
+
} else {
|
|
199
|
+
console.log(pc.bold("Result:"), pc.green(String(result)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(pc.bold(pc.cyan("━".repeat(60))));
|
|
204
|
+
console.log();
|
|
205
|
+
}
|