@supacloud/cli 0.19.0 → 0.20.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 (2) hide show
  1. package/dist/index.js +256 -12
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6572,6 +6572,15 @@ var RELEASE_MUTATION_RESPONSE_TIMEOUT = 5000;
6572
6572
  var RELEASE_MUTATION_RESPONSE_MAX_BYTES = 64 * 1024;
6573
6573
  var MAX_RETRIES = 2;
6574
6574
  var RETRY_BASE_DELAY = 500;
6575
+ function validatedGetResponseLimit(options) {
6576
+ const maxBytes = options.maxResponseBytes;
6577
+ if (maxBytes === undefined)
6578
+ return;
6579
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
6580
+ throw new RangeError("HTTP response limit must be a positive safe integer");
6581
+ }
6582
+ return maxBytes;
6583
+ }
6575
6584
  function isRetryableMethod(method) {
6576
6585
  const normalizedMethod = (method ?? "GET").toUpperCase();
6577
6586
  return normalizedMethod === "GET" || normalizedMethod === "HEAD";
@@ -6787,12 +6796,13 @@ class HttpTransport {
6787
6796
  }
6788
6797
  }
6789
6798
  async get(path, options = {}) {
6799
+ const maxResponseBytes = validatedGetResponseLimit(options);
6790
6800
  try {
6791
6801
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
6792
6802
  method: "GET",
6793
6803
  headers: this.headers()
6794
6804
  });
6795
- const data = options.maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, options.maxResponseBytes);
6805
+ const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
6796
6806
  return { ok: res.ok, status: res.status, data };
6797
6807
  } catch (error) {
6798
6808
  return transportFailure(error);
@@ -7957,14 +7967,15 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7957
7967
  var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7958
7968
  function releaseControlSuccess(operation, payload) {
7959
7969
  return releaseControlResponse({
7970
+ ...payload,
7960
7971
  schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7961
7972
  ok: true,
7962
- operation,
7963
- ...payload
7973
+ operation
7964
7974
  });
7965
7975
  }
7966
- function releaseControlFailure(operation, code, httpStatus) {
7976
+ function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
7967
7977
  return releaseControlErrorResponse({
7978
+ ...safeState,
7968
7979
  schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7969
7980
  ok: false,
7970
7981
  operation,
@@ -9395,7 +9406,210 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
9395
9406
  });
9396
9407
  }
9397
9408
 
9409
+ // src/shared/tools/project-read-projection.ts
9410
+ var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
9411
+ var PROJECT_REF_PATTERN3 = /^[a-z0-9-]{1,20}$/;
9412
+ var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
9413
+ var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
9414
+ var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
9415
+ var DNS_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
9416
+ var DATABASE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
9417
+ var PROJECT_SUMMARY_KEYS = new Set([
9418
+ "id",
9419
+ "ref",
9420
+ "organization_id",
9421
+ "organization_slug",
9422
+ "name",
9423
+ "region",
9424
+ "created_at",
9425
+ "status"
9426
+ ]);
9427
+ var PROJECT_DETAILS_KEYS = new Set([
9428
+ ...PROJECT_SUMMARY_KEYS,
9429
+ "database",
9430
+ "api",
9431
+ "studio",
9432
+ "config",
9433
+ "anon_key",
9434
+ "services"
9435
+ ]);
9436
+ var PROJECT_DATABASE_KEYS = new Set([
9437
+ "host",
9438
+ "version",
9439
+ "postgres_engine",
9440
+ "release_channel"
9441
+ ]);
9442
+ var PROJECT_ENDPOINT_KEYS = new Set(["url"]);
9443
+ function plainRecord(candidate) {
9444
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
9445
+ return null;
9446
+ const prototype = Object.getPrototypeOf(candidate);
9447
+ return prototype === Object.prototype || prototype === null ? candidate : null;
9448
+ }
9449
+ function hasOnlyKeys(record, allowedKeys) {
9450
+ return Object.keys(record).every((key) => allowedKeys.has(key));
9451
+ }
9452
+ function hasWellFormedUnicode(text) {
9453
+ for (let index = 0;index < text.length; index++) {
9454
+ const codeUnit = text.charCodeAt(index);
9455
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
9456
+ if (index + 1 >= text.length)
9457
+ return false;
9458
+ const lowSurrogate = text.charCodeAt(index + 1);
9459
+ if (lowSurrogate < 56320 || lowSurrogate > 57343)
9460
+ return false;
9461
+ index++;
9462
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
9463
+ return false;
9464
+ }
9465
+ }
9466
+ return true;
9467
+ }
9468
+ function boundedText(candidate, maxLength) {
9469
+ return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
9470
+ }
9471
+ function matchingText(candidate, maxLength, pattern) {
9472
+ const candidateText = boundedText(candidate, maxLength);
9473
+ return candidateText && pattern.test(candidateText) ? candidateText : null;
9474
+ }
9475
+ function canonicalTimestamp(candidate) {
9476
+ const timestamp = boundedText(candidate, 64);
9477
+ if (!timestamp)
9478
+ return null;
9479
+ const milliseconds = Date.parse(timestamp);
9480
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === timestamp ? timestamp : null;
9481
+ }
9482
+ function projectedSummary(project) {
9483
+ const summary = {
9484
+ id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
9485
+ ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN3),
9486
+ organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
9487
+ organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
9488
+ name: boundedText(project.name, 100),
9489
+ region: matchingText(project.region, 64, REGION_PATTERN),
9490
+ created_at: canonicalTimestamp(project.created_at),
9491
+ status: matchingText(project.status, 64, STATUS_PATTERN)
9492
+ };
9493
+ return Object.values(summary).every((field) => field !== null) ? summary : null;
9494
+ }
9495
+ function databaseHost(candidate) {
9496
+ const host = boundedText(candidate, 255);
9497
+ if (!host)
9498
+ return null;
9499
+ if (host.startsWith("[") && host.endsWith("]")) {
9500
+ try {
9501
+ const parsedHost = new URL(`http://${host}`);
9502
+ return parsedHost.host === host ? host : null;
9503
+ } catch (error) {
9504
+ if (error instanceof TypeError)
9505
+ return null;
9506
+ throw error;
9507
+ }
9508
+ }
9509
+ const ipv4Parts = host.split(".");
9510
+ if (ipv4Parts.length === 4 && ipv4Parts.every((part) => /^\d{1,3}$/u.test(part))) {
9511
+ return ipv4Parts.every((part) => Number(part) <= 255) ? host : null;
9512
+ }
9513
+ return ipv4Parts.every((label) => DNS_LABEL_PATTERN.test(label)) ? host : null;
9514
+ }
9515
+ function projectDatabase(candidate) {
9516
+ const database = plainRecord(candidate);
9517
+ if (!database || !hasOnlyKeys(database, PROJECT_DATABASE_KEYS))
9518
+ return null;
9519
+ const host = databaseHost(database.host);
9520
+ const version = matchingText(database.version, 64, DATABASE_VERSION_PATTERN);
9521
+ const postgresEngine = matchingText(database.postgres_engine, 64, DATABASE_VERSION_PATTERN);
9522
+ const releaseChannel = matchingText(database.release_channel, 64, DATABASE_VERSION_PATTERN);
9523
+ return host && version && postgresEngine && releaseChannel ? { host, version, postgres_engine: postgresEngine, release_channel: releaseChannel } : null;
9524
+ }
9525
+ function rawUrlHasNoPath(candidate) {
9526
+ if (candidate.trim() !== candidate || candidate.includes("\\"))
9527
+ return false;
9528
+ const schemeEnd = candidate.indexOf("://");
9529
+ const pathStart = candidate.indexOf("/", schemeEnd + 3);
9530
+ return pathStart === -1;
9531
+ }
9532
+ function projectEndpoint(candidate) {
9533
+ const endpoint = plainRecord(candidate);
9534
+ if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
9535
+ return null;
9536
+ const endpointUrl = boundedText(endpoint.url, 2048);
9537
+ if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
9538
+ return null;
9539
+ try {
9540
+ const url = new URL(endpointUrl);
9541
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
9542
+ return null;
9543
+ return { url: url.origin };
9544
+ } catch (error) {
9545
+ if (error instanceof TypeError)
9546
+ return null;
9547
+ throw error;
9548
+ }
9549
+ }
9550
+ function discardedDetailFieldsAreValid(project) {
9551
+ if (project.config !== undefined && plainRecord(project.config) === null)
9552
+ return false;
9553
+ if (project.anon_key !== undefined && boundedText(project.anon_key, 16384) === null)
9554
+ return false;
9555
+ return project.services === undefined || Array.isArray(project.services);
9556
+ }
9557
+ function projectDetails(candidate, expectedRef) {
9558
+ const project = plainRecord(candidate);
9559
+ if (!project || !hasOnlyKeys(project, PROJECT_DETAILS_KEYS))
9560
+ return null;
9561
+ const summary = projectedSummary(project);
9562
+ const database = projectDatabase(project.database);
9563
+ const api = project.api === undefined ? undefined : projectEndpoint(project.api);
9564
+ const studio = project.studio === undefined ? undefined : projectEndpoint(project.studio);
9565
+ if (!summary || summary.ref !== expectedRef || !database || !discardedDetailFieldsAreValid(project) || project.api !== undefined && !api || project.studio !== undefined && !studio)
9566
+ return null;
9567
+ return {
9568
+ ...summary,
9569
+ database,
9570
+ ...api ? { api } : {},
9571
+ ...studio ? { studio } : {}
9572
+ };
9573
+ }
9574
+ function payloadWithinLimit(candidate) {
9575
+ try {
9576
+ const serializedPayload = JSON.stringify(candidate);
9577
+ return serializedPayload !== undefined && new TextEncoder().encode(serializedPayload).byteLength <= PROJECT_READ_RESPONSE_MAX_BYTES;
9578
+ } catch {
9579
+ return false;
9580
+ }
9581
+ }
9582
+ function validHttpStatus(status) {
9583
+ return Number.isSafeInteger(status) && status >= 100 && status <= 599;
9584
+ }
9585
+ function successfulResponse(response) {
9586
+ return response.ok === true && validHttpStatus(response.status) && response.status >= 200 && response.status <= 299;
9587
+ }
9588
+ function failedResult(message) {
9589
+ return { text: `❌ ${message}`, isError: true };
9590
+ }
9591
+ function failedHttpResult(label, status) {
9592
+ return failedResult(validHttpStatus(status) ? `${label} request failed (${status})` : `${label} request failed`);
9593
+ }
9594
+ function successfulResult(payload) {
9595
+ return { text: JSON.stringify(payload, null, 2), isError: false };
9596
+ }
9597
+ function projectGetRead(response, expectedRef) {
9598
+ if (!successfulResponse(response))
9599
+ return failedHttpResult("Project get", response.status);
9600
+ if (!payloadWithinLimit(response.data))
9601
+ return failedResult("Invalid project response");
9602
+ const project = projectDetails(response.data, expectedRef);
9603
+ return project ? successfulResult(project) : failedResult("Invalid project response");
9604
+ }
9605
+
9398
9606
  // src/shared/tools/project-cli-tools.ts
9607
+ function projectReadResponse(readResult) {
9608
+ return {
9609
+ content: [{ type: "text", text: readResult.text }],
9610
+ ...readResult.isError ? { isError: true } : {}
9611
+ };
9612
+ }
9399
9613
  var formatTasks = (data) => {
9400
9614
  if (!Array.isArray(data))
9401
9615
  return JSON.stringify(data, null, 2);
@@ -9551,8 +9765,9 @@ Actions: get, health, logs, api_keys, settings, tasks, task_detail, task_cancel,
9551
9765
  let text;
9552
9766
  switch (action) {
9553
9767
  case "get":
9554
- text = ok(await http.get(`/v1/projects/${resolvedRef}`));
9555
- break;
9768
+ return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
9769
+ maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
9770
+ }), resolvedRef));
9556
9771
  case "health":
9557
9772
  text = ok(await http.get(`/v1/projects/${resolvedRef}/health`));
9558
9773
  break;
@@ -11366,11 +11581,14 @@ var SCHEDULE_TOOL_SCHEMA = {
11366
11581
  var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
11367
11582
  var FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
11368
11583
  var OPERATION_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
11369
- var RESOURCE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
11584
+ var RESOURCE_KEY_PATTERN = /^v1\/(?:[a-z0-9][a-z0-9._-]{0,63})\/([A-Za-z0-9_-]{2,171})$/;
11585
+ var RESOURCE_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
11370
11586
  var FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
11371
11587
  var LEASE_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,254}$/;
11372
11588
  var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
11373
11589
  var MAX_STATUS_RESPONSE_BYTES = 196608;
11590
+ var MAX_RESOURCE_ID_BYTES = 128;
11591
+ var FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
11374
11592
  var MUTATION_STATUSES = new Set([
11375
11593
  "pending",
11376
11594
  "running",
@@ -11415,14 +11633,14 @@ function emptyProjection(candidate) {
11415
11633
  const record = objectRecord3(candidate);
11416
11634
  return record && Object.keys(record).length === 0 ? record : null;
11417
11635
  }
11418
- function canonicalTimestamp(candidate) {
11636
+ function canonicalTimestamp2(candidate) {
11419
11637
  if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
11420
11638
  return false;
11421
11639
  const milliseconds = Date.parse(candidate);
11422
11640
  return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
11423
11641
  }
11424
11642
  function nullableTimestamp(candidate) {
11425
- return candidate === null || canonicalTimestamp(candidate);
11643
+ return candidate === null || canonicalTimestamp2(candidate);
11426
11644
  }
11427
11645
  function safePrincipal(candidate) {
11428
11646
  const principal = exactRecord(candidate, PRINCIPAL_KEYS);
@@ -11471,12 +11689,32 @@ function validMutationLifecycle(mutation, receipt, responseStatus) {
11471
11689
  }
11472
11690
  return true;
11473
11691
  }
11692
+ function canonicalMutationResourceKey(candidate) {
11693
+ if (typeof candidate !== "string")
11694
+ return false;
11695
+ const match = RESOURCE_KEY_PATTERN.exec(candidate);
11696
+ if (!match)
11697
+ return false;
11698
+ const encodedResourceId = match[1];
11699
+ const resourceIdBytes = Buffer.from(encodedResourceId, "base64url");
11700
+ if (resourceIdBytes.byteLength < 1 || resourceIdBytes.byteLength > MAX_RESOURCE_ID_BYTES || resourceIdBytes.toString("base64url") !== encodedResourceId)
11701
+ return false;
11702
+ let resourceId;
11703
+ try {
11704
+ resourceId = FATAL_UTF8_DECODER.decode(resourceIdBytes);
11705
+ } catch (decodeError) {
11706
+ if (decodeError instanceof TypeError)
11707
+ return false;
11708
+ throw decodeError;
11709
+ }
11710
+ return resourceId.trim() === resourceId && !RESOURCE_ID_CONTROL_PATTERN.test(resourceId) && Buffer.from(resourceId, "utf8").equals(resourceIdBytes);
11711
+ }
11474
11712
  function validMutationIdentity(mutation) {
11475
11713
  if (!isMutationId(mutation.mutation_id) || typeof mutation.project_ref !== "string")
11476
11714
  return false;
11477
11715
  if (typeof mutation.operation !== "string" || !OPERATION_PATTERN.test(mutation.operation))
11478
11716
  return false;
11479
- if (mutation.resource_key !== null && (typeof mutation.resource_key !== "string" || !RESOURCE_KEY_PATTERN.test(mutation.resource_key)))
11717
+ if (mutation.resource_key !== null && !canonicalMutationResourceKey(mutation.resource_key))
11480
11718
  return false;
11481
11719
  return typeof mutation.request_fingerprint === "string" && FINGERPRINT_PATTERN.test(mutation.request_fingerprint);
11482
11720
  }
@@ -11485,7 +11723,7 @@ function validMutationTerminalFields(mutation) {
11485
11723
  return false;
11486
11724
  if (mutation.failure_code !== null && (typeof mutation.failure_code !== "string" || !FAILURE_CODE_PATTERN.test(mutation.failure_code)))
11487
11725
  return false;
11488
- return nullableTimestamp(mutation.completed_at) && canonicalTimestamp(mutation.created_at) && canonicalTimestamp(mutation.updated_at);
11726
+ return nullableTimestamp(mutation.completed_at) && canonicalTimestamp2(mutation.created_at) && canonicalTimestamp2(mutation.updated_at);
11489
11727
  }
11490
11728
  function safeMutationStatus(candidate) {
11491
11729
  const mutation = exactRecord(candidate, MUTATION_KEYS);
@@ -11557,6 +11795,12 @@ async function mutationStatus(http, args) {
11557
11795
  if (readback.kind === "invalid") {
11558
11796
  return releaseControlFailure("mutations.status", "INVALID_RESPONSE", null);
11559
11797
  }
11798
+ if (readback.mutation.status !== "succeeded") {
11799
+ return releaseControlFailure("mutations.status", "MUTATION_NOT_SUCCEEDED", null, {
11800
+ project_ref: ref,
11801
+ mutation: readback.mutation
11802
+ });
11803
+ }
11560
11804
  return releaseControlSuccess("mutations.status", { project_ref: ref, mutation: readback.mutation });
11561
11805
  }
11562
11806
  function registerMutationTools(server, http) {
@@ -11570,7 +11814,7 @@ var MUTATION_TOOL_SCHEMA = {
11570
11814
  // package.json
11571
11815
  var package_default = {
11572
11816
  name: "@supacloud/cli",
11573
- version: "0.19.0",
11817
+ version: "0.20.0",
11574
11818
  description: "Project-scoped CLI for SupaCloud users",
11575
11819
  type: "module",
11576
11820
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",