@supacloud/admin 0.24.1 → 0.25.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.
Files changed (3) hide show
  1. package/README.md +10 -0
  2. package/dist/index.js +313 -132
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -70,6 +70,7 @@ production requires the exact project ref confirmation:
70
70
 
71
71
  ```bash
72
72
  supacloud-admin frontend list_releases --ref abc123 --id web
73
+ supacloud-admin frontend get_active_release --ref abc123 --id web
73
74
  supacloud-admin frontend get_release --ref abc123 --id web --release_id <sha256>
74
75
  supacloud-admin frontend upload_release --ref abc123 --id web \
75
76
  --zip_path /secure/site.zip --confirm-production abc123
@@ -77,6 +78,8 @@ supacloud-admin frontend activate_release --ref abc123 --id web \
77
78
  --release_id <sha256> --expected_active_release_id absent \
78
79
  --expected_activation_id absent --mutation_id <uuid-v4> \
79
80
  --confirm-production abc123
81
+ supacloud-admin frontend rollback --ref abc123 --id web \
82
+ --release_id <retained-sha256> --confirm-production abc123
80
83
  ```
81
84
 
82
85
  The CLI reads the archive through a no-follow file descriptor, verifies its
@@ -88,6 +91,13 @@ activation mutations require the Linux held-directory-FD implementation; on
88
91
  other platforms, Management API returns HTTP 503 before it reads the upload
89
92
  body, creates release directories, or writes a mutation journal.
90
93
 
94
+ The active snapshot automatically supports older HTTP 404 endpoints through
95
+ bounded history and exact artifact readback, never authorization or integrity
96
+ failures. `rollback` handles current CAS and mutation IDs automatically while
97
+ requiring an explicit retained target. It never retries a concurrent conflict,
98
+ restores a database or infers the previous version from sorted history.
99
+ Unknown results include the mutation ID for read-only reconciliation.
100
+
91
101
  SSH host keys are fail-closed: setting `SUPACLOUD_HOST` and credentials is not
92
102
  enough to enable SSH actions. Obtain the fingerprint through a trusted channel,
93
103
  compare it out of band, then set it explicitly. For example, the discovery
package/dist/index.js CHANGED
@@ -2855,23 +2855,23 @@ var require_nacl_fast = __commonJS(function(exports, module) {
2855
2855
  randombytes = fn;
2856
2856
  };
2857
2857
  (function() {
2858
- var crypto = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
2859
- if (crypto && crypto.getRandomValues) {
2858
+ var crypto2 = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
2859
+ if (crypto2 && crypto2.getRandomValues) {
2860
2860
  var QUOTA = 65536;
2861
2861
  nacl.setPRNG(function(x, n) {
2862
2862
  var i, v = new Uint8Array(n);
2863
2863
  for (i = 0;i < n; i += QUOTA) {
2864
- crypto.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
2864
+ crypto2.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
2865
2865
  }
2866
2866
  for (i = 0;i < n; i++)
2867
2867
  x[i] = v[i];
2868
2868
  cleanup(v);
2869
2869
  });
2870
2870
  } else if (true) {
2871
- crypto = __require("crypto");
2872
- if (crypto && crypto.randomBytes) {
2871
+ crypto2 = __require("crypto");
2872
+ if (crypto2 && crypto2.randomBytes) {
2873
2873
  nacl.setPRNG(function(x, n) {
2874
- var i, v = crypto.randomBytes(n);
2874
+ var i, v = crypto2.randomBytes(n);
2875
2875
  for (i = 0;i < n; i++)
2876
2876
  x[i] = v[i];
2877
2877
  cleanup(v);
@@ -4135,14 +4135,14 @@ var require_bcrypt_pbkdf = __commonJS(function(exports, module) {
4135
4135
 
4136
4136
  // node_modules/ssh2/lib/protocol/constants.js
4137
4137
  var require_constants = __commonJS(function(exports, module) {
4138
- var crypto = __require("crypto");
4138
+ var crypto2 = __require("crypto");
4139
4139
  var cpuInfo;
4140
4140
  try {
4141
4141
  cpuInfo = __require("cpu-features")();
4142
4142
  } catch {}
4143
4143
  var { bindingAvailable, CIPHER_INFO, MAC_INFO } = require_crypto();
4144
4144
  var eddsaSupported = (() => {
4145
- if (typeof crypto.sign === "function" && typeof crypto.verify === "function") {
4145
+ if (typeof crypto2.sign === "function" && typeof crypto2.verify === "function") {
4146
4146
  const key = `-----BEGIN PRIVATE KEY-----\r
4147
4147
  MC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD` + `/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\r
4148
4148
  -----END PRIVATE KEY-----`;
@@ -4150,14 +4150,14 @@ MC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD` + `/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\r
4150
4150
  let sig;
4151
4151
  let verified;
4152
4152
  try {
4153
- sig = crypto.sign(null, data, key);
4154
- verified = crypto.verify(null, data, key, sig);
4153
+ sig = crypto2.sign(null, data, key);
4154
+ verified = crypto2.verify(null, data, key, sig);
4155
4155
  } catch {}
4156
4156
  return Buffer.isBuffer(sig) && sig.length === 64 && verified === true;
4157
4157
  }
4158
4158
  return false;
4159
4159
  })();
4160
- var curve25519Supported = typeof crypto.diffieHellman === "function" && typeof crypto.generateKeyPairSync === "function" && typeof crypto.createPublicKey === "function";
4160
+ var curve25519Supported = typeof crypto2.diffieHellman === "function" && typeof crypto2.generateKeyPairSync === "function" && typeof crypto2.createPublicKey === "function";
4161
4161
  var DEFAULT_KEX = [
4162
4162
  "ecdh-sha2-nistp256",
4163
4163
  "ecdh-sha2-nistp384",
@@ -4192,7 +4192,7 @@ MC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD` + `/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\r
4192
4192
  "ssh-dss"
4193
4193
  ]);
4194
4194
  var canUseCipher = (() => {
4195
- const ciphers = crypto.getCiphers();
4195
+ const ciphers = crypto2.getCiphers();
4196
4196
  return (name) => ciphers.includes(CIPHER_INFO[name].sslName);
4197
4197
  })();
4198
4198
  var DEFAULT_CIPHER = [
@@ -4227,7 +4227,7 @@ MC4CAQAwBQYDK2VwBCIEIHKj+sVa9WcD` + `/q2DJUJaf43Kptc8xYuUQA4bOFj9vC8T\r
4227
4227
  "arcfour"
4228
4228
  ].filter(canUseCipher));
4229
4229
  var canUseMAC = (() => {
4230
- const hashes = crypto.getHashes();
4230
+ const hashes = crypto2.getHashes();
4231
4231
  return (name) => hashes.includes(MAC_INFO[name].sslName);
4232
4232
  })();
4233
4233
  var DEFAULT_MAC = [
@@ -19618,6 +19618,11 @@ class DecodeBuilder {
19618
19618
  function Codec(type) {
19619
19619
  return new DecodeBuilder(type);
19620
19620
  }
19621
+ function Decode(type, callback) {
19622
+ return Codec(type).Decode(callback).Encode(() => {
19623
+ throw Error("Encode not implemented");
19624
+ });
19625
+ }
19621
19626
  function IsCodec(value) {
19622
19627
  return IsSchema(value) && HasPropertyKey(value, "~codec") && IsObject(value["~codec"]) && HasPropertyKey(value["~codec"], "encode") && HasPropertyKey(value["~codec"], "decode");
19623
19628
  }
@@ -24838,7 +24843,22 @@ var Delete = _Object_({
24838
24843
  });
24839
24844
  var Edit = Union([Insert2, Update2, Delete]);
24840
24845
  // ../cli/src/shared/schema.ts
24841
- var OUTPUT_SCHEMA = "~supacloudOutputSchema";
24846
+ var DECODE_ID = "~supacloudDecodeId";
24847
+
24848
+ class OutputDecodeError extends Error {
24849
+ decodeId;
24850
+ issues;
24851
+ constructor(decodeId, issues) {
24852
+ super(issues.map((issue) => `- ${issue.path}: ${issue.message}`).join(`
24853
+ `));
24854
+ this.decodeId = decodeId;
24855
+ this.issues = issues;
24856
+ }
24857
+ }
24858
+ function decodeIdOf(schema) {
24859
+ const value = Object.getOwnPropertyDescriptor(schema, DECODE_ID)?.value;
24860
+ return typeof value === "symbol" ? value : undefined;
24861
+ }
24842
24862
  function schemaErrorPath(path) {
24843
24863
  return path.replace(/^\//, "").replaceAll("/", ".") || "args";
24844
24864
  }
@@ -24850,42 +24870,46 @@ function transformErrorLine(error) {
24850
24870
  const path = typeof error === "object" && error !== null && "instancePath" in error && typeof error.instancePath === "string" ? schemaErrorPath(error.instancePath) : "args";
24851
24871
  return `- ${path}: ${message}`;
24852
24872
  }
24853
- function parseToolArguments(schema, input) {
24854
- const objectSchema = _Object_(schema, { additionalProperties: false });
24855
- const issues = schemaErrorLines(objectSchema, input);
24873
+ function validateToolArguments(schema, input) {
24874
+ const issues = schemaErrorLines(_Object_(schema, { additionalProperties: false }), input);
24856
24875
  if (issues.length > 0)
24857
24876
  throw new Error(`Invalid arguments:
24858
24877
  ${issues.join(`
24859
24878
  `)}`);
24879
+ }
24880
+ function isRecord(value) {
24881
+ return value !== null && typeof value === "object" && !Array.isArray(value);
24882
+ }
24883
+ function parseToolArguments(schema, input) {
24884
+ const objectSchema = _Object_(schema, { additionalProperties: false });
24885
+ validateToolArguments(schema, input);
24860
24886
  let decoded;
24861
24887
  try {
24862
24888
  decoded = Decode10(objectSchema, input);
24863
24889
  } catch (error) {
24890
+ if (error instanceof OutputDecodeError) {
24891
+ const entry = Object.entries(schema).find(([, property]) => decodeIdOf(property) === error.decodeId);
24892
+ const path = entry?.[0] ?? "args";
24893
+ const issues = error.issues.map((issue) => `- ${issue.path === "args" ? path : `${path}.${issue.path}`}: ${issue.message}`);
24894
+ throw new Error(`Invalid arguments:
24895
+ ${issues.join(`
24896
+ `)}`);
24897
+ }
24864
24898
  throw new Error(`Invalid arguments:
24865
24899
  ${transformErrorLine(error)}`);
24866
24900
  }
24867
- const decodedIssues = [];
24868
- for (const [name, propertySchema] of Object.entries(schema)) {
24869
- const outputSchema = propertySchema?.[OUTPUT_SCHEMA];
24870
- if (outputSchema === undefined || decoded[name] === undefined)
24871
- continue;
24872
- for (const issue of Errors2(outputSchema, decoded[name])) {
24873
- const path = schemaErrorPath(issue.instancePath);
24874
- decodedIssues.push(`- ${path === "args" ? name : `${name}.${path}`}: ${issue.message}`);
24875
- }
24901
+ const decodedFields = decoded;
24902
+ if (!isRecord(decodedFields)) {
24903
+ throw new Error("Invalid arguments: decoded value must be an object");
24876
24904
  }
24877
- if (decodedIssues.length > 0)
24878
- throw new Error(`Invalid arguments:
24879
- ${decodedIssues.join(`
24880
- `)}`);
24881
24905
  return decoded;
24882
24906
  }
24883
24907
  function stringEnum(values, options) {
24884
- const enumRecord = Object.fromEntries(values.map((entry) => [entry, entry]));
24885
- return Enum(enumRecord, options);
24908
+ return Enum([...values], options);
24886
24909
  }
24887
24910
  function mergeSchemaOptions(schema, options) {
24888
- const result = Object.create(Object.getPrototypeOf(schema));
24911
+ const result = { ...schema };
24912
+ Object.setPrototypeOf(result, Object.getPrototypeOf(schema));
24889
24913
  Object.defineProperties(result, Object.getOwnPropertyDescriptors(schema));
24890
24914
  for (const [key, value] of Object.entries(options)) {
24891
24915
  Object.defineProperty(result, key, { value, enumerable: true, writable: true, configurable: true });
@@ -24899,9 +24923,19 @@ function optional(schema, description) {
24899
24923
  return Optional(description ? withDescription(schema, description) : schema);
24900
24924
  }
24901
24925
  function decodedSchema(inputSchema, outputSchema, decode, options) {
24902
- const transform = Codec(inputSchema).Decode((input) => decode(input)).Encode((output) => output);
24926
+ const decodeId = Symbol("decodedSchema");
24927
+ const transform = Decode(inputSchema, (input) => {
24928
+ const value = decode(input);
24929
+ const issues = [...Errors2(outputSchema, value)].map((issue) => ({
24930
+ path: schemaErrorPath(issue.instancePath),
24931
+ message: issue.message
24932
+ }));
24933
+ if (issues.length > 0)
24934
+ throw new OutputDecodeError(decodeId, issues);
24935
+ return Decode10(outputSchema, value);
24936
+ });
24903
24937
  const result = options ? mergeSchemaOptions(transform, options) : transform;
24904
- Object.defineProperty(result, OUTPUT_SCHEMA, { value: outputSchema, enumerable: false, configurable: true });
24938
+ Object.defineProperty(result, DECODE_ID, { value: decodeId, enumerable: false, configurable: true });
24905
24939
  return result;
24906
24940
  }
24907
24941
  function schemaEnumValues(schema) {
@@ -25894,8 +25928,8 @@ var ACTION_POLICY = {
25894
25928
  ]
25895
25929
  },
25896
25930
  frontend: {
25897
- read: ["list_releases", "get_release"],
25898
- write: ["upload_release", "activate_release"]
25931
+ read: ["list_releases", "get_active_release", "get_release"],
25932
+ write: ["upload_release", "activate_release", "rollback"]
25899
25933
  },
25900
25934
  ssh: {
25901
25935
  read: [
@@ -29364,14 +29398,13 @@ function quoteShell(shellText) {
29364
29398
  }
29365
29399
 
29366
29400
  // src/shared/releases/local-upgrade-bundle.ts
29367
- import { spawn } from "node:child_process";
29368
29401
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
29369
29402
  import { accessSync, chmodSync as chmodSync2, constants as fsConstants, copyFileSync, createWriteStream, lstatSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
29370
29403
  import { get } from "node:https";
29371
29404
  import { tmpdir as tmpdir2 } from "node:os";
29372
29405
  import { basename, delimiter, isAbsolute, join } from "node:path";
29373
29406
  import { pipeline as pipeline3 } from "node:stream/promises";
29374
- import { Transform } from "node:stream";
29407
+ import { Readable, Transform } from "node:stream";
29375
29408
 
29376
29409
  // ../management-api/src/release-manifest.ts
29377
29410
  var RELEASE_MANIFEST_NAME = "SUPACLOUD-RELEASE.json";
@@ -30015,70 +30048,81 @@ function spawnGithubCli(arguments_, download = false) {
30015
30048
  environment[key] = process.env[key];
30016
30049
  }
30017
30050
  }
30018
- return spawn(githubCliExecutable(environment), arguments_, {
30051
+ return Bun.spawn([githubCliExecutable(environment), ...arguments_], {
30019
30052
  env: environment,
30020
30053
  stdio: ["ignore", "pipe", "pipe"]
30021
30054
  });
30022
30055
  }
30023
- function githubCliExitCode(child, timeoutMs) {
30024
- return new Promise((resolve, reject) => {
30025
- let timedOut = false;
30026
- let settled = false;
30027
- let forceKillTimer;
30028
- const timeout = setTimeout(() => {
30029
- timedOut = true;
30030
- child.kill("SIGTERM");
30031
- forceKillTimer = setTimeout(() => child.kill("SIGKILL"), GH_TERMINATION_GRACE_MS);
30032
- }, timeoutMs);
30033
- const settleExecution = (error, exitCode) => {
30034
- if (settled)
30035
- return;
30036
- settled = true;
30037
- clearTimeout(timeout);
30038
- if (forceKillTimer)
30039
- clearTimeout(forceKillTimer);
30040
- if (error)
30041
- reject(error);
30042
- else
30043
- resolve(timedOut ? 124 : exitCode);
30044
- };
30045
- child.once("error", (error) => settleExecution(error, 127));
30046
- child.once("close", (code) => settleExecution(undefined, code ?? 1));
30047
- });
30048
- }
30049
30056
  async function runGithubCli(arguments_, timeoutMs) {
30050
30057
  const child = spawnGithubCli(arguments_);
30051
- let stdout = "";
30052
- let stderr = "";
30053
- child.stdout.on("data", (chunk) => {
30054
- stdout = `${stdout}${chunk.toString()}`.slice(-8000);
30055
- });
30056
- child.stderr.on("data", (chunk) => {
30057
- stderr = `${stderr}${chunk.toString()}`.slice(-8000);
30058
- });
30059
- const exitCode = await githubCliExitCode(child, timeoutMs);
30060
- return { exitCode, stdout, stderr };
30058
+ let timedOut = false;
30059
+ let forceKillTimer;
30060
+ const timeout = setTimeout(() => {
30061
+ timedOut = true;
30062
+ child.kill("SIGTERM");
30063
+ forceKillTimer = setTimeout(() => child.kill("SIGKILL"), GH_TERMINATION_GRACE_MS);
30064
+ }, timeoutMs);
30065
+ try {
30066
+ const [rawStdout, rawStderr, exitCode] = await Promise.all([
30067
+ child.stdout ? new Response(child.stdout).text() : Promise.resolve(""),
30068
+ child.stderr ? new Response(child.stderr).text() : Promise.resolve(""),
30069
+ child.exited
30070
+ ]);
30071
+ clearTimeout(timeout);
30072
+ if (forceKillTimer)
30073
+ clearTimeout(forceKillTimer);
30074
+ const stdout = rawStdout.slice(-8000);
30075
+ const stderr = rawStderr.slice(-8000);
30076
+ return { exitCode: timedOut ? 124 : exitCode ?? 1, stdout, stderr };
30077
+ } catch (error) {
30078
+ clearTimeout(timeout);
30079
+ if (forceKillTimer)
30080
+ clearTimeout(forceKillTimer);
30081
+ child.kill("SIGKILL");
30082
+ throw error;
30083
+ }
30061
30084
  }
30062
30085
  async function runGithubCliDownload(arguments_, destination, maxBytes, timeoutMs) {
30063
30086
  const child = spawnGithubCli(arguments_, true);
30064
- let stderr = "";
30065
- child.stderr.on("data", (chunk) => {
30066
- stderr = `${stderr}${chunk.toString()}`.slice(-8000);
30067
- });
30068
- const write = pipeline3(child.stdout, boundedWriter(maxBytes), createWriteStream(destination, { flags: "wx", mode: 384 })).catch((error) => {
30087
+ let timedOut = false;
30088
+ let forceKillTimer;
30089
+ const timeout = setTimeout(() => {
30090
+ timedOut = true;
30091
+ child.kill("SIGTERM");
30092
+ forceKillTimer = setTimeout(() => child.kill("SIGKILL"), GH_TERMINATION_GRACE_MS);
30093
+ }, timeoutMs);
30094
+ const stderrPromise = (async () => {
30095
+ try {
30096
+ return child.stderr ? (await new Response(child.stderr).text()).slice(-8000) : "";
30097
+ } catch {
30098
+ return "";
30099
+ }
30100
+ })();
30101
+ const write = pipeline3(Readable.fromWeb(child.stdout), boundedWriter(maxBytes), createWriteStream(destination, { flags: "wx", mode: 384 })).catch((error) => {
30069
30102
  child.kill("SIGKILL");
30070
30103
  throw error;
30071
30104
  });
30072
- const [writeState, exitState] = await Promise.allSettled([write, githubCliExitCode(child, timeoutMs)]);
30073
- if (writeState.status === "rejected") {
30074
- rmSync2(destination, { force: true });
30075
- throw writeState.reason;
30076
- }
30077
- if (exitState.status === "rejected") {
30105
+ try {
30106
+ const [writeState, exitState, stderrState] = await Promise.allSettled([
30107
+ write,
30108
+ child.exited,
30109
+ stderrPromise
30110
+ ]);
30111
+ if (writeState.status === "rejected")
30112
+ throw writeState.reason;
30113
+ if (exitState.status === "rejected")
30114
+ throw exitState.reason;
30115
+ if (stderrState.status === "rejected")
30116
+ throw stderrState.reason;
30117
+ return { exitCode: timedOut ? 124 : exitState.value, stdout: "", stderr: stderrState.value };
30118
+ } catch (error) {
30078
30119
  rmSync2(destination, { force: true });
30079
- throw exitState.reason;
30120
+ throw error;
30121
+ } finally {
30122
+ clearTimeout(timeout);
30123
+ if (forceKillTimer)
30124
+ clearTimeout(forceKillTimer);
30080
30125
  }
30081
- return { exitCode: exitState.value, stdout: "", stderr };
30082
30126
  }
30083
30127
  function supportsStrictGithubVerification(execution) {
30084
30128
  const tokens = `${execution.stdout}
@@ -33137,18 +33181,25 @@ Actions: list, deploy, deploy_bundle, source, delete, check`, {
33137
33181
  };
33138
33182
  let text;
33139
33183
  const checkSyntax = async (sourceCode) => {
33140
- const fs = __require("fs");
33141
33184
  const os = __require("os");
33142
- const { promisify } = __require("util");
33143
- const execAsync = promisify(__require("child_process").exec);
33185
+ const fs = __require("fs");
33144
33186
  const tmpFile = `${os.tmpdir()}/supacloud_edge_${Date.now()}.ts`;
33145
- fs.writeFileSync(tmpFile, sourceCode);
33187
+ await Bun.write(tmpFile, sourceCode);
33146
33188
  try {
33147
- await execAsync(`bun build ${tmpFile} --external='*'`);
33148
- return { ok: true };
33189
+ const proc = Bun.spawn(["bun", "build", tmpFile, "--external=*"], {
33190
+ stdout: "pipe",
33191
+ stderr: "pipe"
33192
+ });
33193
+ const exitCode = await proc.exited;
33194
+ if (exitCode === 0) {
33195
+ return { ok: true };
33196
+ }
33197
+ const stdout = await new Response(proc.stdout).text();
33198
+ const stderr = await new Response(proc.stderr).text();
33199
+ return { ok: false, err: stdout + `
33200
+ ` + (stderr || `exit code ${exitCode}`) };
33149
33201
  } catch (e) {
33150
- return { ok: false, err: e.stdout + `
33151
- ` + (e.stderr || e.message) };
33202
+ return { ok: false, err: e.message || String(e) };
33152
33203
  } finally {
33153
33204
  try {
33154
33205
  fs.unlinkSync(tmpFile);
@@ -33159,8 +33210,6 @@ Actions: list, deploy, deploy_bundle, source, delete, check`, {
33159
33210
  try {
33160
33211
  const fs = __require("fs");
33161
33212
  const os = __require("os");
33162
- const { promisify } = __require("util");
33163
- const execAsync = promisify(__require("child_process").exec);
33164
33213
  const stat = fs.statSync(pathArg);
33165
33214
  let entrypoint = pathArg;
33166
33215
  if (stat.isDirectory()) {
@@ -33171,10 +33220,17 @@ Actions: list, deploy, deploy_bundle, source, delete, check`, {
33171
33220
  }
33172
33221
  const tmpOut = `${os.tmpdir()}/supacloud_bundled_${Date.now()}.js`;
33173
33222
  try {
33174
- const { stderr } = await execAsync(`bun build ${entrypoint} --target bun --outfile ${tmpOut}`);
33175
- if (!fs.existsSync(tmpOut))
33176
- throw new Error(`Bundle failed: ${stderr}`);
33177
- code = fs.readFileSync(tmpOut, "utf-8");
33223
+ const proc = Bun.spawn(["bun", "build", entrypoint, "--target", "bun", "--outfile", tmpOut], {
33224
+ stdout: "pipe",
33225
+ stderr: "pipe"
33226
+ });
33227
+ const exitCode = await proc.exited;
33228
+ const stderr = await new Response(proc.stderr).text();
33229
+ const fileHandle = Bun.file(tmpOut);
33230
+ if (exitCode !== 0 || !await fileHandle.exists()) {
33231
+ throw new Error(`Bundle failed: ${stderr || `exit code ${exitCode}`}`);
33232
+ }
33233
+ code = await fileHandle.text();
33178
33234
  } finally {
33179
33235
  try {
33180
33236
  fs.unlinkSync(tmpOut);
@@ -33687,13 +33743,13 @@ async function writeProjectEnvFile(prepared, credentials, operations = nodeProje
33687
33743
  throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
33688
33744
  }
33689
33745
  }
33690
- function isRecord(candidate) {
33746
+ function isRecord2(candidate) {
33691
33747
  return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
33692
33748
  }
33693
33749
  function decodedJwtPart(encodedPart) {
33694
33750
  try {
33695
33751
  const decodedPart = JSON.parse(Buffer.from(encodedPart, "base64url").toString("utf8"));
33696
- return isRecord(decodedPart) ? decodedPart : null;
33752
+ return isRecord2(decodedPart) ? decodedPart : null;
33697
33753
  } catch {
33698
33754
  return null;
33699
33755
  }
@@ -33734,28 +33790,28 @@ function projectApiUrl(candidate, projectRef, expectedApiOrigin) {
33734
33790
  return parsedUrl.origin;
33735
33791
  }
33736
33792
  function parseProjectCreateIdentity(responsePayload, expectedApiOrigin) {
33737
- if (!isRecord(responsePayload) || typeof responsePayload.ref !== "string")
33793
+ if (!isRecord2(responsePayload) || typeof responsePayload.ref !== "string")
33738
33794
  return null;
33739
- if (!PROJECT_REF.test(responsePayload.ref) || !isRecord(responsePayload.api))
33795
+ if (!PROJECT_REF.test(responsePayload.ref) || !isRecord2(responsePayload.api))
33740
33796
  return null;
33741
33797
  const apiUrl = projectApiUrl(responsePayload.api.url, responsePayload.ref, expectedApiOrigin);
33742
33798
  return apiUrl ? { projectRef: responsePayload.ref, apiUrl } : null;
33743
33799
  }
33744
33800
  function parseProjectCreateWithoutCredentials(responsePayload, expectedApiOrigin) {
33745
33801
  const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
33746
- if (!identity || !isRecord(responsePayload))
33802
+ if (!identity || !isRecord2(responsePayload))
33747
33803
  return null;
33748
33804
  return Object.hasOwn(responsePayload, "credentials") ? null : identity;
33749
33805
  }
33750
33806
  function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expectedProjectName) {
33751
- if (!expectedApiOrigin || !expectedProjectName || !isRecord(responsePayload))
33807
+ if (!expectedApiOrigin || !expectedProjectName || !isRecord2(responsePayload))
33752
33808
  return null;
33753
33809
  if (responsePayload.name !== expectedProjectName)
33754
33810
  return null;
33755
33811
  const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
33756
33812
  if (!identity)
33757
33813
  return null;
33758
- if (!isRecord(responsePayload.credentials))
33814
+ if (!isRecord2(responsePayload.credentials))
33759
33815
  return null;
33760
33816
  const serviceRoleKey = responsePayload.credentials.service_role_key;
33761
33817
  return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
@@ -34149,7 +34205,7 @@ var POSTGREST_ATTESTATION_STATES = [
34149
34205
  var POSTGREST_DESIRED_STATES = ["running", "stopped"];
34150
34206
  var POSTGREST_ACTUAL_STATES = ["running", "stopped", "starting", "error"];
34151
34207
  var POSTGREST_HEALTH_STATES = ["healthy", "unhealthy", "unknown"];
34152
- function isRecord2(candidate) {
34208
+ function isRecord3(candidate) {
34153
34209
  return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
34154
34210
  }
34155
34211
  function hasExactKeys(candidate, expectedKeys) {
@@ -34198,7 +34254,7 @@ function hasValidSecretsState(candidate) {
34198
34254
  return isLoadedSecretsState(candidate) || isUnverifiedSecretsState(candidate);
34199
34255
  }
34200
34256
  function isRuntimeSecretsSnapshot(payload) {
34201
- if (!isRecord2(payload) || !hasExactKeys(payload, SECRETS_KEYS))
34257
+ if (!isRecord3(payload) || !hasExactKeys(payload, SECRETS_KEYS))
34202
34258
  return false;
34203
34259
  if (!isRevisionOrNull(payload.loaded_revision) || typeof payload.desired_revision !== "string" || !ATTESTED_REVISION_PATTERN.test(payload.desired_revision) || !isEnumMember(payload.load_state, SECRET_LOAD_STATES) || !(payload.load_source === null || isEnumMember(payload.load_source, SECRET_LOAD_SOURCES)) || !isBooleanOrNull(payload.matches_desired) || !isIsoTimestampOrNull(payload.loaded_at))
34204
34260
  return false;
@@ -34231,7 +34287,7 @@ function hasValidPostgrestState(candidate) {
34231
34287
  return candidate.attestation_state === "drifted";
34232
34288
  }
34233
34289
  function isPostgrestRuntimeSnapshot(payload, projectRef) {
34234
- if (!isRecord2(payload) || !hasExactKeys(payload, POSTGREST_KEYS))
34290
+ if (!isRecord3(payload) || !hasExactKeys(payload, POSTGREST_KEYS))
34235
34291
  return false;
34236
34292
  if (typeof payload.desired_revision !== "string" || !ATTESTED_REVISION_PATTERN.test(payload.desired_revision) || !isRevisionOrNull(payload.loaded_revision) || !isEnumMember(payload.attestation_state, POSTGREST_ATTESTATION_STATES) || !isBooleanOrNull(payload.matches_desired) || !isEnumMember(payload.desired, POSTGREST_DESIRED_STATES) || !isEnumMember(payload.actual, POSTGREST_ACTUAL_STATES) || !isEnumMember(payload.health, POSTGREST_HEALTH_STATES) || !Number.isSafeInteger(payload.port) || Number(payload.port) < 1 || Number(payload.port) > 65535 || payload.unit !== `supacloud-pgrst@${projectRef}` || !isIsoTimestampOrNull(payload.loaded_at))
34237
34293
  return false;
@@ -34251,7 +34307,7 @@ function hasCausalLoadTimestamps(snapshot) {
34251
34307
  return [snapshot.secrets.loaded_at, snapshot.postgrest.loaded_at].every((loadedAt) => loadedAt === null || Date.parse(loadedAt) <= capturedAt);
34252
34308
  }
34253
34309
  function parseProjectRuntimeSnapshot(payload, requestedProjectRef) {
34254
- if (!SAFE_PROJECT_REF3.test(requestedProjectRef) || !isRecord2(payload) || !hasExactKeys(payload, SNAPSHOT_KEYS) || payload.schema !== RUNTIME_SNAPSHOT_SCHEMA || payload.project_ref !== requestedProjectRef || !isIsoTimestampOrNull(payload.captured_at) || payload.captured_at === null || !isRuntimeSecretsSnapshot(payload.secrets) || !isPostgrestRuntimeSnapshot(payload.postgrest, requestedProjectRef))
34310
+ if (!SAFE_PROJECT_REF3.test(requestedProjectRef) || !isRecord3(payload) || !hasExactKeys(payload, SNAPSHOT_KEYS) || payload.schema !== RUNTIME_SNAPSHOT_SCHEMA || payload.project_ref !== requestedProjectRef || !isIsoTimestampOrNull(payload.captured_at) || payload.captured_at === null || !isRuntimeSecretsSnapshot(payload.secrets) || !isPostgrestRuntimeSnapshot(payload.postgrest, requestedProjectRef))
34255
34311
  return null;
34256
34312
  const snapshot = payload;
34257
34313
  return hasCausalLoadTimestamps(snapshot) ? sanitizedSnapshot(snapshot) : null;
@@ -34790,6 +34846,21 @@ function registerAdminProjectCliTools(server, http, options = {}) {
34790
34846
  });
34791
34847
  }
34792
34848
 
34849
+ // ../cli/src/shared/tool-server.ts
34850
+ var resultSchema = _Object_({
34851
+ content: _Array_(_Object_({ type: Literal("text"), text: String2() })),
34852
+ isError: Optional(Boolean2())
34853
+ });
34854
+ function registerTool(server, name, description, schema, callback) {
34855
+ server.tool(name, description, schema, async (input) => {
34856
+ const args = parseToolArguments(schema, input);
34857
+ const result = await callback(args);
34858
+ if (!Check2(resultSchema, result))
34859
+ throw new Error(`Invalid result from tool '${name}'`);
34860
+ return result;
34861
+ });
34862
+ }
34863
+
34793
34864
  // ../cli/src/shared/tools/gateway-tools.ts
34794
34865
  function parseCliStringArray(value) {
34795
34866
  if (Array.isArray(value))
@@ -34844,7 +34915,7 @@ var ok2 = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${r
34844
34915
  var simple2 = (res, msg) => res.ok ? `✅ ${msg}` : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
34845
34916
  function registerGatewayTools(server, http, options = {}) {
34846
34917
  const { projectRef } = options;
34847
- server.tool("gateway", `Gateway / Caddy 配置(通过 JSON Admin API 注入)。要求 admin 权限。
34918
+ registerTool(server, "gateway", `Gateway / Caddy 配置(通过 JSON Admin API 注入)。要求 admin 权限。
34848
34919
  Actions: routes, upsert_route, update_route, delete_route, config, get_certificate, update_certificate, issue_certificate, deploy_certificate, rebuild, custom_hostname, set_custom_hostname, delete_custom_hostname, verify_custom_hostname`, {
34849
34920
  action: withDescription(stringEnum([
34850
34921
  "routes",
@@ -35245,12 +35316,15 @@ function releaseInventory(candidate, expected) {
35245
35316
  next_cursor: nextCursor
35246
35317
  };
35247
35318
  }
35248
- function releaseEndpoint(projectRef, deploymentId) {
35319
+ function deploymentEndpoint(projectRef, deploymentId) {
35249
35320
  if (!PROJECT_REF_PATTERN3.test(projectRef))
35250
35321
  throw new Error("'ref' is invalid for frontend releases");
35251
35322
  if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
35252
35323
  throw new Error("'id' is invalid for frontend releases");
35253
- return `/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments/${encodeURIComponent(deploymentId)}/releases`;
35324
+ return `/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments/${encodeURIComponent(deploymentId)}`;
35325
+ }
35326
+ function releaseEndpoint(projectRef, deploymentId) {
35327
+ return `${deploymentEndpoint(projectRef, deploymentId)}/releases`;
35254
35328
  }
35255
35329
  function releasePath(projectRef, deploymentId, releaseId) {
35256
35330
  if (!RELEASE_ID_PATTERN.test(releaseId))
@@ -35455,16 +35529,15 @@ function activationReceipt(candidate, input) {
35455
35529
  return release;
35456
35530
  }
35457
35531
  async function activeReleaseReadback(http, identity) {
35458
- const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
35459
- const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
35460
- maxJsonBytes: RESPONSE_MAX_BYTES
35461
- });
35462
- const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
35463
- projectRef: identity.projectRef,
35464
- deploymentId: identity.deploymentId
35465
- }) : null;
35466
- if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
35467
- return { release: null, status: inventoryRead.status };
35532
+ const snapshot = await readActiveFrontendRelease(http, identity.projectRef, identity.deploymentId);
35533
+ if ("failure" in snapshot)
35534
+ return { release: null, status: snapshot.status };
35535
+ const inventory = snapshot.inventory;
35536
+ if (inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
35537
+ return { release: null, status: 200 };
35538
+ }
35539
+ if (snapshot.exactReadback) {
35540
+ return { release: inventory.releases[0] ?? null, status: 200 };
35468
35541
  }
35469
35542
  const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
35470
35543
  const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
@@ -35474,6 +35547,100 @@ async function activeReleaseReadback(http, identity) {
35474
35547
  }) : null;
35475
35548
  return { release, status: releaseRead.status };
35476
35549
  }
35550
+ async function readActiveFrontendRelease(http, projectRef, deploymentId) {
35551
+ const response = await http.get(`${deploymentEndpoint(projectRef, deploymentId)}/active-release`, {
35552
+ maxJsonBytes: RESPONSE_MAX_BYTES
35553
+ });
35554
+ const inventory = response.ok && response.status === 200 ? releaseInventory(response.data, { projectRef, deploymentId }) : null;
35555
+ if (inventory) {
35556
+ if (inventory.next_cursor !== null || inventory.releases.length !== (inventory.active_release_id === null ? 0 : 1) || inventory.releases[0] && inventory.releases[0].release_id !== inventory.active_release_id) {
35557
+ return { failure: releaseReadFailure("frontend.get_active_release", response), status: response.status };
35558
+ }
35559
+ return { inventory, exactReadback: false };
35560
+ }
35561
+ if (response.ok || response.status !== 404) {
35562
+ return { failure: releaseReadFailure("frontend.get_active_release", response), status: response.status };
35563
+ }
35564
+ const historyResponse = await http.get(`${releaseEndpoint(projectRef, deploymentId)}?limit=1`, {
35565
+ maxJsonBytes: RESPONSE_MAX_BYTES
35566
+ });
35567
+ const history = historyResponse.ok && historyResponse.status === 200 ? releaseInventory(historyResponse.data, { projectRef, deploymentId }) : null;
35568
+ if (!history || history.releases.length > 1) {
35569
+ return {
35570
+ failure: releaseReadFailure("frontend.get_active_release", historyResponse),
35571
+ status: historyResponse.status
35572
+ };
35573
+ }
35574
+ if (history.active_release_id === null) {
35575
+ return { inventory: { ...history, releases: [], next_cursor: null }, exactReadback: false };
35576
+ }
35577
+ const exactResponse = await http.get(releasePath(projectRef, deploymentId, history.active_release_id), {
35578
+ maxJsonBytes: RESPONSE_MAX_BYTES
35579
+ });
35580
+ const release = exactResponse.ok && exactResponse.status === 200 ? releaseEnvelope(exactResponse.data, { projectRef, deploymentId, releaseId: history.active_release_id }) : null;
35581
+ if (!release) {
35582
+ return {
35583
+ failure: releaseReadFailure("frontend.get_active_release", exactResponse),
35584
+ status: exactResponse.status
35585
+ };
35586
+ }
35587
+ return { inventory: { ...history, releases: [release], next_cursor: null }, exactReadback: true };
35588
+ }
35589
+ async function getActiveFrontendRelease(http, projectRef, deploymentId) {
35590
+ const snapshot = await readActiveFrontendRelease(http, projectRef, deploymentId);
35591
+ return "failure" in snapshot ? snapshot.failure : toolResponse(snapshot.inventory);
35592
+ }
35593
+ async function rollbackFrontendRelease(http, projectRef, deploymentId, releaseId) {
35594
+ if (!RELEASE_ID_PATTERN.test(releaseId))
35595
+ throw new Error("'release_id' must be a SHA-256 digest");
35596
+ const authority = await readActiveFrontendRelease(http, projectRef, deploymentId);
35597
+ if ("failure" in authority)
35598
+ return authority.failure;
35599
+ const inventory = authority.inventory;
35600
+ if (inventory.active_release_id === releaseId) {
35601
+ return toolResponse({
35602
+ project_ref: projectRef,
35603
+ deployment_id: deploymentId,
35604
+ active_release_id: releaseId,
35605
+ activation_id: inventory.active_activation_id,
35606
+ unchanged: true
35607
+ });
35608
+ }
35609
+ if (inventory.active_release_id === null || inventory.active_activation_id === null) {
35610
+ return releaseFailure("frontend.rollback", "NO_ACTIVE_RELEASE", 409);
35611
+ }
35612
+ const target = await getFrontendRelease(http, projectRef, deploymentId, releaseId);
35613
+ if (target.isError)
35614
+ return target;
35615
+ const mutationId = crypto.randomUUID();
35616
+ const activation = await activateFrontendRelease(http, {
35617
+ projectRef,
35618
+ deploymentId,
35619
+ releaseId,
35620
+ expectedActiveReleaseId: inventory.active_release_id,
35621
+ expectedActivationId: inventory.active_activation_id,
35622
+ mutationId
35623
+ });
35624
+ if (activation.isError) {
35625
+ return {
35626
+ ...toolResponse({
35627
+ ...JSON.parse(activation.content[0].text),
35628
+ operation: "frontend.rollback",
35629
+ mutation_id: mutationId,
35630
+ target_release_id: releaseId,
35631
+ expected_active_release_id: inventory.active_release_id,
35632
+ expected_activation_id: inventory.active_activation_id
35633
+ }),
35634
+ isError: true
35635
+ };
35636
+ }
35637
+ return toolResponse({
35638
+ ...JSON.parse(activation.content[0].text),
35639
+ rollback: true,
35640
+ previous_release_id: inventory.active_release_id,
35641
+ previous_activation_id: inventory.active_activation_id
35642
+ });
35643
+ }
35477
35644
  async function activateFrontendRelease(http, input) {
35478
35645
  const mutationId = input.mutationId;
35479
35646
  if (!MUTATION_ID_PATTERN.test(mutationId))
@@ -35539,17 +35706,19 @@ async function activationReadback(http, input, mutationId, activationStatus) {
35539
35706
  // src/shared/tools/frontend-tools.ts
35540
35707
  function registerFrontendTools(server, http) {
35541
35708
  server.tool("frontend", `Immutable prebuilt frontend release control.
35542
- Actions: list_releases, get_release, upload_release, activate_release`, {
35709
+ Actions: list_releases, get_active_release, get_release, upload_release, activate_release, rollback`, {
35543
35710
  action: withDescription(stringEnum([
35544
35711
  "list_releases",
35712
+ "get_active_release",
35545
35713
  "get_release",
35546
35714
  "upload_release",
35547
- "activate_release"
35715
+ "activate_release",
35716
+ "rollback"
35548
35717
  ]), "Action"),
35549
35718
  ref: optional(String4(), "Project ref"),
35550
35719
  id: optional(String4(), "Deployment ID"),
35551
35720
  zip_path: optional(String4(), "[upload_release] Local ZIP file path"),
35552
- release_id: optional(String4(), "[get_release/activate_release] SHA-256 release ID"),
35721
+ release_id: optional(String4(), "[get_release/activate_release/rollback] SHA-256 release ID"),
35553
35722
  expected_active_release_id: optional(String4(), "[activate_release] Current release SHA-256 or absent"),
35554
35723
  expected_activation_id: optional(String4(), "[activate_release] Current activation UUIDv4 or absent"),
35555
35724
  mutation_id: optional(String4(), "[activate_release] Required retry-stable UUIDv4"),
@@ -35582,6 +35751,10 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
35582
35751
  need("id", id);
35583
35752
  need("release_id", release_id);
35584
35753
  return getFrontendRelease(http, ref, id, release_id);
35754
+ case "get_active_release":
35755
+ need("ref", ref);
35756
+ need("id", id);
35757
+ return getActiveFrontendRelease(http, ref, id);
35585
35758
  case "upload_release":
35586
35759
  need("ref", ref);
35587
35760
  need("id", id);
@@ -35602,6 +35775,11 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
35602
35775
  expectedActivationId: expected_activation_id,
35603
35776
  mutationId: mutation_id
35604
35777
  });
35778
+ case "rollback":
35779
+ need("ref", ref);
35780
+ need("id", id);
35781
+ need("release_id", release_id);
35782
+ return rollbackFrontendRelease(http, ref, id, release_id);
35605
35783
  default:
35606
35784
  throw new Error("Unknown immutable frontend release action");
35607
35785
  }
@@ -35610,7 +35788,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
35610
35788
  // package.json
35611
35789
  var package_default = {
35612
35790
  name: "@supacloud/admin",
35613
- version: "0.24.1",
35791
+ version: "0.25.0",
35614
35792
  description: "Platform administration CLI for SupaCloud operators",
35615
35793
  type: "module",
35616
35794
  main: "./dist/index.js",
@@ -35643,13 +35821,16 @@ var package_default = {
35643
35821
  },
35644
35822
  dependencies: {
35645
35823
  typebox: "1.3.34",
35646
- "@supacloud/cli": "0.67.0",
35824
+ "@supacloud/cli": "0.76.0",
35647
35825
  ssh2: "^1.17.0"
35648
35826
  },
35649
35827
  devDependencies: {
35650
35828
  "@types/bun": "^1.4.2",
35651
35829
  "@types/ssh2": "^1.15.6",
35652
35830
  typescript: "^7.0.2"
35831
+ },
35832
+ overrides: {
35833
+ "@graphql-tools/utils": "^12.0.3"
35653
35834
  }
35654
35835
  };
35655
35836
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,12 +33,15 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "typebox": "1.3.34",
36
- "@supacloud/cli": "0.67.0",
36
+ "@supacloud/cli": "0.76.0",
37
37
  "ssh2": "^1.17.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/bun": "^1.4.2",
41
41
  "@types/ssh2": "^1.15.6",
42
42
  "typescript": "^7.0.2"
43
+ },
44
+ "overrides": {
45
+ "@graphql-tools/utils": "^12.0.3"
43
46
  }
44
47
  }