@supacloud/cli 0.98.0 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -659,12 +659,19 @@ FA-owned business features are documented in
659
659
 
660
660
  `deploy --target`, `--id`, `--build_command`, `--output_dir`, `--plan`, `--diff`
661
661
  and `--dry_run` are removed. Put build settings in `supacloud.json`; use
662
- `app deploy-plan` for read-only review. Historical project-specific manifests
663
- and standalone Function targets must move to application delivery configuration.
664
- The CLI rejects them rather than silently falling back to a partial Web deployment.
665
- For an intentional Web-only release in a mixed repository, use
666
- `supacloud --env test deploy --assets_only`. This preserves historical manifests,
667
- Functions, schedules and migrations without invoking application compilation.
662
+ `app deploy-plan` for read-only review. With a frontend configured, `supacloud deploy`
663
+ publishes only its static assets, even in a mixed repository. `--assets_only`
664
+ remains an explicit alias. This preserves historical manifests, Functions,
665
+ Storage, schedules and migrations without invoking application compilation.
666
+ Use `supacloud --env test deploy --backend` to publish the configured Application
667
+ independently; it never builds or uploads Web. Unsupported historical backend
668
+ manifests and standalone Function targets still fail closed on that Application path.
669
+
670
+ Application deployments return `accepted` after durable queue insertion, not a
671
+ successful activation. Inspect the receipt with the read-only
672
+ `supacloud --env test deploy --status <deployment-id>`. Queue delivery survives
673
+ restart; interrupted side effects require reconciliation rather than blind retry.
674
+
668
675
  Static publication reuses immutable archives, pre-upload CAS, mutation receipts
669
676
  and exact post-activation readback. It never retries a conflicting activation.
670
677
 
package/dist/index.js CHANGED
@@ -3177,23 +3177,22 @@ function InstanceKeys(value) {
3177
3177
  function IsIEEE754(value) {
3178
3178
  return typeof value === "number";
3179
3179
  }
3180
- var ByteMarker;
3181
- (function(ByteMarker) {
3182
- ByteMarker[ByteMarker["Array"] = 0] = "Array";
3183
- ByteMarker[ByteMarker["BigInt"] = 1] = "BigInt";
3184
- ByteMarker[ByteMarker["Boolean"] = 2] = "Boolean";
3185
- ByteMarker[ByteMarker["Date"] = 3] = "Date";
3186
- ByteMarker[ByteMarker["Constructor"] = 4] = "Constructor";
3187
- ByteMarker[ByteMarker["Function"] = 5] = "Function";
3188
- ByteMarker[ByteMarker["Null"] = 6] = "Null";
3189
- ByteMarker[ByteMarker["Number"] = 7] = "Number";
3190
- ByteMarker[ByteMarker["Object"] = 8] = "Object";
3191
- ByteMarker[ByteMarker["RegExp"] = 9] = "RegExp";
3192
- ByteMarker[ByteMarker["String"] = 10] = "String";
3193
- ByteMarker[ByteMarker["Symbol"] = 11] = "Symbol";
3194
- ByteMarker[ByteMarker["TypeArray"] = 12] = "TypeArray";
3195
- ByteMarker[ByteMarker["Undefined"] = 13] = "Undefined";
3196
- })(ByteMarker || (ByteMarker = {}));
3180
+ var ByteMarker = {
3181
+ Array: 0,
3182
+ BigInt: 1,
3183
+ Boolean: 2,
3184
+ Date: 3,
3185
+ Constructor: 4,
3186
+ Function: 5,
3187
+ Null: 6,
3188
+ Number: 7,
3189
+ Object: 8,
3190
+ RegExp: 9,
3191
+ String: 10,
3192
+ Symbol: 11,
3193
+ TypeArray: 12,
3194
+ Undefined: 13
3195
+ };
3197
3196
  var Accumulator = BigInt("14695981039346656037");
3198
3197
  var [Prime, Size] = [BigInt("1099511628211"), BigInt("18446744073709551616")];
3199
3198
  var Bytes = Array.from({ length: 256 }).map((_, i) => BigInt(i));
@@ -8786,7 +8785,7 @@ function outcomeUnknownGuidance(text) {
8786
8785
  }
8787
8786
 
8788
8787
  // src/shared/deployment-upgrade.ts
8789
- var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
8788
+ var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. With a frontend configured, deploy publishes only Web assets; use `supacloud deploy --backend` for the application backend. `supacloud deploy --assets_only` remains an explicit Web-only option. SupaCloud owns activation; do not retry with legacy commands.";
8790
8789
  var removedFrontendActions = new Set([
8791
8790
  "deploy_git",
8792
8791
  "deploy_upload",
@@ -9339,6 +9338,8 @@ function declaredMode(moduleName, action) {
9339
9338
  return;
9340
9339
  }
9341
9340
  function executionMode(moduleName, action, args) {
9341
+ if (moduleName === "deploy" && typeof args.status === "string")
9342
+ return "read";
9342
9343
  if (moduleName === "deploy" && (args.dry_run === true || args.plan === true || args.diff === true))
9343
9344
  return "read";
9344
9345
  if (moduleName === "database" && ["push_migrations", "baseline_migrations"].includes(action) && args.dry_run === true)
@@ -15194,7 +15195,7 @@ function scaffoldFiles(framework, slug) {
15194
15195
  "package.json": JSON.stringify({
15195
15196
  private: true,
15196
15197
  type: "module",
15197
- dependencies: { elysia: "2.0.0-beta.21" }
15198
+ dependencies: { elysia: "2.0.0-beta.32" }
15198
15199
  }, null, 2) + `
15199
15200
  `,
15200
15201
  "index.ts": `import { Elysia } from "elysia";
@@ -16763,7 +16764,7 @@ async function getFrontendRelease(http, projectRef, deploymentId, releaseId) {
16763
16764
  }
16764
16765
  return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
16765
16766
  }
16766
- async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath) {
16767
+ async function uploadFrontendReleaseWithReadback(http, projectRef, deploymentId, archivePath, verifyReadback) {
16767
16768
  const endpoint = releaseEndpoint(projectRef, deploymentId);
16768
16769
  const archive = await verifiedArchive(archivePath);
16769
16770
  let response;
@@ -16782,13 +16783,15 @@ async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath
16782
16783
  } finally {
16783
16784
  await archive.handle.close();
16784
16785
  }
16785
- const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
16786
+ const release = response.ok && (response.status === 200 || response.status === 201) ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
16786
16787
  if (!release) {
16787
16788
  if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
16788
16789
  return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
16789
16790
  }
16790
16791
  return uploadReadback(http, projectRef, deploymentId, archive.sha256, response.status);
16791
16792
  }
16793
+ if (!verifyReadback)
16794
+ return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
16792
16795
  const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
16793
16796
  const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef, deploymentId, releaseId: release.release_id }) : null;
16794
16797
  if (!verified || verified.tree_sha256 !== release.tree_sha256) {
@@ -16796,65 +16799,11 @@ async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath
16796
16799
  }
16797
16800
  return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release: verified });
16798
16801
  }
16799
- async function uploadProjectWebRelease(http, projectRef, environmentId, deploymentId, archivePath) {
16800
- const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/environments/${encodeURIComponent(environmentId)}/deployments/web-release?deployment_id=${encodeURIComponent(deploymentId)}`;
16801
- const archive = await verifiedArchive(archivePath);
16802
- const successPayload = (value) => {
16803
- const release = value ? releaseRecord(value.release) : null;
16804
- if (!value || value.ok !== true || value.project_ref !== projectRef || value.environment_id !== environmentId || value.deployment_id !== deploymentId || value.release_id !== archive.sha256 || release?.release_id !== archive.sha256 || release.tree_sha256.length !== 64)
16805
- return null;
16806
- return toolResponse({
16807
- project_ref: projectRef,
16808
- environment_id: environmentId,
16809
- deployment_id: deploymentId,
16810
- release_id: archive.sha256,
16811
- release
16812
- });
16813
- };
16814
- try {
16815
- const response = await http.postBinary(endpoint, {
16816
- stream: archiveStream(archive),
16817
- byteLength: archive.sizeBytes
16818
- }, {
16819
- contentType: FRONTEND_ARCHIVE_CONTENT_TYPE,
16820
- contentLength: archive.sizeBytes,
16821
- contentSha256: archive.sha256,
16822
- maxJsonBytes: RESPONSE_MAX_BYTES,
16823
- timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
16824
- responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
16825
- });
16826
- const value = response.data && typeof response.data === "object" && !Array.isArray(response.data) ? response.data : null;
16827
- if (response.ok && response.status === 201) {
16828
- const success = successPayload(value);
16829
- if (success)
16830
- return success;
16831
- }
16832
- if (response.transportError || response.responseReadError || response.status >= 500 || response.status === 202) {
16833
- const readback = await getFrontendRelease(http, projectRef, deploymentId, archive.sha256);
16834
- if (!readback.isError) {
16835
- const readbackValue = JSON.parse(readback.content[0]?.text ?? "{}");
16836
- const release = readbackValue.release;
16837
- if (release && typeof release === "object" && !Array.isArray(release) && release.release_id === archive.sha256) {
16838
- return toolResponse({
16839
- project_ref: projectRef,
16840
- environment_id: environmentId,
16841
- deployment_id: deploymentId,
16842
- release_id: archive.sha256,
16843
- release
16844
- });
16845
- }
16846
- }
16847
- }
16848
- return releaseFailure("frontend.upload_project_web_release", response.status >= 400 && response.status < 500 && !response.transportError ? "HTTP_ERROR" : "OUTCOME_UNKNOWN", response.status);
16849
- } finally {
16850
- await archive.handle.close();
16851
- }
16852
- }
16853
16802
  async function uploadReadback(http, projectRef, deploymentId, releaseId, uploadStatus) {
16854
16803
  const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
16855
16804
  maxJsonBytes: RESPONSE_MAX_BYTES
16856
16805
  });
16857
- const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
16806
+ const release = response.ok && response.status === 200 ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
16858
16807
  if (!release) {
16859
16808
  return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
16860
16809
  }
@@ -16913,13 +16862,9 @@ function activationReceipt(candidate, input) {
16913
16862
  return release;
16914
16863
  }
16915
16864
  async function activeReleaseReadback(http, identity) {
16916
- const snapshot = await readActiveFrontendRelease(http, identity.projectRef, identity.deploymentId);
16917
- if ("failure" in snapshot)
16918
- return { release: null, status: snapshot.status };
16919
- const inventory = snapshot.inventory;
16920
- if (inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
16921
- return { release: null, status: 200 };
16922
- }
16865
+ const snapshot = await activeReleaseSnapshotReadback(http, identity);
16866
+ if (!snapshot.release)
16867
+ return snapshot;
16923
16868
  const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
16924
16869
  const release = releaseRead.ok && releaseRead.status === 200 ? releaseEnvelope(releaseRead.data, {
16925
16870
  projectRef: identity.projectRef,
@@ -16928,6 +16873,16 @@ async function activeReleaseReadback(http, identity) {
16928
16873
  }) : null;
16929
16874
  return { release, status: releaseRead.status };
16930
16875
  }
16876
+ async function activeReleaseSnapshotReadback(http, identity) {
16877
+ const snapshot = await readActiveFrontendRelease(http, identity.projectRef, identity.deploymentId);
16878
+ if ("failure" in snapshot)
16879
+ return { release: null, status: snapshot.status };
16880
+ const inventory = snapshot.inventory;
16881
+ if (inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
16882
+ return { release: null, status: 200 };
16883
+ }
16884
+ return { release: inventory.releases[0] ?? null, status: 200 };
16885
+ }
16931
16886
  async function readActiveFrontendRelease(http, projectRef, deploymentId) {
16932
16887
  const response = await http.get(`${deploymentEndpoint(projectRef, deploymentId)}/active-release`, {
16933
16888
  maxJsonBytes: RESPONSE_MAX_BYTES
@@ -16962,7 +16917,7 @@ async function getActiveFrontendRelease(http, projectRef, deploymentId) {
16962
16917
  const snapshot = await readActiveFrontendRelease(http, projectRef, deploymentId);
16963
16918
  return "failure" in snapshot ? snapshot.failure : toolResponse(snapshot.inventory);
16964
16919
  }
16965
- async function publishFrontendRelease(http, projectRef, deploymentId, archivePath, expectedTreeSha256) {
16920
+ async function publishFrontendRelease(http, projectRef, deploymentId, archiveSource, expectedTreeSha256) {
16966
16921
  const authority = await readActiveFrontendRelease(http, projectRef, deploymentId);
16967
16922
  if ("failure" in authority)
16968
16923
  return authority.failure;
@@ -16979,7 +16934,14 @@ async function publishFrontendRelease(http, projectRef, deploymentId, archivePat
16979
16934
  unchanged: true
16980
16935
  });
16981
16936
  }
16982
- const uploaded = await uploadFrontendRelease(http, projectRef, deploymentId, archivePath);
16937
+ const archive = typeof archiveSource === "string" ? { archivePath: archiveSource } : await archiveSource();
16938
+ let uploaded;
16939
+ try {
16940
+ uploaded = await uploadFrontendReleaseWithReadback(http, projectRef, deploymentId, archive.archivePath, false);
16941
+ } finally {
16942
+ if ("cleanup" in archive)
16943
+ await archive.cleanup();
16944
+ }
16983
16945
  if (uploaded.isError)
16984
16946
  return uploaded;
16985
16947
  const value = JSON.parse(uploaded.content[0].text);
@@ -16990,14 +16952,14 @@ async function publishFrontendRelease(http, projectRef, deploymentId, archivePat
16990
16952
  return releaseFailure("frontend.deploy", "INVALID_RESPONSE", 200);
16991
16953
  }
16992
16954
  const mutationId = crypto.randomUUID();
16993
- const activated = await activateFrontendRelease(http, {
16955
+ const activated = await activateFrontendReleaseWithReadback(http, {
16994
16956
  projectRef,
16995
16957
  deploymentId,
16996
16958
  releaseId: release.release_id,
16997
16959
  mutationId,
16998
16960
  expectedActiveReleaseId: inventory.active_release_id ?? "absent",
16999
16961
  expectedActivationId: inventory.active_activation_id ?? "absent"
17000
- });
16962
+ }, activeReleaseSnapshotReadback);
17001
16963
  const activatedPayload = JSON.parse(activated.content[0].text);
17002
16964
  const active = activatedPayload.release;
17003
16965
  const treeMismatch = !activated.isError && (!active || typeof active !== "object" || !("tree_sha256" in active) || active.tree_sha256 !== expectedTreeSha256);
@@ -17176,6 +17138,9 @@ async function readPreviousFrontendRelease(http, projectRef, deploymentId) {
17176
17138
  };
17177
17139
  }
17178
17140
  async function activateFrontendRelease(http, input) {
17141
+ return activateFrontendReleaseWithReadback(http, input, activeReleaseReadback);
17142
+ }
17143
+ async function activateFrontendReleaseWithReadback(http, input, readActive) {
17179
17144
  const mutationId = input.mutationId;
17180
17145
  if (!MUTATION_ID_PATTERN2.test(mutationId))
17181
17146
  throw new Error("'mutation_id' must be a UUIDv4");
@@ -17201,7 +17166,7 @@ async function activateFrontendRelease(http, input) {
17201
17166
  }
17202
17167
  return activationReadback(http, input, mutationId, response.status);
17203
17168
  }
17204
- const readback = await activeReleaseReadback(http, { ...input, mutationId });
17169
+ const readback = await readActive(http, { ...input, mutationId });
17205
17170
  if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
17206
17171
  return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
17207
17172
  }
@@ -21423,34 +21388,34 @@ var starter_metadata_default = {
21423
21388
  generatedBy: "supacloud/starter-metadata-v1",
21424
21389
  packages: {
21425
21390
  compiler: {
21426
- version: "0.46.1"
21391
+ version: "0.46.3"
21427
21392
  },
21428
21393
  app: {
21429
- version: "0.27.0",
21394
+ version: "0.27.1",
21430
21395
  dependencies: {
21431
21396
  rxjs: "7.8.2"
21432
21397
  }
21433
21398
  },
21434
21399
  elysia: {
21435
- version: "0.30.0"
21400
+ version: "0.31.0"
21436
21401
  },
21437
21402
  commands: {
21438
- version: "0.9.2"
21403
+ version: "0.9.3"
21439
21404
  },
21440
21405
  contracts: {
21441
21406
  version: "0.9.0"
21442
21407
  },
21443
21408
  db: {
21444
- version: "0.17.0",
21409
+ version: "0.17.1",
21445
21410
  peerDependencies: {
21446
- "drizzle-orm": "1.0.0-rc.5-169397b"
21411
+ "drizzle-orm": "1.0.0-rc.5-5935859"
21447
21412
  },
21448
21413
  devDependencies: {
21449
- "drizzle-kit": "1.0.0-rc.5-ab785fc"
21414
+ "drizzle-kit": "1.0.0-rc.5-5935859"
21450
21415
  }
21451
21416
  },
21452
21417
  sdk: {
21453
- version: "0.42.0",
21418
+ version: "0.43.0",
21454
21419
  peerDependencies: {
21455
21420
  "@supabase/supabase-js": "^2.117.3"
21456
21421
  }
@@ -21793,8 +21758,8 @@ function baseFiles(name, sdkDependencies) {
21793
21758
  dependencies: {
21794
21759
  "@supacloud/app": `^${appMetadata.version}`,
21795
21760
  "@supacloud/elysia": `^${elysiaMetadata.version}`,
21796
- effect: "4.0.2",
21797
- elysia: "2.0.0-beta.21",
21761
+ effect: "4.0.5",
21762
+ elysia: "2.0.0-beta.32",
21798
21763
  ...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
21799
21764
  },
21800
21765
  devDependencies: {
@@ -22992,20 +22957,21 @@ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
22992
22957
  ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
22993
22958
  ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
22994
22959
  `;
22995
- var STARTER_REVIEW_POSTGRES = `import { AsyncLocalStorage } from "node:async_hooks";
22996
- import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
22960
+ var STARTER_REVIEW_POSTGRES = `import { plaintextCommandInput } from "@supacloud/commands";
22997
22961
  import { CommandError } from "@supacloud/contracts";
22998
22962
  import { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from "@supacloud/db";
22999
22963
  import {
23000
- ApplicationError, createSupAuthRequestContext, requireTrustedIdentity, requireIdempotencyKey,
22964
+ ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,
22965
+ createTransactionalCommandGovernance,
23001
22966
  type CommandInvocation, type SupAuthContextOptions,
23002
22967
  } from "@supacloud/elysia";
23003
22968
  import type { AppAdapters } from "../application";
23004
22969
  import type { ReviewStore } from "../review/review";
23005
22970
  import type { ReviewUploadPort } from "../review/uploads";
22971
+ import { ApprovalInput, ReviewResult } from "../review/contracts";
23006
22972
 
23007
- interface Approval { id: string; expectedVersion: number }
23008
- interface Review { state: "draft" | "approved"; version: number }
22973
+ const input = createSchemaDecoder(ApprovalInput, { normalize: false });
22974
+ const result = createSchemaDecoder(ReviewResult, { normalize: false });
23009
22975
 
23010
22976
  function isRecord(value: unknown): value is Record<string, unknown> {
23011
22977
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -23022,19 +22988,6 @@ function isPublicConflict(value: unknown): boolean {
23022
22988
  return value instanceof ApplicationError && value.status === 409
23023
22989
  || isRecord(value) && value["expose"] === true && value["status"] === 409 && typeof value["code"] === "string";
23024
22990
  }
23025
- function input(value: unknown): Approval {
23026
- const item = record(value);
23027
- if (typeof item["id"] !== "string" || !item["id"].length || typeof item["expectedVersion"] !== "number"
23028
- || !Number.isSafeInteger(item["expectedVersion"]) || item["expectedVersion"] < 1) throw new TypeError("Invalid approval input");
23029
- return { id: item["id"], expectedVersion: item["expectedVersion"] };
23030
- }
23031
- function result(value: unknown): Review {
23032
- const item = record(value);
23033
- if ((item["state"] !== "draft" && item["state"] !== "approved") || typeof item["version"] !== "number"
23034
- || !Number.isSafeInteger(item["version"]) || item["version"] < 1) throw new TypeError("Invalid review result");
23035
- return { state: item["state"], version: item["version"] };
23036
- }
23037
-
23038
22991
  export interface ReviewPostgresOptions {
23039
22992
  database: CommandDatabase;
23040
22993
  tenantId: string;
@@ -23072,12 +23025,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23072
23025
  } : null;
23073
23026
  },
23074
23027
  });
23075
- const scope = new AsyncLocalStorage<CommandTransaction>();
23076
- const transaction = () => {
23077
- const tx = scope.getStore();
23078
- if (!tx) throw new Error("Review storage requires its durable command transaction");
23079
- return tx;
23080
- };
23028
+ const transaction = () => commandBinding.transaction();
23081
23029
  const store: ReviewStore = {
23082
23030
  ...options.uploads,
23083
23031
  async get(table, id) {
@@ -23106,64 +23054,30 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23106
23054
  }
23107
23055
  return who.subject;
23108
23056
  }
23109
- const commandStore = createPostgresCommandStore(database);
23110
- return {
23111
- deps: { dbClient: store }, requestContext,
23112
- commandGovernance: {
23113
- authorize(invocation) {
23114
- const command = invocation.command;
23115
- if (command.name !== "review.approve" || command.permission !== "review.approve"
23116
- || command.transaction !== "required" || command.idempotency !== "required"
23117
- || command.audit !== "review.approved" || command.rpc !== undefined) {
23118
- throw new ApplicationError("Unsupported review command", { status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" });
23119
- }
23120
- actor(invocation);
23121
- },
23122
- async idempotency(invocation, next) {
23123
- const command = createTransactionalCommand({
23124
- name: invocation.command.name, store: commandStore,
23125
- inputCodec: plaintextCommandInput, input, result,
23126
- async authorize(who, request, tx) {
23127
- const found = rows(await tx.query(
23128
- "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23129
- "CROSS JOIN public.starter_application a " +
23130
- "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23131
- "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23132
- [request.id, who.actorId, projectId, who.tenantId],
23133
- ));
23134
- return found.length === 1 ? "allow" : "deny";
23135
- },
23136
- execute: tx => scope.run(tx, async () => {
23137
- try { return await next(); }
23138
- catch (error) {
23139
- if (isPublicConflict(error)) {
23140
- throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
23141
- }
23142
- throw error;
23143
- }
23144
- }),
23145
- audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23146
- });
23147
- try {
23148
- const receipt = await command.execute(
23149
- { actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),
23150
- { id: invocation.input.params["id"], expectedVersion: record(invocation.input.body)["expectedVersion"] },
23151
- invocation.request.signal,
23152
- );
23153
- if (receipt.status !== "confirmed") throw new CommandError("COMMAND_OUTCOME_UNKNOWN");
23154
- return receipt.result;
23155
- } catch (error) {
23156
- if (!(error instanceof CommandError)) throw error;
23157
- const status = error.code === "COMMAND_REJECTED" ? 403
23158
- : error.code === "COMMAND_IDEMPOTENCY_CONFLICT" ? 409 : 503;
23159
- throw new ApplicationError(error.code, { code: error.code, status });
23160
- }
23057
+ const commandBinding = createTransactionalCommandGovernance({
23058
+ permission: "review.approve",
23059
+ identity: invocation => ({ actorId: actor(invocation), tenantId }),
23060
+ input: invocation => ({
23061
+ ...record(invocation.input.body), id: invocation.input.params["id"],
23062
+ }),
23063
+ rejection: error => isPublicConflict(error) ? new CommandError("COMMAND_IDEMPOTENCY_CONFLICT") : undefined,
23064
+ definition: {
23065
+ name: "review.approve", store: createPostgresCommandStore(database),
23066
+ inputCodec: plaintextCommandInput, input, result,
23067
+ async authorize(who, request, tx) {
23068
+ const found = rows(await tx.query(
23069
+ "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23070
+ "CROSS JOIN public.starter_application a " +
23071
+ "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23072
+ "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23073
+ [request.id, who.actorId, projectId, who.tenantId],
23074
+ ));
23075
+ return found.length === 1 ? "allow" : "deny";
23161
23076
  },
23162
- transaction(_invocation, next) { transaction(); return next(); },
23163
- // The durable command owns the authoritative audit and receipt in the same transaction.
23164
- audit: { succeeded() { transaction(); }, failed() {} },
23077
+ audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23165
23078
  },
23166
- };
23079
+ });
23080
+ return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };
23167
23081
  }
23168
23082
  `;
23169
23083
  var STARTER_REVIEW_POSTGRES_TEST = `import { expect, test } from "bun:test";
@@ -23200,9 +23114,9 @@ test("PostgreSQL host never falls back to demo identity or unbound business stor
23200
23114
  await expect(adapters.requestContext(new Request("http://localhost/reviews/health")))
23201
23115
  .rejects.toMatchObject({ code: "AUTHENTICATION_REQUIRED" });
23202
23116
  const store = adapters.deps["dbClient"] as ReviewStore;
23203
- await expect(store.get("reviews", "example")).rejects.toThrow("durable command transaction");
23117
+ await expect(store.get("reviews", "example")).rejects.toThrow("not active");
23204
23118
  await expect(store.set("reviews", "example", { state: "approved", version: 2 }))
23205
- .rejects.toThrow("durable command transaction");
23119
+ .rejects.toThrow("not active");
23206
23120
  expect(db.queries).toHaveLength(1);
23207
23121
  });
23208
23122
  `;
@@ -23488,6 +23402,58 @@ export default defineConfig({
23488
23402
  };
23489
23403
  }
23490
23404
 
23405
+ // src/shared/tools/app-starter-query.ts
23406
+ var STARTER_REVIEW_QUERY = `import type { SupabaseClient } from "@supabase/supabase-js";
23407
+ import type { ReviewRow } from "../../db/schema";
23408
+
23409
+ // This read projection uses SQL column names shared with the Drizzle source.
23410
+ // Do not expose camelCase Drizzle property names as PostgREST column names.
23411
+ export type ReviewSummary = Pick<ReviewRow, "id" | "state" | "version">;
23412
+ export type ReviewReadDatabase = {
23413
+ public: {
23414
+ Tables: {
23415
+ starter_reviews: {
23416
+ Row: ReviewSummary;
23417
+ Insert: never;
23418
+ Update: never;
23419
+ Relationships: [];
23420
+ };
23421
+ };
23422
+ Views: Record<never, never>;
23423
+ Functions: Record<never, never>;
23424
+ };
23425
+ };
23426
+
23427
+ export interface ReviewPageOptions {
23428
+ page?: number;
23429
+ pageSize?: number;
23430
+ signal?: AbortSignal;
23431
+ }
23432
+
23433
+ // Reuse the application's user-scoped client. Identity verification and row
23434
+ // authorization belong to the gateway and database, never a caller-supplied owner.
23435
+ export async function listReviews(
23436
+ client: SupabaseClient<ReviewReadDatabase>,
23437
+ { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},
23438
+ ) {
23439
+ if (!Number.isSafeInteger(page) || page < 1
23440
+ || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100
23441
+ || !Number.isSafeInteger(page * pageSize)) {
23442
+ throw new RangeError("Invalid review page: page >= 1 and pageSize between 1 and 100 are required");
23443
+ }
23444
+ signal?.throwIfAborted();
23445
+ const start = (page - 1) * pageSize;
23446
+ const query = client.from("starter_reviews")
23447
+ .select("id,state,version", { count: "exact" })
23448
+ .order("id", { ascending: true })
23449
+ .range(start, start + pageSize - 1);
23450
+ const { data, error, count } = await (signal ? query.abortSignal(signal) : query);
23451
+ if (error) throw error;
23452
+ if (data === null || count === null) throw new Error("Incomplete review page response");
23453
+ return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };
23454
+ }
23455
+ `;
23456
+
23491
23457
  // src/shared/tools/app-starter.ts
23492
23458
  var { compiler: compilerMetadata2, app: appMetadata2, elysia: elysiaMetadata2, commands: commandsMetadata, contracts: contractsMetadata, db: dbMetadata, sdk: sdkMetadata } = starter_metadata_default.packages;
23493
23459
  var json2 = (value) => `${JSON.stringify(value, null, 2)}
@@ -23495,6 +23461,7 @@ var json2 = (value) => `${JSON.stringify(value, null, 2)}
23495
23461
  function appStarterFiles(name) {
23496
23462
  return {
23497
23463
  ...starterDatabaseFiles(),
23464
+ "src/review/list-reviews.ts": STARTER_REVIEW_QUERY,
23498
23465
  "package.json": json2({
23499
23466
  name,
23500
23467
  version: "0.0.0",
@@ -23529,8 +23496,8 @@ function appStarterFiles(name) {
23529
23496
  "drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
23530
23497
  "@supacloud/js": `^${sdkMetadata.version}`,
23531
23498
  "@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
23532
- effect: "4.0.2",
23533
- elysia: "2.0.0-beta.21",
23499
+ effect: "4.0.5",
23500
+ elysia: "2.0.0-beta.32",
23534
23501
  rxjs: appMetadata2.dependencies.rxjs
23535
23502
  },
23536
23503
  devDependencies: {
@@ -23784,11 +23751,10 @@ import {
23784
23751
  Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
23785
23752
  defineFeatureSlice, defineFeatureSpec, type Aspect,
23786
23753
  } from "@supacloud/app";
23787
- import { ApplicationError, assertFeatureTransition } from "@supacloud/elysia";
23788
- import { ApproveBody, HealthResult, Params, ReviewResult } from "./contracts";
23754
+ import { ApplicationError, assertFeatureTransition, createSchemaDecoder } from "@supacloud/elysia";
23755
+ import { ApproveBody, HealthResult, Params, ReviewResult, type ApprovalBody, type Review } from "./contracts";
23789
23756
  import { VerifyReviewAttachment } from "./attachment";
23790
23757
  import { ReviewUploads, ReviewUploadsController, type ReviewUploadPort } from "./uploads";
23791
- import type { ReviewRow } from "../../db/schema";
23792
23758
 
23793
23759
  export const reviewSpec = defineFeatureSpec({
23794
23760
  name: "review",
@@ -23803,7 +23769,6 @@ export const reviewSpec = defineFeatureSpec({
23803
23769
  },
23804
23770
  });
23805
23771
 
23806
- type Review = Pick<ReviewRow, "state" | "version">;
23807
23772
  // Production implementations must bind both operations to the current transaction.
23808
23773
  export interface ReviewStore extends Partial<ReviewUploadPort> {
23809
23774
  get(table: string, key: string): unknown;
@@ -23816,13 +23781,12 @@ export const requireRequest: Aspect = (context, next) => {
23816
23781
  return next();
23817
23782
  };
23818
23783
 
23784
+ const decodeReview = createSchemaDecoder(ReviewResult, { normalize: false });
23819
23785
  function readReview(value: unknown): Review {
23820
- if (typeof value !== "object" || value === null ||
23821
- !("state" in value) || (value.state !== "draft" && value.state !== "approved") ||
23822
- !("version" in value) || typeof value.version !== "number") {
23786
+ try { return decodeReview(value); }
23787
+ catch {
23823
23788
  throw new ApplicationError("Review not found", { status: 404, code: "REVIEW_NOT_FOUND" });
23824
23789
  }
23825
- return { state: value.state, version: value.version };
23826
23790
  }
23827
23791
 
23828
23792
  @Command({
@@ -23854,7 +23818,7 @@ type ReviewError =
23854
23818
  | { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
23855
23819
  | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
23856
23820
  | { readonly _tag: "COMMAND_REJECTED" }
23857
- | { readonly _tag: "REVIEW_COMMAND_UNSUPPORTED" }
23821
+ | { readonly _tag: "COMMAND_BINDING_MISMATCH" }
23858
23822
  | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" };
23859
23823
 
23860
23824
  @Controller("/reviews")
@@ -23883,13 +23847,13 @@ export class ReviewController {
23883
23847
  { tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
23884
23848
  { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
23885
23849
  { tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
23886
- { tag: "REVIEW_COMMAND_UNSUPPORTED", status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" },
23850
+ { tag: "COMMAND_BINDING_MISMATCH", status: 501, code: "COMMAND_BINDING_MISMATCH" },
23887
23851
  { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
23888
23852
  ],
23889
23853
  retry: "none",
23890
23854
  },
23891
23855
  })
23892
- approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Effect.Effect<Review, ReviewError, never> {
23856
+ approve(@Param("id") id: string, @Body() body: ApprovalBody): Effect.Effect<Review, ReviewError, never> {
23893
23857
  return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
23894
23858
  }
23895
23859
  }
@@ -23901,13 +23865,16 @@ export const ReviewFeature = defineFeatureSlice({
23901
23865
  });
23902
23866
  `,
23903
23867
  "src/review/attachment.ts": STARTER_REVIEW_JOB,
23904
- "src/review/contracts.ts": `import { t } from "elysia";
23868
+ "src/review/contracts.ts": `import { t, type UnwrapSchema } from "elysia";
23905
23869
  export const Params = t.Object({ id: t.String({ minLength: 1 }) });
23906
- export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1 }) });
23870
+ export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1, maximum: 9007199254740991 }) });
23871
+ export const ApprovalInput = t.Object({ ...Params.properties, ...ApproveBody.properties }, { additionalProperties: false });
23872
+ export type ApprovalBody = UnwrapSchema<typeof ApproveBody>;
23907
23873
  export const ReviewResult = t.Object({
23908
23874
  state: t.Union([t.Literal("draft"), t.Literal("approved")]),
23909
- version: t.Integer({ minimum: 1 }),
23875
+ version: t.Integer({ minimum: 1, maximum: 9007199254740991 }),
23910
23876
  });
23877
+ export type Review = UnwrapSchema<typeof ReviewResult>;
23911
23878
  export const HealthResult = t.Object({ ok: t.Boolean() });
23912
23879
  export const UploadParams = t.Object({ id: t.String({ format: "uuid" }) });
23913
23880
  export const UploadBody = t.Object({
@@ -24049,6 +24016,18 @@ Ordinary reads use PostgREST through the existing user-scoped
24049
24016
  Use the caller's session, RLS and grants; never introduce a second session store.
24050
24017
  Business writes continue through the governed Command/RPC boundary.
24051
24018
 
24019
+ Use listReviews from src/review/list-reviews.ts for the paginated review list.
24020
+ Type the existing client with ReviewReadDatabase (a read-only projection derived
24021
+ from db/schema.ts), then call listReviews(client, { page: 1, pageSize: 25, signal }).
24022
+ The helper returns data, total, page, pageSize and hasNextPage, orders by the unique
24023
+ id and preserves PostgREST errors. Pages are one-based and limited to 100 rows.
24024
+ Exact counts obey RLS but may be expensive on large tables; offset pages are not
24025
+ a cross-request snapshot when records change. No owner filter replaces RLS.
24026
+ Do not create a service-role client or a second auth session for this helper.
24027
+ Existing starters can add this file without a new migration only if their reviewed
24028
+ review-table grants and identity-mapping RLS from migration 003 are already applied.
24029
+ The projection is not a full database export and grants no write access.
24030
+
24052
24031
  GraphQL is not generated or enabled by this starter. For a specific Query Contract,
24053
24032
  explicitly configure graphql.schema with a role-scoped database export and add
24054
24033
  checked query documents. Drizzle and append-only migrations remain the database
@@ -26359,6 +26338,7 @@ async function runAppRelease(request, options, delegate) {
26359
26338
  schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
26360
26339
  ok: true,
26361
26340
  operation: "applications.release",
26341
+ ...deployReceipt.status === "accepted" ? { status: "accepted" } : {},
26362
26342
  project_ref: ref,
26363
26343
  application_id: id,
26364
26344
  environment_id: environmentId,
@@ -26369,16 +26349,17 @@ async function runAppRelease(request, options, delegate) {
26369
26349
  build: buildReceipt ? completed.build : { skipped: true, manifest_path: manifestPath },
26370
26350
  phases: completed,
26371
26351
  no_op: deployReceipt.action === "no-op" || deployReceipt.no_op === true,
26372
- execution_performed: deployReceipt.execution_performed === true,
26352
+ ...deployReceipt.status === "accepted" ? {} : { execution_performed: deployReceipt.execution_performed === true },
26373
26353
  ...typeof deployReceipt.activation_id === "string" ? { activation_id: deployReceipt.activation_id } : {},
26374
26354
  ...deployReceipt.web === undefined ? {} : { web: deployReceipt.web }
26375
26355
  };
26376
26356
  return request.format === "json" || request.json === true ? textResult(JSON.stringify(result, null, 2)) : textResult([
26377
- `${ref}/${id}/${environmentId}: ${result.no_op ? "no-op" : "released"}`,
26357
+ `${ref}/${id}/${environmentId}: ${deployReceipt.status === "accepted" ? "accepted" : result.no_op ? "no-op" : "released"}`,
26378
26358
  `Release: ${releaseId}`,
26379
26359
  `Configuration: ${configurationId}`,
26380
26360
  `Activation: ${result.activation_id ?? "not performed"}`,
26381
26361
  ...result.deployment_id === undefined ? [] : [`Deployment: ${result.deployment_id}`],
26362
+ ...deployReceipt.status === "accepted" && result.deployment_id !== undefined ? [`Status: supacloud deploy --status ${result.deployment_id}`] : [],
26382
26363
  `Build: ${request.skip_build ? "skipped" : "completed"}`
26383
26364
  ].join(`
26384
26365
  `));
@@ -29057,11 +29038,22 @@ async function requireProjectDeploymentProtocol(http, projectRef, environmentId)
29057
29038
  throw new Error("DEPLOYMENT_PROTOCOL_MISMATCH: Upgrade the SupaCloud server and CLI together (`npm install -g @supacloud/cli@latest`), then retry `supacloud deploy`. No upload or deployment was attempted; legacy endpoints will not be used.");
29058
29039
  }
29059
29040
  }
29041
+ var operation2 = "applications.project_deploy";
29060
29042
  var scopeSchema = _Object_({
29061
29043
  project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
29062
29044
  application_id: ApplicationIdSchema,
29063
29045
  environment_id: ApplicationIdSchema
29064
29046
  });
29047
+ var acceptedSchema = _Object_({
29048
+ schema: Literal("supacloud.project-deployment-accepted.v1"),
29049
+ ok: Literal(true),
29050
+ status: Literal("accepted"),
29051
+ deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
29052
+ ...scopeSchema.properties,
29053
+ release_id: ApplicationDeployRequestSchema.properties.release_id,
29054
+ request_sha256: String2({ pattern: "^[a-f0-9]{64}$" }),
29055
+ replayed: Boolean2()
29056
+ }, { additionalProperties: false });
29065
29057
  var statusSchema = _Object_({
29066
29058
  schema: Literal("supacloud.project-deployment-status.v1"),
29067
29059
  deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
@@ -29086,6 +29078,27 @@ var statusSchema = _Object_({
29086
29078
  receipt: Optional(Unknown()),
29087
29079
  failure_code: Optional(String2({ pattern: "^[A-Z][A-Z0-9_]{0,63}$" }))
29088
29080
  }, { additionalProperties: false });
29081
+ async function readProjectDeploymentStatus(http, projectRef, environmentId, deploymentId) {
29082
+ if (!/^[a-z0-9-]{1,20}$/.test(projectRef) || !Check2(ApplicationIdSchema, environmentId) || !Check2(ApplicationDeployRequestSchema.properties.mutation_id, deploymentId)) {
29083
+ return releaseControlFailure(operation2, "INVALID_RESPONSE", null);
29084
+ }
29085
+ const path = `/v1/projects/${encodeURIComponent(projectRef)}/environments/${encodeURIComponent(environmentId)}/deployments/${deploymentId}`;
29086
+ const response = await http.get(path, { retry: false, totalTimeoutMs: 1e4, maxJsonBytes: 131072 });
29087
+ try {
29088
+ const state = response.data;
29089
+ if (!response.ok || response.status !== 200 || !Check2(statusSchema, state) || state.project_ref !== projectRef || state.environment_id !== environmentId || state.deployment_id !== deploymentId)
29090
+ throw new Error;
29091
+ if (state.status === "succeeded") {
29092
+ const receipt = parseApplicationDeployResult(state.receipt);
29093
+ if (state.phase !== "verifying" || receipt.project_ref !== projectRef || receipt.environment_id !== environmentId || receipt.application_id !== state.application_id || receipt.mutation_id !== deploymentId)
29094
+ throw new Error;
29095
+ } else if (state.receipt !== undefined)
29096
+ throw new Error;
29097
+ return state.status === "outcome_unknown" || state.status === "failed_terminal" ? releaseControlFailure(operation2, state.status === "outcome_unknown" ? "OUTCOME_UNKNOWN" : "MUTATION_NOT_SUCCEEDED", response.status, { ...state }) : releaseControlSuccess(operation2, { ...state });
29098
+ } catch {
29099
+ return releaseControlFailure(operation2, "INVALID_RESPONSE", response.status);
29100
+ }
29101
+ }
29089
29102
 
29090
29103
  // src/shared/tools/deploy-tools.ts
29091
29104
  var MAX_SOURCE_BYTES = 256 * 1024 * 1024;
@@ -29102,6 +29115,8 @@ var deployToolSchema = {
29102
29115
  cwd: optional(String2(), "Project directory (default: current directory)"),
29103
29116
  skip_build: optional(Boolean2(), "Use an existing output directory without building"),
29104
29117
  assets_only: optional(Boolean2(), "Publish only configured static assets; leave backend Functions, schedules and migrations unchanged"),
29118
+ backend: optional(Boolean2(), "Publish the application backend independently; do not build or activate frontend assets"),
29119
+ status: optional(String2({ format: "uuid" }), "Read a backend deployment status without publishing"),
29105
29120
  json: optional(Boolean2(), "Print only the final JSON result")
29106
29121
  };
29107
29122
  function record4(candidate) {
@@ -29477,8 +29492,7 @@ async function collectArchiveFiles(root) {
29477
29492
  }
29478
29493
  return { files, bytes, count: paths.length, treeSha256: tree.digest("hex") };
29479
29494
  }
29480
- async function createFrontendArchive(outputDirectory) {
29481
- const collected = await collectArchiveFiles(outputDirectory);
29495
+ async function createCollectedFrontendArchive(outputDirectory, collected) {
29482
29496
  const temporaryDirectory = await mkdtemp2(join18(tmpdir3(), "supacloud-deploy-"));
29483
29497
  const archivePath = join18(temporaryDirectory, `${basename8(outputDirectory)}.tar.zst`);
29484
29498
  try {
@@ -29499,16 +29513,7 @@ async function createFrontendArchive(outputDirectory) {
29499
29513
  throw error;
29500
29514
  }
29501
29515
  }
29502
- function payload(response) {
29503
- if (response.isError)
29504
- throw new Error(response.content[0]?.text || "Deployment operation failed");
29505
- const text = response.content.find((chunk) => chunk.type === "text")?.text;
29506
- const parsed = text ? record4(JSON.parse(text)) : null;
29507
- if (!parsed)
29508
- throw new Error("Deployment operation returned an invalid response");
29509
- return parsed;
29510
- }
29511
- async function stageApplicationWebRelease(http, projectRef, environmentId, configRoot, skipBuild, json, environmentVariables, assetsOnly = false) {
29516
+ async function publishConfiguredFrontend(http, projectRef, configRoot, skipBuild, json, environmentVariables) {
29512
29517
  const target = selectFrontendTarget(configRoot, await readDeployConfig(configRoot));
29513
29518
  if (!target)
29514
29519
  return null;
@@ -29532,27 +29537,8 @@ async function stageApplicationWebRelease(http, projectRef, environmentId, confi
29532
29537
  if (!skipBuild) {
29533
29538
  await runBuild(buildCommand, projectDirectory, build.env, json);
29534
29539
  }
29535
- const archive = await createFrontendArchive(outputDirectory);
29536
- try {
29537
- if (assetsOnly) {
29538
- return { result: await publishFrontendRelease(http, projectRef, selected.id, archive.archivePath, archive.treeSha256) };
29539
- }
29540
- const uploaded = payload(await uploadProjectWebRelease(http, projectRef, environmentId, selected.id, archive.archivePath));
29541
- const release = record4(uploaded.release);
29542
- const releaseId = requiredString(release?.release_id, "release.release_id");
29543
- if (releaseId !== archive.sha256)
29544
- throw new Error("Uploaded Web release identity does not match the local archive");
29545
- if (release?.tree_sha256 !== archive.treeSha256)
29546
- throw new Error("Uploaded Web release tree does not match the local build");
29547
- return {
29548
- request: {
29549
- deployment_id: selected.id,
29550
- release_id: releaseId
29551
- }
29552
- };
29553
- } finally {
29554
- await archive.cleanup();
29555
- }
29540
+ const collected = await collectArchiveFiles(outputDirectory);
29541
+ return publishFrontendRelease(http, projectRef, selected.id, () => createCollectedFrontendArchive(outputDirectory, collected), collected.treeSha256);
29556
29542
  }
29557
29543
  function registerDeployTools(server, http, options = {}) {
29558
29544
  registerTool(server, "deploy", "Build and publish the linked project with one command", deployToolSchema, async (args) => {
@@ -29560,6 +29546,16 @@ function registerDeployTools(server, http, options = {}) {
29560
29546
  const projectRef = String(args.ref || options.projectRef || "").trim();
29561
29547
  if (!projectRef)
29562
29548
  throw new Error("A linked project ref is required. Set SUPACLOUD_PROJECT_REF or pass --ref");
29549
+ if (args.assets_only === true && args.backend === true)
29550
+ throw new Error("Choose either frontend assets or backend deployment");
29551
+ if (typeof args.status === "string") {
29552
+ if (args.assets_only === true || args.backend === true)
29553
+ throw new Error("Status cannot be combined with publishing options");
29554
+ const environmentId = options.environment?.trim() || options.application?.environmentId?.trim();
29555
+ if (!environmentId)
29556
+ throw new Error("Deployment status requires SUPACLOUD_ENV");
29557
+ return readProjectDeploymentStatus(http, projectRef, environmentId, args.status);
29558
+ }
29563
29559
  const configRoot = await findDeployConfigRoot(invocationDirectory, options.environment ?? undefined);
29564
29560
  const config = await readDeployConfig(configRoot);
29565
29561
  const application = options.application?.id ? options.application : config.application ? {
@@ -29567,7 +29563,7 @@ function registerDeployTools(server, http, options = {}) {
29567
29563
  environmentId: options.environment || config.application.environmentId
29568
29564
  } : undefined;
29569
29565
  const legacyManifest = await hasProjectReleaseManifest(configRoot);
29570
- const assetsOnly = args.assets_only === true || !application?.id && selectFrontendTarget(configRoot, config) !== null && !legacyManifest && !Object.values(config.targets ?? {}).some((target) => target.type === "edge_function");
29566
+ const assetsOnly = args.assets_only === true || args.backend !== true && selectFrontendTarget(configRoot, config) !== null;
29571
29567
  if (assetsOnly) {
29572
29568
  if (!selectFrontendTarget(configRoot, config)) {
29573
29569
  throw new Error("Static asset deployment requires a frontend configuration in supacloud.json");
@@ -29577,10 +29573,10 @@ function registerDeployTools(server, http, options = {}) {
29577
29573
  throw new Error("Static asset deployment requires SUPACLOUD_ENV");
29578
29574
  }
29579
29575
  await requireProjectDeploymentProtocol(http, projectRef, environmentId);
29580
- const staged = await stageApplicationWebRelease(http, projectRef, environmentId, configRoot, args.skip_build === true, args.json === true, options.environmentVariables, true);
29581
- if (!staged?.result)
29576
+ const result = await publishConfiguredFrontend(http, projectRef, configRoot, args.skip_build === true, args.json === true, options.environmentVariables);
29577
+ if (!result)
29582
29578
  throw new Error("Static asset deployment did not produce a receipt");
29583
- return staged.result;
29579
+ return result;
29584
29580
  }
29585
29581
  if (legacyManifest) {
29586
29582
  throw new Error(`${DEPLOYMENT_UPGRADE} Replace deploy-manifest.json with the current application delivery configuration; no remote mutation was performed.`);
@@ -29602,7 +29598,6 @@ function registerDeployTools(server, http, options = {}) {
29602
29598
  throw new Error("Application profile must set only one of SUPACLOUD_APPLICATION_CONFIGURATION_PATH or SUPACLOUD_APPLICATION_CONFIGURATION_ID");
29603
29599
  }
29604
29600
  await requireProjectDeploymentProtocol(http, projectRef, environmentId);
29605
- const stagedWeb = await stageApplicationWebRelease(http, projectRef, environmentId, configRoot, args.skip_build === true, args.json === true, options.environmentVariables);
29606
29601
  const result = await options.applicationRelease({
29607
29602
  action: "release",
29608
29603
  ref: projectRef,
@@ -29613,7 +29608,6 @@ function registerDeployTools(server, http, options = {}) {
29613
29608
  ...configurationId ? { configuration_id: configurationId } : {},
29614
29609
  ...application.manifestPath ? { manifest_path: application.manifestPath } : {},
29615
29610
  ...args.skip_build === true ? { skip_build: true } : {},
29616
- ...stagedWeb?.request === undefined ? {} : { web_release: stagedWeb.request },
29617
29611
  ...args.json === true ? { format: "json", json: true } : {}
29618
29612
  });
29619
29613
  return result;
@@ -30408,12 +30402,22 @@ function registerDeveloperExperienceTools(server, options = {}) {
30408
30402
  }
30409
30403
 
30410
30404
  // src/shared/tools/project-deployment.ts
30411
- var operation2 = "applications.project_deploy";
30405
+ var operation3 = "applications.project_deploy";
30412
30406
  var scopeSchema2 = _Object_({
30413
30407
  project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
30414
30408
  application_id: ApplicationIdSchema,
30415
30409
  environment_id: ApplicationIdSchema
30416
30410
  });
30411
+ var acceptedSchema2 = _Object_({
30412
+ schema: Literal("supacloud.project-deployment-accepted.v1"),
30413
+ ok: Literal(true),
30414
+ status: Literal("accepted"),
30415
+ deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
30416
+ ...scopeSchema2.properties,
30417
+ release_id: ApplicationDeployRequestSchema.properties.release_id,
30418
+ request_sha256: String2({ pattern: "^[a-f0-9]{64}$" }),
30419
+ replayed: Boolean2()
30420
+ }, { additionalProperties: false });
30417
30421
  var statusSchema2 = _Object_({
30418
30422
  schema: Literal("supacloud.project-deployment-status.v1"),
30419
30423
  deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
@@ -30457,7 +30461,7 @@ async function runProjectDeployment(http, args) {
30457
30461
  ...args.web_release === undefined ? {} : { web_release: args.web_release }
30458
30462
  };
30459
30463
  if (!Check2(scopeSchema2, scope) || !Check2(ApplicationDeployRequestSchema, request) || request.configuration_id !== undefined && request.configuration_write !== undefined && request.configuration_id !== request.configuration_write.configuration_id) {
30460
- return releaseControlFailure(operation2, "INVALID_RESPONSE", null);
30464
+ return releaseControlFailure(operation3, "INVALID_RESPONSE", null);
30461
30465
  }
30462
30466
  const expected = { ...scope, request };
30463
30467
  const identity = {
@@ -30473,15 +30477,29 @@ async function runProjectDeployment(http, args) {
30473
30477
  deployment_id: mutation_id,
30474
30478
  application_id: scope.application_id
30475
30479
  }, { timeoutMs: 120000, maxJsonBytes: 131072, responseTimeoutMs: 30000 });
30476
- if (result.ok && result.status === 200) {
30480
+ if (result.ok && result.status === 202) {
30481
+ const accepted = result.data;
30482
+ if (Check2(acceptedSchema2, accepted) && accepted.deployment_id === mutation_id && accepted.project_ref === scope.project_ref && accepted.application_id === scope.application_id && accepted.environment_id === scope.environment_id && accepted.release_id === request.release_id && accepted.request_sha256 === mutationRequestFingerprint(request)) {
30483
+ return releaseControlSuccess(operation3, {
30484
+ status: "accepted",
30485
+ deployment_id: mutation_id,
30486
+ project_ref: scope.project_ref,
30487
+ application_id: scope.application_id,
30488
+ environment_id: scope.environment_id,
30489
+ release_id: request.release_id,
30490
+ replayed: accepted.replayed,
30491
+ status_path: `${endpoint}/${mutation_id}`
30492
+ });
30493
+ }
30494
+ } else if (result.ok && result.status === 200) {
30477
30495
  try {
30478
- return releaseControlSuccess(operation2, {
30496
+ return releaseControlSuccess(operation3, {
30479
30497
  ...responseReceipt(result.data, expected),
30480
30498
  deployment_id: mutation_id
30481
30499
  });
30482
30500
  } catch {}
30483
30501
  } else if (!result.ok && !result.transportError && !result.responseReadError && result.status < 500 && result.status !== 408) {
30484
- return releaseControlMutationFailure(operation2, result, identity);
30502
+ return releaseControlMutationFailure(operation3, result, identity);
30485
30503
  }
30486
30504
  const observed = await http.get(`${endpoint}/${encodeURIComponent(mutation_id)}`, {
30487
30505
  retry: false,
@@ -30495,20 +30513,20 @@ async function runProjectDeployment(http, args) {
30495
30513
  if (state.deployment_id !== mutation_id || state.project_ref !== scope.project_ref || state.application_id !== scope.application_id || state.environment_id !== scope.environment_id || state.status !== "succeeded" || state.phase !== "verifying")
30496
30514
  throw new Error;
30497
30515
  const receipt = parseApplicationDeployResult(state.receipt, expected);
30498
- return releaseControlSuccess(operation2, {
30516
+ return releaseControlSuccess(operation3, {
30499
30517
  ...receipt,
30500
30518
  deployment_id: mutation_id,
30501
30519
  replayed: true,
30502
30520
  execution_performed: false
30503
30521
  });
30504
30522
  } catch {
30505
- return releaseControlFailure(operation2, "OUTCOME_UNKNOWN", result.transportError ? null : result.status, identity);
30523
+ return releaseControlFailure(operation3, "OUTCOME_UNKNOWN", result.transportError ? null : result.status, identity);
30506
30524
  }
30507
30525
  }
30508
30526
  // package.json
30509
30527
  var package_default = {
30510
30528
  name: "@supacloud/cli",
30511
- version: "0.98.0",
30529
+ version: "0.101.0",
30512
30530
  description: "Project-scoped CLI for SupaCloud users",
30513
30531
  type: "module",
30514
30532
  main: "./dist/index.js",
@@ -30560,21 +30578,21 @@ var package_default = {
30560
30578
  directory: "packages/cli"
30561
30579
  },
30562
30580
  dependencies: {
30563
- "@supacloud/compiler": "^0.46.1",
30564
- "@supacloud/delivery": "0.18.0",
30565
- "@supacloud/db": "0.17.0",
30566
- typebox: "1.3.36"
30581
+ "@supacloud/compiler": "^0.46.3",
30582
+ "@supacloud/delivery": "0.18.1",
30583
+ "@supacloud/db": "0.17.1",
30584
+ typebox: "1.3.37"
30567
30585
  },
30568
30586
  overrides: {
30569
30587
  "@supacloud/contracts": "0.9.0",
30570
- "@supacloud/delivery": "0.18.0",
30571
- "@graphql-tools/utils": "^12.0.3"
30588
+ "@supacloud/delivery": "0.18.1",
30589
+ "@graphql-tools/utils": "^12.0.4"
30572
30590
  },
30573
30591
  devDependencies: {
30574
- "@supacloud/elysia": "^0.24.1",
30592
+ "@supacloud/elysia": "0.31.0",
30575
30593
  "@types/bun": "^1.4.3",
30576
- effect: "4.0.2",
30577
- elysia: "2.0.0-beta.21",
30594
+ effect: "4.0.5",
30595
+ elysia: "2.0.0-beta.32",
30578
30596
  typescript: "^7.0.2"
30579
30597
  }
30580
30598
  };
@@ -1,2 +1,2 @@
1
- export declare const DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
1
+ export declare const DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. With a frontend configured, deploy publishes only Web assets; use `supacloud deploy --backend` for the application backend. `supacloud deploy --assets_only` remains an explicit Web-only option. SupaCloud owns activation; do not retry with legacy commands.";
2
2
  export declare function assertCurrentDeploymentInput(tool: string, input: unknown): void;
@@ -1,4 +1,4 @@
1
1
  export declare const STARTER_REVIEW_SCHEMA = "-- Apply with the platform migration owner, never from the HTTP process.\n-- One application database belongs to one explicitly provisioned project/tenant.\nCREATE TABLE public.starter_application (\n singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),\n project_id text NOT NULL CHECK (length(project_id) > 0),\n tenant_id text NOT NULL CHECK (length(tenant_id) > 0)\n);\nCREATE TABLE public.starter_reviews (\n id text PRIMARY KEY, owner_id text NOT NULL,\n state text NOT NULL CHECK (state IN ('draft','approved')),\n version integer NOT NULL CHECK (version > 0)\n);\nCREATE TABLE public.starter_members (\n subject text PRIMARY KEY, enabled boolean NOT NULL DEFAULT true,\n can_approve boolean NOT NULL DEFAULT false\n);\nALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;\n";
2
- export declare const STARTER_REVIEW_POSTGRES = "import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { createTransactionalCommand, plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSupAuthRequestContext, requireTrustedIdentity, requireIdempotencyKey,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\n\ninterface Approval { id: string; expectedVersion: number }\ninterface Review { state: \"draft\" | \"approved\"; version: number }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nfunction input(value: unknown): Approval {\n const item = record(value);\n if (typeof item[\"id\"] !== \"string\" || !item[\"id\"].length || typeof item[\"expectedVersion\"] !== \"number\"\n || !Number.isSafeInteger(item[\"expectedVersion\"]) || item[\"expectedVersion\"] < 1) throw new TypeError(\"Invalid approval input\");\n return { id: item[\"id\"], expectedVersion: item[\"expectedVersion\"] };\n}\nfunction result(value: unknown): Review {\n const item = record(value);\n if ((item[\"state\"] !== \"draft\" && item[\"state\"] !== \"approved\") || typeof item[\"version\"] !== \"number\"\n || !Number.isSafeInteger(item[\"version\"]) || item[\"version\"] < 1) throw new TypeError(\"Invalid review result\");\n return { state: item[\"state\"], version: item[\"version\"] };\n}\n\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const scope = new AsyncLocalStorage<CommandTransaction>();\n const transaction = () => {\n const tx = scope.getStore();\n if (!tx) throw new Error(\"Review storage requires its durable command transaction\");\n return tx;\n };\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandStore = createPostgresCommandStore(database);\n return {\n deps: { dbClient: store }, requestContext,\n commandGovernance: {\n authorize(invocation) {\n const command = invocation.command;\n if (command.name !== \"review.approve\" || command.permission !== \"review.approve\"\n || command.transaction !== \"required\" || command.idempotency !== \"required\"\n || command.audit !== \"review.approved\" || command.rpc !== undefined) {\n throw new ApplicationError(\"Unsupported review command\", { status: 501, code: \"REVIEW_COMMAND_UNSUPPORTED\" });\n }\n actor(invocation);\n },\n async idempotency(invocation, next) {\n const command = createTransactionalCommand({\n name: invocation.command.name, store: commandStore,\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n execute: tx => scope.run(tx, async () => {\n try { return await next(); }\n catch (error) {\n if (isPublicConflict(error)) {\n throw new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\");\n }\n throw error;\n }\n }),\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n });\n try {\n const receipt = await command.execute(\n { actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),\n { id: invocation.input.params[\"id\"], expectedVersion: record(invocation.input.body)[\"expectedVersion\"] },\n invocation.request.signal,\n );\n if (receipt.status !== \"confirmed\") throw new CommandError(\"COMMAND_OUTCOME_UNKNOWN\");\n return receipt.result;\n } catch (error) {\n if (!(error instanceof CommandError)) throw error;\n const status = error.code === \"COMMAND_REJECTED\" ? 403\n : error.code === \"COMMAND_IDEMPOTENCY_CONFLICT\" ? 409 : 503;\n throw new ApplicationError(error.code, { code: error.code, status });\n }\n },\n transaction(_invocation, next) { transaction(); return next(); },\n // The durable command owns the authoritative audit and receipt in the same transaction.\n audit: { succeeded() { transaction(); }, failed() {} },\n },\n };\n}\n";
3
- export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"durable command transaction\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"durable command transaction\");\n expect(db.queries).toHaveLength(1);\n});\n";
2
+ export declare const STARTER_REVIEW_POSTGRES = "import { plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,\n createTransactionalCommandGovernance,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\nimport { ApprovalInput, ReviewResult } from \"../review/contracts\";\n\nconst input = createSchemaDecoder(ApprovalInput, { normalize: false });\nconst result = createSchemaDecoder(ReviewResult, { normalize: false });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const transaction = () => commandBinding.transaction();\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandBinding = createTransactionalCommandGovernance({\n permission: \"review.approve\",\n identity: invocation => ({ actorId: actor(invocation), tenantId }),\n input: invocation => ({\n ...record(invocation.input.body), id: invocation.input.params[\"id\"],\n }),\n rejection: error => isPublicConflict(error) ? new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\") : undefined,\n definition: {\n name: \"review.approve\", store: createPostgresCommandStore(database),\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n },\n });\n return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };\n}\n";
3
+ export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"not active\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"not active\");\n expect(db.queries).toHaveLength(1);\n});\n";
4
4
  export declare const STARTER_REVIEW_DELIVERY_HOST = "import { SQL } from \"bun\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { createBunCommandDatabase } from \"@supacloud/db/bun\";\nimport { createApplication, type CompiledModule } from \"@supacloud/elysia\";\nimport { createReviewPostgresAdapters } from \"./host/review-postgres\";\nimport { createReviewAttachmentAdapters } from \"./host/review-attachments\";\nimport { createReviewUploadAdapters } from \"./host/review-uploads\";\n\nfunction required(name: string): string {\n const value = process.env[name];\n if (!value?.trim()) throw new Error(\"Missing required host setting: \" + name);\n return value;\n}\n\nfunction databaseConnection(): SQL.Options {\n const socket = process.env[\"DATABASE_SOCKET_PATH\"];\n if (socket && process.env[\"DATABASE_URL\"]) throw new Error(\"Conflicting database connection settings\");\n if (socket) {\n return { adapter: \"postgres\", path: socket, database: required(\"DATABASE_NAME\"), username: required(\"DATABASE_USER\") };\n }\n const url = new URL(required(\"DATABASE_URL\"));\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") throw new Error(\"Invalid database URL\");\n return {\n adapter: \"postgres\", url: url.href, hostname: url.hostname.replace(/^\\[|\\]$/g, \"\"), port: Number(url.port || 5432),\n username: decodeURIComponent(url.username), password: decodeURIComponent(url.password),\n database: decodeURIComponent(url.pathname.slice(1)),\n };\n}\n\nexport async function createDeliveryApplication(modules: CompiledModule[], lifecycle: { signal: AbortSignal }) {\n lifecycle.signal.throwIfAborted();\n const connection = databaseConnection();\n const attachments = process.env[\"REVIEW_ATTACHMENTS\"];\n if (attachments !== undefined && attachments !== \"enabled\") throw new Error(\"Invalid attachment configuration\");\n const settings = {\n tenantId: required(\"APP_TENANT_ID\"),\n identity: {\n issuer: required(\"SUPAUTH_ISSUER\"), audience: required(\"SUPAUTH_AUDIENCE\"),\n clientId: required(\"SUPAUTH_CLIENT_ID\"), projectId: required(\"SUPACLOUD_PROJECT_ID\"),\n jwksUrl: required(\"SUPAUTH_JWKS_URL\"),\n },\n };\n const pool = new SQL({ ...connection, max: 12, connectionTimeout: 5 });\n let closing: Promise<void> | undefined;\n const close = () => closing ??= pool.close({ timeout: 5 });\n let stage: \"attachments\" | \"uploads\" | \"postgres\" | \"application\" = \"attachments\";\n try {\n const database = createBunCommandDatabase(pool);\n const attachmentOptions = attachments === \"enabled\" ? {\n database, tenantId: settings.tenantId, projectId: settings.identity.projectId,\n service: createClient(required(\"SUPACLOUD_URL\"), required(\"SUPACLOUD_SERVICE_ROLE_KEY\"), {\n auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n }),\n } : undefined;\n const durable = attachmentOptions ? await createReviewAttachmentAdapters(attachmentOptions) : undefined;\n stage = \"uploads\";\n const uploads = attachmentOptions ? await createReviewUploadAdapters(attachmentOptions) : undefined;\n stage = \"postgres\";\n const adapters = await createReviewPostgresAdapters({\n database, tenantId: settings.tenantId, identity: settings.identity,\n ...(uploads ? { uploads } : {}),\n ...(durable ? { afterApproved: durable.enqueue } : {}),\n });\n lifecycle.signal.throwIfAborted();\n stage = \"application\";\n const app = createApplication({ name: \"review\", modules, ...adapters });\n return { fetch: (request: Request) => app.handle(request), close };\n } catch (error) {\n await close();\n const code = error !== null && typeof error === \"object\" && \"code\" in error\n && typeof error.code === \"string\" && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : \"UNKNOWN\";\n console.error(JSON.stringify({ event: \"review-host-initialization-failed\", stage, code }));\n // Database/identity errors can contain connection details; never echo the underlying cause.\n throw new Error(\"Review host initialization failed\");\n }\n}\n";
@@ -0,0 +1 @@
1
+ export declare const STARTER_REVIEW_QUERY = "import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { ReviewRow } from \"../../db/schema\";\n\n// This read projection uses SQL column names shared with the Drizzle source.\n// Do not expose camelCase Drizzle property names as PostgREST column names.\nexport type ReviewSummary = Pick<ReviewRow, \"id\" | \"state\" | \"version\">;\nexport type ReviewReadDatabase = {\n public: {\n Tables: {\n starter_reviews: {\n Row: ReviewSummary;\n Insert: never;\n Update: never;\n Relationships: [];\n };\n };\n Views: Record<never, never>;\n Functions: Record<never, never>;\n };\n};\n\nexport interface ReviewPageOptions {\n page?: number;\n pageSize?: number;\n signal?: AbortSignal;\n}\n\n// Reuse the application's user-scoped client. Identity verification and row\n// authorization belong to the gateway and database, never a caller-supplied owner.\nexport async function listReviews(\n client: SupabaseClient<ReviewReadDatabase>,\n { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},\n) {\n if (!Number.isSafeInteger(page) || page < 1\n || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100\n || !Number.isSafeInteger(page * pageSize)) {\n throw new RangeError(\"Invalid review page: page >= 1 and pageSize between 1 and 100 are required\");\n }\n signal?.throwIfAborted();\n const start = (page - 1) * pageSize;\n const query = client.from(\"starter_reviews\")\n .select(\"id,state,version\", { count: \"exact\" })\n .order(\"id\", { ascending: true })\n .range(start, start + pageSize - 1);\n const { data, error, count } = await (signal ? query.abortSignal(signal) : query);\n if (error) throw error;\n if (data === null || count === null) throw new Error(\"Incomplete review page response\");\n return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };\n}\n";
@@ -27,6 +27,8 @@ export declare const deployToolSchema: {
27
27
  cwd: Type.TOptional<Type.TString>;
28
28
  skip_build: Type.TOptional<Type.TBoolean>;
29
29
  assets_only: Type.TOptional<Type.TBoolean>;
30
+ backend: Type.TOptional<Type.TBoolean>;
31
+ status: Type.TOptional<Type.TString>;
30
32
  json: Type.TOptional<Type.TBoolean>;
31
33
  };
32
34
  export declare function findDeployConfigRoot(startDirectory: string, environmentName?: string): Promise<string>;
@@ -46,7 +46,10 @@ export declare function uploadProjectWebRelease(http: FrontendReleaseHttp, proje
46
46
  export declare function uploadFrontendSource(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, archivePath: string): Promise<ToolResponse>;
47
47
  export declare function getActiveFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string): Promise<ToolResponse>;
48
48
  export declare function readFrontendReleaseAuthority(http: FrontendReleaseHttp, projectRef: string, deploymentId: string): Promise<ToolResponse>;
49
- export declare function publishFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, archivePath: string, expectedTreeSha256: string): Promise<ToolResponse>;
49
+ export declare function publishFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, archiveSource: string | (() => Promise<{
50
+ archivePath: string;
51
+ cleanup(): Promise<void>;
52
+ }>), expectedTreeSha256: string): Promise<ToolResponse>;
50
53
  export declare function rollbackFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, releaseId?: string, options?: {
51
54
  expectedActiveReleaseId?: string;
52
55
  expectedActivationId?: string;
@@ -2,3 +2,4 @@ import type { ToolResult } from "../tool-server";
2
2
  import type { HttpTransport } from "../transports/http";
3
3
  export declare function requireProjectDeploymentProtocol(http: Pick<HttpTransport, "get">, projectRef: string, environmentId: string): Promise<void>;
4
4
  export declare function runProjectDeployment(http: Pick<HttpTransport, "post" | "get">, args: Record<string, unknown>): Promise<ToolResult>;
5
+ export declare function readProjectDeploymentStatus(http: Pick<HttpTransport, "get">, projectRef: string, environmentId: string, deploymentId: string): Promise<ToolResult>;
@@ -131,7 +131,7 @@ import { Type as Type2 } from "typebox";
131
131
  import { Value as Value2 } from "typebox/value";
132
132
 
133
133
  // src/shared/deployment-upgrade.ts
134
- var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
134
+ var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. With a frontend configured, deploy publishes only Web assets; use `supacloud deploy --backend` for the application backend. `supacloud deploy --assets_only` remains an explicit Web-only option. SupaCloud owns activation; do not retry with legacy commands.";
135
135
  var removedFrontendActions = new Set([
136
136
  "deploy_git",
137
137
  "deploy_upload",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.98.0",
3
+ "version": "0.101.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,21 +52,21 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.46.1",
56
- "@supacloud/delivery": "0.18.0",
57
- "@supacloud/db": "0.17.0",
58
- "typebox": "1.3.36"
55
+ "@supacloud/compiler": "^0.46.3",
56
+ "@supacloud/delivery": "0.18.1",
57
+ "@supacloud/db": "0.17.1",
58
+ "typebox": "1.3.37"
59
59
  },
60
60
  "overrides": {
61
61
  "@supacloud/contracts": "0.9.0",
62
- "@supacloud/delivery": "0.18.0",
63
- "@graphql-tools/utils": "^12.0.3"
62
+ "@supacloud/delivery": "0.18.1",
63
+ "@graphql-tools/utils": "^12.0.4"
64
64
  },
65
65
  "devDependencies": {
66
- "@supacloud/elysia": "^0.24.1",
66
+ "@supacloud/elysia": "0.31.0",
67
67
  "@types/bun": "^1.4.3",
68
- "effect": "4.0.2",
69
- "elysia": "2.0.0-beta.21",
68
+ "effect": "4.0.5",
69
+ "elysia": "2.0.0-beta.32",
70
70
  "typescript": "^7.0.2"
71
71
  }
72
72
  }