@vwork/cli 0.1.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/dist/apps.js +53 -0
- package/dist/auth/oidc.js +23 -0
- package/dist/auth/store.js +63 -0
- package/dist/client.js +45 -0
- package/dist/codegen.js +140 -0
- package/dist/command.js +70 -0
- package/dist/config.js +36 -0
- package/dist/db.js +124 -0
- package/dist/functions.js +237 -0
- package/dist/index.js +35 -0
- package/dist/kv.js +73 -0
- package/dist/login.js +176 -0
- package/dist/migrations.js +76 -0
- package/dist/observability.js +20 -0
- package/dist/oclif-command.js +357 -0
- package/dist/output.js +12 -0
- package/dist/queues.js +71 -0
- package/dist/runtime.js +11 -0
- package/dist/secrets.js +16 -0
- package/package.json +31 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { stringFlag } from "./config.js";
|
|
4
|
+
export async function runFunctionCommand(command, client, config, cwd) {
|
|
5
|
+
const action = command.path[1];
|
|
6
|
+
if (action === "validate") {
|
|
7
|
+
return { value: await client.request("POST", "/apps/functions/validate-source", { source_code: sourceInput(command, cwd) }) };
|
|
8
|
+
}
|
|
9
|
+
const app = await resolveApp(command, client, config);
|
|
10
|
+
const appPath = `/apps/${encodeURIComponent(String(app.id))}`;
|
|
11
|
+
if (action === "previews") {
|
|
12
|
+
return runPreviewCommand(command, client, appPath);
|
|
13
|
+
}
|
|
14
|
+
if (action === "routes") {
|
|
15
|
+
return runFunctionRouteCommand(command, client, appPath);
|
|
16
|
+
}
|
|
17
|
+
if (action === "queue-bindings") {
|
|
18
|
+
return runFunctionQueueBindingsCommand(command, client, appPath);
|
|
19
|
+
}
|
|
20
|
+
if (action === "list") {
|
|
21
|
+
const body = await client.request("GET", `${appPath}/functions`);
|
|
22
|
+
return { rows: body.functions };
|
|
23
|
+
}
|
|
24
|
+
if (action === "inspect") {
|
|
25
|
+
const name = requiredPosition(command, 0, "function name");
|
|
26
|
+
const body = await client.request("GET", `${appPath}/functions`);
|
|
27
|
+
return { value: body.functions.find((item) => item.name === name) ?? null };
|
|
28
|
+
}
|
|
29
|
+
if (action === "create") {
|
|
30
|
+
const name = requiredPosition(command, 0, "function name");
|
|
31
|
+
return {
|
|
32
|
+
value: await client.request("POST", `${appPath}/functions`, {
|
|
33
|
+
name,
|
|
34
|
+
auth_required: command.flags["no-auth"] === true ? false : true,
|
|
35
|
+
source_code: sourceInput(command, cwd),
|
|
36
|
+
...(stringFlag(command.flags.route) ? { route_path_prefix: stringFlag(command.flags.route) } : {})
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (action === "deploy") {
|
|
41
|
+
const name = requiredPosition(command, 0, "function name");
|
|
42
|
+
return { value: await client.request("POST", functionPath(appPath, name, "deploy"), { source_code: sourceInput(command, cwd) }) };
|
|
43
|
+
}
|
|
44
|
+
if (action === "artifact") {
|
|
45
|
+
const name = requiredPosition(command, 0, "function name");
|
|
46
|
+
return { value: await client.request("POST", functionPath(appPath, name, "artifacts"), { source_code: sourceInput(command, cwd) }) };
|
|
47
|
+
}
|
|
48
|
+
if (action === "artifacts") {
|
|
49
|
+
const name = requiredPosition(command, 0, "function name");
|
|
50
|
+
const body = await client.request("GET", functionPath(appPath, name, "artifacts"));
|
|
51
|
+
return { rows: body.artifacts };
|
|
52
|
+
}
|
|
53
|
+
if (action === "publish" || action === "rollback") {
|
|
54
|
+
const name = requiredPosition(command, 0, "function name");
|
|
55
|
+
return {
|
|
56
|
+
value: await client.request("POST", functionPath(appPath, name, action), { artifact_id: requiredFlag(command, "artifact") })
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (action === "invoke") {
|
|
60
|
+
const name = requiredPosition(command, 0, "function name");
|
|
61
|
+
const body = optionalJsonObjectFlag(command, "body");
|
|
62
|
+
const artifact = stringFlag(command.flags.artifact);
|
|
63
|
+
if (artifact) {
|
|
64
|
+
return { value: await client.request("POST", `${functionPath(appPath, name, "artifacts")}/${encodeURIComponent(artifact)}/invoke`, { body }) };
|
|
65
|
+
}
|
|
66
|
+
return { value: await client.request("POST", functionPath(appPath, name, "draft-invoke"), { source_code: sourceInput(command, cwd), body }) };
|
|
67
|
+
}
|
|
68
|
+
if (action === "delete") {
|
|
69
|
+
const name = requiredPosition(command, 0, "function name");
|
|
70
|
+
return { value: await client.request("DELETE", `${appPath}/functions/${encodeURIComponent(name)}`) };
|
|
71
|
+
}
|
|
72
|
+
if (action === "metrics") {
|
|
73
|
+
const name = requiredPosition(command, 0, "function name");
|
|
74
|
+
const limit = stringFlag(command.flags.limit) ?? "50";
|
|
75
|
+
const body = await client.request("GET", `${functionPath(appPath, name, "runtime-metrics")}?limit=${encodeURIComponent(limit)}`);
|
|
76
|
+
return { rows: body.recent_invocations };
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unknown functions command: ${action ?? ""}`);
|
|
79
|
+
}
|
|
80
|
+
async function runPreviewCommand(command, client, appPath) {
|
|
81
|
+
const action = command.path[2];
|
|
82
|
+
const name = requiredPosition(command, 0, "function name");
|
|
83
|
+
const basePath = functionPath(appPath, name, "previews");
|
|
84
|
+
if (action === "create") {
|
|
85
|
+
return {
|
|
86
|
+
value: await client.request("POST", basePath, {
|
|
87
|
+
artifact_id: requiredFlag(command, "artifact"),
|
|
88
|
+
ttl_seconds: parseTtlSeconds(stringFlag(command.flags.ttl) ?? "1h")
|
|
89
|
+
})
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (action === "list") {
|
|
93
|
+
const body = await client.request("GET", basePath);
|
|
94
|
+
return { rows: body.previews };
|
|
95
|
+
}
|
|
96
|
+
if (action === "invoke") {
|
|
97
|
+
const previewId = requiredFlag(command, "preview");
|
|
98
|
+
return { value: await client.request("POST", `${basePath}/${encodeURIComponent(previewId)}/invoke`, { body: optionalJsonObjectFlag(command, "body") }) };
|
|
99
|
+
}
|
|
100
|
+
if (action === "delete") {
|
|
101
|
+
const previewId = requiredFlag(command, "preview");
|
|
102
|
+
return { value: await client.request("DELETE", `${basePath}/${encodeURIComponent(previewId)}`) };
|
|
103
|
+
}
|
|
104
|
+
throw new Error(`Unknown functions previews command: ${action ?? ""}`);
|
|
105
|
+
}
|
|
106
|
+
async function runFunctionRouteCommand(command, client, appPath) {
|
|
107
|
+
const action = command.path[2];
|
|
108
|
+
const basePath = `${appPath}/function-routes`;
|
|
109
|
+
if (action === "list") {
|
|
110
|
+
const body = await client.request("GET", basePath);
|
|
111
|
+
return { rows: body.routes };
|
|
112
|
+
}
|
|
113
|
+
if (action === "add" || action === "create") {
|
|
114
|
+
const functionName = stringFlag(command.flags.function) ?? requiredPosition(command, 0, "function name");
|
|
115
|
+
const pathPrefix = stringFlag(command.flags.path) ?? stringFlag(command.flags.prefix);
|
|
116
|
+
if (!pathPrefix)
|
|
117
|
+
throw new Error("Missing --path or --prefix");
|
|
118
|
+
return {
|
|
119
|
+
value: await client.request("POST", basePath, {
|
|
120
|
+
function_name: functionName,
|
|
121
|
+
path_prefix: pathPrefix,
|
|
122
|
+
enabled: command.flags.disabled === true ? false : true
|
|
123
|
+
})
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (action === "enable" || action === "disable") {
|
|
127
|
+
const routeId = requiredPosition(command, 0, "route id");
|
|
128
|
+
return {
|
|
129
|
+
value: await client.request("PATCH", `${basePath}/${encodeURIComponent(routeId)}`, {
|
|
130
|
+
enabled: action === "enable"
|
|
131
|
+
})
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (action === "delete" || action === "remove") {
|
|
135
|
+
const routeId = requiredPosition(command, 0, "route id");
|
|
136
|
+
return { value: await client.request("DELETE", `${basePath}/${encodeURIComponent(routeId)}`) };
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`Unknown functions routes command: ${action ?? ""}`);
|
|
139
|
+
}
|
|
140
|
+
async function runFunctionQueueBindingsCommand(command, client, appPath) {
|
|
141
|
+
const action = command.path[2];
|
|
142
|
+
const name = requiredPosition(command, 0, "function name");
|
|
143
|
+
const basePath = functionPath(appPath, name, "queue-bindings");
|
|
144
|
+
if (action === "list") {
|
|
145
|
+
const body = await client.request("GET", basePath);
|
|
146
|
+
return { rows: body.queue_bindings };
|
|
147
|
+
}
|
|
148
|
+
if (action === "replace") {
|
|
149
|
+
const queueSpecs = stringListFlag(command.flags.queue);
|
|
150
|
+
const queuesBody = await client.request("GET", `${appPath}/queues`);
|
|
151
|
+
const queuesById = new Map(queuesBody.queues.map((queue) => [queue.id, queue]));
|
|
152
|
+
return {
|
|
153
|
+
value: await client.request("PUT", basePath, {
|
|
154
|
+
queues: queueSpecs.map((spec) => queueBindingInput(spec, queuesById))
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`Unknown functions queue-bindings command: ${action ?? ""}`);
|
|
159
|
+
}
|
|
160
|
+
async function resolveApp(command, client, config) {
|
|
161
|
+
const target = stringFlag(command.flags.app) ?? config.appId;
|
|
162
|
+
if (!target)
|
|
163
|
+
throw new Error("Missing --app");
|
|
164
|
+
return client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
165
|
+
}
|
|
166
|
+
function functionPath(appPath, name, suffix) {
|
|
167
|
+
return `${appPath}/functions/${encodeURIComponent(name)}/${suffix}`;
|
|
168
|
+
}
|
|
169
|
+
function sourceInput(command, cwd) {
|
|
170
|
+
const inline = stringFlag(command.flags["source-code"]);
|
|
171
|
+
if (inline)
|
|
172
|
+
return inline;
|
|
173
|
+
const file = stringFlag(command.flags.bundle) ?? stringFlag(command.flags.file);
|
|
174
|
+
if (!file)
|
|
175
|
+
throw new Error("Missing --file, --bundle, or --source-code");
|
|
176
|
+
return readFileSync(resolve(cwd, file), "utf8").trim();
|
|
177
|
+
}
|
|
178
|
+
function optionalJsonObjectFlag(command, key) {
|
|
179
|
+
const value = stringFlag(command.flags[key]);
|
|
180
|
+
if (!value)
|
|
181
|
+
return undefined;
|
|
182
|
+
const parsed = JSON.parse(value);
|
|
183
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
184
|
+
throw new Error(`--${key} must be a JSON object`);
|
|
185
|
+
}
|
|
186
|
+
return parsed;
|
|
187
|
+
}
|
|
188
|
+
function stringListFlag(value) {
|
|
189
|
+
if (Array.isArray(value))
|
|
190
|
+
return value;
|
|
191
|
+
if (typeof value === "string")
|
|
192
|
+
return [value];
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
function queueBindingInput(spec, queuesById) {
|
|
196
|
+
const [queueId, mode = "producer_consumer"] = spec.split(":");
|
|
197
|
+
if (!queueId)
|
|
198
|
+
throw new Error("--queue must include a queue id");
|
|
199
|
+
if (mode !== "producer" && mode !== "consumer" && mode !== "producer_consumer") {
|
|
200
|
+
throw new Error("--queue mode must be producer, consumer, or producer_consumer");
|
|
201
|
+
}
|
|
202
|
+
const queue = queuesById.get(queueId);
|
|
203
|
+
if (!queue)
|
|
204
|
+
throw new Error(`Queue not found for --queue ${queueId}`);
|
|
205
|
+
return {
|
|
206
|
+
binding: defaultQueueBindingName(queue.name),
|
|
207
|
+
queue_id: queue.id,
|
|
208
|
+
mode
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function defaultQueueBindingName(queueName) {
|
|
212
|
+
const normalized = queueName.trim().toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
213
|
+
if (!normalized)
|
|
214
|
+
return "QUEUE";
|
|
215
|
+
return /^[A-Z_]/.test(normalized) ? normalized : `QUEUE_${normalized}`;
|
|
216
|
+
}
|
|
217
|
+
function parseTtlSeconds(value) {
|
|
218
|
+
const match = /^(\d+)([smhd])?$/.exec(value.trim());
|
|
219
|
+
if (!match)
|
|
220
|
+
throw new Error("--ttl must be a duration like 30m, 1h, or 2d");
|
|
221
|
+
const amount = Number(match[1]);
|
|
222
|
+
const unit = match[2] ?? "s";
|
|
223
|
+
const multiplier = unit === "d" ? 86_400 : unit === "h" ? 3_600 : unit === "m" ? 60 : 1;
|
|
224
|
+
return amount * multiplier;
|
|
225
|
+
}
|
|
226
|
+
function requiredFlag(command, key) {
|
|
227
|
+
const value = stringFlag(command.flags[key]);
|
|
228
|
+
if (!value)
|
|
229
|
+
throw new Error(`Missing --${key}`);
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
function requiredPosition(command, index, label) {
|
|
233
|
+
const value = command.positionals[index];
|
|
234
|
+
if (!value)
|
|
235
|
+
throw new Error(`Missing ${label}`);
|
|
236
|
+
return value;
|
|
237
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { runOclifCli } from "./oclif-command.js";
|
|
4
|
+
export async function runCli(argv, env, cwd) {
|
|
5
|
+
await withRuntime(env, cwd, () => runOclifCli(argv));
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
export function isDirectCliEntrypoint(moduleUrl, argvPath) {
|
|
9
|
+
if (!argvPath)
|
|
10
|
+
return false;
|
|
11
|
+
return normalizePath(fileURLToPath(moduleUrl)) === normalizePath(argvPath);
|
|
12
|
+
}
|
|
13
|
+
function normalizePath(value) {
|
|
14
|
+
return value.replace(/\\/g, "/");
|
|
15
|
+
}
|
|
16
|
+
function withRuntime(env, cwd, run) {
|
|
17
|
+
const originalEnv = process.env;
|
|
18
|
+
const originalCwd = process.cwd();
|
|
19
|
+
process.env = env;
|
|
20
|
+
if (cwd !== originalCwd)
|
|
21
|
+
process.chdir(cwd);
|
|
22
|
+
return run().finally(() => {
|
|
23
|
+
process.env = originalEnv;
|
|
24
|
+
if (process.cwd() !== originalCwd)
|
|
25
|
+
process.chdir(originalCwd);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (isDirectCliEntrypoint(import.meta.url, process.argv[1])) {
|
|
29
|
+
runCli(process.argv.slice(2), process.env, process.cwd()).then((code) => {
|
|
30
|
+
process.exitCode = code;
|
|
31
|
+
}).catch((error) => {
|
|
32
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/kv.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { stringFlag } from "./config.js";
|
|
2
|
+
export async function runKvCommand(command, client, config) {
|
|
3
|
+
const group = command.path[1];
|
|
4
|
+
const action = command.path[2];
|
|
5
|
+
const app = await resolveApp(command, client, config);
|
|
6
|
+
if (group === "namespaces" && action === "list") {
|
|
7
|
+
const body = await client.request("GET", `/apps/${encodeURIComponent(String(app.id))}/kv-namespaces`);
|
|
8
|
+
return { rows: body.kv_namespaces };
|
|
9
|
+
}
|
|
10
|
+
if (group === "namespaces" && action === "create") {
|
|
11
|
+
const body = namespaceCreateBody(command);
|
|
12
|
+
return {
|
|
13
|
+
value: await client.request("POST", `/apps/${encodeURIComponent(String(app.id))}/kv-namespaces`, body)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (group === "entries" && action === "list") {
|
|
17
|
+
const namespace = requiredFlag(command, "namespace");
|
|
18
|
+
const body = await client.request("GET", withQuery(`/apps/${encodeURIComponent(String(app.id))}/kv-namespaces/${encodeURIComponent(namespace)}/entries`, {
|
|
19
|
+
prefix: stringFlag(command.flags.prefix),
|
|
20
|
+
limit: stringFlag(command.flags.limit),
|
|
21
|
+
cursor: stringFlag(command.flags.cursor)
|
|
22
|
+
}));
|
|
23
|
+
return { rows: body.keys };
|
|
24
|
+
}
|
|
25
|
+
if (group === "stats") {
|
|
26
|
+
const namespace = requiredFlag(command, "namespace");
|
|
27
|
+
const body = await client.request("GET", `/apps/${encodeURIComponent(String(app.id))}/kv-namespaces/${encodeURIComponent(namespace)}/stats`);
|
|
28
|
+
return { value: body.stats };
|
|
29
|
+
}
|
|
30
|
+
if (group === "metrics") {
|
|
31
|
+
const namespace = requiredFlag(command, "namespace");
|
|
32
|
+
const body = await client.request("GET", withQuery(`/apps/${encodeURIComponent(String(app.id))}/kv-namespaces/${encodeURIComponent(namespace)}/metrics`, {
|
|
33
|
+
range: stringFlag(command.flags.range),
|
|
34
|
+
step: stringFlag(command.flags.step),
|
|
35
|
+
granularity: stringFlag(command.flags.granularity),
|
|
36
|
+
from: stringFlag(command.flags.from),
|
|
37
|
+
to: stringFlag(command.flags.to)
|
|
38
|
+
}));
|
|
39
|
+
return { value: body.metrics };
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unknown kv command: ${command.path.join(" ")}`);
|
|
42
|
+
}
|
|
43
|
+
async function resolveApp(command, client, config) {
|
|
44
|
+
const target = stringFlag(command.flags.app) ?? config.appId;
|
|
45
|
+
if (!target)
|
|
46
|
+
throw new Error("Missing --app");
|
|
47
|
+
return client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
48
|
+
}
|
|
49
|
+
function namespaceCreateBody(command) {
|
|
50
|
+
const title = stringFlag(command.flags.title);
|
|
51
|
+
const name = stringFlag(command.flags.name);
|
|
52
|
+
if (!title && !name)
|
|
53
|
+
throw new Error("Missing --title");
|
|
54
|
+
return {
|
|
55
|
+
...(title ? { title } : {}),
|
|
56
|
+
...(name ? { name } : {})
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function requiredFlag(command, key) {
|
|
60
|
+
const value = stringFlag(command.flags[key]);
|
|
61
|
+
if (!value)
|
|
62
|
+
throw new Error(`Missing --${key}`);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function withQuery(path, query) {
|
|
66
|
+
const searchParams = new URLSearchParams();
|
|
67
|
+
for (const [key, value] of Object.entries(query)) {
|
|
68
|
+
if (value)
|
|
69
|
+
searchParams.set(key, value);
|
|
70
|
+
}
|
|
71
|
+
const queryString = searchParams.toString();
|
|
72
|
+
return queryString ? `${path}?${queryString}` : path;
|
|
73
|
+
}
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { PlatformClient } from "@vwork/platform-client";
|
|
3
|
+
import { buildAuthorizationUrl, createPkcePair, createState } from "./auth/oidc.js";
|
|
4
|
+
const DEFAULT_OIDC_CALLBACK_TIMEOUT_MS = 120_000;
|
|
5
|
+
export async function runLoginCommand(input) {
|
|
6
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
7
|
+
const apiUrl = input.apiUrl.replace(/\/+$/, "");
|
|
8
|
+
const response = await fetchImpl(new Request(`${apiUrl}/auth/oidc/config`, {
|
|
9
|
+
headers: { accept: "application/json" }
|
|
10
|
+
}));
|
|
11
|
+
const config = await parseJsonResponse(response);
|
|
12
|
+
if (!config.cli_redirect_uri) {
|
|
13
|
+
throw new Error("CLI OIDC redirect URI is not configured. Configure OIDC_CLI_REDIRECT_URI or use dashboard login.");
|
|
14
|
+
}
|
|
15
|
+
const pkce = createPkcePair();
|
|
16
|
+
const state = createState();
|
|
17
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
18
|
+
authorizationEndpoint: config.authorization_endpoint,
|
|
19
|
+
clientId: config.client_id,
|
|
20
|
+
redirectUri: config.cli_redirect_uri,
|
|
21
|
+
scopes: config.scopes,
|
|
22
|
+
state,
|
|
23
|
+
codeChallenge: pkce.codeChallenge
|
|
24
|
+
});
|
|
25
|
+
const createCallbackWaiter = input.createCallbackWaiter ?? createLocalOidcCallbackWaiter;
|
|
26
|
+
const callbackWaiter = await createCallbackWaiter({ redirectUri: config.cli_redirect_uri });
|
|
27
|
+
try {
|
|
28
|
+
await input.openBrowser(authorizationUrl);
|
|
29
|
+
const callback = await waitForCallbackWithTimeout(callbackWaiter.waitForCallback, input.callbackTimeoutMs ?? DEFAULT_OIDC_CALLBACK_TIMEOUT_MS);
|
|
30
|
+
if (!callback.state)
|
|
31
|
+
throw new Error("OIDC callback state is missing.");
|
|
32
|
+
if (callback.state !== state)
|
|
33
|
+
throw new Error("OIDC callback state did not match.");
|
|
34
|
+
if (callback.error) {
|
|
35
|
+
throw new Error(`OIDC login failed: ${callback.error}${callback.errorDescription ? `: ${callback.errorDescription}` : ""}`);
|
|
36
|
+
}
|
|
37
|
+
if (!callback.code)
|
|
38
|
+
throw new Error("OIDC callback code is missing.");
|
|
39
|
+
const callbackResponse = await fetchImpl(new Request(`${apiUrl}/auth/oidc/callback`, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: { accept: "application/json", "content-type": "application/json" },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
code: callback.code,
|
|
44
|
+
code_verifier: pkce.codeVerifier,
|
|
45
|
+
redirect_uri: config.cli_redirect_uri
|
|
46
|
+
})
|
|
47
|
+
}));
|
|
48
|
+
const result = await parseJsonResponse(callbackResponse);
|
|
49
|
+
if (!result.api_key)
|
|
50
|
+
throw new Error("Platform API did not return CLI credentials.");
|
|
51
|
+
await input.saveApiKey({ baseUrl: apiUrl, apiKey: result.api_key });
|
|
52
|
+
return `Logged in to ${apiUrl} as ${result.user?.email ?? result.user?.display_name ?? "unknown"}.\n`;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await callbackWaiter.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function runWhoamiCommand(input) {
|
|
59
|
+
const client = new PlatformClient({
|
|
60
|
+
apiUrl: input.apiUrl,
|
|
61
|
+
trustedUserId: null,
|
|
62
|
+
apiKey: input.credentials.api_key,
|
|
63
|
+
fetch: input.fetch
|
|
64
|
+
});
|
|
65
|
+
const me = await client.request("GET", "/auth/me");
|
|
66
|
+
const user = me.user ?? {};
|
|
67
|
+
return [
|
|
68
|
+
`API URL: ${input.apiUrl}`,
|
|
69
|
+
`User: ${user.email ?? user.display_name ?? "unknown"}`,
|
|
70
|
+
`Role: ${user.platform_role ?? "unknown"}`,
|
|
71
|
+
""
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
async function parseJsonResponse(response) {
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = await response.json();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Platform API request failed with status ${response.status}`);
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`Platform API request failed with status ${response.status}`);
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
async function waitForCallbackWithTimeout(callback, timeoutMs) {
|
|
91
|
+
let timeout = null;
|
|
92
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
93
|
+
timeout = setTimeout(() => {
|
|
94
|
+
reject(new Error(`Timed out waiting for OIDC callback after ${timeoutMs}ms.`));
|
|
95
|
+
}, timeoutMs);
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
return await Promise.race([callback, timeoutPromise]);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timeout)
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function createLocalOidcCallbackWaiter(input) {
|
|
106
|
+
const redirectUrl = new URL(input.redirectUri);
|
|
107
|
+
const listenHost = assertLoopbackRedirectUrl(redirectUrl);
|
|
108
|
+
let settled = false;
|
|
109
|
+
let closePromise = null;
|
|
110
|
+
let resolveCallback;
|
|
111
|
+
let rejectCallback;
|
|
112
|
+
const waitForCallback = new Promise((resolve, reject) => {
|
|
113
|
+
resolveCallback = resolve;
|
|
114
|
+
rejectCallback = reject;
|
|
115
|
+
});
|
|
116
|
+
const server = createServer((request, response) => {
|
|
117
|
+
const requestUrl = new URL(request.url ?? "/", redirectUrl.origin);
|
|
118
|
+
if (request.method !== "GET" || requestUrl.pathname !== redirectUrl.pathname) {
|
|
119
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
120
|
+
response.end("Not found");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!settled) {
|
|
124
|
+
settled = true;
|
|
125
|
+
resolveCallback({
|
|
126
|
+
code: requestUrl.searchParams.get("code"),
|
|
127
|
+
state: requestUrl.searchParams.get("state"),
|
|
128
|
+
error: requestUrl.searchParams.get("error"),
|
|
129
|
+
errorDescription: requestUrl.searchParams.get("error_description")
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
133
|
+
response.end("<!doctype html><title>VWork login</title><p>VWork login complete. You can close this window.</p>");
|
|
134
|
+
});
|
|
135
|
+
const port = Number(redirectUrl.port || "80");
|
|
136
|
+
await new Promise((resolve, reject) => {
|
|
137
|
+
const rejectListen = (error) => {
|
|
138
|
+
reject(error);
|
|
139
|
+
};
|
|
140
|
+
server.once("error", rejectListen);
|
|
141
|
+
server.listen(port, listenHost, () => {
|
|
142
|
+
server.off("error", rejectListen);
|
|
143
|
+
resolve();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
server.on("error", (error) => {
|
|
147
|
+
if (!settled) {
|
|
148
|
+
settled = true;
|
|
149
|
+
rejectCallback(error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
waitForCallback,
|
|
154
|
+
close: async () => {
|
|
155
|
+
if (closePromise)
|
|
156
|
+
return closePromise;
|
|
157
|
+
closePromise = new Promise((resolve, reject) => {
|
|
158
|
+
server.close((error) => {
|
|
159
|
+
if (!error || error.code === "ERR_SERVER_NOT_RUNNING") {
|
|
160
|
+
resolve();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
reject(error);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
return closePromise;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function assertLoopbackRedirectUrl(redirectUrl) {
|
|
171
|
+
const hostname = redirectUrl.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
172
|
+
if (redirectUrl.protocol !== "http:" || (hostname !== "localhost" && hostname !== "127.0.0.1" && hostname !== "::1")) {
|
|
173
|
+
throw new Error("CLI OIDC redirect URI must use an http:// loopback host for the local callback listener.");
|
|
174
|
+
}
|
|
175
|
+
return hostname;
|
|
176
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { stringFlag } from "./config.js";
|
|
4
|
+
export async function runMigrationCommand(command, client, config, cwd) {
|
|
5
|
+
const action = command.path[1];
|
|
6
|
+
const app = await resolveApp(command, client, config);
|
|
7
|
+
const appPath = `/apps/${encodeURIComponent(String(app.id))}`;
|
|
8
|
+
if (action === "list") {
|
|
9
|
+
const response = await client.request("GET", `${appPath}/migration-artifacts`);
|
|
10
|
+
return { rows: response.migration_artifacts.map((artifact) => ({ version: artifact.version, schema: artifact.schema_name })) };
|
|
11
|
+
}
|
|
12
|
+
if (action === "runs") {
|
|
13
|
+
const response = await client.request("GET", `${appPath}/migrations`);
|
|
14
|
+
return { rows: response.migration_runs.map((run) => ({ version: run.version, direction: run.direction, status: run.status })) };
|
|
15
|
+
}
|
|
16
|
+
if (action === "create" || action === "up") {
|
|
17
|
+
const version = requiredPosition(command, 0, "migration version");
|
|
18
|
+
const body = {
|
|
19
|
+
version,
|
|
20
|
+
schema_name: stringFlag(command.flags.schema) ?? config.schema,
|
|
21
|
+
up_sql: sqlInput(command, "up", "up-sql", cwd),
|
|
22
|
+
down_sql: optionalSqlInput(command, "down", "down-sql", cwd)
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
value: await client.request("POST", `${appPath}/migrations${action === "up" ? "/up" : ""}`, body)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (action === "down") {
|
|
29
|
+
const version = requiredPosition(command, 0, "migration version");
|
|
30
|
+
return {
|
|
31
|
+
value: await client.request("POST", `${appPath}/migrations/down`, {
|
|
32
|
+
version,
|
|
33
|
+
reason: requiredFlag(command, "reason")
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (action === "delete") {
|
|
38
|
+
const version = requiredPosition(command, 0, "migration version");
|
|
39
|
+
return { value: await client.request("DELETE", `${appPath}/migration-artifacts/${encodeURIComponent(version)}`) };
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unknown migrations command: ${action ?? ""}`);
|
|
42
|
+
}
|
|
43
|
+
async function resolveApp(command, client, config) {
|
|
44
|
+
const target = stringFlag(command.flags.app) ?? config.appId;
|
|
45
|
+
if (!target)
|
|
46
|
+
throw new Error("Missing --app");
|
|
47
|
+
return client.request("GET", `/apps/lookup/${encodeURIComponent(target)}`);
|
|
48
|
+
}
|
|
49
|
+
function sqlInput(command, fileFlag, inlineFlag, cwd) {
|
|
50
|
+
const inline = stringFlag(command.flags[inlineFlag]);
|
|
51
|
+
if (inline)
|
|
52
|
+
return inline;
|
|
53
|
+
const file = stringFlag(command.flags[fileFlag]);
|
|
54
|
+
if (!file)
|
|
55
|
+
throw new Error(`Missing --${fileFlag} or --${inlineFlag}`);
|
|
56
|
+
return readFileSync(resolve(cwd, file), "utf8").trim();
|
|
57
|
+
}
|
|
58
|
+
function optionalSqlInput(command, fileFlag, inlineFlag, cwd) {
|
|
59
|
+
const inline = stringFlag(command.flags[inlineFlag]);
|
|
60
|
+
if (inline)
|
|
61
|
+
return inline;
|
|
62
|
+
const file = stringFlag(command.flags[fileFlag]);
|
|
63
|
+
return file ? readFileSync(resolve(cwd, file), "utf8").trim() : null;
|
|
64
|
+
}
|
|
65
|
+
function requiredFlag(command, key) {
|
|
66
|
+
const value = stringFlag(command.flags[key]);
|
|
67
|
+
if (!value)
|
|
68
|
+
throw new Error(`Missing --${key}`);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function requiredPosition(command, index, label) {
|
|
72
|
+
const value = command.positionals[index];
|
|
73
|
+
if (!value)
|
|
74
|
+
throw new Error(`Missing ${label}`);
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { stringFlag } from "./config.js";
|
|
2
|
+
export async function runObservabilityCommand(command, client) {
|
|
3
|
+
const action = command.path[1];
|
|
4
|
+
if (action === "access-logs") {
|
|
5
|
+
const query = new URLSearchParams();
|
|
6
|
+
const source = stringFlag(command.flags.source);
|
|
7
|
+
const app = stringFlag(command.flags.app);
|
|
8
|
+
const limit = stringFlag(command.flags.limit);
|
|
9
|
+
if (source)
|
|
10
|
+
query.set("source", source);
|
|
11
|
+
if (app)
|
|
12
|
+
query.set("app_id", app);
|
|
13
|
+
if (limit)
|
|
14
|
+
query.set("limit", limit);
|
|
15
|
+
const queryString = query.toString();
|
|
16
|
+
const body = await client.request("GET", `/observability/access-logs${queryString ? `?${queryString}` : ""}`);
|
|
17
|
+
return { rows: body.access_logs };
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`Unknown observability command: ${action ?? ""}`);
|
|
20
|
+
}
|