@supacloud/admin 0.24.2 → 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 +222 -66
  3. package/package.json +2 -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: [
@@ -33709,13 +33743,13 @@ async function writeProjectEnvFile(prepared, credentials, operations = nodeProje
33709
33743
  throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
33710
33744
  }
33711
33745
  }
33712
- function isRecord(candidate) {
33746
+ function isRecord2(candidate) {
33713
33747
  return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
33714
33748
  }
33715
33749
  function decodedJwtPart(encodedPart) {
33716
33750
  try {
33717
33751
  const decodedPart = JSON.parse(Buffer.from(encodedPart, "base64url").toString("utf8"));
33718
- return isRecord(decodedPart) ? decodedPart : null;
33752
+ return isRecord2(decodedPart) ? decodedPart : null;
33719
33753
  } catch {
33720
33754
  return null;
33721
33755
  }
@@ -33756,28 +33790,28 @@ function projectApiUrl(candidate, projectRef, expectedApiOrigin) {
33756
33790
  return parsedUrl.origin;
33757
33791
  }
33758
33792
  function parseProjectCreateIdentity(responsePayload, expectedApiOrigin) {
33759
- if (!isRecord(responsePayload) || typeof responsePayload.ref !== "string")
33793
+ if (!isRecord2(responsePayload) || typeof responsePayload.ref !== "string")
33760
33794
  return null;
33761
- if (!PROJECT_REF.test(responsePayload.ref) || !isRecord(responsePayload.api))
33795
+ if (!PROJECT_REF.test(responsePayload.ref) || !isRecord2(responsePayload.api))
33762
33796
  return null;
33763
33797
  const apiUrl = projectApiUrl(responsePayload.api.url, responsePayload.ref, expectedApiOrigin);
33764
33798
  return apiUrl ? { projectRef: responsePayload.ref, apiUrl } : null;
33765
33799
  }
33766
33800
  function parseProjectCreateWithoutCredentials(responsePayload, expectedApiOrigin) {
33767
33801
  const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
33768
- if (!identity || !isRecord(responsePayload))
33802
+ if (!identity || !isRecord2(responsePayload))
33769
33803
  return null;
33770
33804
  return Object.hasOwn(responsePayload, "credentials") ? null : identity;
33771
33805
  }
33772
33806
  function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expectedProjectName) {
33773
- if (!expectedApiOrigin || !expectedProjectName || !isRecord(responsePayload))
33807
+ if (!expectedApiOrigin || !expectedProjectName || !isRecord2(responsePayload))
33774
33808
  return null;
33775
33809
  if (responsePayload.name !== expectedProjectName)
33776
33810
  return null;
33777
33811
  const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
33778
33812
  if (!identity)
33779
33813
  return null;
33780
- if (!isRecord(responsePayload.credentials))
33814
+ if (!isRecord2(responsePayload.credentials))
33781
33815
  return null;
33782
33816
  const serviceRoleKey = responsePayload.credentials.service_role_key;
33783
33817
  return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
@@ -34171,7 +34205,7 @@ var POSTGREST_ATTESTATION_STATES = [
34171
34205
  var POSTGREST_DESIRED_STATES = ["running", "stopped"];
34172
34206
  var POSTGREST_ACTUAL_STATES = ["running", "stopped", "starting", "error"];
34173
34207
  var POSTGREST_HEALTH_STATES = ["healthy", "unhealthy", "unknown"];
34174
- function isRecord2(candidate) {
34208
+ function isRecord3(candidate) {
34175
34209
  return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
34176
34210
  }
34177
34211
  function hasExactKeys(candidate, expectedKeys) {
@@ -34220,7 +34254,7 @@ function hasValidSecretsState(candidate) {
34220
34254
  return isLoadedSecretsState(candidate) || isUnverifiedSecretsState(candidate);
34221
34255
  }
34222
34256
  function isRuntimeSecretsSnapshot(payload) {
34223
- if (!isRecord2(payload) || !hasExactKeys(payload, SECRETS_KEYS))
34257
+ if (!isRecord3(payload) || !hasExactKeys(payload, SECRETS_KEYS))
34224
34258
  return false;
34225
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))
34226
34260
  return false;
@@ -34253,7 +34287,7 @@ function hasValidPostgrestState(candidate) {
34253
34287
  return candidate.attestation_state === "drifted";
34254
34288
  }
34255
34289
  function isPostgrestRuntimeSnapshot(payload, projectRef) {
34256
- if (!isRecord2(payload) || !hasExactKeys(payload, POSTGREST_KEYS))
34290
+ if (!isRecord3(payload) || !hasExactKeys(payload, POSTGREST_KEYS))
34257
34291
  return false;
34258
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))
34259
34293
  return false;
@@ -34273,7 +34307,7 @@ function hasCausalLoadTimestamps(snapshot) {
34273
34307
  return [snapshot.secrets.loaded_at, snapshot.postgrest.loaded_at].every((loadedAt) => loadedAt === null || Date.parse(loadedAt) <= capturedAt);
34274
34308
  }
34275
34309
  function parseProjectRuntimeSnapshot(payload, requestedProjectRef) {
34276
- 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))
34277
34311
  return null;
34278
34312
  const snapshot = payload;
34279
34313
  return hasCausalLoadTimestamps(snapshot) ? sanitizedSnapshot(snapshot) : null;
@@ -34812,6 +34846,21 @@ function registerAdminProjectCliTools(server, http, options = {}) {
34812
34846
  });
34813
34847
  }
34814
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
+
34815
34864
  // ../cli/src/shared/tools/gateway-tools.ts
34816
34865
  function parseCliStringArray(value) {
34817
34866
  if (Array.isArray(value))
@@ -34866,7 +34915,7 @@ var ok2 = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${r
34866
34915
  var simple2 = (res, msg) => res.ok ? `✅ ${msg}` : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
34867
34916
  function registerGatewayTools(server, http, options = {}) {
34868
34917
  const { projectRef } = options;
34869
- server.tool("gateway", `Gateway / Caddy 配置(通过 JSON Admin API 注入)。要求 admin 权限。
34918
+ registerTool(server, "gateway", `Gateway / Caddy 配置(通过 JSON Admin API 注入)。要求 admin 权限。
34870
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`, {
34871
34920
  action: withDescription(stringEnum([
34872
34921
  "routes",
@@ -35267,12 +35316,15 @@ function releaseInventory(candidate, expected) {
35267
35316
  next_cursor: nextCursor
35268
35317
  };
35269
35318
  }
35270
- function releaseEndpoint(projectRef, deploymentId) {
35319
+ function deploymentEndpoint(projectRef, deploymentId) {
35271
35320
  if (!PROJECT_REF_PATTERN3.test(projectRef))
35272
35321
  throw new Error("'ref' is invalid for frontend releases");
35273
35322
  if (!DEPLOYMENT_ID_PATTERN.test(deploymentId))
35274
35323
  throw new Error("'id' is invalid for frontend releases");
35275
- 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`;
35276
35328
  }
35277
35329
  function releasePath(projectRef, deploymentId, releaseId) {
35278
35330
  if (!RELEASE_ID_PATTERN.test(releaseId))
@@ -35477,16 +35529,15 @@ function activationReceipt(candidate, input) {
35477
35529
  return release;
35478
35530
  }
35479
35531
  async function activeReleaseReadback(http, identity) {
35480
- const endpoint = releaseEndpoint(identity.projectRef, identity.deploymentId);
35481
- const inventoryRead = await http.get(`${endpoint}?limit=${RELEASE_LIST_LIMIT_MAX}`, {
35482
- maxJsonBytes: RESPONSE_MAX_BYTES
35483
- });
35484
- const inventory = inventoryRead.ok ? releaseInventory(inventoryRead.data, {
35485
- projectRef: identity.projectRef,
35486
- deploymentId: identity.deploymentId
35487
- }) : null;
35488
- if (!inventory || inventory.releases.length > RELEASE_LIST_LIMIT_MAX || inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
35489
- 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 };
35490
35541
  }
35491
35542
  const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
35492
35543
  const release = releaseRead.ok ? releaseEnvelope(releaseRead.data, {
@@ -35496,6 +35547,100 @@ async function activeReleaseReadback(http, identity) {
35496
35547
  }) : null;
35497
35548
  return { release, status: releaseRead.status };
35498
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
+ }
35499
35644
  async function activateFrontendRelease(http, input) {
35500
35645
  const mutationId = input.mutationId;
35501
35646
  if (!MUTATION_ID_PATTERN.test(mutationId))
@@ -35561,17 +35706,19 @@ async function activationReadback(http, input, mutationId, activationStatus) {
35561
35706
  // src/shared/tools/frontend-tools.ts
35562
35707
  function registerFrontendTools(server, http) {
35563
35708
  server.tool("frontend", `Immutable prebuilt frontend release control.
35564
- Actions: list_releases, get_release, upload_release, activate_release`, {
35709
+ Actions: list_releases, get_active_release, get_release, upload_release, activate_release, rollback`, {
35565
35710
  action: withDescription(stringEnum([
35566
35711
  "list_releases",
35712
+ "get_active_release",
35567
35713
  "get_release",
35568
35714
  "upload_release",
35569
- "activate_release"
35715
+ "activate_release",
35716
+ "rollback"
35570
35717
  ]), "Action"),
35571
35718
  ref: optional(String4(), "Project ref"),
35572
35719
  id: optional(String4(), "Deployment ID"),
35573
35720
  zip_path: optional(String4(), "[upload_release] Local ZIP file path"),
35574
- 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"),
35575
35722
  expected_active_release_id: optional(String4(), "[activate_release] Current release SHA-256 or absent"),
35576
35723
  expected_activation_id: optional(String4(), "[activate_release] Current activation UUIDv4 or absent"),
35577
35724
  mutation_id: optional(String4(), "[activate_release] Required retry-stable UUIDv4"),
@@ -35604,6 +35751,10 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
35604
35751
  need("id", id);
35605
35752
  need("release_id", release_id);
35606
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);
35607
35758
  case "upload_release":
35608
35759
  need("ref", ref);
35609
35760
  need("id", id);
@@ -35624,6 +35775,11 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
35624
35775
  expectedActivationId: expected_activation_id,
35625
35776
  mutationId: mutation_id
35626
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);
35627
35783
  default:
35628
35784
  throw new Error("Unknown immutable frontend release action");
35629
35785
  }
@@ -35632,7 +35788,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
35632
35788
  // package.json
35633
35789
  var package_default = {
35634
35790
  name: "@supacloud/admin",
35635
- version: "0.24.2",
35791
+ version: "0.25.0",
35636
35792
  description: "Platform administration CLI for SupaCloud operators",
35637
35793
  type: "module",
35638
35794
  main: "./dist/index.js",
@@ -35665,7 +35821,7 @@ var package_default = {
35665
35821
  },
35666
35822
  dependencies: {
35667
35823
  typebox: "1.3.34",
35668
- "@supacloud/cli": "0.68.1",
35824
+ "@supacloud/cli": "0.76.0",
35669
35825
  ssh2: "^1.17.0"
35670
35826
  },
35671
35827
  devDependencies: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.24.2",
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,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "typebox": "1.3.34",
36
- "@supacloud/cli": "0.68.1",
36
+ "@supacloud/cli": "0.76.0",
37
37
  "ssh2": "^1.17.0"
38
38
  },
39
39
  "devDependencies": {