@supacloud/cli 0.95.1 → 0.96.1

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
@@ -26,11 +26,15 @@ supacloud --env test deploy
26
26
  overwriting an existing project configuration. `env list` and `env check` are
27
27
  local-only, redact credentials, and provide stable JSON with `--json` for CI or
28
28
  AI tooling. The real `.env.supacloud.*` files are added to `.gitignore`.
29
- Fill in `SUPACLOUD_APPLICATION_ID` and the Management credentials in the
30
- selected profile, and configure the application's current delivery build.
31
- Standalone frontend deployment is no longer supported. Upgrade the CLI with
29
+ Fill in the Management credentials and project/environment in the selected profile.
30
+ Static frontends need only `frontend` build configuration; no application binding
31
+ or backend delivery build is required. Set `SUPACLOUD_APPLICATION_ID` only for
32
+ executable application delivery. Upgrade the CLI with
32
33
  `npm install -g @supacloud/cli@latest` and upgrade the SupaCloud server together.
33
34
  `deploy` checks the server protocol before building or uploading.
35
+ Use `frontend.name` to select exactly one named site in each environment when
36
+ deployment IDs differ. A missing or ambiguous name fails before upload; it never
37
+ falls back to another site.
34
38
 
35
39
  ## Application Starter
36
40
 
@@ -645,7 +649,12 @@ FA-owned business features are documented in
645
649
  and `--dry_run` are removed. Put build settings in `supacloud.json`; use
646
650
  `app deploy-plan` for read-only review. Historical project-specific manifests
647
651
  and standalone Function targets must move to application delivery configuration.
648
- The CLI rejects them rather than falling back to a partial Web deployment.
652
+ The CLI rejects them rather than silently falling back to a partial Web deployment.
653
+ For an intentional Web-only release in a mixed repository, use
654
+ `supacloud --env test deploy --assets_only`. This preserves historical manifests,
655
+ Functions, schedules and migrations without invoking application compilation.
656
+ Static publication reuses immutable archives, pre-upload CAS, mutation receipts
657
+ and exact post-activation readback. It never retries a conflicting activation.
649
658
 
650
659
  The legacy `.env` fallback is unclassified and therefore does not enable the
651
660
  production confirmation gate. Production automation must select a `prod` or
package/dist/index.js CHANGED
@@ -8767,7 +8767,7 @@ function outcomeUnknownGuidance(text) {
8767
8767
  }
8768
8768
 
8769
8769
  // src/shared/deployment-upgrade.ts
8770
- var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment is no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, bind application.id and application.environmentId in supacloud.json (or SUPACLOUD_APPLICATION_ID and SUPACLOUD_ENV), then run `supacloud deploy`. SupaCloud owns deployment planning and activation; do not retry with legacy commands.";
8770
+ 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.";
8771
8771
  var removedFrontendActions = new Set([
8772
8772
  "deploy_git",
8773
8773
  "deploy_upload",
@@ -9964,22 +9964,28 @@ function validateWorkerExecution(groups) {
9964
9964
  var objectOptions = { additionalProperties: false };
9965
9965
  var reference = String2({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z][A-Za-z0-9_.:/-]*$" });
9966
9966
  var references = _Array_(reference, { uniqueItems: true });
9967
- var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook")]);
9967
+ var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
9968
9968
  var isolation = Union([Literal("shared"), Literal("process")]);
9969
9969
  var DeliveryFilePathSchema = String2({
9970
9970
  maxLength: 512,
9971
9971
  pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
9972
9972
  });
9973
+ var functionTarget = _Object_({
9974
+ slug: String2({ pattern: "^[a-z][a-z0-9-]{0,62}$" }),
9975
+ entrypoint: DeliveryFilePathSchema,
9976
+ verifyJwt: Boolean2()
9977
+ }, objectOptions);
9973
9978
  var DeliveryOptionsSchema = _Object_({
9974
9979
  version: Literal(1),
9975
9980
  execution: Optional(WorkerExecutionSchema),
9976
9981
  targets: Optional(_Array_(_Object_({
9977
9982
  name: String2({ pattern: "^[a-z][a-z0-9-]{0,62}$" }),
9978
9983
  kind,
9979
- modules: _Array_(reference, { minItems: 1, uniqueItems: true }),
9984
+ modules: Optional(_Array_(reference, { minItems: 1, uniqueItems: true })),
9980
9985
  isolation: Optional(isolation),
9981
9986
  capabilities: Optional(references),
9982
- compute: Optional(ComputeResourcesSchema)
9987
+ compute: Optional(ComputeResourcesSchema),
9988
+ function: Optional(functionTarget)
9983
9989
  }, objectOptions))),
9984
9990
  runtime: Optional(_Object_({
9985
9991
  processIsolation: Boolean2(),
@@ -10057,6 +10063,7 @@ var DeliveryTargetSchema = _Object_({
10057
10063
  routes: _Array_(routeSchema),
10058
10064
  jobs: _Array_(jobSchema),
10059
10065
  externalTokens: _Array_(String2()),
10066
+ function: Optional(functionTarget),
10060
10067
  requirements: _Object_({
10061
10068
  processIsolation: Boolean2(),
10062
10069
  durableQueue: Boolean2(),
@@ -16343,6 +16350,39 @@ async function getFrontendRelease(http, projectRef, deploymentId, releaseId) {
16343
16350
  }
16344
16351
  return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
16345
16352
  }
16353
+ async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath) {
16354
+ const endpoint = releaseEndpoint(projectRef, deploymentId);
16355
+ const archive = await verifiedArchive(archivePath);
16356
+ let response;
16357
+ try {
16358
+ response = await http.postBinary(endpoint, {
16359
+ stream: archiveStream(archive),
16360
+ byteLength: archive.sizeBytes
16361
+ }, {
16362
+ contentType: FRONTEND_ARCHIVE_CONTENT_TYPE,
16363
+ contentLength: archive.sizeBytes,
16364
+ contentSha256: archive.sha256,
16365
+ maxJsonBytes: RESPONSE_MAX_BYTES,
16366
+ timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
16367
+ responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
16368
+ });
16369
+ } finally {
16370
+ await archive.handle.close();
16371
+ }
16372
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
16373
+ if (!release) {
16374
+ if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
16375
+ return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
16376
+ }
16377
+ return uploadReadback(http, projectRef, deploymentId, archive.sha256, response.status);
16378
+ }
16379
+ const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
16380
+ const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef, deploymentId, releaseId: release.release_id }) : null;
16381
+ if (!verified || verified.tree_sha256 !== release.tree_sha256) {
16382
+ return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
16383
+ }
16384
+ return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release: verified });
16385
+ }
16346
16386
  async function uploadProjectWebRelease(http, projectRef, environmentId, deploymentId, archivePath) {
16347
16387
  const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/environments/${encodeURIComponent(environmentId)}/deployments/web-release?deployment_id=${encodeURIComponent(deploymentId)}`;
16348
16388
  const archive = await verifiedArchive(archivePath);
@@ -16397,6 +16437,16 @@ async function uploadProjectWebRelease(http, projectRef, environmentId, deployme
16397
16437
  await archive.handle.close();
16398
16438
  }
16399
16439
  }
16440
+ async function uploadReadback(http, projectRef, deploymentId, releaseId, uploadStatus) {
16441
+ const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
16442
+ maxJsonBytes: RESPONSE_MAX_BYTES
16443
+ });
16444
+ const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
16445
+ if (!release) {
16446
+ return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
16447
+ }
16448
+ return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
16449
+ }
16400
16450
  function stableJson(candidate) {
16401
16451
  if (candidate === null || typeof candidate !== "object")
16402
16452
  return JSON.stringify(candidate);
@@ -16516,6 +16566,57 @@ async function getActiveFrontendRelease(http, projectRef, deploymentId) {
16516
16566
  const snapshot = await readActiveFrontendRelease(http, projectRef, deploymentId);
16517
16567
  return "failure" in snapshot ? snapshot.failure : toolResponse(snapshot.inventory);
16518
16568
  }
16569
+ async function publishFrontendRelease(http, projectRef, deploymentId, archivePath, expectedTreeSha256) {
16570
+ const authority = await readActiveFrontendRelease(http, projectRef, deploymentId);
16571
+ if ("failure" in authority)
16572
+ return authority.failure;
16573
+ const inventory = authority.inventory;
16574
+ const current = inventory.releases[0];
16575
+ if (current?.tree_sha256 === expectedTreeSha256) {
16576
+ return toolResponse({
16577
+ operation: "frontend.deploy",
16578
+ project_ref: projectRef,
16579
+ deployment_id: deploymentId,
16580
+ active_release_id: inventory.active_release_id,
16581
+ activation_id: inventory.active_activation_id,
16582
+ release: current,
16583
+ unchanged: true
16584
+ });
16585
+ }
16586
+ const uploaded = await uploadFrontendRelease(http, projectRef, deploymentId, archivePath);
16587
+ if (uploaded.isError)
16588
+ return uploaded;
16589
+ const value = JSON.parse(uploaded.content[0].text);
16590
+ const envelope = value && typeof value === "object" && !Array.isArray(value) ? value : null;
16591
+ const candidate = envelope?.release;
16592
+ const release = candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
16593
+ if (!release || release.tree_sha256 !== expectedTreeSha256 || typeof release.release_id !== "string" || !RELEASE_ID_PATTERN.test(release.release_id)) {
16594
+ return releaseFailure("frontend.deploy", "INVALID_RESPONSE", 200);
16595
+ }
16596
+ const mutationId = crypto.randomUUID();
16597
+ const activated = await activateFrontendRelease(http, {
16598
+ projectRef,
16599
+ deploymentId,
16600
+ releaseId: release.release_id,
16601
+ mutationId,
16602
+ expectedActiveReleaseId: inventory.active_release_id ?? "absent",
16603
+ expectedActivationId: inventory.active_activation_id ?? "absent"
16604
+ });
16605
+ const activatedPayload = JSON.parse(activated.content[0].text);
16606
+ const active = activatedPayload.release;
16607
+ const treeMismatch = !activated.isError && (!active || typeof active !== "object" || !("tree_sha256" in active) || active.tree_sha256 !== expectedTreeSha256);
16608
+ return {
16609
+ ...toolResponse({
16610
+ ...treeMismatch ? { ok: false, error: { code: "OUTCOME_UNKNOWN", http_status: 200 } } : activatedPayload,
16611
+ operation: "frontend.deploy",
16612
+ mutation_id: mutationId,
16613
+ target_release_id: release.release_id,
16614
+ previous_release_id: inventory.active_release_id,
16615
+ previous_activation_id: inventory.active_activation_id
16616
+ }),
16617
+ ...activated.isError || treeMismatch ? { isError: true } : {}
16618
+ };
16619
+ }
16519
16620
  async function rollbackFrontendRelease(http, projectRef, deploymentId, releaseId) {
16520
16621
  if (releaseId !== undefined && !RELEASE_ID_PATTERN.test(releaseId)) {
16521
16622
  throw new Error("'release_id' must be a SHA-256 digest");
@@ -21099,7 +21200,7 @@ var starter_metadata_default = {
21099
21200
  generatedBy: "supacloud/starter-metadata-v1",
21100
21201
  packages: {
21101
21202
  compiler: {
21102
- version: "0.45.0"
21203
+ version: "0.46.0"
21103
21204
  },
21104
21205
  app: {
21105
21206
  version: "0.26.1",
@@ -21117,7 +21218,7 @@ var starter_metadata_default = {
21117
21218
  version: "0.8.1"
21118
21219
  },
21119
21220
  db: {
21120
- version: "0.16.2",
21221
+ version: "0.17.0",
21121
21222
  peerDependencies: {
21122
21223
  "drizzle-orm": "1.0.0-rc.5-169397b"
21123
21224
  },
@@ -21126,7 +21227,7 @@ var starter_metadata_default = {
21126
21227
  }
21127
21228
  },
21128
21229
  sdk: {
21129
- version: "0.41.2",
21230
+ version: "0.42.0",
21130
21231
  peerDependencies: {
21131
21232
  "@supabase/supabase-js": "^2.117.3"
21132
21233
  }
@@ -22135,7 +22236,11 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22135
22236
 
22136
22237
  // src/shared/tools/app-starter-attachment-worker.ts
22137
22238
  var STARTER_ATTACHMENT_WORKER = `import { createWorker, type CompiledModule } from "@supacloud/elysia";
22138
- import type { SupaCloudWorkflowClaim, SupaCloudWorkflowsClient } from "@supacloud/js";
22239
+ import {
22240
+ createSupaCloudWorkflowTransport,
22241
+ type SupaCloudWorkflowClaim,
22242
+ type SupaCloudWorkflowsClient,
22243
+ } from "@supacloud/js";
22139
22244
  import { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from "./review-attachments";
22140
22245
 
22141
22246
  export interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {
@@ -22160,9 +22265,6 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22160
22265
  halted = true;
22161
22266
  failure.reject(new Error("Attachment worker requires recovery"));
22162
22267
  };
22163
- const attempt = (claim: SupaCloudWorkflowClaim) => ({
22164
- stepId: claim.stepId, messageId: claim.messageId, attempt: claim.attempt, workerId: claim.workerId,
22165
- });
22166
22268
  async function ownedClaim(claim: SupaCloudWorkflowClaim) {
22167
22269
  if (claim.workflowName !== "review.verify-attachment" || claim.workflowVersion !== "1"
22168
22270
  || claim.stepKey !== "verify" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {
@@ -22178,6 +22280,9 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22178
22280
  ));
22179
22281
  if (!Array.isArray(bound) || bound.length !== 1) throw new Error("Attachment workflow ownership mismatch");
22180
22282
  }
22283
+ const workflowTransport = createSupaCloudWorkflowTransport({
22284
+ workflows, workerId, visibilityTimeoutSeconds: 120, retryDelaySeconds: 5, acceptClaim: ownedClaim,
22285
+ });
22181
22286
  const worker = createWorker({
22182
22287
  modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,
22183
22288
  pollIntervalMs: options.pollIntervalMs ?? 1000,
@@ -22186,11 +22291,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22186
22291
  async claim(signal): Promise<SupaCloudWorkflowClaim | null> {
22187
22292
  if (halted || signal.aborted) return null;
22188
22293
  try {
22189
- const claim = await workflows.claim({ workerId, visibilityTimeoutSeconds: 120 });
22190
- if (signal.aborted || halted) return null;
22191
- if (!claim || claim.status !== "claimed") return null;
22192
- await ownedClaim(claim);
22193
- return claim;
22294
+ return await workflowTransport.claim(signal);
22194
22295
  } catch {
22195
22296
  if (!signal.aborted) fatal();
22196
22297
  throw new Error("Attachment workflow claim failed");
@@ -22198,9 +22299,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22198
22299
  },
22199
22300
  async ack(claim, output) {
22200
22301
  try {
22201
- if (!output || typeof output !== "object" || Array.isArray(output)) throw new Error("Invalid Job output");
22202
- // The compiled Job validates output; Workflow SDK validates the receipt.
22203
- await workflows.complete({ ...attempt(claim), runOutput: { ...output } });
22302
+ await workflowTransport.ack(claim, output);
22204
22303
  } catch {
22205
22304
  fatal();
22206
22305
  throw new Error("Attachment workflow completion is unconfirmed");
@@ -22208,9 +22307,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22208
22307
  },
22209
22308
  async fail(claim) {
22210
22309
  try {
22211
- const request: Parameters<typeof workflows.fail>[0] = { ...attempt(claim), errorMessage: "Attachment verification failed" };
22212
- if (claim.attempt < claim.maxAttempts) await workflows.retry({ ...request, delaySeconds: 5 });
22213
- else await workflows.fail(request);
22310
+ await workflowTransport.fail(claim, "Attachment verification failed");
22214
22311
  } catch {
22215
22312
  fatal();
22216
22313
  throw new Error("Attachment workflow failure receipt is unconfirmed");
@@ -22394,9 +22491,11 @@ export class ReviewUploadsController {
22394
22491
  var STARTER_UPLOAD_SCHEMA = `-- Apply explicitly with the migration owner.
22395
22492
  ALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;
22396
22493
  GRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;
22494
+ DROP POLICY IF EXISTS starter_member_read ON public.starter_members;
22397
22495
  CREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated
22398
22496
  USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());
22399
22497
  GRANT SELECT ON public.starter_reviews TO authenticated;
22498
+ DROP POLICY IF EXISTS starter_review_read ON public.starter_reviews;
22400
22499
  CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated
22401
22500
  USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (
22402
22501
  SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id
@@ -22404,11 +22503,14 @@ CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authen
22404
22503
  ));
22405
22504
  INSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)
22406
22505
  VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);
22506
+ DROP POLICY IF EXISTS starter_attachment_read ON storage.objects;
22407
22507
  CREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated
22408
22508
  USING (bucket_id='review-attachments');
22509
+ DROP POLICY IF EXISTS starter_attachment_insert ON storage.objects;
22409
22510
  CREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated
22410
22511
  WITH CHECK (bucket_id='review-attachments');
22411
22512
  -- Restrictive fences also apply when the host already has permissive Storage policies.
22513
+ DROP POLICY IF EXISTS starter_attachment_owner_fence ON storage.objects;
22412
22514
  CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated
22413
22515
  USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (
22414
22516
  SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id
@@ -22421,8 +22523,10 @@ CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE F
22421
22523
  WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')
22422
22524
  AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve
22423
22525
  )));
22526
+ DROP POLICY IF EXISTS starter_attachment_update_fence ON storage.objects;
22424
22527
  CREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated
22425
22528
  USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');
22529
+ DROP POLICY IF EXISTS starter_attachment_delete_fence ON storage.objects;
22426
22530
  CREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated
22427
22531
  USING (bucket_id<>'review-attachments');
22428
22532
  `;
@@ -22570,6 +22674,11 @@ function renderStarterRuntimeRolesSchema(httpRole, workerRole) {
22570
22674
  CREATE ROLE ${httpRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
22571
22675
  CREATE ROLE ${workerRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
22572
22676
  GRANT USAGE ON SCHEMA public TO ${httpRole}, ${workerRole};
22677
+ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
22678
+ ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
22679
+ ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
22680
+ ALTER TABLE public.starter_attachments ENABLE ROW LEVEL SECURITY;
22681
+ ALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;
22573
22682
  GRANT SELECT ON public.starter_application, public.starter_members,
22574
22683
  public.starter_reviews, public.starter_attachments TO ${httpRole}, ${workerRole};
22575
22684
 
@@ -22583,34 +22692,49 @@ GRANT UPDATE(id) ON public.starter_reviews TO ${workerRole};
22583
22692
  GRANT INSERT ON public.starter_attachments TO ${httpRole};
22584
22693
  GRANT SELECT,INSERT ON public.starter_attachment_results TO ${workerRole};
22585
22694
 
22695
+ DROP POLICY IF EXISTS starter_backend_binding_read ON public.starter_application;
22586
22696
  CREATE POLICY starter_backend_binding_read ON public.starter_application FOR SELECT
22587
22697
  TO ${httpRole}, ${workerRole} USING (true);
22698
+ DROP POLICY IF EXISTS starter_backend_binding_lock ON public.starter_application;
22588
22699
  CREATE POLICY starter_backend_binding_lock ON public.starter_application FOR UPDATE
22589
22700
  TO ${httpRole}, ${workerRole} USING (true);
22701
+ DROP POLICY IF EXISTS starter_backend_binding_immutable ON public.starter_application;
22590
22702
  CREATE POLICY starter_backend_binding_immutable ON public.starter_application AS RESTRICTIVE FOR UPDATE
22591
22703
  TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
22704
+ DROP POLICY IF EXISTS starter_backend_member_read ON public.starter_members;
22592
22705
  CREATE POLICY starter_backend_member_read ON public.starter_members FOR SELECT
22593
22706
  TO ${httpRole}, ${workerRole} USING (true);
22707
+ DROP POLICY IF EXISTS starter_backend_member_lock ON public.starter_members;
22594
22708
  CREATE POLICY starter_backend_member_lock ON public.starter_members FOR UPDATE
22595
22709
  TO ${httpRole}, ${workerRole} USING (true);
22710
+ DROP POLICY IF EXISTS starter_backend_member_immutable ON public.starter_members;
22596
22711
  CREATE POLICY starter_backend_member_immutable ON public.starter_members AS RESTRICTIVE FOR UPDATE
22597
22712
  TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
22713
+ DROP POLICY IF EXISTS starter_backend_review_read ON public.starter_reviews;
22598
22714
  CREATE POLICY starter_backend_review_read ON public.starter_reviews FOR SELECT
22599
22715
  TO ${httpRole}, ${workerRole} USING (true);
22716
+ DROP POLICY IF EXISTS starter_backend_review_update ON public.starter_reviews;
22600
22717
  CREATE POLICY starter_backend_review_update ON public.starter_reviews FOR UPDATE
22601
22718
  TO ${httpRole}, ${workerRole} USING (true);
22719
+ DROP POLICY IF EXISTS starter_worker_review_immutable ON public.starter_reviews;
22602
22720
  CREATE POLICY starter_worker_review_immutable ON public.starter_reviews AS RESTRICTIVE FOR UPDATE
22603
22721
  TO ${workerRole} USING (true) WITH CHECK (false);
22722
+ DROP POLICY IF EXISTS starter_backend_attachment_read ON public.starter_attachments;
22604
22723
  CREATE POLICY starter_backend_attachment_read ON public.starter_attachments FOR SELECT
22605
22724
  TO ${httpRole}, ${workerRole} USING (true);
22725
+ DROP POLICY IF EXISTS starter_backend_attachment_lock ON public.starter_attachments;
22606
22726
  CREATE POLICY starter_backend_attachment_lock ON public.starter_attachments FOR UPDATE
22607
22727
  TO ${httpRole}, ${workerRole} USING (true);
22728
+ DROP POLICY IF EXISTS starter_backend_attachment_immutable ON public.starter_attachments;
22608
22729
  CREATE POLICY starter_backend_attachment_immutable ON public.starter_attachments AS RESTRICTIVE FOR UPDATE
22609
22730
  TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
22731
+ DROP POLICY IF EXISTS starter_http_attachment_insert ON public.starter_attachments;
22610
22732
  CREATE POLICY starter_http_attachment_insert ON public.starter_attachments FOR INSERT
22611
22733
  TO ${httpRole} WITH CHECK (true);
22734
+ DROP POLICY IF EXISTS starter_worker_result_read ON public.starter_attachment_results;
22612
22735
  CREATE POLICY starter_worker_result_read ON public.starter_attachment_results FOR SELECT
22613
22736
  TO ${workerRole} USING (true);
22737
+ DROP POLICY IF EXISTS starter_worker_result_insert ON public.starter_attachment_results;
22614
22738
  CREATE POLICY starter_worker_result_insert ON public.starter_attachment_results FOR INSERT
22615
22739
  TO ${workerRole} WITH CHECK (true);
22616
22740
 
@@ -23001,6 +23125,80 @@ export const attachmentResults = pgTable.withRLS("starter_attachment_results", {
23001
23125
  export type ReviewRow = typeof reviews.$inferSelect;
23002
23126
  export type NewReview = typeof reviews.$inferInsert;
23003
23127
  `;
23128
+ var STARTER_DATABASE_MODULES = `import { defineDatabaseModule } from "@supacloud/db";
23129
+ import { getTableConfig } from "drizzle-orm/pg-core";
23130
+ import { application, reviews, members, attachments, attachmentResults } from "./schema";
23131
+
23132
+ const backendPolicies = [
23133
+ ["starter_backend_binding_read", "public.starter_application", "select", ["starter_review_http", "starter_review_worker"]],
23134
+ ["starter_backend_binding_lock", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
23135
+ ["starter_backend_binding_immutable", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
23136
+ ["starter_backend_member_read", "public.starter_members", "select", ["starter_review_http", "starter_review_worker"]],
23137
+ ["starter_backend_member_lock", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
23138
+ ["starter_backend_member_immutable", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
23139
+ ["starter_backend_review_read", "public.starter_reviews", "select", ["starter_review_http", "starter_review_worker"]],
23140
+ ["starter_backend_review_update", "public.starter_reviews", "update", ["starter_review_http"]],
23141
+ ["starter_worker_review_immutable", "public.starter_reviews", "update", ["starter_review_worker"]],
23142
+ ["starter_backend_attachment_read", "public.starter_attachments", "select", ["starter_review_http", "starter_review_worker"]],
23143
+ ["starter_backend_attachment_lock", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
23144
+ ["starter_backend_attachment_immutable", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
23145
+ ["starter_http_attachment_insert", "public.starter_attachments", "insert", ["starter_review_http"]],
23146
+ ["starter_worker_result_read", "public.starter_attachment_results", "select", ["starter_review_worker"]],
23147
+ ["starter_worker_result_insert", "public.starter_attachment_results", "insert", ["starter_review_worker"]],
23148
+ ] as const;
23149
+
23150
+ // The module is the single live-Catalog ownership contract for starter tables.
23151
+ export default defineDatabaseModule({
23152
+ name: "starter",
23153
+ tables: [application, reviews, members, attachments, attachmentResults].map(table => {
23154
+ const config = getTableConfig(table);
23155
+ return (config.schema ?? "public") + "." + config.name;
23156
+ }),
23157
+ policies: [
23158
+ ...backendPolicies.map(([name, table, operation, roles]) => ({
23159
+ name, table, operation, roles: [...roles], source: "migrations/004-review-runtime-roles.sql",
23160
+ tests: ["tests/database-governance.sql"],
23161
+ })),
23162
+ {
23163
+ name: "starter_member_read", table: "public.starter_members", operation: "select",
23164
+ roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
23165
+ tests: ["tests/database-governance.sql"],
23166
+ },
23167
+ {
23168
+ name: "starter_review_read", table: "public.starter_reviews", operation: "select",
23169
+ roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
23170
+ tests: ["tests/database-governance.sql"],
23171
+ },
23172
+ ],
23173
+ });
23174
+ `;
23175
+ var STARTER_DATABASE_TEST = `-- Read-only catalog assertions; behavioral authorization tests remain separate.
23176
+ BEGIN READ ONLY;
23177
+ DO $test$
23178
+ DECLARE
23179
+ owned text[] := ARRAY['starter_application','starter_reviews','starter_members',
23180
+ 'starter_attachments','starter_attachment_results'];
23181
+ BEGIN
23182
+ IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
23183
+ WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN
23184
+ RAISE EXCEPTION 'All starter tables must exist with RLS enabled';
23185
+ END IF;
23186
+ IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid
23187
+ JOIN pg_namespace n ON n.oid=c.relnamespace
23188
+ WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN
23189
+ RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';
23190
+ END IF;
23191
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')
23192
+ AND (rolsuper OR rolbypassrls)) THEN
23193
+ RAISE EXCEPTION 'Starter roles must not bypass row security';
23194
+ END IF;
23195
+ IF (SELECT count(*) FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')) <> 2 THEN
23196
+ RAISE EXCEPTION 'Starter runtime roles are missing';
23197
+ END IF;
23198
+ END
23199
+ $test$;
23200
+ ROLLBACK;
23201
+ `;
23004
23202
  var STARTER_DATABASE_GUIDE = `
23005
23203
  ## Database Source Workflow
23006
23204
 
@@ -23012,13 +23210,14 @@ Opaque JSON still needs runtime decoding.
23012
23210
  After installing dependencies, run \`bun run db:generate\` once and commit db/contracts/.
23013
23211
  The default \`bun run check\` and build fail on stale database contracts before any
23014
23212
  compiler regeneration. After an intentional source change, run db:generate again,
23015
- review the diff, then run check. Ordinary checks are offline.
23016
- The compiler also runs this source-contract gate on compile/check/dev through
23017
- databaseGovernance.mode = "check", including incremental cache hits. Missing
23018
- adoption files or stale contracts fail closed; no contracts are silently regenerated.
23019
- This is not SQL lint or live Catalog reconciliation. Before promotion, declare
23020
- the real database modules, their SQL and tests, then explicitly use mode "enforce"
23021
- with a separately selected read-only local/test database connection.
23213
+ review the diff, then run check.
23214
+ The command starter registers databaseGovernance.moduleFile = "db/modules.ts" and
23215
+ keeps the default \`databaseGovernance.mode = "check"\`: compile/check/build run
23216
+ the offline source-contract gate without a database. Use \`bun run dev:integration\`
23217
+ with the CLI's explicit loopback database URL, or set databaseGovernance.mode to
23218
+ "enforce" with an explicitly supplied \`DATABASE_URL\`, to run SQL lint and live
23219
+ Catalog reconciliation. Missing connections, stale source contracts and Catalog
23220
+ drift fail closed; no contracts or migrations are silently regenerated.
23022
23221
 
23023
23222
  - Maintain one schema-qualified function with explicit grants per db/functions/*.sql.
23024
23223
  - Preserve migrations/ and its existing executor/ledger. Add forward migrations only.
@@ -23042,6 +23241,8 @@ function starterDatabaseFiles() {
23042
23241
  "database.sources.json": JSON.stringify(STARTER_DATABASE_CONFIG, null, 2) + `
23043
23242
  `,
23044
23243
  "db/schema.ts": STARTER_DATABASE_SCHEMA,
23244
+ "db/modules.ts": STARTER_DATABASE_MODULES,
23245
+ "tests/database-governance.sql": STARTER_DATABASE_TEST,
23045
23246
  "db/functions/README.md": `Maintain one schema-qualified function and its explicit EXECUTE ACL per SQL file. No RPC functions are needed by the initial transactional starter.
23046
23247
  `,
23047
23248
  "drizzle.config.ts": `import { defineConfig } from "drizzle-kit";
@@ -23134,7 +23335,7 @@ function appStarterFiles(name) {
23134
23335
  noEmit: true,
23135
23336
  types: ["bun"]
23136
23337
  },
23137
- include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
23338
+ include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/modules.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
23138
23339
  }),
23139
23340
  "bunfig.toml": `env = false
23140
23341
  `,
@@ -23168,7 +23369,7 @@ export default defineSupacloudConfig({
23168
23369
  outDir: "generated",
23169
23370
  strict: true,
23170
23371
  moduleBoundaryPreset: "modular-monolith",
23171
- databaseGovernance: { mode: "check" },
23372
+ databaseGovernance: { mode: "check", moduleFile: "db/modules.ts", databaseUrlEnv: "DATABASE_URL" },
23172
23373
  effect: {
23173
23374
  requireRouteEffects: true,
23174
23375
  requireErrorMappings: true,
@@ -23182,7 +23383,6 @@ export default defineSupacloudConfig({
23182
23383
  typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
23183
23384
  disallowControllerDirectDb: true,
23184
23385
  detectOrphanModules: true,
23185
- graphql: { schema: "graphql/schema.graphql" },
23186
23386
  commandCapabilities: { permission: true, transaction: true, idempotency: true, audit: true },
23187
23387
  delivery: {
23188
23388
  version: 1,
@@ -23196,50 +23396,6 @@ export default defineSupacloudConfig({
23196
23396
  },
23197
23397
  },
23198
23398
  });
23199
- `,
23200
- "graphql/schema.graphql": `# Offline query-contract example, not a deployed database schema.
23201
- # SYNTHETIC TEST FIXTURE ONLY. Database First is the sole server-schema model.
23202
- # Do not extend this fixture as a server schema; change database declarations and export.
23203
- # Replace using a snapshot exported with the intended caller role before integration.
23204
- type Query {
23205
- reviewCollection(first: Int): ReviewConnection!
23206
- }
23207
- type ReviewConnection {
23208
- edges: [ReviewEdge!]!
23209
- }
23210
- type ReviewEdge {
23211
- node: Review!
23212
- }
23213
- type Review {
23214
- id: ID!
23215
- state: String!
23216
- version: Int!
23217
- }
23218
- `,
23219
- "src/review/reviews.graphql": `query ReviewList($first: Int = 20) {
23220
- reviewCollection(first: $first) {
23221
- edges { node { id state version } }
23222
- }
23223
- }
23224
- `,
23225
- "tests/graphql.test.ts": `import { expect, test } from "bun:test";
23226
- import { createGraphqlClient, type ReviewListQuery } from "../generated/graphql";
23227
-
23228
- test("generated query client preserves its read contract without a live database", async () => {
23229
- const data: ReviewListQuery = {
23230
- reviewCollection: { edges: [{ node: { id: "demo", state: "draft", version: 1 } }] },
23231
- };
23232
- const queries = createGraphqlClient({
23233
- url: "https://project.example.test",
23234
- getAccessToken: async () => "synthetic-user-token",
23235
- fetch: async (_url, request) => {
23236
- expect(new Headers(request?.headers).get("Authorization")).toBe("Bearer synthetic-user-token");
23237
- expect(JSON.parse(String(request?.body)).variables).toEqual({ first: 10 });
23238
- return Response.json({ data });
23239
- },
23240
- });
23241
- expect(await queries.ReviewList({ first: 10 })).toEqual(data);
23242
- });
23243
23399
  `,
23244
23400
  "REACTIVE.md": STARTER_REACTIVE_GUIDE,
23245
23401
  "AGENTS.md": STARTER_REACTIVE_AGENTS + STARTER_DATABASE_GUIDE,
@@ -23249,11 +23405,30 @@ test("generated query client preserves its read contract without a live database
23249
23405
  "scripts/dev.ts": `import { watchProject, compileOptionsFromConfig, loadSupacloudConfig } from "@supacloud/compiler";
23250
23406
 
23251
23407
  if (process.env["APP_ENV"] !== "development") throw new Error("The demo server is development-only");
23408
+ const profile = process.env["SUPACLOUD_DEV_PROFILE"] === "integration" ? "integration" : "fast";
23409
+ if (profile === "integration") {
23410
+ const databaseUrl = process.env["SUPACLOUD_DEV_DATABASE_URL"];
23411
+ if (!databaseUrl) throw new Error("Integration requires an explicit loopback PostgreSQL database");
23412
+ const url = new URL(databaseUrl);
23413
+ if (!["postgres:", "postgresql:"].includes(url.protocol)
23414
+ || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
23415
+ || url.pathname.length <= 1 || url.search || url.hash
23416
+ || process.env["DATABASE_SOCKET_PATH"]
23417
+ || (process.env["DATABASE_URL"] && process.env["DATABASE_URL"] !== databaseUrl)) {
23418
+ throw new Error("Integration requires an explicit loopback PostgreSQL database without conflicting settings");
23419
+ }
23420
+ process.env["DATABASE_URL"] = databaseUrl;
23421
+ }
23422
+ const configured = compileOptionsFromConfig(await loadSupacloudConfig());
23423
+ const governanceMode = profile === "integration" ? "enforce" as const : "check" as const;
23424
+ const options = configured.databaseGovernance
23425
+ ? { ...configured, databaseGovernance: { ...configured.databaseGovernance, mode: governanceMode } }
23426
+ : configured;
23252
23427
  let server: ReturnType<typeof Bun.spawn> | undefined;
23253
23428
  let restarts = Promise.resolve();
23254
23429
  let closing = false;
23255
23430
  const watcher = watchProject({
23256
- ...compileOptionsFromConfig(await loadSupacloudConfig()),
23431
+ ...options,
23257
23432
  writeOnError: false,
23258
23433
  onEvent: (event) => {
23259
23434
  if (event.type === "compile-error") console.error(event.diagnostics);
@@ -23603,7 +23778,6 @@ test("the production composition root rejects missing governance adapters", () =
23603
23778
  \`\`\`sh
23604
23779
  bun install
23605
23780
  bun run db:generate
23606
- bun run check
23607
23781
  bun run dev
23608
23782
  \`\`\`
23609
23783
 
@@ -23639,55 +23813,23 @@ origin. The authenticated transport rejects missing credentials and redirects
23639
23813
  and does not replay a write after 401. Its underlying fetch must also be
23640
23814
  single-attempt.
23641
23815
 
23642
- Reuse the same session resolver for generated/graphql.ts when adopting queries.
23643
23816
  The local memory demo is not this authenticated production integration.
23644
23817
  Never use supacloud.commands or supacloud.workflows from a browser action:
23645
23818
  those RPCs require service-role credentials. Browser business commands use
23646
23819
  application endpoints, optionally composed through @supacloud/js/contracts.
23647
23820
  Management API tokens and service-role keys remain on trusted servers.
23648
23821
 
23649
- ## Typed Queries
23650
-
23651
- This starter uses Database First as its only GraphQL server-schema model.
23652
- Drizzle and reviewed SQL define the database; pg_graphql reflects the migrated
23653
- database under the caller's role. Do not add GraphQL resolver/decorator classes
23654
- or independently authored server SDL. Business writes continue through Commands.
23822
+ ## Reads
23655
23823
 
23656
- GraphQL is preconfigured as the recommended read path. Edit src/review/reviews.graphql;
23657
- compile, check and dev validate it against graphql/schema.graphql and generate
23658
- generated/graphql.ts. Import createGraphqlClient from that file and call
23659
- queries.ReviewList({ first: 20 }). Its variables and selected results are typed.
23660
- Pass a public project key and a getAccessToken callback that reads the current
23661
- user session. Never pass a service-role key to browser code.
23824
+ Ordinary reads use PostgREST through the existing user-scoped
23825
+ @supabase/supabase-js client: client.from("starter_reviews").select("id,state,version").
23826
+ Use the caller's session, RLS and grants; never introduce a second session store.
23827
+ Business writes continue through the governed Command/RPC boundary.
23662
23828
 
23663
- The included schema is a synthetic offline test fixture, not a deployed database.
23664
- Do not hand-edit graphql/schema.graphql to add application fields. Change database
23665
- declarations, apply migrations and export the actual schema. Replace the fixture
23666
- with a snapshot exported under the intended caller role before real integration:
23667
-
23668
- \`\`\`sh
23669
- bun node_modules/@supacloud/compiler/dist/cli.js graphql-schema \\
23670
- --url https://your-project.example \\
23671
- --key-env SUPACLOUD_PUBLISHABLE_KEY --token-env APP_USER_ACCESS_TOKEN
23672
- \`\`\`
23673
-
23674
- After migrations or grants change, and before promotion, run the same command
23675
- with --check --json to detect drift without writing. If it fails, explicitly
23676
- re-export, review the snapshot diff, compile and run application typechecks and
23677
- role/RLS tests. Normal compile/check/dev remain offline; they cannot prove that
23678
- a snapshot came from a database or is current. Keep the exported snapshot and
23679
- generated types in version control.
23680
-
23681
- The flags name environment variables, not credential values. Export requires
23682
- GraphQL and introspection already enabled on the selected development project.
23683
- The compiler does not enable extensions or alter database permissions.
23684
- Production introspection may remain disabled. Queries do not pass through
23685
- application Command/AOP; RLS and grants remain mandatory. Business writes stay
23686
- in Commands. Existing projects without GraphQL configuration remain unaffected.
23687
- Enabled contracts always reject invalid queries, including with strict: false.
23688
- To opt this starter out entirely, set graphql: false and remove its example
23689
- query test and unused generated client; do not replace checked queries with
23690
- untyped calls to bypass validation.
23829
+ GraphQL is not generated or enabled by this starter. For a specific Query Contract,
23830
+ explicitly configure graphql.schema with a role-scoped database export and add
23831
+ checked query documents. Drizzle and append-only migrations remain the database
23832
+ authority; the export is a protocol snapshot, not a second schema source.
23691
23833
 
23692
23834
  ## Environments
23693
23835
 
@@ -23903,8 +24045,8 @@ bunx supacloud-compiler fix ./fix.json --dry-run
23903
24045
  \`\`\`
23904
24046
 
23905
24047
  Context packs include directional dependencies, relevant aspect files, diagnostics
23906
- and static execution plans. Ordinary context can include declared source expressions;
23907
- inspect it before sharing. The optional --events example expects a trusted host
24048
+ and static execution plans, without provider values or source expressions.
24049
+ The optional --events example expects a trusted host
23908
24050
  to capture approved onExecution metadata in { "version": 1, "events": [...] }.
23909
24051
  It selects one opaque request ID, omits payloads and source expressions, and applies
23910
24052
  input/output size limits. It does not read arbitrary logs, prove deployment versions,
@@ -24861,6 +25003,13 @@ async function runContext(args) {
24861
25003
  fix: "supacloud app fix --fix <fix.json>",
24862
25004
  graph: "supacloud app graph --format json",
24863
25005
  explain: "supacloud app explain --target <name>"
25006
+ },
25007
+ defaults: {
25008
+ readTransport: "postgrest-supabase-js",
25009
+ writeTransport: "command-rpc",
25010
+ writeRetry: "disabled",
25011
+ externalOutcome: "receipt-reconcile",
25012
+ databaseSource: "drizzle-append-only"
24864
25013
  }
24865
25014
  };
24866
25015
  if (args.format === "json")
@@ -28131,6 +28280,7 @@ var deployToolSchema = {
28131
28280
  ref: optional(String2(), "Project ref (defaults to the linked project)"),
28132
28281
  cwd: optional(String2(), "Project directory (default: current directory)"),
28133
28282
  skip_build: optional(Boolean2(), "Use an existing output directory without building"),
28283
+ assets_only: optional(Boolean2(), "Publish only configured static assets; leave backend Functions, schedules and migrations unchanged"),
28134
28284
  json: optional(Boolean2(), "Print only the final JSON result")
28135
28285
  };
28136
28286
  function record4(candidate) {
@@ -28225,6 +28375,7 @@ function deployTargetConfig(candidate, name) {
28225
28375
  type: value.type,
28226
28376
  root: optionalText(value.root, `targets.${name}.root`),
28227
28377
  id: optionalText(value.id, `targets.${name}.id`),
28378
+ name: optionalText(value.name, `targets.${name}.name`),
28228
28379
  slug: optionalText(value.slug, `targets.${name}.slug`),
28229
28380
  buildCommand: optionalText(value.buildCommand, `targets.${name}.buildCommand`),
28230
28381
  outputDirectory: optionalText(value.outputDirectory, `targets.${name}.outputDirectory`),
@@ -28275,6 +28426,7 @@ async function readDeployConfig(configRoot) {
28275
28426
  targets: targetValues ? Object.fromEntries(Object.entries(targetValues).map(([name, value]) => [name, deployTargetConfig(value, name)])) : undefined,
28276
28427
  frontend: frontend ? {
28277
28428
  id: optionalText(frontend.id, "frontend.id"),
28429
+ name: optionalText(frontend.name, "frontend.name"),
28278
28430
  root: optionalText(frontend.root, "frontend.root"),
28279
28431
  buildCommand: optionalText(frontend.buildCommand, "frontend.buildCommand"),
28280
28432
  outputDirectory: optionalText(frontend.outputDirectory, "frontend.outputDirectory"),
@@ -28337,13 +28489,22 @@ function deploymentList(candidate) {
28337
28489
  throw new Error("Invalid frontend deployment list response");
28338
28490
  return values.map(frontendDeployment);
28339
28491
  }
28340
- function selectDeployment(deployments, requestedId, projectName) {
28492
+ function selectDeployment(deployments, requestedId, projectName, requestedName) {
28341
28493
  if (requestedId) {
28342
28494
  const selected = deployments.find((deployment) => deployment.id === requestedId);
28343
28495
  if (!selected)
28344
28496
  throw new Error(`Frontend deployment '${requestedId}' was not found`);
28497
+ if (requestedName && selected.name !== requestedName) {
28498
+ throw new Error(`Frontend deployment '${requestedId}' does not match name '${requestedName}'`);
28499
+ }
28345
28500
  return selected;
28346
28501
  }
28502
+ if (requestedName) {
28503
+ const matches = deployments.filter((deployment) => deployment.name === requestedName);
28504
+ if (matches.length !== 1)
28505
+ throw new Error(`Expected exactly one frontend deployment named '${requestedName}'`);
28506
+ return matches[0];
28507
+ }
28347
28508
  if (deployments.length === 1)
28348
28509
  return deployments[0];
28349
28510
  if (projectName) {
@@ -28361,6 +28522,7 @@ function selectFrontendTarget(configRoot, config) {
28361
28522
  type: "frontend",
28362
28523
  root: config.frontend?.root,
28363
28524
  id: config.frontend?.id,
28525
+ name: config.frontend?.name,
28364
28526
  buildCommand: config.frontend?.buildCommand,
28365
28527
  outputDirectory: config.frontend?.outputDirectory,
28366
28528
  buildEnv: config.frontend?.buildEnv
@@ -28525,7 +28687,7 @@ function payload(response) {
28525
28687
  throw new Error("Deployment operation returned an invalid response");
28526
28688
  return parsed;
28527
28689
  }
28528
- async function stageApplicationWebRelease(http, projectRef, environmentId, configRoot, skipBuild, json, environmentVariables) {
28690
+ async function stageApplicationWebRelease(http, projectRef, environmentId, configRoot, skipBuild, json, environmentVariables, assetsOnly = false) {
28529
28691
  const target = selectFrontendTarget(configRoot, await readDeployConfig(configRoot));
28530
28692
  if (!target)
28531
28693
  return null;
@@ -28538,7 +28700,7 @@ async function stageApplicationWebRelease(http, projectRef, environmentId, confi
28538
28700
  const listResponse = await http.get(`/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments`);
28539
28701
  if (!listResponse.ok)
28540
28702
  throw new Error(`Failed to list frontend deployments (HTTP ${listResponse.status})`);
28541
- const selected = selectDeployment(deploymentList(listResponse.data), requestedId, await packageName(projectDirectory));
28703
+ const selected = selectDeployment(deploymentList(listResponse.data), requestedId, await packageName(projectDirectory), target.config.name);
28542
28704
  const configuredBuildCommand = String(target.config.buildCommand || selected.buildCommand || "").trim();
28543
28705
  const buildCommand = configuredBuildCommand || (skipBuild ? "" : await defaultBuildCommand(projectDirectory, configRoot));
28544
28706
  const configuredOutput = String(target.config.outputDirectory || "").trim();
@@ -28551,6 +28713,9 @@ async function stageApplicationWebRelease(http, projectRef, environmentId, confi
28551
28713
  }
28552
28714
  const archive = await createFrontendArchive(outputDirectory);
28553
28715
  try {
28716
+ if (assetsOnly) {
28717
+ return { result: await publishFrontendRelease(http, projectRef, selected.id, archive.archivePath, archive.treeSha256) };
28718
+ }
28554
28719
  const uploaded = payload(await uploadProjectWebRelease(http, projectRef, environmentId, selected.id, archive.archivePath));
28555
28720
  const release = record4(uploaded.release);
28556
28721
  const releaseId = requiredString(release?.release_id, "release.release_id");
@@ -28580,7 +28745,23 @@ function registerDeployTools(server, http, options = {}) {
28580
28745
  ...config.application,
28581
28746
  environmentId: options.environment || config.application.environmentId
28582
28747
  } : undefined;
28583
- if (await hasProjectReleaseManifest(configRoot)) {
28748
+ const legacyManifest = await hasProjectReleaseManifest(configRoot);
28749
+ const assetsOnly = args.assets_only === true || !application?.id && selectFrontendTarget(configRoot, config) !== null && !legacyManifest && !Object.values(config.targets ?? {}).some((target) => target.type === "edge_function");
28750
+ if (assetsOnly) {
28751
+ if (!selectFrontendTarget(configRoot, config)) {
28752
+ throw new Error("Static asset deployment requires a frontend configuration in supacloud.json");
28753
+ }
28754
+ const environmentId = options.environment?.trim() || application?.environmentId?.trim();
28755
+ if (!environmentId || !Check2(ApplicationIdSchema, environmentId)) {
28756
+ throw new Error("Static asset deployment requires SUPACLOUD_ENV");
28757
+ }
28758
+ await requireProjectDeploymentProtocol(http, projectRef, environmentId);
28759
+ const staged = await stageApplicationWebRelease(http, projectRef, environmentId, configRoot, args.skip_build === true, args.json === true, options.environmentVariables, true);
28760
+ if (!staged?.result)
28761
+ throw new Error("Static asset deployment did not produce a receipt");
28762
+ return staged.result;
28763
+ }
28764
+ if (legacyManifest) {
28584
28765
  throw new Error(`${DEPLOYMENT_UPGRADE} Replace deploy-manifest.json with the current application delivery configuration; no remote mutation was performed.`);
28585
28766
  }
28586
28767
  if (application?.id) {
@@ -28908,7 +29089,7 @@ var TEST_PROFILE = [
28908
29089
  "SUPACLOUD_API_URL=https://management.test.example.com",
28909
29090
  "SUPACLOUD_API_TOKEN=<test-management-api-token>",
28910
29091
  "SUPACLOUD_PROJECT_REF=<test-project-ref>",
28911
- "SUPACLOUD_APPLICATION_ID=<application-id>",
29092
+ "# Optional for executable application delivery: SUPACLOUD_APPLICATION_ID=<application-id>",
28912
29093
  ""
28913
29094
  ].join(`
28914
29095
  `);
@@ -28917,7 +29098,7 @@ var PRODUCTION_PROFILE = [
28917
29098
  "SUPACLOUD_API_URL=https://management.example.com",
28918
29099
  "SUPACLOUD_API_TOKEN=<production-management-api-token>",
28919
29100
  "SUPACLOUD_PROJECT_REF=<production-project-ref>",
28920
- "SUPACLOUD_APPLICATION_ID=<application-id>",
29101
+ "# Optional for executable application delivery: SUPACLOUD_APPLICATION_ID=<application-id>",
28921
29102
  ""
28922
29103
  ].join(`
28923
29104
  `);
@@ -29041,7 +29222,7 @@ async function initializeProject(request, options) {
29041
29222
  },
29042
29223
  next: [
29043
29224
  "cp .env.supacloud.test.example .env.supacloud.test",
29044
- "Set SUPACLOUD_APPLICATION_ID to the application's ID and configure its current SupaCloud delivery build.",
29225
+ "Configure frontend assets; set SUPACLOUD_APPLICATION_ID only for executable application delivery.",
29045
29226
  "supacloud env check",
29046
29227
  "supacloud --env test deploy"
29047
29228
  ]
@@ -29089,12 +29270,12 @@ function statusMissing(context) {
29089
29270
  }
29090
29271
  return ["SUPACLOUD_API_URL", "SUPACLOUD_API_TOKEN", "SUPACLOUD_PROJECT_REF"];
29091
29272
  }
29092
- function deployMissing(context, application) {
29273
+ function deployMissing(context, application, hasFrontend = false) {
29093
29274
  const missing = statusMissing(context);
29094
29275
  if (context.credentialScope !== "management") {
29095
29276
  return [...missing, "Management API context"];
29096
29277
  }
29097
- if (!Check2(ApplicationIdSchema, context.applicationId || application?.id)) {
29278
+ if (!(hasFrontend && !context.applicationId && !application) && !Check2(ApplicationIdSchema, context.applicationId || application?.id)) {
29098
29279
  missing.push("valid SUPACLOUD_APPLICATION_ID or supacloud.json application.id");
29099
29280
  }
29100
29281
  if (!Check2(ApplicationIdSchema, context.applicationEnvironmentId || application?.environmentId)) {
@@ -29114,7 +29295,8 @@ async function inspectProfile(profile) {
29114
29295
  throw error;
29115
29296
  });
29116
29297
  const config = record5(JSON.parse(configText));
29117
- const deploy = deployMissing(context, record5(config?.application));
29298
+ const hasFrontend = record5(config?.frontend) !== null || Object.values(record5(config?.targets) ?? {}).some((target) => record5(target)?.type === "frontend");
29299
+ const deploy = deployMissing(context, record5(config?.application), hasFrontend);
29118
29300
  return {
29119
29301
  name: profile.name,
29120
29302
  path: profile.path,
@@ -29210,7 +29392,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
29210
29392
  // package.json
29211
29393
  var package_default = {
29212
29394
  name: "@supacloud/cli",
29213
- version: "0.95.1",
29395
+ version: "0.96.1",
29214
29396
  description: "Project-scoped CLI for SupaCloud users",
29215
29397
  type: "module",
29216
29398
  main: "./dist/index.js",
@@ -29262,14 +29444,14 @@ var package_default = {
29262
29444
  directory: "packages/cli"
29263
29445
  },
29264
29446
  dependencies: {
29265
- "@supacloud/compiler": "^0.45.0",
29266
- "@supacloud/delivery": "0.16.0",
29267
- "@supacloud/db": "0.16.2",
29447
+ "@supacloud/compiler": "^0.46.0",
29448
+ "@supacloud/delivery": "0.17.0",
29449
+ "@supacloud/db": "0.17.0",
29268
29450
  typebox: "1.3.36"
29269
29451
  },
29270
29452
  overrides: {
29271
29453
  "@supacloud/contracts": "0.8.1",
29272
- "@supacloud/delivery": "0.16.0",
29454
+ "@supacloud/delivery": "0.17.0",
29273
29455
  "@graphql-tools/utils": "^12.0.3"
29274
29456
  },
29275
29457
  devDependencies: {
@@ -1,2 +1,2 @@
1
- export declare const DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment is no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, bind application.id and application.environmentId in supacloud.json (or SUPACLOUD_APPLICATION_ID and SUPACLOUD_ENV), then run `supacloud deploy`. SupaCloud owns deployment planning and 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`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
2
2
  export declare function assertCurrentDeploymentInput(tool: string, input: unknown): void;
@@ -1,2 +1,2 @@
1
- export declare const STARTER_ATTACHMENT_WORKER = "import { createWorker, type CompiledModule } from \"@supacloud/elysia\";\nimport type { SupaCloudWorkflowClaim, SupaCloudWorkflowsClient } from \"@supacloud/js\";\nimport { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from \"./review-attachments\";\n\nexport interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {\n modules: readonly CompiledModule[];\n workflows: Pick<SupaCloudWorkflowsClient, \"claim\" | \"complete\" | \"retry\" | \"fail\">;\n workerId: string;\n queueOwnership: \"exclusive-review-attachments\";\n pollIntervalMs?: number;\n}\n\nexport async function createReviewAttachmentWorker(options: ReviewAttachmentWorkerOptions) {\n if (options.queueOwnership !== \"exclusive-review-attachments\") {\n throw new Error(\"Attachment worker requires an exclusively owned Workflow queue\");\n }\n const { store } = await createReviewAttachmentAdapters(options);\n const { workflows, workerId } = options;\n const failure = Promise.withResolvers<never>();\n // The executable attaches its fatal listener after this async factory returns.\n void failure.promise.catch(() => {});\n let halted: boolean = false;\n const fatal = () => {\n halted = true;\n failure.reject(new Error(\"Attachment worker requires recovery\"));\n };\n const attempt = (claim: SupaCloudWorkflowClaim) => ({\n stepId: claim.stepId, messageId: claim.messageId, attempt: claim.attempt, workerId: claim.workerId,\n });\n async function ownedClaim(claim: SupaCloudWorkflowClaim) {\n if (claim.workflowName !== \"review.verify-attachment\" || claim.workflowVersion !== \"1\"\n || claim.stepKey !== \"verify\" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {\n throw new Error(\"Unsupported attachment workflow\");\n }\n const { reviewId, artifactId } = claim.input;\n if (typeof reviewId !== \"string\" || typeof artifactId !== \"string\") {\n throw new Error(\"Invalid attachment workflow input\");\n }\n const bound = await options.database.transaction(tx => tx.query(\n \"SELECT run_id FROM public.starter_attachments WHERE review_id=$1 AND artifact_id=$2 AND run_id=$3\",\n [reviewId, artifactId, claim.runId],\n ));\n if (!Array.isArray(bound) || bound.length !== 1) throw new Error(\"Attachment workflow ownership mismatch\");\n }\n const worker = createWorker({\n modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,\n pollIntervalMs: options.pollIntervalMs ?? 1000,\n onError: fatal,\n transport: {\n async claim(signal): Promise<SupaCloudWorkflowClaim | null> {\n if (halted || signal.aborted) return null;\n try {\n const claim = await workflows.claim({ workerId, visibilityTimeoutSeconds: 120 });\n if (signal.aborted || halted) return null;\n if (!claim || claim.status !== \"claimed\") return null;\n await ownedClaim(claim);\n return claim;\n } catch {\n if (!signal.aborted) fatal();\n throw new Error(\"Attachment workflow claim failed\");\n }\n },\n async ack(claim, output) {\n try {\n if (!output || typeof output !== \"object\" || Array.isArray(output)) throw new Error(\"Invalid Job output\");\n // The compiled Job validates output; Workflow SDK validates the receipt.\n await workflows.complete({ ...attempt(claim), runOutput: { ...output } });\n } catch {\n fatal();\n throw new Error(\"Attachment workflow completion is unconfirmed\");\n }\n },\n async fail(claim) {\n try {\n const request: Parameters<typeof workflows.fail>[0] = { ...attempt(claim), errorMessage: \"Attachment verification failed\" };\n if (claim.attempt < claim.maxAttempts) await workflows.retry({ ...request, delaySeconds: 5 });\n else await workflows.fail(request);\n } catch {\n fatal();\n throw new Error(\"Attachment workflow failure receipt is unconfirmed\");\n }\n },\n },\n mapClaim: (claim: SupaCloudWorkflowClaim) => ({\n id: claim.stepId, jobName: claim.workflowName, input: claim.input, attempt: claim.attempt,\n requestContext: { workerId, jobId: claim.stepId, requestId: claim.runId },\n }),\n });\n if (worker.jobNames.length !== 1 || worker.jobNames[0] !== \"review.verify-attachment\") {\n throw new Error(\"Attachment worker requires its compiled reference Job\");\n }\n let closing: Promise<void> | undefined;\n return {\n failure: failure.promise,\n async start() {\n if (halted) throw new Error(\"Attachment worker is not startable\");\n await worker.start();\n },\n close() {\n halted = true;\n return closing ??= worker.stop();\n },\n };\n}\n";
1
+ export declare const STARTER_ATTACHMENT_WORKER = "import { createWorker, type CompiledModule } from \"@supacloud/elysia\";\nimport {\n createSupaCloudWorkflowTransport,\n type SupaCloudWorkflowClaim,\n type SupaCloudWorkflowsClient,\n} from \"@supacloud/js\";\nimport { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from \"./review-attachments\";\n\nexport interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {\n modules: readonly CompiledModule[];\n workflows: Pick<SupaCloudWorkflowsClient, \"claim\" | \"complete\" | \"retry\" | \"fail\">;\n workerId: string;\n queueOwnership: \"exclusive-review-attachments\";\n pollIntervalMs?: number;\n}\n\nexport async function createReviewAttachmentWorker(options: ReviewAttachmentWorkerOptions) {\n if (options.queueOwnership !== \"exclusive-review-attachments\") {\n throw new Error(\"Attachment worker requires an exclusively owned Workflow queue\");\n }\n const { store } = await createReviewAttachmentAdapters(options);\n const { workflows, workerId } = options;\n const failure = Promise.withResolvers<never>();\n // The executable attaches its fatal listener after this async factory returns.\n void failure.promise.catch(() => {});\n let halted: boolean = false;\n const fatal = () => {\n halted = true;\n failure.reject(new Error(\"Attachment worker requires recovery\"));\n };\n async function ownedClaim(claim: SupaCloudWorkflowClaim) {\n if (claim.workflowName !== \"review.verify-attachment\" || claim.workflowVersion !== \"1\"\n || claim.stepKey !== \"verify\" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {\n throw new Error(\"Unsupported attachment workflow\");\n }\n const { reviewId, artifactId } = claim.input;\n if (typeof reviewId !== \"string\" || typeof artifactId !== \"string\") {\n throw new Error(\"Invalid attachment workflow input\");\n }\n const bound = await options.database.transaction(tx => tx.query(\n \"SELECT run_id FROM public.starter_attachments WHERE review_id=$1 AND artifact_id=$2 AND run_id=$3\",\n [reviewId, artifactId, claim.runId],\n ));\n if (!Array.isArray(bound) || bound.length !== 1) throw new Error(\"Attachment workflow ownership mismatch\");\n }\n const workflowTransport = createSupaCloudWorkflowTransport({\n workflows, workerId, visibilityTimeoutSeconds: 120, retryDelaySeconds: 5, acceptClaim: ownedClaim,\n });\n const worker = createWorker({\n modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,\n pollIntervalMs: options.pollIntervalMs ?? 1000,\n onError: fatal,\n transport: {\n async claim(signal): Promise<SupaCloudWorkflowClaim | null> {\n if (halted || signal.aborted) return null;\n try {\n return await workflowTransport.claim(signal);\n } catch {\n if (!signal.aborted) fatal();\n throw new Error(\"Attachment workflow claim failed\");\n }\n },\n async ack(claim, output) {\n try {\n await workflowTransport.ack(claim, output);\n } catch {\n fatal();\n throw new Error(\"Attachment workflow completion is unconfirmed\");\n }\n },\n async fail(claim) {\n try {\n await workflowTransport.fail(claim, \"Attachment verification failed\");\n } catch {\n fatal();\n throw new Error(\"Attachment workflow failure receipt is unconfirmed\");\n }\n },\n },\n mapClaim: (claim: SupaCloudWorkflowClaim) => ({\n id: claim.stepId, jobName: claim.workflowName, input: claim.input, attempt: claim.attempt,\n requestContext: { workerId, jobId: claim.stepId, requestId: claim.runId },\n }),\n });\n if (worker.jobNames.length !== 1 || worker.jobNames[0] !== \"review.verify-attachment\") {\n throw new Error(\"Attachment worker requires its compiled reference Job\");\n }\n let closing: Promise<void> | undefined;\n return {\n failure: failure.promise,\n async start() {\n if (halted) throw new Error(\"Attachment worker is not startable\");\n await worker.start();\n },\n close() {\n halted = true;\n return closing ??= worker.stop();\n },\n };\n}\n";
2
2
  export declare const STARTER_ATTACHMENT_DELIVERY_WORKER = "import { SQL } from \"bun\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { SupaCloudWorkflowsClient } from \"@supacloud/js\";\nimport { createBunCommandDatabase } from \"@supacloud/db/bun\";\nimport type { CompiledModule } from \"@supacloud/elysia\";\nimport { createReviewAttachmentWorker } from \"./host/review-attachment-worker\";\n\nfunction required(name: string): string {\n const value = process.env[name];\n if (!value?.trim()) throw new Error(\"Missing required worker 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 createDeliveryWorker(modules: CompiledModule[], lifecycle: { signal: AbortSignal }) {\n lifecycle.signal.throwIfAborted();\n if (required(\"REVIEW_QUEUE_OWNERSHIP\") !== \"exclusive-review-attachments\") {\n throw new Error(\"Attachment worker requires an exclusively owned Workflow queue\");\n }\n const connection = databaseConnection();\n const settings = {\n projectId: required(\"SUPACLOUD_PROJECT_ID\"), tenantId: required(\"APP_TENANT_ID\"),\n workerId: required(\"REVIEW_WORKER_ID\"), url: required(\"SUPACLOUD_URL\"), key: required(\"SUPACLOUD_SERVICE_ROLE_KEY\"),\n };\n const service = createClient(settings.url, settings.key, {\n auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n });\n const pool = new SQL({ ...connection, max: 4, connectionTimeout: 5 });\n let closing: Promise<void> | undefined;\n const closePool = () => closing ??= pool.close({ timeout: 5 });\n try {\n const worker = await createReviewAttachmentWorker({\n database: createBunCommandDatabase(pool), service, projectId: settings.projectId,\n tenantId: settings.tenantId, modules, workerId: settings.workerId,\n workflows: new SupaCloudWorkflowsClient(service), queueOwnership: \"exclusive-review-attachments\",\n });\n lifecycle.signal.throwIfAborted();\n return {\n start: () => worker.start(), failure: worker.failure,\n async close() { try { await worker.close(); } finally { await closePool(); } },\n };\n } catch (error) {\n await closePool();\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-worker-initialization-failed\", code }));\n throw new Error(\"Attachment worker initialization failed\");\n }\n}\n";
@@ -10,5 +10,7 @@ export declare const STARTER_DATABASE_CONFIG: {
10
10
  role: string;
11
11
  };
12
12
  export declare const STARTER_DATABASE_SCHEMA = "import { sql } from \"drizzle-orm\";\nimport { boolean, check, integer, jsonb, pgTable, primaryKey, text, uuid } from \"drizzle-orm/pg-core\";\n\n// Structure for the reviewed starter migrations. Grants/policies stay in those migrations.\nexport const application = pgTable.withRLS(\"starter_application\", {\n singleton: boolean().default(true).primaryKey(),\n projectId: text(\"project_id\").notNull(),\n tenantId: text(\"tenant_id\").notNull(),\n}, table => [\n check(\"starter_application_singleton_check\", sql`${table.singleton}`),\n check(\"starter_application_project_id_check\", sql`length(${table.projectId}) > 0`),\n check(\"starter_application_tenant_id_check\", sql`length(${table.tenantId}) > 0`),\n]);\n\nexport const reviews = pgTable.withRLS(\"starter_reviews\", {\n id: text().primaryKey(),\n ownerId: text(\"owner_id\").notNull(),\n state: text({ enum: [\"draft\", \"approved\"] }).notNull(),\n version: integer().notNull(),\n}, table => [\n check(\"starter_reviews_state_check\", sql`${table.state} IN ('draft', 'approved')`),\n check(\"starter_reviews_version_check\", sql`${table.version} > 0`),\n]);\n\nexport const members = pgTable.withRLS(\"starter_members\", {\n subject: text().primaryKey(),\n enabled: boolean().default(true).notNull(),\n canApprove: boolean(\"can_approve\").default(false).notNull(),\n storageSubject: uuid(\"storage_subject\").unique(),\n});\n\nexport const attachments = pgTable.withRLS(\"starter_attachments\", {\n reviewId: text(\"review_id\").primaryKey().references(() => reviews.id),\n artifactId: uuid(\"artifact_id\").notNull().unique(),\n ownerId: text(\"owner_id\").notNull(),\n runId: uuid(\"run_id\").notNull().unique(),\n objectPath: text(\"object_path\").notNull().unique(),\n});\n\nexport const attachmentResults = pgTable.withRLS(\"starter_attachment_results\", {\n reviewId: text(\"review_id\").notNull().references(() => reviews.id),\n version: integer().notNull(),\n artifactId: uuid(\"artifact_id\").notNull(),\n result: jsonb().$type<unknown>().notNull(),\n}, table => [\n primaryKey({ columns: [table.reviewId, table.version, table.artifactId] }),\n check(\"starter_attachment_results_version_check\", sql`${table.version} >= 2`),\n]);\n\nexport type ReviewRow = typeof reviews.$inferSelect;\nexport type NewReview = typeof reviews.$inferInsert;\n";
13
- export declare const STARTER_DATABASE_GUIDE = "\n## Database Source Workflow\n\nDrizzle owns table declarations and inferred row types in db/schema.ts.\nUse the existing @supacloud/db/drizzle-bun adapters for typed queries and governed\ntransactions; never replace permission, idempotency or audit checks with plain CRUD.\nOpaque JSON still needs runtime decoding.\n\nAfter installing dependencies, run `bun run db:generate` once and commit db/contracts/.\nThe default `bun run check` and build fail on stale database contracts before any\ncompiler regeneration. After an intentional source change, run db:generate again,\nreview the diff, then run check. Ordinary checks are offline.\nThe compiler also runs this source-contract gate on compile/check/dev through\ndatabaseGovernance.mode = \"check\", including incremental cache hits. Missing\nadoption files or stale contracts fail closed; no contracts are silently regenerated.\nThis is not SQL lint or live Catalog reconciliation. Before promotion, declare\nthe real database modules, their SQL and tests, then explicitly use mode \"enforce\"\nwith a separately selected read-only local/test database connection.\n\n- Maintain one schema-qualified function with explicit grants per db/functions/*.sql.\n- Preserve migrations/ and its existing executor/ledger. Add forward migrations only.\n- `bun run db:diff` generates Drizzle drafts under db/migration-candidates/, never\n into the deployment directory. The starter has existing SQL history, so establish\n and review a matching Drizzle baseline before promoting any draft; the first\n draft is not automatically an incremental production migration.\n- `bun run db:pull` requires an explicitly selected DATABASE_URL and writes only\n output/database-audit/drizzle-candidate/. Review candidates; never overwrite source blindly.\n- Full SQL dumps belong under the ignored output/database-audit/ directory. They are\n audit/recovery artifacts, never ordinary type, application or test inputs.\n- `bun run db:assess` explains adoption/findings without writing or connecting.\n- GraphQL/PostgREST exports remain legitimate protocol contracts. This check is not\n proof of deployed schema, effective role permissions or complete RLS equivalence.\n\nDo not introduce any/@ts-ignore to bypass a contract. Use Drizzle-inferred types,\npreserve bigint/numeric precision and decode unknown results at the boundary.\n";
13
+ export declare const STARTER_DATABASE_MODULES = "import { defineDatabaseModule } from \"@supacloud/db\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport { application, reviews, members, attachments, attachmentResults } from \"./schema\";\n\nconst backendPolicies = [\n [\"starter_backend_binding_read\", \"public.starter_application\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_binding_lock\", \"public.starter_application\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_binding_immutable\", \"public.starter_application\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_member_read\", \"public.starter_members\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_member_lock\", \"public.starter_members\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_member_immutable\", \"public.starter_members\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_review_read\", \"public.starter_reviews\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_review_update\", \"public.starter_reviews\", \"update\", [\"starter_review_http\"]],\n [\"starter_worker_review_immutable\", \"public.starter_reviews\", \"update\", [\"starter_review_worker\"]],\n [\"starter_backend_attachment_read\", \"public.starter_attachments\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_attachment_lock\", \"public.starter_attachments\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_attachment_immutable\", \"public.starter_attachments\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_http_attachment_insert\", \"public.starter_attachments\", \"insert\", [\"starter_review_http\"]],\n [\"starter_worker_result_read\", \"public.starter_attachment_results\", \"select\", [\"starter_review_worker\"]],\n [\"starter_worker_result_insert\", \"public.starter_attachment_results\", \"insert\", [\"starter_review_worker\"]],\n] as const;\n\n// The module is the single live-Catalog ownership contract for starter tables.\nexport default defineDatabaseModule({\n name: \"starter\",\n tables: [application, reviews, members, attachments, attachmentResults].map(table => {\n const config = getTableConfig(table);\n return (config.schema ?? \"public\") + \".\" + config.name;\n }),\n policies: [\n ...backendPolicies.map(([name, table, operation, roles]) => ({\n name, table, operation, roles: [...roles], source: \"migrations/004-review-runtime-roles.sql\",\n tests: [\"tests/database-governance.sql\"],\n })),\n {\n name: \"starter_member_read\", table: \"public.starter_members\", operation: \"select\",\n roles: [\"authenticated\"], source: \"migrations/003-review-uploads.sql\",\n tests: [\"tests/database-governance.sql\"],\n },\n {\n name: \"starter_review_read\", table: \"public.starter_reviews\", operation: \"select\",\n roles: [\"authenticated\"], source: \"migrations/003-review-uploads.sql\",\n tests: [\"tests/database-governance.sql\"],\n },\n ],\n});\n";
14
+ export declare const STARTER_DATABASE_TEST = "-- Read-only catalog assertions; behavioral authorization tests remain separate.\nBEGIN READ ONLY;\nDO $test$\nDECLARE\n owned text[] := ARRAY['starter_application','starter_reviews','starter_members',\n 'starter_attachments','starter_attachment_results'];\nBEGIN\n IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace\n WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN\n RAISE EXCEPTION 'All starter tables must exist with RLS enabled';\n END IF;\n IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid\n JOIN pg_namespace n ON n.oid=c.relnamespace\n WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN\n RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';\n END IF;\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')\n AND (rolsuper OR rolbypassrls)) THEN\n RAISE EXCEPTION 'Starter roles must not bypass row security';\n END IF;\n IF (SELECT count(*) FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')) <> 2 THEN\n RAISE EXCEPTION 'Starter runtime roles are missing';\n END IF;\nEND\n$test$;\nROLLBACK;\n";
15
+ export declare const STARTER_DATABASE_GUIDE = "\n## Database Source Workflow\n\nDrizzle owns table declarations and inferred row types in db/schema.ts.\nUse the existing @supacloud/db/drizzle-bun adapters for typed queries and governed\ntransactions; never replace permission, idempotency or audit checks with plain CRUD.\nOpaque JSON still needs runtime decoding.\n\nAfter installing dependencies, run `bun run db:generate` once and commit db/contracts/.\nThe default `bun run check` and build fail on stale database contracts before any\ncompiler regeneration. After an intentional source change, run db:generate again,\nreview the diff, then run check.\nThe command starter registers databaseGovernance.moduleFile = \"db/modules.ts\" and\nkeeps the default `databaseGovernance.mode = \"check\"`: compile/check/build run\nthe offline source-contract gate without a database. Use `bun run dev:integration`\nwith the CLI's explicit loopback database URL, or set databaseGovernance.mode to\n\"enforce\" with an explicitly supplied `DATABASE_URL`, to run SQL lint and live\nCatalog reconciliation. Missing connections, stale source contracts and Catalog\ndrift fail closed; no contracts or migrations are silently regenerated.\n\n- Maintain one schema-qualified function with explicit grants per db/functions/*.sql.\n- Preserve migrations/ and its existing executor/ledger. Add forward migrations only.\n- `bun run db:diff` generates Drizzle drafts under db/migration-candidates/, never\n into the deployment directory. The starter has existing SQL history, so establish\n and review a matching Drizzle baseline before promoting any draft; the first\n draft is not automatically an incremental production migration.\n- `bun run db:pull` requires an explicitly selected DATABASE_URL and writes only\n output/database-audit/drizzle-candidate/. Review candidates; never overwrite source blindly.\n- Full SQL dumps belong under the ignored output/database-audit/ directory. They are\n audit/recovery artifacts, never ordinary type, application or test inputs.\n- `bun run db:assess` explains adoption/findings without writing or connecting.\n- GraphQL/PostgREST exports remain legitimate protocol contracts. This check is not\n proof of deployed schema, effective role permissions or complete RLS equivalence.\n\nDo not introduce any/@ts-ignore to bypass a contract. Use Drizzle-inferred types,\npreserve bigint/numeric precision and decode unknown results at the boundary.\n";
14
16
  export declare function starterDatabaseFiles(): Record<string, string>;
@@ -1,3 +1,3 @@
1
1
  export declare const STARTER_UPLOAD_FEATURE = "import { Effect } from \"effect\";\nimport { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from \"@supacloud/app\";\nimport { ApplicationError } from \"@supacloud/elysia\";\nimport { UploadBody, UploadLocation, UploadParams, UploadResult } from \"./contracts\";\n\nexport interface ReviewUploadInput { reviewId: string; artifactId: string; expectedVersion: number }\nexport interface ReviewUploadLocation { artifactId: string; bucketId: string; objectPath: string }\nexport interface ReviewUploadResult { artifactId: string; runId: string; objectPath: string; sha256: string; bytes: number }\ntype PreviewUploadError =\n | { readonly _tag: \"AUTHENTICATION_REQUIRED\" }\n | { readonly _tag: \"UPLOAD_INPUT_INVALID\" }\n | { readonly _tag: \"UPLOAD_DENIED\" }\n | { readonly _tag: \"UPLOADS_UNAVAILABLE\" };\ntype RegisterUploadError =\n | PreviewUploadError\n | { readonly _tag: \"UPLOAD_CONTENT_INVALID\" }\n | { readonly _tag: \"COMMAND_IDEMPOTENCY_CONFLICT\" }\n | { readonly _tag: \"COMMAND_REJECTED\" }\n | { readonly _tag: \"UPLOAD_OUTCOME_UNKNOWN\" };\nexport interface ReviewUploadPort {\n previewUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadLocation>;\n registerUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadResult>;\n}\n@Injectable({ scope: \"request\" })\nexport class ReviewUploads {\n constructor(@Inject(DB_CLIENT) private readonly store: Partial<ReviewUploadPort>,\n @Inject(REQUEST_CONTEXT) private readonly context: unknown) {}\n\n preview(input: ReviewUploadInput): Promise<ReviewUploadLocation> {\n if (!this.store.previewUpload) throw new ApplicationError(\"Attachment uploads are not configured\", { status: 501, code: \"UPLOADS_UNAVAILABLE\" });\n return this.store.previewUpload(this.context, input);\n }\n register(input: ReviewUploadInput): Promise<ReviewUploadResult> {\n if (!this.store.registerUpload) throw new ApplicationError(\"Attachment uploads are not configured\", { status: 501, code: \"UPLOADS_UNAVAILABLE\" });\n return this.store.registerUpload(this.context, input);\n }\n}\n\n@Controller(\"/reviews\")\nexport class ReviewUploadsController {\n constructor(@Inject(ReviewUploads) private readonly uploads: ReviewUploads) {}\n\n @Post(\"/:id/attachment-upload\", {\n params: UploadParams,\n body: UploadBody,\n responses: { 200: UploadLocation },\n effect: {\n required: true,\n dependencies: [],\n errors: [\n { tag: \"AUTHENTICATION_REQUIRED\", status: 401, code: \"AUTHENTICATION_REQUIRED\" },\n { tag: \"UPLOAD_INPUT_INVALID\", status: 400, code: \"UPLOAD_INPUT_INVALID\" },\n { tag: \"UPLOAD_DENIED\", status: 403, code: \"UPLOAD_DENIED\" },\n { tag: \"UPLOADS_UNAVAILABLE\", status: 501, code: \"UPLOADS_UNAVAILABLE\" },\n ],\n retry: \"none\",\n },\n })\n preview(@Param(\"id\") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadLocation, PreviewUploadError, never> {\n return Effect.promise(() => this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));\n }\n @Post(\"/:id/attachment-registration\", {\n params: UploadParams,\n body: UploadBody,\n responses: { 200: UploadResult },\n effect: {\n required: true,\n dependencies: [],\n errors: [\n { tag: \"AUTHENTICATION_REQUIRED\", status: 401, code: \"AUTHENTICATION_REQUIRED\" },\n { tag: \"UPLOAD_INPUT_INVALID\", status: 400, code: \"UPLOAD_INPUT_INVALID\" },\n { tag: \"UPLOAD_DENIED\", status: 403, code: \"UPLOAD_DENIED\" },\n { tag: \"UPLOAD_CONTENT_INVALID\", status: 400, code: \"UPLOAD_CONTENT_INVALID\" },\n { tag: \"COMMAND_IDEMPOTENCY_CONFLICT\", status: 409, code: \"COMMAND_IDEMPOTENCY_CONFLICT\" },\n { tag: \"COMMAND_REJECTED\", status: 403, code: \"COMMAND_REJECTED\" },\n { tag: \"UPLOADS_UNAVAILABLE\", status: 501, code: \"UPLOADS_UNAVAILABLE\" },\n { tag: \"UPLOAD_OUTCOME_UNKNOWN\", status: 503, code: \"UPLOAD_OUTCOME_UNKNOWN\" },\n ],\n retry: \"none\",\n },\n })\n register(@Param(\"id\") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadResult, RegisterUploadError, never> {\n return Effect.promise(() => this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));\n }\n}\n";
2
- export declare const STARTER_UPLOAD_SCHEMA = "-- Apply explicitly with the migration owner.\nALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;\nGRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;\nCREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated\n USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());\nGRANT SELECT ON public.starter_reviews TO authenticated;\nCREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated\n USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (\n SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id\n AND m.enabled AND m.can_approve AND m.storage_subject=auth.uid()\n ));\nINSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)\n VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);\nCREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated\n USING (bucket_id='review-attachments');\nCREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated\n WITH CHECK (bucket_id='review-attachments');\n-- Restrictive fences also apply when the host already has permissive Storage policies.\nCREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated\n USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )))\n WITH CHECK (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text\n AND name ~ '^[a-f0-9-]{36}/[a-f0-9-]{36}/[a-f0-9-]{36}[.]txt$' AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )));\nCREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated\n USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');\nCREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated\n USING (bucket_id<>'review-attachments');\n";
2
+ export declare const STARTER_UPLOAD_SCHEMA = "-- Apply explicitly with the migration owner.\nALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;\nGRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;\nDROP POLICY IF EXISTS starter_member_read ON public.starter_members;\nCREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated\n USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());\nGRANT SELECT ON public.starter_reviews TO authenticated;\nDROP POLICY IF EXISTS starter_review_read ON public.starter_reviews;\nCREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated\n USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (\n SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id\n AND m.enabled AND m.can_approve AND m.storage_subject=auth.uid()\n ));\nINSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)\n VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);\nDROP POLICY IF EXISTS starter_attachment_read ON storage.objects;\nCREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated\n USING (bucket_id='review-attachments');\nDROP POLICY IF EXISTS starter_attachment_insert ON storage.objects;\nCREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated\n WITH CHECK (bucket_id='review-attachments');\n-- Restrictive fences also apply when the host already has permissive Storage policies.\nDROP POLICY IF EXISTS starter_attachment_owner_fence ON storage.objects;\nCREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated\n USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )))\n WITH CHECK (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text\n AND name ~ '^[a-f0-9-]{36}/[a-f0-9-]{36}/[a-f0-9-]{36}[.]txt$' AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )));\nDROP POLICY IF EXISTS starter_attachment_update_fence ON storage.objects;\nCREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated\n USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');\nDROP POLICY IF EXISTS starter_attachment_delete_fence ON storage.objects;\nCREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated\n USING (bucket_id<>'review-attachments');\n";
3
3
  export declare const STARTER_UPLOAD_ADAPTER = "import { MIMEType } from \"node:util\";\nimport { SupaCloudArtifactsClient } from \"@supacloud/js\";\nimport { createTransactionalCommand, plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandTransaction } from \"@supacloud/db\";\nimport { ApplicationError, requireTrustedIdentity } from \"@supacloud/elysia\";\nimport type { ReviewAttachmentOptions } from \"./review-attachments\";\nimport type { ReviewUploadInput, ReviewUploadPort, ReviewUploadResult } from \"../review/uploads\";\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 upload record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid upload database result\");\n return value.map(record);\n}\nfunction capture(value: unknown): ReviewUploadInput {\n const item = record(value), uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n if (typeof item[\"reviewId\"] !== \"string\" || !uuid.test(item[\"reviewId\"])\n || typeof item[\"artifactId\"] !== \"string\" || !uuid.test(item[\"artifactId\"])\n || typeof item[\"expectedVersion\"] !== \"number\" || !Number.isSafeInteger(item[\"expectedVersion\"]) || item[\"expectedVersion\"] < 1) {\n throw new ApplicationError(\"Invalid upload input\", { status: 400, code: \"UPLOAD_INPUT_INVALID\" });\n }\n return { reviewId: item[\"reviewId\"].toLowerCase(), artifactId: item[\"artifactId\"].toLowerCase(), expectedVersion: item[\"expectedVersion\"] };\n}\nfunction result(value: unknown): ReviewUploadResult {\n const item = record(value);\n if (typeof item[\"artifactId\"] !== \"string\" || typeof item[\"runId\"] !== \"string\" || typeof item[\"objectPath\"] !== \"string\"\n || typeof item[\"sha256\"] !== \"string\" || typeof item[\"bytes\"] !== \"number\") throw new TypeError(\"Invalid upload receipt\");\n return { artifactId: item[\"artifactId\"], runId: item[\"runId\"], objectPath: item[\"objectPath\"], sha256: item[\"sha256\"], bytes: item[\"bytes\"] };\n}\n\nexport async function createReviewUploadAdapters(options: ReviewAttachmentOptions): Promise<ReviewUploadPort> {\n const { database, service, projectId, tenantId } = options;\n const binding = rows(await database.transaction(tx => tx.query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n )));\n if (!projectId.trim() || !tenantId.trim() || binding.length !== 1\n || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Upload database project/tenant binding mismatch\");\n }\n const artifacts = new SupaCloudArtifactsClient(service);\n function actor(context: unknown): string {\n const who = requireTrustedIdentity(context);\n const access = record(record(context)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Upload permission denied\", { status: 403, code: \"UPLOAD_DENIED\" });\n }\n return who.subject;\n }\n async function authorize(tx: CommandTransaction, subject: string, input: ReviewUploadInput, replay = false) {\n const allowed = rows(await tx.query(\n \"SELECT m.storage_subject::text AS storage_subject FROM public.starter_reviews r \" +\n \"JOIN public.starter_members m ON m.subject=r.owner_id CROSS JOIN public.starter_application p \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND ((r.state='draft' AND r.version=$3) OR \" +\n \"($6::boolean AND r.state='approved' AND r.version=$3+1 AND EXISTS (\" +\n \"SELECT 1 FROM public.starter_attachments b WHERE b.review_id=r.id AND b.artifact_id=$7::uuid AND b.owner_id=$2 \" +\n \"AND b.object_path=m.storage_subject::text || '/' || r.id || '/' || $7::text || '.txt'))) \" +\n \"AND m.enabled AND m.can_approve AND m.storage_subject IS NOT NULL \" +\n \"AND p.singleton AND p.project_id=$4 AND p.tenant_id=$5 FOR UPDATE OF r,m FOR SHARE OF p\",\n [input.reviewId, subject, input.expectedVersion, projectId, tenantId, replay, input.artifactId],\n ));\n if (allowed.length !== 1 || typeof allowed[0]?.[\"storage_subject\"] !== \"string\") {\n throw new ApplicationError(\"Upload ownership, permission or revision changed\", { status: 403, code: \"UPLOAD_DENIED\" });\n }\n return allowed[0][\"storage_subject\"] + \"/\" + input.reviewId + \"/\" + input.artifactId + \".txt\";\n }\n return {\n async previewUpload(context, value) {\n const input = capture(value), subject = actor(context);\n const objectPath = await database.transaction(tx => authorize(tx, subject, input));\n return { artifactId: input.artifactId, bucketId: \"review-attachments\", objectPath };\n },\n async registerUpload(context, value) {\n const input = capture(value), subject = actor(context);\n const objectPath = await database.transaction(tx => authorize(tx, subject, input, true));\n try {\n const downloaded = await service.storage.from(\"review-attachments\").download(objectPath);\n if (downloaded.error || !downloaded.data) throw new Error(\"Upload unavailable\");\n const blob = downloaded.data;\n if (blob.size < 1 || blob.size > 1048576 || new MIMEType(blob.type).essence !== \"text/plain\") {\n throw new ApplicationError(\"Unsupported upload\", { status: 400, code: \"UPLOAD_CONTENT_INVALID\" });\n }\n const sha256 = Array.from(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", await blob.arrayBuffer())),\n byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n await artifacts.register({ artifactId: input.artifactId, bucketId: \"review-attachments\", objectPath,\n artifactType: \"review.attachment\", sha256, sizeBytes: blob.size, mimeType: \"text/plain\" });\n const command = createTransactionalCommand({\n name: \"review.attach\", store: createPostgresCommandStore(database), inputCodec: plaintextCommandInput,\n input: capture, result,\n async authorize(who, request, tx) {\n try {\n const currentPath = await authorize(tx, who.actorId, request, true);\n return currentPath === objectPath ? \"allow\" : \"deny\";\n } catch (error) {\n if (error instanceof ApplicationError && error.code === \"UPLOAD_DENIED\") return \"deny\";\n throw error;\n }\n },\n async execute(tx, request) {\n const existing = rows(await tx.query(\"SELECT artifact_id FROM public.starter_attachments WHERE review_id=$1\", [request.reviewId]));\n if (existing.length) throw new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\");\n const bound: ReviewUploadResult = { artifactId: request.artifactId, runId: request.artifactId, objectPath, sha256, bytes: blob.size };\n await tx.query(\"INSERT INTO public.starter_attachments VALUES($1,$2,$3,$4,$5)\",\n [request.reviewId, bound.artifactId, subject, bound.runId, bound.objectPath]);\n return bound;\n },\n audit: { event: \"review.attachment-bound\", details: request => ({ reviewId: request.reviewId, artifactId: request.artifactId }) },\n });\n const receipt = await command.execute({ actorId: subject, tenantId }, input.artifactId, input);\n if (receipt.status !== \"confirmed\") throw new CommandError(\"COMMAND_OUTCOME_UNKNOWN\");\n return receipt.result;\n } catch (error) {\n if (error instanceof ApplicationError) throw error;\n if (error instanceof CommandError && error.code === \"COMMAND_IDEMPOTENCY_CONFLICT\") {\n throw new ApplicationError(\"Conflicting attachment binding\", { status: 409, code: error.code });\n }\n if (error instanceof CommandError && error.code === \"COMMAND_REJECTED\") {\n throw new ApplicationError(\"Upload permission denied\", { status: 403, code: error.code });\n }\n // Registration may have committed even when binding failed; never delete immutable evidence here.\n throw new ApplicationError(\"Upload registration could not be confirmed\", { status: 503, code: \"UPLOAD_OUTCOME_UNKNOWN\" });\n }\n },\n };\n}\n";
@@ -26,6 +26,7 @@ export declare const deployToolSchema: {
26
26
  ref: Type.TOptional<Type.TString>;
27
27
  cwd: Type.TOptional<Type.TString>;
28
28
  skip_build: Type.TOptional<Type.TBoolean>;
29
+ assets_only: Type.TOptional<Type.TBoolean>;
29
30
  json: Type.TOptional<Type.TBoolean>;
30
31
  };
31
32
  export declare function findDeployConfigRoot(startDirectory: string, environmentName?: string): Promise<string>;
@@ -15,7 +15,7 @@ export declare const environmentToolSchema: ToolSchema;
15
15
  declare function outputDirectoryFor(manifest: PackageManifest): string;
16
16
  declare function profileNameFromFile(name: string): string | null;
17
17
  declare function statusMissing(context: ResolvedContext): string[];
18
- declare function deployMissing(context: ResolvedContext, application?: Record<string, unknown> | null): string[];
18
+ declare function deployMissing(context: ResolvedContext, application?: Record<string, unknown> | null, hasFrontend?: boolean): string[];
19
19
  export declare function registerDeveloperExperienceTools(server: ToolServer, options?: DeveloperExperienceOptions): void;
20
20
  export declare const __internal: {
21
21
  outputDirectoryFor: typeof outputDirectoryFor;
@@ -46,6 +46,7 @@ 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
50
  export declare function rollbackFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, releaseId?: string): Promise<ToolResponse>;
50
51
  export declare function activateFrontendRelease(http: FrontendReleaseHttp, input: {
51
52
  projectRef: string;
@@ -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 is no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, bind application.id and application.environmentId in supacloud.json (or SUPACLOUD_APPLICATION_ID and SUPACLOUD_ENV), then run `supacloud deploy`. SupaCloud owns deployment planning and 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`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. 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.95.1",
3
+ "version": "0.96.1",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,14 +52,14 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.45.0",
56
- "@supacloud/delivery": "0.16.0",
57
- "@supacloud/db": "0.16.2",
55
+ "@supacloud/compiler": "^0.46.0",
56
+ "@supacloud/delivery": "0.17.0",
57
+ "@supacloud/db": "0.17.0",
58
58
  "typebox": "1.3.36"
59
59
  },
60
60
  "overrides": {
61
61
  "@supacloud/contracts": "0.8.1",
62
- "@supacloud/delivery": "0.16.0",
62
+ "@supacloud/delivery": "0.17.0",
63
63
  "@graphql-tools/utils": "^12.0.3"
64
64
  },
65
65
  "devDependencies": {