@supacloud/cli 0.101.4 → 0.103.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/dist/index.js CHANGED
@@ -9317,7 +9317,7 @@ var ACTION_POLICY = {
9317
9317
  env: { local: ["list", "check"] },
9318
9318
  app: {
9319
9319
  local: ["init", "generate", "dev", "watch", "verify-plan", "compile", "check", "graph", "explain", "export-tools", "context", "doctor", "fix", "plan", "build"],
9320
- read: ["status", "logs", "transfer-plan", "promote-plan", "rollback-plan", "history", "deploy-plan", "diff", "promote-status", "preview-plan", "previews", "preview-list", "preview-status"],
9320
+ read: ["status", "evidence", "logs", "transfer-plan", "promote-plan", "rollback-plan", "history", "deploy-plan", "diff", "promote-status", "preview-plan", "previews", "preview-list", "preview-status"],
9321
9321
  write: ["release", "upload", "configure", "transfer", "promote", "promote-reconcile", "deploy", "rollback", "reconcile", "retire", "preview", "preview-create", "preview-reconcile", "preview-cleanup"]
9322
9322
  },
9323
9323
  db: {
@@ -10955,6 +10955,65 @@ function parseDeploymentEvidence(value) {
10955
10955
  }
10956
10956
  return structuredClone(value);
10957
10957
  }
10958
+ function formatDeploymentEvidence(evidence) {
10959
+ const report = summarizeDeploymentEvidence(evidence);
10960
+ return [
10961
+ `DEPLOYMENT ${evidence.scope.project_ref}/${evidence.scope.application_id}/${evidence.scope.environment_id}`,
10962
+ ` status: ${report.status}`,
10963
+ ...report.status !== evidence.status ? [` recorded status: ${evidence.status} (additional proof missing)`] : [],
10964
+ ` manifest: ${evidence.source.manifest_sha256.slice(0, 12)}`,
10965
+ ` database: ${evidence.database.provider} ${evidence.database.version} (${evidence.database.topology})`,
10966
+ ` activation: ${evidence.activation.activation_id}`,
10967
+ ` rollback: ${evidence.rollback.status}`,
10968
+ ` health: ${evidence.health.status}; smoke=${evidence.health.authenticated_smoke}`,
10969
+ ` release: ${evidence.activation.release_id}`,
10970
+ ` recorded: ${evidence.recorded_at} (snapshot, not a live probe)`,
10971
+ ...report.issues.map((issue) => ` ${issue.status} ${issue.component}: ${issue.nextAction}`),
10972
+ " recovery: code rollback does not restore database or object storage",
10973
+ " retries: do not replay mutations with unknown outcomes; read back the original identity"
10974
+ ].join(`
10975
+ `);
10976
+ }
10977
+ function summarizeDeploymentEvidence(value) {
10978
+ const evidence = parseDeploymentEvidence(value);
10979
+ const issues = [];
10980
+ const observe = (component, state, complete, nextAction) => {
10981
+ if (state !== "confirmed" || !complete)
10982
+ issues.push({
10983
+ component,
10984
+ status: state === "failed" ? "failed" : state === "unknown" ? "unknown" : "incomplete",
10985
+ nextAction
10986
+ });
10987
+ };
10988
+ for (const component of evidence.components) {
10989
+ observe(component.name, component.status, component.checked_at !== null && component.health_check !== null, "Inspect this component's health and logs for the recorded activation before changing or restarting it.");
10990
+ }
10991
+ observe("database.migration", evidence.database.migration.status, evidence.database.migration.compatibility === "verified" && evidence.database.migration.inventory_sha256 !== null, "Read the migration inventory and compare its digest; do not reapply an unconfirmed migration.");
10992
+ observe("database.backup", evidence.database.backup.status, evidence.database.backup.latest_success_at !== null && evidence.database.backup.freshness_seconds !== null, "Read backup inventory and freshness for this project; a successful deployment is not a backup.");
10993
+ observe("database.recovery", evidence.database.recovery.status, evidence.database.recovery.drill_id !== null && evidence.database.recovery.rpo_seconds !== null && evidence.database.recovery.rto_seconds !== null, "Obtain a separate approved restore drill and verify data, permissions and storage; never restore automatically.");
10994
+ observe("health", evidence.health.status, evidence.health.checked_at !== null, "Read back this activation's readiness; process health alone does not prove a business operation.");
10995
+ observe("authenticated-smoke", evidence.health.authenticated_smoke, true, "Run the selected business smoke with a real authorized session and verify forbidden access remains denied.");
10996
+ observe("code-rollback", evidence.rollback.status === "ready" ? "confirmed" : evidence.rollback.status, evidence.rollback.release_id !== null && evidence.rollback.configuration_id !== null, "Inspect the explicit rollback release and configuration; data recovery requires a separate plan.");
10997
+ if (evidence.rollback.result !== null) {
10998
+ observe("code-rollback.result", evidence.rollback.result, true, "Read back the rollback activation and authenticated smoke before another mutation.");
10999
+ }
11000
+ return {
11001
+ version: 1,
11002
+ status: evidence.status === "confirmed" && issues.length > 0 ? "incomplete" : evidence.status,
11003
+ recordedStatus: evidence.status,
11004
+ scope: evidence.scope,
11005
+ recordedAt: evidence.recorded_at,
11006
+ correlation: {
11007
+ releaseId: evidence.activation.release_id,
11008
+ activationId: evidence.activation.activation_id,
11009
+ manifestSha256: evidence.source.manifest_sha256
11010
+ },
11011
+ issues,
11012
+ automaticMutationRetry: false,
11013
+ codeRollbackRestoresData: false,
11014
+ liveObservationPerformed: false
11015
+ };
11016
+ }
10958
11017
  // node_modules/@supacloud/delivery/src/delivery-tuple.ts
10959
11018
  var sha2563 = String2({ pattern: "^[a-f0-9]{64}$" });
10960
11019
  var commit = String2({ pattern: "^(?:[a-f0-9]{40}|[a-f0-9]{64})$" });
@@ -21388,7 +21447,7 @@ var starter_metadata_default = {
21388
21447
  generatedBy: "supacloud/starter-metadata-v1",
21389
21448
  packages: {
21390
21449
  compiler: {
21391
- version: "0.46.4"
21450
+ version: "0.46.5"
21392
21451
  },
21393
21452
  app: {
21394
21453
  version: "0.27.1",
@@ -21397,16 +21456,16 @@ var starter_metadata_default = {
21397
21456
  }
21398
21457
  },
21399
21458
  elysia: {
21400
- version: "0.31.1"
21459
+ version: "0.33.1"
21401
21460
  },
21402
21461
  commands: {
21403
21462
  version: "0.9.3"
21404
21463
  },
21405
21464
  contracts: {
21406
- version: "0.11.0"
21465
+ version: "0.11.1"
21407
21466
  },
21408
21467
  db: {
21409
- version: "0.17.1",
21468
+ version: "0.18.0",
21410
21469
  peerDependencies: {
21411
21470
  "drizzle-orm": "1.0.0-rc.5-5935859"
21412
21471
  },
@@ -22450,7 +22509,7 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22450
22509
  `;
22451
22510
 
22452
22511
  // src/shared/tools/app-starter-attachment-worker.ts
22453
- var STARTER_ATTACHMENT_WORKER = `import { createWorker, type CompiledModule } from "@supacloud/elysia";
22512
+ var STARTER_ATTACHMENT_WORKER = `import { createWorker, type CompiledModule, type ExecutionObserver } from "@supacloud/elysia";
22454
22513
  import {
22455
22514
  createSupaCloudWorkflowTransport,
22456
22515
  type SupaCloudWorkflowClaim,
@@ -22464,6 +22523,7 @@ export interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {
22464
22523
  workerId: string;
22465
22524
  queueOwnership: "exclusive-review-attachments";
22466
22525
  pollIntervalMs?: number;
22526
+ onExecution?: ExecutionObserver;
22467
22527
  }
22468
22528
 
22469
22529
  export async function createReviewAttachmentWorker(options: ReviewAttachmentWorkerOptions) {
@@ -22490,7 +22550,9 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22490
22550
  throw new Error("Invalid attachment workflow input");
22491
22551
  }
22492
22552
  const bound = await options.database.transaction(tx => tx.query(
22493
- "SELECT run_id FROM public.starter_attachments WHERE review_id=$1 AND artifact_id=$2 AND run_id=$3",
22553
+ "SELECT a.run_id FROM public.starter_attachments a WHERE a.review_id=$1 AND a.artifact_id=$2 " +
22554
+ "AND COALESCE((SELECT r.run_id FROM public.starter_attachment_retries r " +
22555
+ "WHERE r.review_id=a.review_id ORDER BY r.generation DESC LIMIT 1),a.run_id)=$3",
22494
22556
  [reviewId, artifactId, claim.runId],
22495
22557
  ));
22496
22558
  if (!Array.isArray(bound) || bound.length !== 1) throw new Error("Attachment workflow ownership mismatch");
@@ -22502,6 +22564,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22502
22564
  modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,
22503
22565
  pollIntervalMs: options.pollIntervalMs ?? 1000,
22504
22566
  onError: fatal,
22567
+ ...(options.onExecution === undefined ? {} : { onExecution: options.onExecution }),
22505
22568
  transport: {
22506
22569
  async claim(signal): Promise<SupaCloudWorkflowClaim | null> {
22507
22570
  if (halted || signal.aborted) return null;
@@ -22557,6 +22620,7 @@ import { SupaCloudWorkflowsClient } from "@supacloud/js";
22557
22620
  import { createBunCommandDatabase } from "@supacloud/db/bun";
22558
22621
  import type { CompiledModule } from "@supacloud/elysia";
22559
22622
  import { createReviewAttachmentWorker } from "./host/review-attachment-worker";
22623
+ import { createReviewWorkerObserver } from "./host/review-worker-observability";
22560
22624
 
22561
22625
  function required(name: string): string {
22562
22626
  const value = process.env[name];
@@ -22600,6 +22664,7 @@ export async function createDeliveryWorker(modules: CompiledModule[], lifecycle:
22600
22664
  database: createBunCommandDatabase(pool), service, projectId: settings.projectId,
22601
22665
  tenantId: settings.tenantId, modules, workerId: settings.workerId,
22602
22666
  workflows: new SupaCloudWorkflowsClient(service), queueOwnership: "exclusive-review-attachments",
22667
+ onExecution: createReviewWorkerObserver(settings.workerId),
22603
22668
  });
22604
22669
  lifecycle.signal.throwIfAborted();
22605
22670
  return {
@@ -22616,15 +22681,50 @@ export async function createDeliveryWorker(modules: CompiledModule[], lifecycle:
22616
22681
  }
22617
22682
  `;
22618
22683
 
22684
+ // src/shared/tools/app-starter-execution-observability.ts
22685
+ var STARTER_EXECUTION_OBSERVABILITY = `import type { ExecutionObserver } from "@supacloud/elysia";
22686
+
22687
+ function correlationId(value: string | undefined): string | undefined {
22688
+ return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value) ? value : undefined;
22689
+ }
22690
+
22691
+ function createObserver(source: "http" | "worker", workerId?: string): ExecutionObserver {
22692
+ return metadata => {
22693
+ // Select fields explicitly: future observer extensions must not leak payloads.
22694
+ console.info(JSON.stringify({
22695
+ event: source === "http" ? "review-http-execution" : "review-worker-execution",
22696
+ component: source === "http" ? "review-http" : "review-attachment-worker",
22697
+ timestamp: new Date().toISOString(),
22698
+ workerId: correlationId(workerId),
22699
+ kind: metadata.kind,
22700
+ operation: metadata.operation,
22701
+ stage: metadata.stage,
22702
+ phase: metadata.phase,
22703
+ requestId: correlationId(metadata.requestId),
22704
+ durationMs: typeof metadata.durationMs === "number" && Number.isFinite(metadata.durationMs)
22705
+ && metadata.durationMs >= 0 ? metadata.durationMs : undefined,
22706
+ }));
22707
+ };
22708
+ }
22709
+
22710
+ export function createReviewHttpObserver(): ExecutionObserver {
22711
+ return createObserver("http");
22712
+ }
22713
+
22714
+ export function createReviewWorkerObserver(workerId: string): ExecutionObserver {
22715
+ return createObserver("worker", workerId);
22716
+ }
22717
+ `;
22718
+ var STARTER_WORKER_OBSERVABILITY = `export { createReviewWorkerObserver } from "./review-execution-observability";
22719
+ `;
22720
+
22619
22721
  // src/shared/tools/app-starter-upload.ts
22620
22722
  var STARTER_UPLOAD_FEATURE = `import { Effect } from "effect";
22621
22723
  import { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from "@supacloud/app";
22622
22724
  import { ApplicationError } from "@supacloud/elysia";
22623
- import { UploadBody, UploadLocation, UploadParams, UploadResult } from "./contracts";
22624
-
22625
- export interface ReviewUploadInput { reviewId: string; artifactId: string; expectedVersion: number }
22626
- export interface ReviewUploadLocation { artifactId: string; bucketId: string; objectPath: string }
22627
- export interface ReviewUploadResult { artifactId: string; runId: string; objectPath: string; sha256: string; bytes: number }
22725
+ import { UploadBody, UploadLocation, UploadParams, UploadResult,
22726
+ type ReviewUploadBody, type ReviewUploadInput, type ReviewUploadLocation, type ReviewUploadResult } from "./contracts";
22727
+ export type { ReviewUploadInput, ReviewUploadLocation, ReviewUploadResult } from "./contracts";
22628
22728
  type PreviewUploadError =
22629
22729
  | { readonly _tag: "AUTHENTICATION_REQUIRED" }
22630
22730
  | { readonly _tag: "UPLOAD_INPUT_INVALID" }
@@ -22639,6 +22739,7 @@ type RegisterUploadError =
22639
22739
  export interface ReviewUploadPort {
22640
22740
  previewUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadLocation>;
22641
22741
  registerUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadResult>;
22742
+ lookupUpload?(context: unknown, input: ReviewUploadInput): Promise<boolean>;
22642
22743
  }
22643
22744
  @Injectable({ scope: "request" })
22644
22745
  export class ReviewUploads {
@@ -22675,7 +22776,7 @@ export class ReviewUploadsController {
22675
22776
  retry: "none",
22676
22777
  },
22677
22778
  })
22678
- preview(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadLocation, PreviewUploadError, never> {
22779
+ preview(@Param("id") id: string, @Body() body: ReviewUploadBody): Effect.Effect<ReviewUploadLocation, PreviewUploadError, never> {
22679
22780
  return Effect.promise(() => this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
22680
22781
  }
22681
22782
  @Post("/:id/attachment-registration", {
@@ -22698,7 +22799,7 @@ export class ReviewUploadsController {
22698
22799
  retry: "none",
22699
22800
  },
22700
22801
  })
22701
- register(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadResult, RegisterUploadError, never> {
22802
+ register(@Param("id") id: string, @Body() body: ReviewUploadBody): Effect.Effect<ReviewUploadResult, RegisterUploadError, never> {
22702
22803
  return Effect.promise(() => this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
22703
22804
  }
22704
22805
  }
@@ -22750,7 +22851,8 @@ import { SupaCloudArtifactsClient } from "@supacloud/js";
22750
22851
  import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
22751
22852
  import { CommandError } from "@supacloud/contracts";
22752
22853
  import { createPostgresCommandStore, type CommandTransaction } from "@supacloud/db";
22753
- import { ApplicationError, requireTrustedIdentity } from "@supacloud/elysia";
22854
+ import { ApplicationError, createSchemaDecoder, requireTrustedIdentity } from "@supacloud/elysia";
22855
+ import { UploadInput, UploadResult } from "../review/contracts";
22754
22856
  import type { ReviewAttachmentOptions } from "./review-attachments";
22755
22857
  import type { ReviewUploadInput, ReviewUploadPort, ReviewUploadResult } from "../review/uploads";
22756
22858
 
@@ -22765,20 +22867,15 @@ function rows(value: unknown): Record<string, unknown>[] {
22765
22867
  if (!Array.isArray(value)) throw new TypeError("Invalid upload database result");
22766
22868
  return value.map(record);
22767
22869
  }
22870
+ const decodeInput = createSchemaDecoder(UploadInput, { normalize: false });
22871
+ const result = createSchemaDecoder(UploadResult, { normalize: false });
22768
22872
  function capture(value: unknown): ReviewUploadInput {
22769
- 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;
22770
- if (typeof item["reviewId"] !== "string" || !uuid.test(item["reviewId"])
22771
- || typeof item["artifactId"] !== "string" || !uuid.test(item["artifactId"])
22772
- || typeof item["expectedVersion"] !== "number" || !Number.isSafeInteger(item["expectedVersion"]) || item["expectedVersion"] < 1) {
22873
+ try {
22874
+ const input = decodeInput(value);
22875
+ return { ...input, reviewId: input.reviewId.toLowerCase(), artifactId: input.artifactId.toLowerCase() };
22876
+ } catch {
22773
22877
  throw new ApplicationError("Invalid upload input", { status: 400, code: "UPLOAD_INPUT_INVALID" });
22774
22878
  }
22775
- return { reviewId: item["reviewId"].toLowerCase(), artifactId: item["artifactId"].toLowerCase(), expectedVersion: item["expectedVersion"] };
22776
- }
22777
- function result(value: unknown): ReviewUploadResult {
22778
- const item = record(value);
22779
- if (typeof item["artifactId"] !== "string" || typeof item["runId"] !== "string" || typeof item["objectPath"] !== "string"
22780
- || typeof item["sha256"] !== "string" || typeof item["bytes"] !== "number") throw new TypeError("Invalid upload receipt");
22781
- return { artifactId: item["artifactId"], runId: item["runId"], objectPath: item["objectPath"], sha256: item["sha256"], bytes: item["bytes"] };
22782
22879
  }
22783
22880
 
22784
22881
  export async function createReviewUploadAdapters(options: ReviewAttachmentOptions): Promise<ReviewUploadPort> {
@@ -22805,7 +22902,7 @@ export async function createReviewUploadAdapters(options: ReviewAttachmentOption
22805
22902
  "SELECT m.storage_subject::text AS storage_subject FROM public.starter_reviews r " +
22806
22903
  "JOIN public.starter_members m ON m.subject=r.owner_id CROSS JOIN public.starter_application p " +
22807
22904
  "WHERE r.id=$1 AND r.owner_id=$2 AND ((r.state='draft' AND r.version=$3) OR " +
22808
- "($6::boolean AND r.state='approved' AND r.version=$3+1 AND EXISTS (" +
22905
+ "($6::boolean AND r.state='approved' AND r.version=$3::bigint+1 AND EXISTS (" +
22809
22906
  "SELECT 1 FROM public.starter_attachments b WHERE b.review_id=r.id AND b.artifact_id=$7::uuid AND b.owner_id=$2 " +
22810
22907
  "AND b.object_path=m.storage_subject::text || '/' || r.id || '/' || $7::text || '.txt'))) " +
22811
22908
  "AND m.enabled AND m.can_approve AND m.storage_subject IS NOT NULL " +
@@ -22817,7 +22914,24 @@ export async function createReviewUploadAdapters(options: ReviewAttachmentOption
22817
22914
  }
22818
22915
  return allowed[0]["storage_subject"] + "/" + input.reviewId + "/" + input.artifactId + ".txt";
22819
22916
  }
22917
+ const receiptLookup = createTransactionalCommand({
22918
+ name: "review.attach", store: createPostgresCommandStore(database), inputCodec: plaintextCommandInput,
22919
+ input: capture, result,
22920
+ async authorize(who, request, tx) {
22921
+ try { await authorize(tx, who.actorId, request, true); return "allow"; }
22922
+ catch (error) {
22923
+ if (error instanceof ApplicationError && error.code === "UPLOAD_DENIED") return "deny";
22924
+ throw error;
22925
+ }
22926
+ },
22927
+ execute: async () => { throw new Error("Receipt lookup cannot register an upload"); },
22928
+ audit: { event: "review.attachment-bound", details: request => ({ reviewId: request.reviewId, artifactId: request.artifactId }) },
22929
+ });
22820
22930
  return {
22931
+ async lookupUpload(context, value) {
22932
+ const input = capture(value), subject = actor(context);
22933
+ return (await receiptLookup.lookup({ actorId: subject, tenantId }, input.artifactId, input))?.status === "confirmed";
22934
+ },
22821
22935
  async previewUpload(context, value) {
22822
22936
  const input = capture(value), subject = actor(context);
22823
22937
  const objectPath = await database.transaction(tx => authorize(tx, subject, input));
@@ -22825,8 +22939,14 @@ export async function createReviewUploadAdapters(options: ReviewAttachmentOption
22825
22939
  },
22826
22940
  async registerUpload(context, value) {
22827
22941
  const input = capture(value), subject = actor(context);
22828
- const objectPath = await database.transaction(tx => authorize(tx, subject, input, true));
22829
22942
  try {
22943
+ // Only an authorized, validated receipt can bypass external registration.
22944
+ const saved = await receiptLookup.lookup({ actorId: subject, tenantId }, input.artifactId, input);
22945
+ if (saved !== null) {
22946
+ if (saved.status !== "confirmed") throw new CommandError("COMMAND_RECEIPT_INVALID");
22947
+ return saved.result;
22948
+ }
22949
+ const objectPath = await database.transaction(tx => authorize(tx, subject, input, true));
22830
22950
  const downloaded = await service.storage.from("review-attachments").download(objectPath);
22831
22951
  if (downloaded.error || !downloaded.data) throw new Error("Upload unavailable");
22832
22952
  const blob = downloaded.data;
@@ -22984,21 +23104,24 @@ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
22984
23104
  ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
22985
23105
  ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
22986
23106
  `;
22987
- var STARTER_REVIEW_POSTGRES = `import { plaintextCommandInput } from "@supacloud/commands";
23107
+ var STARTER_REVIEW_POSTGRES = `import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
22988
23108
  import { CommandError } from "@supacloud/contracts";
22989
23109
  import { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from "@supacloud/db";
22990
23110
  import {
22991
23111
  ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,
22992
23112
  createTransactionalCommandGovernance,
22993
- type CommandInvocation, type SupAuthContextOptions,
23113
+ type SupAuthContextOptions,
22994
23114
  } from "@supacloud/elysia";
22995
23115
  import type { AppAdapters } from "../application";
22996
23116
  import type { ReviewStore } from "../review/review";
22997
23117
  import type { ReviewUploadPort } from "../review/uploads";
23118
+ import type { ReviewTaskPort } from "../review/tasks";
22998
23119
  import { ApprovalInput, ReviewResult } from "../review/contracts";
23120
+ import type { ReviewRow } from "../../db/schema";
22999
23121
 
23000
23122
  const input = createSchemaDecoder(ApprovalInput, { normalize: false });
23001
- const result = createSchemaDecoder(ReviewResult, { normalize: false });
23123
+ const result: (value: unknown) => Pick<ReviewRow, "state" | "version"> =
23124
+ createSchemaDecoder(ReviewResult, { normalize: false });
23002
23125
 
23003
23126
  function isRecord(value: unknown): value is Record<string, unknown> {
23004
23127
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -23020,6 +23143,7 @@ export interface ReviewPostgresOptions {
23020
23143
  tenantId: string;
23021
23144
  identity: Omit<SupAuthContextOptions, "resolveAccess">;
23022
23145
  uploads?: ReviewUploadPort;
23146
+ tasks?: ReviewTaskPort;
23023
23147
  // Only database effects here: they commit/rollback with the approval and receipt.
23024
23148
  afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;
23025
23149
  }
@@ -23055,6 +23179,38 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23055
23179
  const transaction = () => commandBinding.transaction();
23056
23180
  const store: ReviewStore = {
23057
23181
  ...options.uploads,
23182
+ ...options.tasks,
23183
+ async lookupOperation(context, operation) {
23184
+ const subject = actor(context);
23185
+ try {
23186
+ let confirmed: boolean = false;
23187
+ if (operation.kind === "upload") {
23188
+ if (!options.uploads?.lookupUpload) {
23189
+ throw new ApplicationError("Upload receipt lookup unavailable", { status: 503, code: "RECEIPTS_UNAVAILABLE" });
23190
+ }
23191
+ confirmed = await options.uploads.lookupUpload(context, {
23192
+ reviewId: operation.reviewId, artifactId: operation.operationId, expectedVersion: operation.expectedVersion,
23193
+ });
23194
+ } else {
23195
+ const command = createTransactionalCommand({
23196
+ name: "review.approve", store: commandStore, inputCodec: plaintextCommandInput, input, result,
23197
+ authorize: authorizeApproval,
23198
+ execute: async () => { throw new Error("Receipt lookup cannot execute a command"); },
23199
+ audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23200
+ });
23201
+ const receipt = await command.lookup({ actorId: subject, tenantId }, operation.operationId,
23202
+ { id: operation.reviewId, expectedVersion: operation.expectedVersion });
23203
+ confirmed = receipt?.status === "confirmed";
23204
+ }
23205
+ return { status: confirmed ? "confirmed" : "unknown", operationId: operation.operationId, reviewId: operation.reviewId };
23206
+ } catch (error) {
23207
+ if (error instanceof ApplicationError) throw error;
23208
+ if (error instanceof CommandError && ["COMMAND_REJECTED", "COMMAND_IDEMPOTENCY_CONFLICT"].includes(error.code)) {
23209
+ throw new ApplicationError(error.code, { code: error.code, status: error.code === "COMMAND_REJECTED" ? 403 : 409 });
23210
+ }
23211
+ throw new ApplicationError("Receipt lookup unavailable", { status: 503, code: "RECEIPTS_UNAVAILABLE" });
23212
+ }
23213
+ },
23058
23214
  async get(table, id) {
23059
23215
  if (table !== "reviews") throw new Error("Unsupported review table");
23060
23216
  return rows(await transaction().query(
@@ -23072,35 +23228,37 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23072
23228
  await afterApproved?.(transaction(), id, review.version);
23073
23229
  },
23074
23230
  };
23075
- function actor(invocation: CommandInvocation): string {
23076
- const who = requireTrustedIdentity(invocation.requestContext);
23077
- const access = record(record(invocation.requestContext)["access"]);
23231
+ function actor(context: unknown): string {
23232
+ const who = requireTrustedIdentity(context);
23233
+ const access = record(record(context)["access"]);
23078
23234
  if (access["projectId"] !== projectId || access["tenantId"] !== tenantId
23079
23235
  || !Array.isArray(access["permissions"]) || !access["permissions"].includes("review.approve")) {
23080
23236
  throw new ApplicationError("Command permission denied", { status: 403, code: "REVIEW_PERMISSION_DENIED" });
23081
23237
  }
23082
23238
  return who.subject;
23083
23239
  }
23240
+ const commandStore = createPostgresCommandStore(database);
23241
+ async function authorizeApproval(who: { actorId: string; tenantId: string }, request: ReturnType<typeof input>, tx: CommandTransaction): Promise<"allow" | "deny"> {
23242
+ const found = rows(await tx.query(
23243
+ "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23244
+ "CROSS JOIN public.starter_application a " +
23245
+ "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23246
+ "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23247
+ [request.id, who.actorId, projectId, who.tenantId],
23248
+ ));
23249
+ return found.length === 1 ? "allow" : "deny";
23250
+ }
23084
23251
  const commandBinding = createTransactionalCommandGovernance({
23085
23252
  permission: "review.approve",
23086
- identity: invocation => ({ actorId: actor(invocation), tenantId }),
23253
+ identity: invocation => ({ actorId: actor(invocation.requestContext), tenantId }),
23087
23254
  input: invocation => ({
23088
23255
  ...record(invocation.input.body), id: invocation.input.params["id"],
23089
23256
  }),
23090
23257
  rejection: error => isPublicConflict(error) ? new CommandError("COMMAND_IDEMPOTENCY_CONFLICT") : undefined,
23091
23258
  definition: {
23092
- name: "review.approve", store: createPostgresCommandStore(database),
23259
+ name: "review.approve", store: commandStore,
23093
23260
  inputCodec: plaintextCommandInput, input, result,
23094
- async authorize(who, request, tx) {
23095
- const found = rows(await tx.query(
23096
- "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23097
- "CROSS JOIN public.starter_application a " +
23098
- "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23099
- "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23100
- [request.id, who.actorId, projectId, who.tenantId],
23101
- ));
23102
- return found.length === 1 ? "allow" : "deny";
23103
- },
23261
+ authorize: authorizeApproval,
23104
23262
  audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23105
23263
  },
23106
23264
  });
@@ -23154,6 +23312,8 @@ import { createApplication, type CompiledModule } from "@supacloud/elysia";
23154
23312
  import { createReviewPostgresAdapters } from "./host/review-postgres";
23155
23313
  import { createReviewAttachmentAdapters } from "./host/review-attachments";
23156
23314
  import { createReviewUploadAdapters } from "./host/review-uploads";
23315
+ import { createReviewTaskAdapters } from "./host/review-tasks";
23316
+ import { createReviewHttpObserver } from "./host/review-execution-observability";
23157
23317
 
23158
23318
  function required(name: string): string {
23159
23319
  const value = process.env[name];
@@ -23204,15 +23364,17 @@ export async function createDeliveryApplication(modules: CompiledModule[], lifec
23204
23364
  const durable = attachmentOptions ? await createReviewAttachmentAdapters(attachmentOptions) : undefined;
23205
23365
  stage = "uploads";
23206
23366
  const uploads = attachmentOptions ? await createReviewUploadAdapters(attachmentOptions) : undefined;
23367
+ const tasks = attachmentOptions ? await createReviewTaskAdapters(attachmentOptions) : undefined;
23207
23368
  stage = "postgres";
23208
23369
  const adapters = await createReviewPostgresAdapters({
23209
23370
  database, tenantId: settings.tenantId, identity: settings.identity,
23210
23371
  ...(uploads ? { uploads } : {}),
23372
+ ...(tasks ? { tasks } : {}),
23211
23373
  ...(durable ? { afterApproved: durable.enqueue } : {}),
23212
23374
  });
23213
23375
  lifecycle.signal.throwIfAborted();
23214
23376
  stage = "application";
23215
- const app = createApplication({ name: "review", modules, ...adapters });
23377
+ const app = createApplication({ name: "review", modules, ...adapters, onExecution: createReviewHttpObserver() });
23216
23378
  return { fetch: (request: Request) => app.handle(request), close };
23217
23379
  } catch (error) {
23218
23380
  await close();
@@ -23238,7 +23400,7 @@ var STARTER_DATABASE_CONFIG = {
23238
23400
  role: "starter_review_http"
23239
23401
  };
23240
23402
  var STARTER_DATABASE_SCHEMA = `import { sql } from "drizzle-orm";
23241
- import { boolean, check, integer, jsonb, pgTable, primaryKey, text, uuid } from "drizzle-orm/pg-core";
23403
+ import { boolean, check, integer, jsonb, pgTable, primaryKey, text, unique, uuid } from "drizzle-orm/pg-core";
23242
23404
 
23243
23405
  // Structure for the reviewed starter migrations. Grants/policies stay in those migrations.
23244
23406
  export const application = pgTable.withRLS("starter_application", {
@@ -23286,12 +23448,23 @@ export const attachmentResults = pgTable.withRLS("starter_attachment_results", {
23286
23448
  check("starter_attachment_results_version_check", sql\`\${table.version} >= 2\`),
23287
23449
  ]);
23288
23450
 
23451
+ export const attachmentRetries = pgTable.withRLS("starter_attachment_retries", {
23452
+ runId: uuid("run_id").primaryKey(),
23453
+ reviewId: text("review_id").notNull().references(() => attachments.reviewId),
23454
+ previousRunId: uuid("previous_run_id").notNull().unique(),
23455
+ generation: integer().notNull(),
23456
+ }, table => [
23457
+ unique("starter_attachment_retries_review_id_generation_key").on(table.reviewId, table.generation),
23458
+ check("starter_attachment_retries_generation_check", sql\`\${table.generation} > 0\`),
23459
+ check("starter_attachment_retries_check", sql\`\${table.runId} <> \${table.previousRunId}\`),
23460
+ ]);
23461
+
23289
23462
  export type ReviewRow = typeof reviews.$inferSelect;
23290
23463
  export type NewReview = typeof reviews.$inferInsert;
23291
23464
  `;
23292
23465
  var STARTER_DATABASE_MODULES = `import { defineDatabaseModule } from "@supacloud/db";
23293
23466
  import { getTableConfig } from "drizzle-orm/pg-core";
23294
- import { application, reviews, members, attachments, attachmentResults } from "./schema";
23467
+ import { application, reviews, members, attachments, attachmentResults, attachmentRetries } from "./schema";
23295
23468
 
23296
23469
  const backendPolicies = [
23297
23470
  ["starter_backend_binding_read", "public.starter_application", "select", ["starter_review_http", "starter_review_worker"]],
@@ -23314,11 +23487,19 @@ const backendPolicies = [
23314
23487
  // The module is the single live-Catalog ownership contract for starter tables.
23315
23488
  export default defineDatabaseModule({
23316
23489
  name: "starter",
23317
- tables: [application, reviews, members, attachments, attachmentResults].map(table => {
23490
+ tables: [application, reviews, members, attachments, attachmentResults, attachmentRetries].map(table => {
23318
23491
  const config = getTableConfig(table);
23319
23492
  return (config.schema ?? "public") + "." + config.name;
23320
23493
  }),
23321
23494
  policies: [
23495
+ ...([
23496
+ ["starter_http_retry_read", "select", "starter_review_http"],
23497
+ ["starter_http_retry_insert", "insert", "starter_review_http"],
23498
+ ["starter_worker_retry_read", "select", "starter_review_worker"],
23499
+ ] as const).map(([name, operation, role]) => ({
23500
+ name, table: "public.starter_attachment_retries", operation, roles: [role],
23501
+ source: "migrations/005-review-task-recovery.sql", tests: ["tests/database-governance.sql"],
23502
+ })),
23322
23503
  ...backendPolicies.map(([name, table, operation, roles]) => ({
23323
23504
  name, table, operation, roles: [...roles], source: "migrations/004-review-runtime-roles.sql",
23324
23505
  tests: ["tests/database-governance.sql"],
@@ -23341,15 +23522,15 @@ BEGIN READ ONLY;
23341
23522
  DO $test$
23342
23523
  DECLARE
23343
23524
  owned text[] := ARRAY['starter_application','starter_reviews','starter_members',
23344
- 'starter_attachments','starter_attachment_results'];
23525
+ 'starter_attachments','starter_attachment_results','starter_attachment_retries'];
23345
23526
  BEGIN
23346
23527
  IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
23347
- WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN
23528
+ WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 6 THEN
23348
23529
  RAISE EXCEPTION 'All starter tables must exist with RLS enabled';
23349
23530
  END IF;
23350
23531
  IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid
23351
23532
  JOIN pg_namespace n ON n.oid=c.relnamespace
23352
- WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN
23533
+ WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 20 THEN
23353
23534
  RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';
23354
23535
  END IF;
23355
23536
  IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')
@@ -23429,6 +23610,831 @@ export default defineConfig({
23429
23610
  };
23430
23611
  }
23431
23612
 
23613
+ // src/shared/tools/app-starter-client.ts
23614
+ var STARTER_REVIEW_CLIENT = String.raw`import type { ReviewOperation, ReviewReceipt } from "../review/contracts";
23615
+ import { REVIEW_VERSION_MAX } from "../review/limits";
23616
+ export type { ReviewOperation } from "../review/contracts";
23617
+
23618
+ export interface ReviewRow { id: string; state: "draft" | "approved"; version: number }
23619
+ export type ReviewWriteResult =
23620
+ | (ReviewReceipt & { status: "confirmed" })
23621
+ | { status: "denied" | "conflict" | "unknown" };
23622
+ export interface ReviewTransport {
23623
+ list(): Promise<ReviewRow[]>;
23624
+ approve(operation: ReviewOperation): Promise<ReviewWriteResult>;
23625
+ upload(operation: ReviewOperation, file: Blob): Promise<ReviewWriteResult>;
23626
+ // Read-only receipt lookup, bound to current identity. A current row alone is not a receipt.
23627
+ lookup?(operation: ReviewOperation): Promise<ReviewWriteResult>;
23628
+ }
23629
+ export interface ReviewClientState {
23630
+ list: { status: "idle" | "loading" | "ready" | "empty" | "error" | "denied"; rows: ReviewRow[] };
23631
+ mutation: {
23632
+ status: "idle" | "submitting" | "confirmed" | "denied" | "conflict" | "unknown";
23633
+ operation: ReviewOperation | null;
23634
+ };
23635
+ }
23636
+
23637
+ export interface ReviewPendingStore {
23638
+ load(): ReviewOperation | null;
23639
+ save(operation: ReviewOperation): void;
23640
+ clear(operation: ReviewOperation): void;
23641
+ }
23642
+
23643
+ /** Keep operation identity, not tokens, file bytes or row data. Storage is not authorization. */
23644
+ export function createReviewPendingStore(storage: Pick<Storage, "getItem" | "setItem" | "removeItem">,
23645
+ scope: { projectId: string; tenantId: string; applicationId: string; subject: string }): ReviewPendingStore {
23646
+ const identity = [scope.projectId, scope.tenantId, scope.applicationId, scope.subject];
23647
+ if (identity.some(value => !value.trim())) throw new Error("Explicit recovery scope required");
23648
+ const key = "supacloud.review.pending.v1:" + JSON.stringify(identity);
23649
+ function isRecord(value: unknown): value is Record<string, unknown> {
23650
+ return value !== null && typeof value === "object" && !Array.isArray(value);
23651
+ }
23652
+ function decode(value: unknown): ReviewOperation {
23653
+ if (!isRecord(value)) throw new Error("Invalid pending operation");
23654
+ const item = value;
23655
+ if ((item["kind"] !== "approve" && item["kind"] !== "upload")
23656
+ || typeof item["reviewId"] !== "string" || !item["reviewId"].trim()
23657
+ || typeof item["operationId"] !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(item["operationId"])
23658
+ || typeof item["expectedVersion"] !== "number" || !Number.isSafeInteger(item["expectedVersion"])
23659
+ || item["expectedVersion"] < 1 || item["expectedVersion"] > REVIEW_VERSION_MAX) {
23660
+ throw new Error("Invalid pending operation");
23661
+ }
23662
+ return { kind: item["kind"], reviewId: item["reviewId"], operationId: item["operationId"], expectedVersion: item["expectedVersion"] };
23663
+ }
23664
+ const load = () => {
23665
+ const value = storage.getItem(key);
23666
+ if (value === null) return null;
23667
+ const parsed: unknown = JSON.parse(value);
23668
+ return decode(parsed);
23669
+ };
23670
+ return {
23671
+ load,
23672
+ save(operation) {
23673
+ const previous = load();
23674
+ if (previous && JSON.stringify(previous) !== JSON.stringify(decode(operation))) {
23675
+ throw new Error("Another unresolved operation exists");
23676
+ }
23677
+ storage.setItem(key, JSON.stringify(decode(operation)));
23678
+ },
23679
+ clear(operation) {
23680
+ const previous = load();
23681
+ if (previous && JSON.stringify(previous) !== JSON.stringify(decode(operation))) {
23682
+ throw new Error("Pending operation identity changed");
23683
+ }
23684
+ storage.removeItem(key);
23685
+ },
23686
+ };
23687
+ }
23688
+
23689
+ export function createReviewClient(transport: ReviewTransport, pending?: ReviewPendingStore) {
23690
+ let state: ReviewClientState = { list: { status: "idle", rows: [] }, mutation: { status: "idle", operation: null } };
23691
+ try {
23692
+ const operation = pending?.load();
23693
+ if (operation) state.mutation = { status: "unknown", operation };
23694
+ } catch { state.mutation = { status: "unknown", operation: null }; }
23695
+ let generation: number = 0, closed: boolean = false, recovering: boolean = false;
23696
+ const listeners = new Set<() => void>();
23697
+ const snapshot = (): ReviewClientState => structuredClone(state);
23698
+ const notify = () => {
23699
+ for (const listener of listeners) {
23700
+ // A view subscriber cannot interrupt persistence or change a write outcome.
23701
+ try { listener(); } catch {}
23702
+ }
23703
+ };
23704
+ function finish(operation: ReviewOperation, result: ReviewWriteResult) {
23705
+ if (closed) return;
23706
+ let status = result.status === "confirmed"
23707
+ && (result.operationId !== operation.operationId || result.reviewId !== operation.reviewId)
23708
+ ? "unknown" : result.status;
23709
+ if (status !== "unknown") {
23710
+ try {
23711
+ if (pending) {
23712
+ pending.clear(structuredClone(operation));
23713
+ if (pending.load() !== null) throw new Error("Pending operation was not cleared");
23714
+ }
23715
+ } catch { status = "unknown"; }
23716
+ }
23717
+ state = { ...state, mutation: { status, operation } };
23718
+ notify();
23719
+ }
23720
+ async function load() {
23721
+ if (closed) return;
23722
+ const request = ++generation;
23723
+ state = { ...state, list: { status: "loading", rows: [] } };
23724
+ notify();
23725
+ try {
23726
+ const rows = await transport.list();
23727
+ if (closed || request !== generation) return;
23728
+ state = { ...state, list: { status: rows.length ? "ready" : "empty", rows: structuredClone(rows) } };
23729
+ } catch (error) {
23730
+ if (closed || request !== generation) return;
23731
+ state = { ...state, list: { status: error instanceof ReviewAccessDenied ? "denied" : "error", rows: [] } };
23732
+ }
23733
+ notify();
23734
+ }
23735
+ async function submit(kind: ReviewOperation["kind"], row: ReviewRow, file?: Blob) {
23736
+ if (closed || recovering || state.mutation.status === "submitting" || state.mutation.status === "unknown") return;
23737
+ if (row.state !== "draft" || !Number.isSafeInteger(row.version) || row.version < 1 || !row.id.trim()) return;
23738
+ if (row.version > REVIEW_VERSION_MAX || (kind === "approve" && row.version === REVIEW_VERSION_MAX)) return;
23739
+ // Upload contracts are narrower than approval/list IDs and PostgreSQL-safe versions.
23740
+ // Reject locally before creating an unresolved operation for an impossible request.
23741
+ if (kind === "upload" && (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(row.id)
23742
+ || !file || file.size < 1 || file.size > 1048576
23743
+ || file.type.split(";")[0]?.trim().toLowerCase() !== "text/plain")) return;
23744
+ const operation: ReviewOperation = {
23745
+ kind, reviewId: row.id, expectedVersion: row.version, operationId: crypto.randomUUID(),
23746
+ };
23747
+ try {
23748
+ if (pending) {
23749
+ pending.save(structuredClone(operation));
23750
+ const stored = pending.load();
23751
+ if (!stored || stored.kind !== operation.kind || stored.reviewId !== operation.reviewId
23752
+ || stored.expectedVersion !== operation.expectedVersion || stored.operationId !== operation.operationId) {
23753
+ throw new Error("Pending operation was not persisted");
23754
+ }
23755
+ }
23756
+ } catch {
23757
+ let original: ReviewOperation | null = null;
23758
+ try { original = pending?.load() ?? null; } catch {}
23759
+ state = { ...state, mutation: { status: "unknown", operation: original } };
23760
+ notify();
23761
+ return;
23762
+ }
23763
+ state = { ...state, mutation: { status: "submitting", operation } };
23764
+ notify();
23765
+ if (closed) return;
23766
+ let result: ReviewWriteResult;
23767
+ try {
23768
+ result = kind === "approve" ? await transport.approve(structuredClone(operation))
23769
+ : file ? await transport.upload(structuredClone(operation), file) : { status: "unknown" };
23770
+ } catch {
23771
+ result = { status: "unknown" };
23772
+ }
23773
+ finish(operation, result);
23774
+ if (state.mutation.status === "confirmed" || state.mutation.status === "conflict") await load();
23775
+ }
23776
+ return {
23777
+ snapshot,
23778
+ subscribe(listener: () => void) { listeners.add(listener); return () => { listeners.delete(listener); }; },
23779
+ load,
23780
+ approve: (row: ReviewRow) => submit("approve", row),
23781
+ upload: (row: ReviewRow, file: Blob) => submit("upload", row, file),
23782
+ async reconcile() {
23783
+ const operation = state.mutation.operation;
23784
+ if (closed || recovering || state.mutation.status !== "unknown" || !operation || !transport.lookup) return;
23785
+ recovering = true;
23786
+ try {
23787
+ const result = await transport.lookup(structuredClone(operation));
23788
+ // Denial or conflict during lookup does not prove the original write did not commit.
23789
+ finish(operation, result.status === "confirmed" ? result : { status: "unknown" });
23790
+ if (snapshot().mutation.status === "confirmed") await load();
23791
+ } catch {
23792
+ finish(operation, { status: "unknown" });
23793
+ } finally { recovering = false; }
23794
+ },
23795
+ // Dispose on logout/identity change; do not reuse rows or results across sessions.
23796
+ dispose() { closed = true; generation++; listeners.clear(); },
23797
+ };
23798
+ }
23799
+
23800
+ export class ReviewAccessDenied extends Error {}
23801
+ `;
23802
+ var STARTER_REVIEW_TRANSPORT = String.raw`import type { SingleAttemptFetch } from "@supacloud/js/contracts";
23803
+ import { ReviewAccessDenied, type ReviewOperation, type ReviewRow, type ReviewTransport, type ReviewWriteResult } from "./reviews";
23804
+
23805
+ /** Structural SDK boundary: application data is decoded, never propagated as SDK generics. */
23806
+ export interface ReviewSupabaseClient {
23807
+ from(table: "starter_reviews"): {
23808
+ select(columns: "id,state,version"): {
23809
+ order(column: "id"): { limit(count: number): PromiseLike<{ data: unknown; error: unknown; status: number }> };
23810
+ };
23811
+ };
23812
+ storage: {
23813
+ from(bucket: string): {
23814
+ upload(path: string, file: Blob, options: { upsert: boolean; contentType: string }): Promise<{ error: unknown }>;
23815
+ };
23816
+ };
23817
+ }
23818
+ function isRecord(value: unknown): value is Record<string, unknown> {
23819
+ return value !== null && typeof value === "object" && !Array.isArray(value);
23820
+ }
23821
+ function record(value: unknown): Record<string, unknown> {
23822
+ if (!isRecord(value)) throw new Error("Invalid response");
23823
+ return value;
23824
+ }
23825
+ function isList(value: unknown): value is unknown[] { return Array.isArray(value); }
23826
+ function row(value: unknown): ReviewRow {
23827
+ const item = record(value);
23828
+ if (typeof item["id"] !== "string" || (item["state"] !== "draft" && item["state"] !== "approved")
23829
+ || typeof item["version"] !== "number" || !Number.isSafeInteger(item["version"]) || item["version"] < 1) throw new Error("Invalid row");
23830
+ return { id: item["id"], state: item["state"], version: item["version"] };
23831
+ }
23832
+ function rejected(status: number, body: unknown): ReviewWriteResult {
23833
+ const code = record(body)["code"];
23834
+ if ((status === 401 || status === 403) && [
23835
+ "AUTHENTICATION_REQUIRED", "REVIEW_PERMISSION_DENIED", "COMMAND_REJECTED", "UPLOAD_DENIED",
23836
+ ].includes(String(code))) return { status: "denied" };
23837
+ if (status === 409 && ["REVIEW_CONFLICT", "FEATURE_TRANSITION_CONFLICT", "COMMAND_IDEMPOTENCY_CONFLICT"]
23838
+ .includes(String(code))) return { status: "conflict" };
23839
+ return { status: "unknown" };
23840
+ }
23841
+
23842
+ /** Use the same user-scoped Supabase client and createAuthenticatedFetch session supplier. */
23843
+ export function createReviewTransport(options: {
23844
+ supabase: ReviewSupabaseClient;
23845
+ authenticatedFetch: SingleAttemptFetch;
23846
+ apiOrigin: string;
23847
+ lookup?: ReviewTransport["lookup"];
23848
+ }): ReviewTransport {
23849
+ const origin = new URL(options.apiOrigin);
23850
+ if (origin.protocol !== "https:" || origin.username || origin.password || origin.search || origin.hash
23851
+ || origin.pathname !== "/") throw new Error("A fixed trusted HTTPS origin is required");
23852
+ async function request(path: string, body: unknown, operationId: string): Promise<{ response: Response; body: unknown }> {
23853
+ const response = await options.authenticatedFetch(new URL(path, origin), {
23854
+ method: "POST", redirect: "error", signal: AbortSignal.timeout(15000),
23855
+ headers: { "content-type": "application/json", "idempotency-key": operationId, "x-request-id": operationId },
23856
+ body: JSON.stringify(body),
23857
+ });
23858
+ const result: unknown = await response.json();
23859
+ return { response, body: result };
23860
+ }
23861
+ return {
23862
+ lookup: options.lookup ?? (async operation => {
23863
+ const url = new URL("/reviews/" + encodeURIComponent(operation.reviewId)
23864
+ + "/operations/" + encodeURIComponent(operation.operationId), origin);
23865
+ url.searchParams.set("kind", operation.kind);
23866
+ url.searchParams.set("expectedVersion", String(operation.expectedVersion));
23867
+ const response = await options.authenticatedFetch(url, {
23868
+ method: "GET", redirect: "error", cache: "no-store", signal: AbortSignal.timeout(15000),
23869
+ headers: { "x-request-id": operation.operationId },
23870
+ });
23871
+ if (!response.ok) return { status: "unknown" };
23872
+ const body: unknown = await response.json();
23873
+ const receipt = record(body);
23874
+ if (receipt["status"] !== "confirmed" || receipt["operationId"] !== operation.operationId
23875
+ || receipt["reviewId"] !== operation.reviewId) return { status: "unknown" };
23876
+ return { status: "confirmed", operationId: operation.operationId, reviewId: operation.reviewId };
23877
+ }),
23878
+ async list() {
23879
+ const { data, error, status } = await options.supabase.from("starter_reviews")
23880
+ .select("id,state,version").order("id").limit(100);
23881
+ if (error) {
23882
+ if (status === 401 || status === 403) throw new ReviewAccessDenied();
23883
+ throw new Error("Review read failed");
23884
+ }
23885
+ if (!isList(data)) throw new Error("Invalid list");
23886
+ return data.map(row);
23887
+ },
23888
+ async approve(operation: ReviewOperation) {
23889
+ const result = await request("/reviews/" + encodeURIComponent(operation.reviewId) + "/approve",
23890
+ { expectedVersion: operation.expectedVersion }, operation.operationId);
23891
+ if (!result.response.ok) return rejected(result.response.status, result.body);
23892
+ const value = record(result.body);
23893
+ if (value["state"] !== "approved" || value["version"] !== operation.expectedVersion + 1) return { status: "unknown" };
23894
+ return { status: "confirmed", operationId: operation.operationId, reviewId: operation.reviewId };
23895
+ },
23896
+ async upload(operation: ReviewOperation, file: Blob) {
23897
+ const input = { artifactId: operation.operationId, expectedVersion: operation.expectedVersion };
23898
+ const base = "/reviews/" + encodeURIComponent(operation.reviewId);
23899
+ const preview = await request(base + "/attachment-upload", input, operation.operationId);
23900
+ if (!preview.response.ok) {
23901
+ // These explicit preview rejections happen before any object or registration write.
23902
+ const code = record(preview.body)["code"];
23903
+ if ((preview.response.status === 501 && code === "UPLOADS_UNAVAILABLE")
23904
+ || (preview.response.status === 400 && code === "UPLOAD_INPUT_INVALID")) return { status: "denied" };
23905
+ return rejected(preview.response.status, preview.body);
23906
+ }
23907
+ const location = record(preview.body);
23908
+ if (location["artifactId"] !== operation.operationId || location["bucketId"] !== "review-attachments"
23909
+ || typeof location["objectPath"] !== "string"
23910
+ || !location["objectPath"].endsWith("/" + operation.reviewId + "/" + operation.operationId + ".txt")) {
23911
+ return { status: "unknown" };
23912
+ }
23913
+ // Never overwrite an object or repeat this upload after a network failure.
23914
+ const stored = await options.supabase.storage.from("review-attachments")
23915
+ .upload(location["objectPath"], file, { upsert: false, contentType: "text/plain" });
23916
+ if (stored.error) return { status: "unknown" };
23917
+ const registered = await request(base + "/attachment-registration", input, operation.operationId);
23918
+ // The object is already stored. A rejection cannot roll back that side effect.
23919
+ if (!registered.response.ok) return { status: "unknown" };
23920
+ const value = record(registered.body);
23921
+ const sha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await file.arrayBuffer())),
23922
+ byte => byte.toString(16).padStart(2, "0")).join("");
23923
+ if (value["artifactId"] !== operation.operationId || value["objectPath"] !== location["objectPath"]
23924
+ || typeof value["runId"] !== "string" || value["sha256"] !== sha256
23925
+ || value["bytes"] !== file.size) return { status: "unknown" };
23926
+ return { status: "confirmed", operationId: operation.operationId, reviewId: operation.reviewId };
23927
+ },
23928
+ };
23929
+ }
23930
+ `;
23931
+ var STARTER_REVIEW_CLIENT_TEST = String.raw`import { expect, test } from "bun:test";
23932
+ import { createReviewClient, createReviewPendingStore, ReviewAccessDenied, type ReviewOperation, type ReviewTransport, type ReviewRow, type ReviewWriteResult } from "../src/client/reviews";
23933
+ import { createReviewTransport } from "../src/client/review-transport";
23934
+
23935
+ const draft: ReviewRow = { id: "review-one", state: "draft", version: 1 };
23936
+ const uploadDraft: ReviewRow = { ...draft, id: "11111111-1111-4111-8111-111111111111" };
23937
+ function transport(overrides: Partial<ReviewTransport> = {}): ReviewTransport {
23938
+ return {
23939
+ list: async () => [draft],
23940
+ approve: async op => ({ status: "confirmed", operationId: op.operationId, reviewId: op.reviewId }),
23941
+ upload: async op => ({ status: "confirmed", operationId: op.operationId, reviewId: op.reviewId }),
23942
+ ...overrides,
23943
+ };
23944
+ }
23945
+ test("list has loading, empty, permission and failure states; old results cannot replace new reads", async () => {
23946
+ let resolve: (rows: ReviewRow[]) => void = () => {};
23947
+ let calls = 0;
23948
+ const client = createReviewClient(transport({ list: () => ++calls === 1
23949
+ ? new Promise(done => { resolve = done; }) : Promise.resolve([]) }));
23950
+ const loading = client.load();
23951
+ expect(client.snapshot().list.status).toBe("loading");
23952
+ await client.load();
23953
+ resolve([draft]); await loading;
23954
+ expect(client.snapshot().list.status).toBe("empty");
23955
+ for (const [error, status] of [[new ReviewAccessDenied(), "denied"], [new Error("private"), "error"]] as const) {
23956
+ const failed = createReviewClient(transport({ list: async () => { throw error; } }));
23957
+ await failed.load();
23958
+ expect(failed.snapshot().list).toEqual({ status, rows: [] });
23959
+ expect(JSON.stringify(failed.snapshot())).not.toContain("private");
23960
+ }
23961
+ });
23962
+ test("double submit sends one write; unknown retains its identity and only a lookup can confirm it", async () => {
23963
+ let sends = 0, lookups = 0, resolve: (result: ReviewWriteResult) => void = () => {};
23964
+ const client = createReviewClient(transport({
23965
+ approve: () => { sends++; return new Promise(done => { resolve = done; }); },
23966
+ lookup: async op => { lookups++; return { status: "confirmed", operationId: op.operationId, reviewId: op.reviewId }; },
23967
+ }));
23968
+ const pending = client.approve(draft);
23969
+ const operation = client.snapshot().mutation.operation;
23970
+ await client.approve(draft);
23971
+ resolve({ status: "unknown" }); await pending;
23972
+ await client.approve(draft);
23973
+ expect(sends).toBe(1);
23974
+ expect(client.snapshot().mutation).toEqual({ status: "unknown", operation });
23975
+ await client.reconcile();
23976
+ expect(sends).toBe(1); expect(lookups).toBe(1);
23977
+ expect(client.snapshot().mutation.status).toBe("confirmed");
23978
+ });
23979
+ test("conflict refreshes but does not retry the write; wrong receipt remains unknown", async () => {
23980
+ let writes = 0, reads = 0;
23981
+ const client = createReviewClient(transport({
23982
+ list: async () => { reads++; return []; },
23983
+ approve: async () => { writes++; return { status: "conflict" }; },
23984
+ }));
23985
+ await client.approve(draft);
23986
+ expect(writes).toBe(1); expect(reads).toBe(1);
23987
+ expect(client.snapshot().mutation.status).toBe("conflict");
23988
+ const wrong = createReviewClient(transport({
23989
+ approve: async () => ({ status: "confirmed", operationId: "foreign", reviewId: draft.id }),
23990
+ }));
23991
+ await wrong.approve(draft);
23992
+ expect(wrong.snapshot().mutation.status).toBe("unknown");
23993
+ });
23994
+ test("upload validates size/type, retains failure identity, and disposal drops late results", async () => {
23995
+ let uploads = 0;
23996
+ const client = createReviewClient(transport({ upload: async () => { uploads++; throw new Error("private"); } }));
23997
+ await client.upload(uploadDraft, new Blob(["bad"], { type: "application/pdf" }));
23998
+ expect(uploads).toBe(0);
23999
+ await client.upload(uploadDraft, new Blob(["ok"], { type: "text/plain" }));
24000
+ expect(uploads).toBe(1);
24001
+ expect(client.snapshot().mutation.status).toBe("unknown");
24002
+ await client.reconcile();
24003
+ expect(uploads).toBe(1);
24004
+ client.dispose();
24005
+ await client.upload(uploadDraft, new Blob(["ok"], { type: "text/plain" }));
24006
+ expect(uploads).toBe(1);
24007
+ });
24008
+
24009
+ test("upload-only input constraints never persist or dispatch an impossible request or block approval", async () => {
24010
+ let saves = 0, uploads = 0, approvals = 0, saved: ReviewOperation | null = null;
24011
+ const client = createReviewClient(transport({
24012
+ upload: async () => { uploads++; return { status: "unknown" }; },
24013
+ approve: async op => { approvals++; return { status: "confirmed", operationId: op.operationId, reviewId: op.reviewId }; },
24014
+ }), {
24015
+ load: () => saved, save: op => { saves++; saved = op; }, clear: () => { saved = null; },
24016
+ });
24017
+ await client.load();
24018
+ expect(client.snapshot().list.rows).toEqual([draft]);
24019
+ const file = new Blob(["hello"], { type: "text/plain" });
24020
+ await client.upload(draft, file);
24021
+ await client.upload({ ...uploadDraft, version: 2147483648 }, file);
24022
+ expect(uploads).toBe(0); expect(saves).toBe(0); expect(saved).toBeNull();
24023
+ expect(client.snapshot().mutation).toEqual({ status: "idle", operation: null });
24024
+ await client.approve(draft);
24025
+ expect(approvals).toBe(1); expect(saves).toBe(1);
24026
+ expect(client.snapshot().mutation.status).toBe("confirmed");
24027
+ });
24028
+
24029
+ test("explicit upload preview rejection clears pending before side effects, without retrying", async () => {
24030
+ for (const [status, code] of [[501, "UPLOADS_UNAVAILABLE"], [400, "UPLOAD_INPUT_INVALID"]] as const) {
24031
+ let requests = 0, uploads = 0, saved: ReviewOperation | null = null;
24032
+ const adapter = createReviewTransport({
24033
+ supabase: {
24034
+ storage: { from: () => ({ upload: async () => { uploads++; return { error: null }; } }) },
24035
+ } as unknown as Parameters<typeof createReviewTransport>[0]["supabase"],
24036
+ apiOrigin: "https://reviews.example.test",
24037
+ authenticatedFetch: async url => {
24038
+ requests++;
24039
+ return String(url).endsWith("/attachment-upload")
24040
+ ? Response.json({ code }, { status }) : Response.json({ state: "approved", version: 2 });
24041
+ },
24042
+ });
24043
+ const client = createReviewClient({ ...adapter, list: async () => [uploadDraft] }, {
24044
+ load: () => saved, save: op => { saved = op; }, clear: () => { saved = null; },
24045
+ });
24046
+ await client.upload(uploadDraft, new Blob(["hello"], { type: "text/plain" }));
24047
+ expect(client.snapshot().mutation.status).toBe("denied");
24048
+ expect(saved).toBeNull(); expect(uploads).toBe(0); expect(requests).toBe(1);
24049
+ await client.reconcile();
24050
+ expect(requests).toBe(1);
24051
+ await client.approve(uploadDraft);
24052
+ expect(client.snapshot().mutation.status).toBe("confirmed");
24053
+ expect(requests).toBe(2);
24054
+ }
24055
+ });
24056
+
24057
+ test("HTTP approval makes one authenticated request and preserves denied, conflict and unknown results", async () => {
24058
+ const operation = { kind: "approve" as const, reviewId: draft.id, expectedVersion: 1, operationId: crypto.randomUUID() };
24059
+ for (const [httpStatus, body, expected] of [
24060
+ [200, { state: "approved", version: 2 }, "confirmed"],
24061
+ [403, { code: "REVIEW_PERMISSION_DENIED" }, "denied"],
24062
+ [409, { code: "COMMAND_IDEMPOTENCY_CONFLICT" }, "conflict"],
24063
+ [500, { code: "INTERNAL_ERROR" }, "unknown"],
24064
+ [200, { state: "approved", version: 99 }, "unknown"],
24065
+ ] as const) {
24066
+ let calls = 0;
24067
+ const adapter = createReviewTransport({
24068
+ supabase: {} as Parameters<typeof createReviewTransport>[0]["supabase"],
24069
+ apiOrigin: "https://reviews.example.test",
24070
+ authenticatedFetch: async (url, init) => {
24071
+ calls++;
24072
+ expect(String(url)).toBe("https://reviews.example.test/reviews/review-one/approve");
24073
+ expect(new Headers(init?.headers).get("idempotency-key")).toBe(operation.operationId);
24074
+ expect(init?.redirect).toBe("error");
24075
+ expect(JSON.parse(String(init?.body))).toEqual({ expectedVersion: 1 });
24076
+ return Response.json(body, { status: httpStatus });
24077
+ },
24078
+ });
24079
+ expect((await adapter.approve(operation)).status).toBe(expected);
24080
+ expect(calls).toBe(1);
24081
+ }
24082
+ });
24083
+
24084
+ test("private upload verifies its binding and checksum; a lost registration response is never replayed", async () => {
24085
+ const operationId = crypto.randomUUID();
24086
+ const file = new Blob(["hello"], { type: "text/plain" });
24087
+ const path = "owner/" + uploadDraft.id + "/" + operationId + ".txt";
24088
+ let uploads = 0, requests = 0;
24089
+ const supabase = {
24090
+ storage: { from(bucket: string) {
24091
+ expect(bucket).toBe("review-attachments");
24092
+ return { async upload(objectPath: string, body: Blob, options: { upsert: boolean }) {
24093
+ uploads++;
24094
+ expect(objectPath).toBe(path); expect(body).toBe(file); expect(options.upsert).toBe(false);
24095
+ return { error: null };
24096
+ } };
24097
+ } },
24098
+ } as unknown as Parameters<typeof createReviewTransport>[0]["supabase"];
24099
+ const adapter = createReviewTransport({
24100
+ supabase, apiOrigin: "https://reviews.example.test",
24101
+ authenticatedFetch: async url => {
24102
+ requests++;
24103
+ if (String(url).endsWith("/attachment-upload")) {
24104
+ return Response.json({ artifactId: operationId, bucketId: "review-attachments", objectPath: path });
24105
+ }
24106
+ throw new Error("Registration response lost");
24107
+ },
24108
+ });
24109
+ const operation = { kind: "upload" as const, reviewId: uploadDraft.id, expectedVersion: 1, operationId };
24110
+ await expect(adapter.upload(operation, file)).rejects.toThrow("Registration response lost");
24111
+ expect(uploads).toBe(1); expect(requests).toBe(2);
24112
+ const confirmed = createReviewTransport({
24113
+ supabase, apiOrigin: "https://reviews.example.test",
24114
+ authenticatedFetch: async url => String(url).endsWith("/attachment-upload")
24115
+ ? Response.json({ artifactId: operationId, bucketId: "review-attachments", objectPath: path })
24116
+ : Response.json({ artifactId: operationId, objectPath: path, runId: "run-one", bytes: 5,
24117
+ sha256: "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" }),
24118
+ });
24119
+ expect((await confirmed.upload(operation, file)).status).toBe("confirmed");
24120
+ });
24121
+
24122
+ test("reload restores pending identity, denies replay and resolves only a matching durable receipt", async () => {
24123
+ const values = new Map<string, string>();
24124
+ const storage = {
24125
+ getItem: (key: string) => values.get(key) ?? null,
24126
+ setItem: (key: string, value: string) => { values.set(key, value); },
24127
+ removeItem: (key: string) => { values.delete(key); },
24128
+ };
24129
+ const scope = { projectId: "p", tenantId: "t", applicationId: "reviews", subject: "owner" };
24130
+ const pending = createReviewPendingStore(storage, scope);
24131
+ let writes = 0;
24132
+ const first = createReviewClient(transport({ approve: async () => { writes++; return { status: "unknown" }; } }), pending);
24133
+ await first.approve(draft);
24134
+ const operation = first.snapshot().mutation.operation!;
24135
+ first.dispose();
24136
+ const reloaded = createReviewClient(transport({
24137
+ approve: async () => { writes++; return { status: "unknown" }; },
24138
+ lookup: async op => ({ status: "confirmed", operationId: op.operationId, reviewId: op.reviewId }),
24139
+ }), createReviewPendingStore(storage, scope));
24140
+ expect(reloaded.snapshot().mutation).toEqual({ status: "unknown", operation });
24141
+ await reloaded.approve(draft);
24142
+ expect(writes).toBe(1);
24143
+ expect(createReviewPendingStore(storage, { ...scope, subject: "other" }).load()).toBeNull();
24144
+ expect(createReviewPendingStore(storage, { ...scope, projectId: "other" }).load()).toBeNull();
24145
+ await reloaded.reconcile();
24146
+ expect(reloaded.snapshot().mutation.status).toBe("confirmed");
24147
+ expect(pending.load()).toBeNull();
24148
+ });
24149
+
24150
+ test("denied or conflicting recovery never unlocks an unknown operation", async () => {
24151
+ for (const status of ["denied", "conflict"] as const) {
24152
+ let writes = 0;
24153
+ const client = createReviewClient(transport({
24154
+ approve: async () => { writes++; return { status: "unknown" }; },
24155
+ lookup: async () => ({ status }),
24156
+ }));
24157
+ await client.approve(draft);
24158
+ const original = client.snapshot().mutation.operation;
24159
+ await client.reconcile(); await client.approve(draft);
24160
+ expect(writes).toBe(1);
24161
+ expect(client.snapshot().mutation).toEqual({ status: "unknown", operation: original });
24162
+ }
24163
+ });
24164
+
24165
+ test("unavailable recovery storage prevents dispatch, and disposal leaves pending evidence", async () => {
24166
+ let writes = 0;
24167
+ const client = createReviewClient(transport({ approve: async () => { writes++; return { status: "unknown" }; } }), {
24168
+ load: () => null, save: () => { throw new Error("Storage unavailable"); }, clear: () => {},
24169
+ });
24170
+ await client.approve(draft);
24171
+ expect(writes).toBe(0);
24172
+ expect(client.snapshot().mutation.status).toBe("unknown");
24173
+ let saved: ReviewOperation | null = null, cleared = 0, resolve: (result: ReviewWriteResult) => void = () => {};
24174
+ const closed = createReviewClient(transport({ approve: () => new Promise(done => { resolve = done; }) }), {
24175
+ load: () => saved, save: op => { saved = op; }, clear: () => { cleared++; },
24176
+ });
24177
+ const request = closed.approve(draft);
24178
+ const op = closed.snapshot().mutation.operation!;
24179
+ closed.dispose();
24180
+ resolve({ status: "confirmed", operationId: op.operationId, reviewId: op.reviewId }); await request;
24181
+ expect<ReviewOperation | null>(saved).toEqual(op); expect(cleared).toBe(0);
24182
+ });
24183
+
24184
+ test("default recovery transport performs one read without body or mutation retry", async () => {
24185
+ let calls = 0;
24186
+ const operation = { kind: "approve" as const, reviewId: draft.id, expectedVersion: 1, operationId: crypto.randomUUID() };
24187
+ const adapter = createReviewTransport({
24188
+ supabase: {} as Parameters<typeof createReviewTransport>[0]["supabase"],
24189
+ apiOrigin: "https://reviews.example.test",
24190
+ authenticatedFetch: async (input, init) => {
24191
+ calls++;
24192
+ const url = new URL(String(input));
24193
+ expect(url.pathname).toBe("/reviews/" + draft.id + "/operations/" + operation.operationId);
24194
+ expect(url.searchParams.get("expectedVersion")).toBe("1");
24195
+ expect(init?.method).toBe("GET"); expect(init?.body).toBeUndefined();
24196
+ expect(init?.cache).toBe("no-store");
24197
+ return Response.json({ status: "confirmed", operationId: operation.operationId, reviewId: operation.reviewId });
24198
+ },
24199
+ });
24200
+ expect((await adapter.lookup!(operation)).status).toBe("confirmed");
24201
+ expect(calls).toBe(1);
24202
+ });
24203
+
24204
+ test("corrupt pending records fail closed and cannot be overwritten by another operation", async () => {
24205
+ let value: string | null = "{broken", writes = 0;
24206
+ const pending = createReviewPendingStore({
24207
+ getItem: () => value, setItem: (_key, next) => { value = next; }, removeItem: () => { value = null; },
24208
+ }, { projectId: "p", tenantId: "t", applicationId: "a", subject: "s" });
24209
+ const blocked = createReviewClient(transport({ approve: async () => { writes++; return { status: "unknown" }; } }), pending);
24210
+ await blocked.approve(draft);
24211
+ expect(writes).toBe(0);
24212
+ expect(blocked.snapshot().mutation).toEqual({ status: "unknown", operation: null });
24213
+ expect(value).toBe("{broken");
24214
+ value = null;
24215
+ const original = { kind: "approve" as const, reviewId: draft.id, expectedVersion: 1, operationId: crypto.randomUUID() };
24216
+ pending.save(original);
24217
+ const other = { ...original, operationId: crypto.randomUUID() };
24218
+ expect(() => pending.save(other)).toThrow("Another unresolved operation");
24219
+ expect(() => pending.clear(other)).toThrow("identity changed");
24220
+ expect(pending.load()).toEqual(original);
24221
+ });
24222
+
24223
+ test("a competing pending operation is recovered instead of the undispatched attempt", async () => {
24224
+ let value: string | null = null, writes = 0, lookedUp: unknown;
24225
+ const pending = createReviewPendingStore({
24226
+ getItem: () => value, setItem: (_key, next) => { value = next; }, removeItem: () => { value = null; },
24227
+ }, { projectId: "p", tenantId: "t", applicationId: "a", subject: "s" });
24228
+ const client = createReviewClient(transport({
24229
+ approve: async () => { writes++; return { status: "unknown" }; },
24230
+ lookup: async op => {
24231
+ lookedUp = op;
24232
+ return { status: "confirmed", operationId: op.operationId, reviewId: op.reviewId };
24233
+ },
24234
+ }), pending);
24235
+ const original = { kind: "upload" as const, reviewId: uploadDraft.id, expectedVersion: 1, operationId: crypto.randomUUID() };
24236
+ pending.save(original);
24237
+ await client.approve(draft);
24238
+ expect(writes).toBe(0);
24239
+ expect(client.snapshot().mutation).toEqual({ status: "unknown", operation: original });
24240
+ await client.reconcile();
24241
+ expect(lookedUp).toEqual(original);
24242
+ expect(pending.load()).toBeNull();
24243
+ });
24244
+
24245
+ test("disposal during submitting notification prevents dispatch and preserves pending identity", async () => {
24246
+ let writes = 0, saved: ReviewOperation | null = null;
24247
+ const client = createReviewClient(transport({
24248
+ approve: async () => { writes++; return { status: "unknown" }; },
24249
+ }), { load: () => saved, save: op => { saved = op; }, clear: () => { saved = null; } });
24250
+ client.subscribe(() => { if (client.snapshot().mutation.status === "submitting") client.dispose(); });
24251
+ await client.approve(draft);
24252
+ expect(writes).toBe(0);
24253
+ expect(client.snapshot().mutation.operation).toEqual(saved);
24254
+ expect(saved).not.toBeNull();
24255
+ });
24256
+
24257
+ test("registration rejection after object upload retains pending identity and never submits a replacement", async () => {
24258
+ for (const [status, code] of [[403, "UPLOAD_DENIED"], [409, "COMMAND_IDEMPOTENCY_CONFLICT"]] as const) {
24259
+ let uploads = 0, requests = 0, saved: ReviewOperation | null = null;
24260
+ const adapter = createReviewTransport({
24261
+ supabase: {
24262
+ storage: { from: () => ({ upload: async () => { uploads++; return { error: null }; } }) },
24263
+ } as unknown as Parameters<typeof createReviewTransport>[0]["supabase"],
24264
+ apiOrigin: "https://reviews.example.test",
24265
+ authenticatedFetch: async (input, init) => {
24266
+ requests++;
24267
+ if (String(input).endsWith("/attachment-upload")) {
24268
+ const operationId = new Headers(init?.headers).get("idempotency-key")!;
24269
+ return Response.json({ artifactId: operationId, bucketId: "review-attachments",
24270
+ objectPath: "owner/" + uploadDraft.id + "/" + operationId + ".txt" });
24271
+ }
24272
+ return Response.json({ code }, { status });
24273
+ },
24274
+ });
24275
+ const client = createReviewClient(adapter, {
24276
+ load: () => saved, save: op => { saved = op; }, clear: () => { saved = null; },
24277
+ });
24278
+ const file = new Blob(["hello"], { type: "text/plain" });
24279
+ await client.upload(uploadDraft, file);
24280
+ expect(client.snapshot().mutation.status).toBe("unknown");
24281
+ expect(client.snapshot().mutation.operation).toEqual(saved);
24282
+ expect(saved).not.toBeNull();
24283
+ await client.upload(uploadDraft, file);
24284
+ expect(uploads).toBe(1);
24285
+ expect(requests).toBe(2);
24286
+ }
24287
+ });
24288
+
24289
+
24290
+ for (const kind of ["approve", "upload"] as const) {
24291
+ const submit = (client: ReturnType<typeof createReviewClient>) => kind === "approve"
24292
+ ? client.approve(draft) : client.upload(uploadDraft, new Blob(["hello"], { type: "text/plain" }));
24293
+ for (const failure of ["silent", "read-error", "operation", "version", "kind"] as const) {
24294
+ test(kind + " verifies pending readback before any transport: " + failure, async () => {
24295
+ let saved: ReviewOperation | null = null, attempted = false, sends = 0;
24296
+ const client = createReviewClient(transport({
24297
+ approve: async () => { sends++; return { status: "unknown" }; },
24298
+ upload: async () => { sends++; return { status: "unknown" }; },
24299
+ }), {
24300
+ load() {
24301
+ if (attempted && failure === "read-error") throw new Error("private storage failure");
24302
+ return saved;
24303
+ },
24304
+ save(operation) {
24305
+ attempted = true;
24306
+ if (failure === "silent") return;
24307
+ saved = { ...operation };
24308
+ if (failure === "operation") saved.operationId = crypto.randomUUID();
24309
+ if (failure === "version") saved.expectedVersion++;
24310
+ if (failure === "kind") saved.kind = kind === "approve" ? "upload" : "approve";
24311
+ },
24312
+ clear() { throw new Error("Must not clear an unconfirmed operation"); },
24313
+ });
24314
+ await submit(client);
24315
+ await submit(client);
24316
+ expect(sends).toBe(0);
24317
+ expect(client.snapshot().mutation.status).toBe("unknown");
24318
+ expect(JSON.stringify(client.snapshot())).not.toContain("private storage failure");
24319
+ expect(client.snapshot().mutation.operation).toEqual(failure === "read-error" ? null : saved);
24320
+ });
24321
+ }
24322
+ for (const resultStatus of ["confirmed", "denied", "conflict"] as const) {
24323
+ test(kind + " retains pending after silent cleanup: " + resultStatus, async () => {
24324
+ let saved: ReviewOperation | null = null, sends = 0, clears = 0;
24325
+ const send = async (operation: ReviewOperation): Promise<ReviewWriteResult> => {
24326
+ sends++;
24327
+ return resultStatus === "confirmed"
24328
+ ? { status: "confirmed", operationId: operation.operationId, reviewId: operation.reviewId }
24329
+ : { status: resultStatus };
24330
+ };
24331
+ const client = createReviewClient(transport({ approve: send, upload: send }), {
24332
+ load: () => saved, save: operation => { saved = operation; }, clear: () => { clears++; },
24333
+ });
24334
+ await submit(client);
24335
+ expect(client.snapshot().mutation).toEqual({ status: "unknown", operation: saved });
24336
+ await submit(client);
24337
+ expect(sends).toBe(1);
24338
+ expect(clears).toBe(1);
24339
+ expect(saved).not.toBeNull();
24340
+ });
24341
+ }
24342
+ test(kind + " accepts exact JSON readback and recovers once without another write", async () => {
24343
+ const values = new Map<string, string>();
24344
+ const pending = createReviewPendingStore({
24345
+ getItem: key => values.get(key) ?? null,
24346
+ setItem: (key, value) => { values.set(key, value); },
24347
+ removeItem: key => { values.delete(key); },
24348
+ }, { projectId: "project", tenantId: "tenant", applicationId: "reviews", subject: "owner" });
24349
+ let sends = 0, reads = 0;
24350
+ const send = async (operation: ReviewOperation): Promise<ReviewWriteResult> => {
24351
+ sends++;
24352
+ // Stored field ordering differs from the newly created operation.
24353
+ expect(pending.load()).toEqual(operation);
24354
+ return { status: "unknown" };
24355
+ };
24356
+ const client = createReviewClient(transport({
24357
+ approve: send, upload: send,
24358
+ lookup: async operation => {
24359
+ reads++;
24360
+ return { status: "confirmed", operationId: operation.operationId, reviewId: operation.reviewId };
24361
+ },
24362
+ }), pending);
24363
+ await submit(client);
24364
+ await submit(client);
24365
+ expect(sends).toBe(1);
24366
+ await client.reconcile();
24367
+ expect(client.snapshot().mutation.status).toBe("confirmed");
24368
+ expect(pending.load()).toBeNull();
24369
+ expect(sends).toBe(1);
24370
+ expect(reads).toBe(1);
24371
+ });
24372
+ }
24373
+ `;
24374
+
24375
+ // src/shared/tools/app-starter-receipts.ts
24376
+ var STARTER_REVIEW_RECEIPTS = `import { Effect } from "effect";
24377
+ import { Controller, DB_CLIENT, Get, Inject, Injectable, Param, Query, REQUEST_CONTEXT } from "@supacloud/app";
24378
+ import { ApplicationError, createSchemaDecoder } from "@supacloud/elysia";
24379
+ import { ReceiptOperation, ReceiptParams, ReceiptQuery, ReceiptResult,
24380
+ type ReviewOperation, type ReviewReceipt } from "./contracts";
24381
+ export type { ReviewOperation, ReviewReceipt } from "./contracts";
24382
+
24383
+ const decodeOperation = createSchemaDecoder(ReceiptOperation, { normalize: false });
24384
+ export interface ReviewReceiptPort {
24385
+ lookupOperation(context: unknown, operation: ReviewOperation): Promise<ReviewReceipt>;
24386
+ }
24387
+ type ReceiptError =
24388
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
24389
+ | { readonly _tag: "REVIEW_PERMISSION_DENIED" }
24390
+ | { readonly _tag: "UPLOAD_DENIED" }
24391
+ | { readonly _tag: "COMMAND_REJECTED" }
24392
+ | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
24393
+ | { readonly _tag: "RECEIPT_INPUT_INVALID" }
24394
+ | { readonly _tag: "RECEIPTS_UNAVAILABLE" };
24395
+
24396
+ @Injectable({ scope: "request" })
24397
+ export class ReviewReceipts {
24398
+ constructor(@Inject(DB_CLIENT) private readonly store: Partial<ReviewReceiptPort>,
24399
+ @Inject(REQUEST_CONTEXT) private readonly context: unknown) {}
24400
+ lookup(operation: ReviewOperation): Promise<ReviewReceipt> {
24401
+ let input: ReviewOperation;
24402
+ try { input = decodeOperation(operation); }
24403
+ catch {
24404
+ throw new ApplicationError("Invalid receipt input", { status: 400, code: "RECEIPT_INPUT_INVALID" });
24405
+ }
24406
+ if (!this.store.lookupOperation) {
24407
+ throw new ApplicationError("Receipt lookup is unavailable", { status: 503, code: "RECEIPTS_UNAVAILABLE" });
24408
+ }
24409
+ return this.store.lookupOperation(this.context, input);
24410
+ }
24411
+ }
24412
+
24413
+ @Controller("/reviews")
24414
+ export class ReviewReceiptsController {
24415
+ constructor(@Inject(ReviewReceipts) private readonly receipts: ReviewReceipts) {}
24416
+ @Get("/:id/operations/:operationId", {
24417
+ params: ReceiptParams, query: ReceiptQuery, responses: { 200: ReceiptResult },
24418
+ effect: {
24419
+ required: true, dependencies: [], retry: "none",
24420
+ errors: [
24421
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
24422
+ { tag: "REVIEW_PERMISSION_DENIED", status: 403, code: "REVIEW_PERMISSION_DENIED" },
24423
+ { tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
24424
+ { tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
24425
+ { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
24426
+ { tag: "RECEIPT_INPUT_INVALID", status: 400, code: "RECEIPT_INPUT_INVALID" },
24427
+ { tag: "RECEIPTS_UNAVAILABLE", status: 503, code: "RECEIPTS_UNAVAILABLE" },
24428
+ ],
24429
+ },
24430
+ })
24431
+ lookup(@Param("id") reviewId: string, @Param("operationId") operationId: string,
24432
+ @Query("kind") kind: ReviewOperation["kind"], @Query("expectedVersion") expectedVersion: string): Effect.Effect<ReviewReceipt, ReceiptError, never> {
24433
+ return Effect.promise(() => this.receipts.lookup({ reviewId, operationId, kind, expectedVersion: Number(expectedVersion) }));
24434
+ }
24435
+ }
24436
+ `;
24437
+
23432
24438
  // src/shared/tools/app-starter-query.ts
23433
24439
  var STARTER_REVIEW_QUERY = `import type { SupabaseClient } from "@supabase/supabase-js";
23434
24440
  import type { ReviewRow } from "../../db/schema";
@@ -23481,6 +24487,330 @@ export async function listReviews(
23481
24487
  }
23482
24488
  `;
23483
24489
 
24490
+ // src/shared/tools/app-starter-tasks.ts
24491
+ var STARTER_TASK_CONTRACTS = `import { t, type UnwrapSchema } from "elysia";
24492
+ import { AttachmentResult } from "./attachment";
24493
+ import { REVIEW_VERSION_MAX } from "./limits";
24494
+ import { ReviewVersion } from "./contracts";
24495
+
24496
+ export const TaskParams = t.Object({ id: t.String({ minLength: 1, maxLength: 100 }) }, { additionalProperties: false });
24497
+ export const RetryBody = t.Object({
24498
+ expectedVersion: t.Integer({ minimum: 2, maximum: REVIEW_VERSION_MAX }),
24499
+ expectedRunId: t.String({ format: "uuid" }),
24500
+ recoveryId: t.String({ format: "uuid" }),
24501
+ }, { additionalProperties: false });
24502
+ export const RetryInput = t.Object({
24503
+ reviewId: TaskParams.properties.id, ...RetryBody.properties,
24504
+ }, { additionalProperties: false });
24505
+ export const TaskStatus = t.Object({
24506
+ reviewId: TaskParams.properties.id,
24507
+ version: ReviewVersion,
24508
+ artifactId: t.Nullable(t.String({ format: "uuid" })),
24509
+ runId: t.Nullable(t.String({ format: "uuid" })),
24510
+ state: t.Union([t.Literal("not_submitted"), t.Literal("queued"), t.Literal("running"),
24511
+ t.Literal("completed"), t.Literal("failed"), t.Literal("cancelled")]),
24512
+ attempts: t.Integer({ minimum: 0 }),
24513
+ maxAttempts: t.Literal(3),
24514
+ canRetry: t.Boolean(),
24515
+ result: t.Nullable(AttachmentResult),
24516
+ }, { additionalProperties: false });
24517
+ export const RetryResult = t.Object({
24518
+ reviewId: TaskParams.properties.id,
24519
+ previousRunId: t.String({ format: "uuid" }),
24520
+ runId: t.String({ format: "uuid" }),
24521
+ state: t.Literal("queued"),
24522
+ }, { additionalProperties: false });
24523
+ export type ReviewTaskStatus = UnwrapSchema<typeof TaskStatus>;
24524
+ export type ReviewRetryBody = UnwrapSchema<typeof RetryBody>;
24525
+ export type ReviewRetryInput = UnwrapSchema<typeof RetryInput>;
24526
+ export type ReviewRetryResult = UnwrapSchema<typeof RetryResult>;
24527
+ `;
24528
+ var STARTER_TASK_FEATURE = `import { Effect } from "effect";
24529
+ import { Body, Controller, DB_CLIENT, Get, Inject, Injectable, Param, Post, REQUEST_CONTEXT } from "@supacloud/app";
24530
+ import { ApplicationError } from "@supacloud/elysia";
24531
+ import { RetryBody, RetryResult, TaskParams, TaskStatus,
24532
+ type ReviewRetryBody, type ReviewRetryInput, type ReviewRetryResult, type ReviewTaskStatus } from "./task-contracts";
24533
+
24534
+ export interface ReviewTaskPort {
24535
+ attachmentTask(context: unknown, reviewId: string): Promise<ReviewTaskStatus>;
24536
+ retryAttachmentTask(context: unknown, input: ReviewRetryInput): Promise<ReviewRetryResult>;
24537
+ }
24538
+ type TaskError =
24539
+ | { readonly _tag: "AUTHENTICATION_REQUIRED" }
24540
+ | { readonly _tag: "TASK_INPUT_INVALID" }
24541
+ | { readonly _tag: "TASK_DENIED" }
24542
+ | { readonly _tag: "TASK_CONFLICT" }
24543
+ | { readonly _tag: "TASK_UNAVAILABLE" }
24544
+ | { readonly _tag: "TASK_OUTCOME_UNKNOWN" };
24545
+
24546
+ @Injectable({ scope: "request" })
24547
+ export class ReviewTasks {
24548
+ constructor(@Inject(DB_CLIENT) private readonly store: Partial<ReviewTaskPort>,
24549
+ @Inject(REQUEST_CONTEXT) private readonly context: unknown) {}
24550
+
24551
+ status(id: string): Promise<ReviewTaskStatus> {
24552
+ if (!this.store.attachmentTask) throw new ApplicationError("Attachment tasks are not configured", { status: 501, code: "TASK_UNAVAILABLE" });
24553
+ return this.store.attachmentTask(this.context, id);
24554
+ }
24555
+ retry(input: ReviewRetryInput): Promise<ReviewRetryResult> {
24556
+ if (!this.store.retryAttachmentTask) throw new ApplicationError("Attachment tasks are not configured", { status: 501, code: "TASK_UNAVAILABLE" });
24557
+ return this.store.retryAttachmentTask(this.context, input);
24558
+ }
24559
+ }
24560
+
24561
+ @Controller("/reviews")
24562
+ export class ReviewTasksController {
24563
+ constructor(@Inject(ReviewTasks) private readonly tasks: ReviewTasks) {}
24564
+
24565
+ @Get("/:id/attachment-task", {
24566
+ params: TaskParams, responses: { 200: TaskStatus },
24567
+ effect: { required: true, dependencies: [], retry: "none", errors: [
24568
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
24569
+ { tag: "TASK_INPUT_INVALID", status: 400, code: "TASK_INPUT_INVALID" },
24570
+ { tag: "TASK_DENIED", status: 403, code: "TASK_DENIED" },
24571
+ { tag: "TASK_CONFLICT", status: 409, code: "TASK_CONFLICT" },
24572
+ { tag: "TASK_UNAVAILABLE", status: 501, code: "TASK_UNAVAILABLE" },
24573
+ { tag: "TASK_OUTCOME_UNKNOWN", status: 503, code: "TASK_OUTCOME_UNKNOWN" },
24574
+ ] },
24575
+ })
24576
+ status(@Param("id") id: string): Effect.Effect<ReviewTaskStatus, TaskError, never> {
24577
+ return Effect.promise(() => this.tasks.status(id));
24578
+ }
24579
+
24580
+ @Post("/:id/attachment-task/retry", {
24581
+ params: TaskParams, body: RetryBody, responses: { 200: RetryResult },
24582
+ effect: { required: true, dependencies: [], retry: "none", errors: [
24583
+ { tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
24584
+ { tag: "TASK_INPUT_INVALID", status: 400, code: "TASK_INPUT_INVALID" },
24585
+ { tag: "TASK_DENIED", status: 403, code: "TASK_DENIED" },
24586
+ { tag: "TASK_CONFLICT", status: 409, code: "TASK_CONFLICT" },
24587
+ { tag: "TASK_UNAVAILABLE", status: 501, code: "TASK_UNAVAILABLE" },
24588
+ { tag: "TASK_OUTCOME_UNKNOWN", status: 503, code: "TASK_OUTCOME_UNKNOWN" },
24589
+ ] },
24590
+ })
24591
+ retry(@Param("id") id: string, @Body() body: ReviewRetryBody): Effect.Effect<ReviewRetryResult, TaskError, never> {
24592
+ return Effect.promise(() => this.tasks.retry({ ...body, reviewId: id }));
24593
+ }
24594
+ }
24595
+ `;
24596
+ var STARTER_TASK_SCHEMA = `-- Forward-only operator provisioning after 004 and the platform Workflow runtime.
24597
+ CREATE TABLE public.starter_attachment_retries (
24598
+ run_id uuid PRIMARY KEY,
24599
+ review_id text NOT NULL REFERENCES public.starter_attachments(review_id),
24600
+ previous_run_id uuid NOT NULL UNIQUE,
24601
+ generation integer NOT NULL CHECK (generation > 0),
24602
+ UNIQUE(review_id,generation),
24603
+ CHECK (run_id <> previous_run_id)
24604
+ );
24605
+ ALTER TABLE public.starter_attachment_retries ENABLE ROW LEVEL SECURITY;
24606
+ REVOKE ALL ON public.starter_attachment_retries FROM PUBLIC, anon, authenticated;
24607
+ GRANT SELECT,INSERT ON public.starter_attachment_retries TO starter_review_http;
24608
+ GRANT SELECT ON public.starter_attachment_retries TO starter_review_worker;
24609
+ DROP POLICY IF EXISTS starter_http_retry_read ON public.starter_attachment_retries;
24610
+ CREATE POLICY starter_http_retry_read ON public.starter_attachment_retries
24611
+ FOR SELECT TO starter_review_http USING (true);
24612
+ DROP POLICY IF EXISTS starter_http_retry_insert ON public.starter_attachment_retries;
24613
+ CREATE POLICY starter_http_retry_insert ON public.starter_attachment_retries
24614
+ FOR INSERT TO starter_review_http WITH CHECK (true);
24615
+ DROP POLICY IF EXISTS starter_worker_retry_read ON public.starter_attachment_retries;
24616
+ CREATE POLICY starter_worker_retry_read ON public.starter_attachment_retries
24617
+ FOR SELECT TO starter_review_worker USING (true);
24618
+ -- Trusted HTTP adapters authorize the review before inspecting its bound run.
24619
+ GRANT EXECUTE ON FUNCTION supacloud_workflows.snapshot(uuid,boolean) TO starter_review_http;
24620
+ `;
24621
+ var STARTER_TASK_HOST = `import { t } from "elysia";
24622
+ import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
24623
+ import { CommandError } from "@supacloud/contracts";
24624
+ import { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from "@supacloud/db";
24625
+ import { ApplicationError, createSchemaDecoder, requireTrustedIdentity } from "@supacloud/elysia";
24626
+ import { AttachmentInput, AttachmentResult } from "../review/attachment";
24627
+ import { ReviewResult } from "../review/contracts";
24628
+ import { RetryInput, RetryResult, TaskParams, TaskStatus, type ReviewRetryInput } from "../review/task-contracts";
24629
+ import type { ReviewTaskPort } from "../review/tasks";
24630
+ import type { attachments, attachmentRetries, ReviewRow } from "../../db/schema";
24631
+
24632
+ export interface ReviewTaskOptions { database: CommandDatabase; projectId: string; tenantId: string }
24633
+ type Binding = Pick<typeof attachments.$inferSelect, "artifactId" | "runId">
24634
+ & Pick<typeof attachmentRetries.$inferSelect, "generation" | "runId">;
24635
+ const decodeInput = createSchemaDecoder(RetryInput, { normalize: false });
24636
+ const decodeRetry = createSchemaDecoder(RetryResult, { normalize: false });
24637
+ const decodeStatus = createSchemaDecoder(TaskStatus, { normalize: false });
24638
+ const decodeParams = createSchemaDecoder(TaskParams, { normalize: false });
24639
+ const decodeReview: (value: unknown) => Pick<ReviewRow, "state" | "version"> =
24640
+ createSchemaDecoder(ReviewResult, { normalize: false });
24641
+ const decodeResult = createSchemaDecoder(AttachmentResult, { normalize: false });
24642
+ const decodeBinding: (value: unknown) => Binding = createSchemaDecoder(t.Object({
24643
+ artifactId: t.String({ format: "uuid" }), runId: t.String({ format: "uuid" }),
24644
+ generation: t.Integer({ minimum: 0 }),
24645
+ }), { normalize: false });
24646
+ const decodeRun = createSchemaDecoder(t.Object({
24647
+ runId: t.String({ format: "uuid" }), workflowName: t.Literal("review.verify-attachment"),
24648
+ workflowVersion: t.Literal("1"),
24649
+ status: t.Union([t.Literal("queued"), t.Literal("running"), t.Literal("completed"), t.Literal("failed"), t.Literal("cancelled")]),
24650
+ input: AttachmentInput, output: t.Unknown(), idempotent: t.Boolean(),
24651
+ steps: t.Array(t.Object({
24652
+ stepKey: t.Literal("verify"), attempts: t.Integer({ minimum: 0 }),
24653
+ maxAttempts: t.Literal(3),
24654
+ status: t.Union([t.Literal("queued"), t.Literal("running"), t.Literal("completed"), t.Literal("failed"), t.Literal("dead_lettered"), t.Literal("cancelled")]),
24655
+ }, { additionalProperties: true }), { minItems: 1, maxItems: 1 }),
24656
+ }, { additionalProperties: true }), { normalize: false });
24657
+ function isRecord(value: unknown): value is Record<string, unknown> {
24658
+ return value !== null && typeof value === "object" && !Array.isArray(value);
24659
+ }
24660
+ function record(value: unknown): Record<string, unknown> {
24661
+ if (!isRecord(value)) throw new Error("Invalid task record");
24662
+ return value;
24663
+ }
24664
+ function rows(value: unknown): Record<string, unknown>[] {
24665
+ if (!Array.isArray(value)) throw new Error("Invalid task rows");
24666
+ return value.map(record);
24667
+ }
24668
+ function capture(value: unknown): ReviewRetryInput {
24669
+ try {
24670
+ const input = decodeInput(value);
24671
+ return { ...input, recoveryId: input.recoveryId.toLowerCase(), expectedRunId: input.expectedRunId.toLowerCase() };
24672
+ } catch { throw new ApplicationError("Invalid task request", { status: 400, code: "TASK_INPUT_INVALID" }); }
24673
+ }
24674
+ function failure(error: unknown): never {
24675
+ if (error instanceof ApplicationError) throw error;
24676
+ if (error instanceof CommandError && error.code === "COMMAND_REJECTED") {
24677
+ throw new ApplicationError("Task access denied", { status: 403, code: "TASK_DENIED" });
24678
+ }
24679
+ if (error instanceof CommandError && error.code === "COMMAND_IDEMPOTENCY_CONFLICT") {
24680
+ throw new ApplicationError("Task changed or is not retryable", { status: 409, code: "TASK_CONFLICT" });
24681
+ }
24682
+ throw new ApplicationError("Task outcome could not be confirmed", { status: 503, code: "TASK_OUTCOME_UNKNOWN" });
24683
+ }
24684
+ const conflict = () => { throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT"); };
24685
+
24686
+ export async function createReviewTaskAdapters(options: ReviewTaskOptions): Promise<ReviewTaskPort> {
24687
+ const { database, projectId, tenantId } = options;
24688
+ const configured = rows(await database.transaction(tx => tx.query(
24689
+ "SELECT project_id,tenant_id FROM public.starter_application WHERE singleton",
24690
+ )));
24691
+ if (!projectId.trim() || !tenantId.trim() || configured.length !== 1
24692
+ || configured[0]?.["project_id"] !== projectId || configured[0]?.["tenant_id"] !== tenantId) {
24693
+ throw new Error("Task database project/tenant binding mismatch");
24694
+ }
24695
+ function actor(context: unknown): string {
24696
+ const who = requireTrustedIdentity(context);
24697
+ const access = record(record(context)["access"]);
24698
+ if (access["projectId"] !== projectId || access["tenantId"] !== tenantId
24699
+ || !Array.isArray(access["permissions"]) || !access["permissions"].includes("review.approve")) {
24700
+ throw new ApplicationError("Task access denied", { status: 403, code: "TASK_DENIED" });
24701
+ }
24702
+ return who.subject;
24703
+ }
24704
+ async function authorize(tx: CommandTransaction, subject: string, reviewId: string) {
24705
+ const found = rows(await tx.query(
24706
+ "SELECT r.state,r.version FROM public.starter_reviews r " +
24707
+ "JOIN public.starter_members m ON m.subject=r.owner_id CROSS JOIN public.starter_application p " +
24708
+ "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
24709
+ "AND p.singleton AND p.project_id=$3 AND p.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF p",
24710
+ [reviewId, subject, projectId, tenantId],
24711
+ ));
24712
+ if (found.length !== 1) throw new ApplicationError("Task access denied", { status: 403, code: "TASK_DENIED" });
24713
+ return decodeReview(found[0]);
24714
+ }
24715
+ async function binding(tx: CommandTransaction, reviewId: string) {
24716
+ const found = rows(await tx.query(
24717
+ 'SELECT a.artifact_id AS "artifactId",COALESCE(retry.run_id,a.run_id) AS "runId",COALESCE(retry.generation,0) AS generation ' +
24718
+ "FROM public.starter_attachments a LEFT JOIN LATERAL (" +
24719
+ "SELECT run_id,generation FROM public.starter_attachment_retries WHERE review_id=a.review_id ORDER BY generation DESC LIMIT 1" +
24720
+ ") retry ON true WHERE a.review_id=$1", [reviewId],
24721
+ ));
24722
+ return found.length === 0 ? null : decodeBinding(found[0]);
24723
+ }
24724
+ async function snapshot(tx: CommandTransaction, runId: string) {
24725
+ return rows(await tx.query("SELECT supacloud_workflows.snapshot($1::uuid,false) AS run", [runId]))[0]?.["run"];
24726
+ }
24727
+ function boundRun(value: unknown, reviewId: string, version: number, bound: NonNullable<Awaited<ReturnType<typeof binding>>>) {
24728
+ const run = decodeRun(value);
24729
+ if (run.runId !== bound.runId || run.input.reviewId !== reviewId
24730
+ || run.input.version !== version || run.input.artifactId !== bound.artifactId) {
24731
+ throw new Error("Task Workflow binding mismatch");
24732
+ }
24733
+ const step = run.steps[0];
24734
+ if (!step || (run.status === "queued" && step.status !== "queued")
24735
+ || (run.status === "running" && step.status !== "queued" && step.status !== "running")
24736
+ || (run.status === "completed" && step.status !== "completed")
24737
+ || (run.status === "failed" && step.status !== "failed" && step.status !== "dead_lettered")
24738
+ || (run.status === "cancelled" && step.status !== "cancelled")) {
24739
+ throw new Error("Task Workflow state mismatch");
24740
+ }
24741
+ return run;
24742
+ }
24743
+ const retry = createTransactionalCommand({
24744
+ name: "review.retry-attachment", store: createPostgresCommandStore(database),
24745
+ inputCodec: plaintextCommandInput, input: capture, result: decodeRetry,
24746
+ async authorize(who, input, tx) {
24747
+ try {
24748
+ const review = await authorize(tx, who.actorId, input.reviewId);
24749
+ return review.state === "approved" && review.version === input.expectedVersion ? "allow" : "deny";
24750
+ } catch (error) {
24751
+ if (error instanceof ApplicationError && error.code === "TASK_DENIED") return "deny";
24752
+ throw error;
24753
+ }
24754
+ },
24755
+ async execute(tx, input) {
24756
+ const bound = await binding(tx, input.reviewId);
24757
+ if (!bound || bound.runId !== input.expectedRunId || input.recoveryId === bound.runId) return conflict();
24758
+ const previous = boundRun(await snapshot(tx, bound.runId), input.reviewId, input.expectedVersion, bound);
24759
+ if (previous.status !== "failed") return conflict();
24760
+ if (await snapshot(tx, input.recoveryId) !== null) return conflict();
24761
+ const started = rows(await tx.query(
24762
+ "SELECT supacloud_workflows.start_run($1::uuid,'review.verify-attachment','1','verify',$2::text::jsonb,3) AS run",
24763
+ [input.recoveryId, JSON.stringify(previous.input)],
24764
+ ))[0]?.["run"];
24765
+ const next = boundRun(started, input.reviewId, input.expectedVersion, { ...bound, runId: input.recoveryId });
24766
+ if (next.idempotent || next.status !== "queued") return conflict();
24767
+ await tx.query("INSERT INTO public.starter_attachment_retries(run_id,review_id,previous_run_id,generation) VALUES($1,$2,$3,$4)",
24768
+ [input.recoveryId, input.reviewId, bound.runId, bound.generation + 1]);
24769
+ return { reviewId: input.reviewId, previousRunId: bound.runId, runId: input.recoveryId, state: "queued" as const };
24770
+ },
24771
+ audit: { event: "review.attachment-retried", details: input => ({
24772
+ reviewId: input.reviewId, previousRunId: input.expectedRunId, runId: input.recoveryId,
24773
+ }) },
24774
+ });
24775
+ return {
24776
+ async attachmentTask(context, reviewId) {
24777
+ try {
24778
+ const subject = actor(context);
24779
+ try { decodeParams({ id: reviewId }); }
24780
+ catch { throw new ApplicationError("Invalid task request", { status: 400, code: "TASK_INPUT_INVALID" }); }
24781
+ return await database.transaction(async tx => {
24782
+ const review = await authorize(tx, subject, reviewId);
24783
+ const bound = await binding(tx, reviewId);
24784
+ const empty = { reviewId, version: review.version, artifactId: bound?.artifactId ?? null,
24785
+ runId: null, state: "not_submitted", attempts: 0, maxAttempts: 3, canRetry: false, result: null } as const;
24786
+ if (!bound) return decodeStatus(empty);
24787
+ const value = await snapshot(tx, bound.runId);
24788
+ if (value === null && review.state === "draft" && bound.generation === 0) return decodeStatus(empty);
24789
+ const run = boundRun(value, reviewId, review.version, bound);
24790
+ const step = run.steps[0];
24791
+ if (!step) throw new Error("Missing task step");
24792
+ const result = run.status === "completed" ? decodeResult(run.output) : null;
24793
+ if (result && (result.reviewId !== reviewId || result.version !== review.version || result.artifactId !== bound.artifactId)) {
24794
+ throw new Error("Task result binding mismatch");
24795
+ }
24796
+ return decodeStatus({ ...empty, runId: run.runId,
24797
+ state: run.status === "running" && step.status === "queued" ? "queued" : run.status,
24798
+ attempts: step.attempts, canRetry: run.status === "failed", result });
24799
+ });
24800
+ } catch (error) { return failure(error); }
24801
+ },
24802
+ async retryAttachmentTask(context, value) {
24803
+ try {
24804
+ const subject = actor(context), input = capture(value);
24805
+ const receipt = await retry.execute({ actorId: subject, tenantId }, input.recoveryId, input);
24806
+ if (receipt.status !== "confirmed") throw new CommandError("COMMAND_OUTCOME_UNKNOWN");
24807
+ return receipt.result;
24808
+ } catch (error) { return failure(error); }
24809
+ },
24810
+ };
24811
+ }
24812
+ `;
24813
+
23484
24814
  // src/shared/tools/app-starter.ts
23485
24815
  var { compiler: compilerMetadata2, app: appMetadata2, elysia: elysiaMetadata2, commands: commandsMetadata, contracts: contractsMetadata, db: dbMetadata, sdk: sdkMetadata } = starter_metadata_default.packages;
23486
24816
  var json2 = (value) => `${JSON.stringify(value, null, 2)}
@@ -23489,6 +24819,10 @@ function appStarterFiles(name) {
23489
24819
  return {
23490
24820
  ...starterDatabaseFiles(),
23491
24821
  "src/review/list-reviews.ts": STARTER_REVIEW_QUERY,
24822
+ "src/review/task-contracts.ts": STARTER_TASK_CONTRACTS,
24823
+ "src/review/tasks.ts": STARTER_TASK_FEATURE,
24824
+ "src/host/review-tasks.ts": STARTER_TASK_HOST,
24825
+ "migrations/005-review-task-recovery.sql": STARTER_TASK_SCHEMA,
23492
24826
  "package.json": json2({
23493
24827
  name,
23494
24828
  version: "0.0.0",
@@ -23609,6 +24943,7 @@ export default defineSupacloudConfig({
23609
24943
  { source: "migrations/002-review-attachments.sql", version: "2", name: "review_attachments", executor: "project-migration" },
23610
24944
  { source: "migrations/003-review-uploads.sql", version: "3", name: "review_uploads", executor: "operator-provisioning" },
23611
24945
  { source: "migrations/004-review-runtime-roles.sql", version: "4", name: "review_runtime_roles", executor: "operator-provisioning" },
24946
+ { source: "migrations/005-review-task-recovery.sql", version: "5", name: "review_task_recovery", executor: "operator-provisioning" },
23612
24947
  ],
23613
24948
  },
23614
24949
  },
@@ -23780,8 +25115,11 @@ import {
23780
25115
  } from "@supacloud/app";
23781
25116
  import { ApplicationError, assertFeatureTransition, createSchemaDecoder } from "@supacloud/elysia";
23782
25117
  import { ApproveBody, HealthResult, Params, ReviewResult, type ApprovalBody, type Review } from "./contracts";
25118
+ import { REVIEW_VERSION_MAX } from "./limits";
23783
25119
  import { VerifyReviewAttachment } from "./attachment";
23784
25120
  import { ReviewUploads, ReviewUploadsController, type ReviewUploadPort } from "./uploads";
25121
+ import { ReviewReceipts, ReviewReceiptsController, type ReviewReceiptPort } from "./receipts";
25122
+ import { ReviewTasks, ReviewTasksController, type ReviewTaskPort } from "./tasks";
23785
25123
 
23786
25124
  export const reviewSpec = defineFeatureSpec({
23787
25125
  name: "review",
@@ -23797,7 +25135,7 @@ export const reviewSpec = defineFeatureSpec({
23797
25135
  });
23798
25136
 
23799
25137
  // Production implementations must bind both operations to the current transaction.
23800
- export interface ReviewStore extends Partial<ReviewUploadPort> {
25138
+ export interface ReviewStore extends Partial<ReviewUploadPort>, Partial<ReviewReceiptPort>, Partial<ReviewTaskPort> {
23801
25139
  get(table: string, key: string): unknown;
23802
25140
  set(table: string, key: string, value: Review): void | Promise<void>;
23803
25141
  }
@@ -23828,6 +25166,9 @@ export class ApproveReview {
23828
25166
  if (current.version !== expectedVersion) {
23829
25167
  throw new ApplicationError("Review state or version changed", { status: 409, code: "REVIEW_CONFLICT" });
23830
25168
  }
25169
+ if (current.version === REVIEW_VERSION_MAX) {
25170
+ throw new ApplicationError("Review version is exhausted", { status: 409, code: "REVIEW_CONFLICT" });
25171
+ }
23831
25172
  const next = {
23832
25173
  state: assertFeatureTransition(reviewSpec, current.state, "approve"),
23833
25174
  version: current.version + 1,
@@ -23887,25 +25228,34 @@ export class ReviewController {
23887
25228
 
23888
25229
  export const ReviewFeature = defineFeatureSlice({
23889
25230
  name: "review", tags: ["type:feature"], spec: reviewSpec,
23890
- providers: [ApproveReview, VerifyReviewAttachment, ReviewUploads], controllers: [ReviewController, ReviewUploadsController],
25231
+ providers: [ApproveReview, VerifyReviewAttachment, ReviewUploads, ReviewReceipts, ReviewTasks],
25232
+ controllers: [ReviewController, ReviewUploadsController, ReviewReceiptsController, ReviewTasksController],
23891
25233
  jobs: [VerifyReviewAttachment],
23892
25234
  });
23893
25235
  `,
23894
25236
  "src/review/attachment.ts": STARTER_REVIEW_JOB,
25237
+ "src/review/limits.ts": `// Matches the existing PostgreSQL integer version columns; widening requires a migration.
25238
+ export const REVIEW_VERSION_MAX = 2147483647;
25239
+ `,
23895
25240
  "src/review/contracts.ts": `import { t, type UnwrapSchema } from "elysia";
25241
+ import { REVIEW_VERSION_MAX } from "./limits";
23896
25242
  export const Params = t.Object({ id: t.String({ minLength: 1 }) });
23897
- export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1, maximum: 9007199254740991 }) });
25243
+ export const ReviewVersion = t.Integer({ minimum: 1, maximum: REVIEW_VERSION_MAX });
25244
+ export const ApproveBody = t.Object({ expectedVersion: ReviewVersion });
23898
25245
  export const ApprovalInput = t.Object({ ...Params.properties, ...ApproveBody.properties }, { additionalProperties: false });
23899
25246
  export type ApprovalBody = UnwrapSchema<typeof ApproveBody>;
23900
25247
  export const ReviewResult = t.Object({
23901
25248
  state: t.Union([t.Literal("draft"), t.Literal("approved")]),
23902
- version: t.Integer({ minimum: 1, maximum: 9007199254740991 }),
25249
+ version: ReviewVersion,
23903
25250
  });
23904
25251
  export type Review = UnwrapSchema<typeof ReviewResult>;
23905
25252
  export const HealthResult = t.Object({ ok: t.Boolean() });
23906
25253
  export const UploadParams = t.Object({ id: t.String({ format: "uuid" }) });
23907
25254
  export const UploadBody = t.Object({
23908
- artifactId: t.String({ format: "uuid" }), expectedVersion: t.Integer({ minimum: 1 }),
25255
+ artifactId: t.String({ format: "uuid" }), expectedVersion: ReviewVersion,
25256
+ }, { additionalProperties: false });
25257
+ export const UploadInput = t.Object({
25258
+ reviewId: UploadParams.properties.id, ...UploadBody.properties,
23909
25259
  }, { additionalProperties: false });
23910
25260
  export const UploadLocation = t.Object({
23911
25261
  artifactId: t.String({ format: "uuid" }), bucketId: t.String(), objectPath: t.String(),
@@ -23914,12 +25264,41 @@ export const UploadResult = t.Object({
23914
25264
  artifactId: t.String({ format: "uuid" }), runId: t.String({ format: "uuid" }),
23915
25265
  objectPath: t.String(), sha256: t.String({ pattern: "^[a-f0-9]{64}$" }), bytes: t.Integer({ minimum: 1, maximum: 1048576 }),
23916
25266
  });
25267
+ export const ReceiptParams = t.Object({
25268
+ id: t.String({ minLength: 1 }), operationId: t.String({ format: "uuid" }),
25269
+ });
25270
+ export const ReceiptQuery = t.Object({
25271
+ kind: t.Union([t.Literal("approve"), t.Literal("upload")]),
25272
+ expectedVersion: t.String({ pattern: "^[1-9][0-9]*$", maxLength: 16 }),
25273
+ }, { additionalProperties: false });
25274
+ export const ReceiptResult = t.Object({
25275
+ status: t.Union([t.Literal("confirmed"), t.Literal("unknown")]),
25276
+ operationId: t.String({ format: "uuid" }), reviewId: t.String({ minLength: 1 }),
25277
+ });
25278
+ export const ReceiptOperation = t.Object({
25279
+ kind: ReceiptQuery.properties.kind,
25280
+ reviewId: ReceiptParams.properties.id,
25281
+ operationId: ReceiptParams.properties.operationId,
25282
+ expectedVersion: ApprovalInput.properties.expectedVersion,
25283
+ }, { additionalProperties: false });
25284
+ export type ReviewOperation = UnwrapSchema<typeof ReceiptOperation>;
25285
+ export type ReviewReceipt = UnwrapSchema<typeof ReceiptResult>;
25286
+ export type ReviewUploadBody = UnwrapSchema<typeof UploadBody>;
25287
+ export type ReviewUploadInput = UnwrapSchema<typeof UploadInput>;
25288
+ export type ReviewUploadLocation = UnwrapSchema<typeof UploadLocation>;
25289
+ export type ReviewUploadResult = UnwrapSchema<typeof UploadResult>;
23917
25290
  `,
25291
+ "src/review/receipts.ts": STARTER_REVIEW_RECEIPTS,
23918
25292
  "src/review/uploads.ts": STARTER_UPLOAD_FEATURE,
25293
+ "src/client/reviews.ts": STARTER_REVIEW_CLIENT,
25294
+ "src/client/review-transport.ts": STARTER_REVIEW_TRANSPORT,
25295
+ "tests/review-client.test.ts": STARTER_REVIEW_CLIENT_TEST,
23919
25296
  "src/host/review-uploads.ts": STARTER_UPLOAD_ADAPTER,
23920
25297
  "src/host/review-postgres.ts": STARTER_REVIEW_POSTGRES,
23921
25298
  "src/host/review-attachments.ts": STARTER_ATTACHMENT_POSTGRES,
23922
25299
  "src/host/review-attachment-worker.ts": STARTER_ATTACHMENT_WORKER,
25300
+ "src/host/review-worker-observability.ts": STARTER_WORKER_OBSERVABILITY,
25301
+ "src/host/review-execution-observability.ts": STARTER_EXECUTION_OBSERVABILITY,
23923
25302
  "src/delivery-worker.ts": STARTER_ATTACHMENT_DELIVERY_WORKER,
23924
25303
  "src/delivery-host.ts": STARTER_REVIEW_DELIVERY_HOST,
23925
25304
  "migrations/001-review.sql": STARTER_REVIEW_SCHEMA,
@@ -24017,6 +25396,25 @@ It is not a complete Maker-Checker workflow or a production authorization policy
24017
25396
 
24018
25397
  ## Frontend SDK Boundary
24019
25398
 
25399
+ src/client/reviews.ts supplies framework-neutral loading/empty/denied/error and
25400
+ submission/conflict/unknown states. src/client/review-transport.ts binds ordinary
25401
+ reads and private uploads to the existing user-scoped Supabase client, and
25402
+ approval/registration to createAuthenticatedFetch. It never uses a service key.
25403
+ Bind snapshot/subscribe to the view or SVAdmin store; dispose on identity change.
25404
+ The example lists at most 100 rows. Loading and permission failures clear rows.
25405
+ Unknown writes keep their operation ID and block resubmission. The default
25406
+ lookup calls GET /reviews/:id/operations/:operationId with kind and
25407
+ expectedVersion. It rechecks current authorization and reads the original
25408
+ durable receipt; current row state alone cannot confirm the operation.
25409
+ Construct createReviewPendingStore(localStorage, { projectId, tenantId,
25410
+ applicationId, subject }) and pass it as createReviewClient's second argument.
25411
+ The scope must come from the current authenticated session and application
25412
+ binding. It stores no token or file bytes. Reload restores unknown, then
25413
+ reconcile() performs only a receipt read; denied/conflicting lookup stays
25414
+ unknown. Storage failures block writes. Dispose never deletes pending evidence.
25415
+ Use one active mutation owner per identity/application: localStorage is not a
25416
+ cross-tab atomic lock. Do not clear unresolved records to enable another write.
25417
+
24020
25418
  Use @supacloud/js as the platform SDK around your existing user-scoped
24021
25419
  @supabase/supabase-js client. Pass that same client to existing UI providers;
24022
25420
  supacloud.supabase exposes it unchanged. Keep UI routing, forms and query
@@ -24126,11 +25524,12 @@ This is not production-role acceptance. Ordinary API clients cannot edit members
24126
25524
  or the database binding. No migration, binding, member or review is auto-created
24127
25525
  by the HTTP host. This SQL file is not a migration runner or a recovery receipt.
24128
25526
 
24129
- The starter's delivery.build.migrations explicitly declares these four files.
25527
+ The starter's delivery.build.migrations explicitly declares these five files.
24130
25528
  Immutable targets archive their exact bytes and a migrations.json inventory;
24131
25529
  project migrations and operator provisioning use separate directories.
24132
25530
  001-002 declare project migration handling. 003 changes Storage policies and
24133
- 004 creates cluster-wide roles, so both declare operator provisioning; ordinary
25531
+ 004 creates cluster-wide roles and 005 adds the task recovery ledger and scoped
25532
+ backend grants, so these declare operator provisioning; ordinary
24134
25533
  project-role permission to perform those changes has not been established.
24135
25534
  These raw SQL hashes are not the platform ledger's normalized-statement
24136
25535
  checksums. Building or copying an archive never applies SQL, proves compatibility
@@ -24157,6 +25556,16 @@ authenticated upload requests return 501. With that setting, SUPACLOUD_URL and
24157
25556
  SUPACLOUD_SERVICE_ROLE_KEY are required; shipped adapters register/bind uploads
24158
25557
  and enqueue attachment verification through afterApproved in the approval
24159
25558
  transaction. A separately delivered worker consumes it.
25559
+ With attachments enabled, GET /reviews/:id/attachment-task returns the current
25560
+ authorized task state. POST /reviews/:id/attachment-task/retry takes expectedVersion,
25561
+ expectedRunId and a new recoveryId UUID. Reuse that same body/recoveryId after an
25562
+ uncertain response; do not generate another key until the outcome is reconciled.
25563
+ Only failed runs can be retried. A retry creates a linked new run, receipt and audit
25564
+ in one transaction, preserving the old run and its attempts. Concurrent distinct
25565
+ recovery requests cannot both replace the same failed run. Read status after the
25566
+ receipt: its queued state describes submission, not necessarily the current state.
25567
+ Apply migration 005 before updating either HTTP or Worker code; it extends only
25568
+ backend capabilities. Do not grant Workflow access to browser roles.
24160
25569
  An HTTP listener or immutable build does not prove full-platform identity,
24161
25570
  migration compatibility, activation, application rollback or data recovery.
24162
25571
 
@@ -24269,6 +25678,28 @@ committed idempotent result. The host's failure promise makes the executable exi
24269
25678
  nonzero with bounded cleanup. Restart only after investigating an ownership or
24270
25679
  receipt failure; repeated automatic restart can exhaust an unknown run's attempts.
24271
25680
 
25681
+ The delivery worker emits review-worker-execution JSON lines to stdout using
25682
+ src/host/review-worker-observability.ts, which re-exports the shared observer
25683
+ from src/host/review-execution-observability.ts. Events contain timestamp, workerId,
25684
+ kind, operation, stage, phase and optional requestId/durationMs. For attachment
25685
+ jobs, requestId is the Workflow run ID; join it to starter_attachments.run_id
25686
+ or the current starter_attachment_retries lineage, not to an HTTP request ID.
25687
+ Only selected execution metadata is serialized, never input, output, errors,
25688
+ credentials or database parameters. Correlation identifiers must be opaque,
25689
+ non-secret identifiers; malformed identifiers and invalid durations are omitted.
25690
+ Logging is best effort and cannot alter a job outcome. A succeeded stage is not
25691
+ proof of queue settlement: confirm the Workflow receipt and durable attachment
25692
+ result separately. Configure stdout collection in the deployment environment;
25693
+ the starter does not install a collector or prove gateway trace propagation.
25694
+
25695
+ The HTTP delivery host uses the same serializer for review-http-execution events
25696
+ with component review-http. Route and command events preserve their requestId
25697
+ from the existing request context and do not contain a workerId. Do not equate
25698
+ that request ID with a Workflow run ID or an idempotency key. Identity resolution
25699
+ and failures before an execution observer is reached are not covered by these
25700
+ events; use the platform's existing request/identity logs for those failures.
25701
+ Both hosts emit only execution metadata, not access logs or audit receipts.
25702
+
24272
25703
  The memory demo does not start this worker. The included unit tests exercise the
24273
25704
  handler contract, not production Storage, queue delivery or external identity.
24274
25705
 
@@ -24286,6 +25717,14 @@ Context packs include directional dependencies, relevant aspect files, diagnosti
24286
25717
  and static execution plans, without provider values or source expressions.
24287
25718
  The optional --events example expects a trusted host
24288
25719
  to capture approved onExecution metadata in { "version": 1, "events": [...] }.
25720
+ It also accepts a bounded JSON-lines export containing only the generated
25721
+ review-http-execution and review-worker-execution records. Pass that file
25722
+ directly as --events execution.ndjson; use the HTTP request ID or Workflow run
25723
+ ID appropriate to the selected operation. Unknown log records, extra fields,
25724
+ malformed lines and oversized files fail the entire read rather than being
25725
+ silently skipped. Wrapper timestamps, component names and worker IDs are
25726
+ validated then excluded from the context metadata. Review the export before
25727
+ sharing; context correlation still does not authenticate the log source.
24289
25728
  It selects one opaque request ID, omits payloads and source expressions, and applies
24290
25729
  input/output size limits. It does not read arbitrary logs, prove deployment versions,
24291
25730
  identify a root cause or apply repairs. Never put credentials or business data in
@@ -25013,26 +26452,47 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
25013
26452
  const sources = [
25014
26453
  owner.file,
25015
26454
  ...owner.providers.map((item) => item.file),
25016
- ...owner.controllers.map((item) => item.file)
26455
+ ...owner.controllers.map((item) => item.file),
26456
+ ...graph.moduleHandlerFiles?.[owner.name] ?? [],
26457
+ ...(owner.aspects ?? []).flatMap((item) => item.file ? [item.file] : []),
26458
+ ...owner.commands.flatMap((item) => (item.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])),
26459
+ ...(owner.jobs ?? []).flatMap((item) => (item.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])),
26460
+ ...owner.controllers.flatMap((item) => item.routes.flatMap((route) => (route.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])))
25017
26461
  ].filter((file) => typeof file === "string");
25018
26462
  const directories = [...new Set(sources.map((file) => dirname6(resolve15(sourceRoot, file))))].sort();
25019
26463
  const tests = new Set;
25020
26464
  const manual = [];
26465
+ const coverage = [];
25021
26466
  for (const directory of directories) {
25022
26467
  if (!inside2(requestedRoot, directory)) {
25023
26468
  manual.push("A source directory is outside the project; select its owning project explicitly");
25024
26469
  continue;
25025
26470
  }
25026
- const actual = await realpath3(directory);
26471
+ const actual = await realpath3(directory).catch((error) => {
26472
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
26473
+ return null;
26474
+ throw error;
26475
+ });
26476
+ if (actual === null) {
26477
+ manual.push(`Source directory is missing: ${relative5(requestedRoot, directory).replaceAll("\\", "/")}`);
26478
+ continue;
26479
+ }
25027
26480
  if (!inside2(project, actual)) {
25028
26481
  manual.push("A source directory is outside the project; select its owning project explicitly");
25029
26482
  continue;
25030
26483
  }
26484
+ const local = [];
25031
26485
  for (const entry of await readdir4(actual, { withFileTypes: true })) {
25032
26486
  if (entry.isFile() && /\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
25033
- tests.add(relative5(project, resolve15(actual, entry.name)).replaceAll("\\", "/"));
26487
+ const file = relative5(project, resolve15(actual, entry.name)).replaceAll("\\", "/");
26488
+ tests.add(file);
26489
+ local.push(file);
25034
26490
  }
25035
26491
  }
26492
+ const path = relative5(project, actual).replaceAll("\\", "/") || ".";
26493
+ coverage.push({ directory: path, tests: local.sort(), basis: "colocated-not-proven" });
26494
+ if (!local.length)
26495
+ manual.push(`No colocated tests for source directory: ${path}`);
25036
26496
  }
25037
26497
  const selected = [...tests].sort();
25038
26498
  if (!selected.length)
@@ -25051,6 +26511,12 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
25051
26511
  ["git", "diff", "--check"]
25052
26512
  ] : [],
25053
26513
  manual,
26514
+ coverage,
26515
+ limitations: [
26516
+ "Colocation selects candidates; it does not prove behavioral coverage",
26517
+ "Dependent modules and database behavior require explicit integration acceptance",
26518
+ "No recursive scan, test execution or generated-file repair is performed"
26519
+ ],
25054
26520
  releaseOnly: ["generated artifact drift", "consumer types", "database/RLS and authenticated integration", "delivery read-back"],
25055
26521
  executed: false
25056
26522
  };
@@ -25259,6 +26725,7 @@ var REMOTE_APP_ACTIONS = {
25259
26725
  "deploy-plan": "get_deploy_plan",
25260
26726
  diff: "get_deploy_plan",
25261
26727
  status: "get_runtime",
26728
+ evidence: "get_deployment_evidence",
25262
26729
  rollback: "rollback_release",
25263
26730
  "rollback-plan": "get_rollback_snapshot",
25264
26731
  history: "get_history",
@@ -25603,11 +27070,49 @@ function formatModuleContextPack(pack) {
25603
27070
  `);
25604
27071
  }
25605
27072
  async function runContext(args) {
25606
- const { root, configFile, outDir, result } = await projectCompileConfig(args);
27073
+ const { root, sourceDir, configFile, outDir, result } = await projectCompileConfig(args);
25607
27074
  const subject = args.target?.trim();
25608
27075
  if (subject) {
25609
27076
  const pack = createContextPack({ ...result.graph, diagnostics: result.diagnostics }, subject);
25610
- return textResult(args.format === "json" ? JSON.stringify(pack, null, 2) : formatModuleContextPack(pack));
27077
+ const verification = await createVerificationPlan(root, result.graph, pack.subject, sourceDir);
27078
+ const repairs = createDiagnosticRepairPlan(pack.diagnostics);
27079
+ const workflow = {
27080
+ root,
27081
+ sourceRoot: sourceDir,
27082
+ generatedDir: outDir,
27083
+ allowedDependencies: await readAllowedDependencies(root),
27084
+ editFiles: verification.files,
27085
+ referenceFiles: pack.files,
27086
+ verification,
27087
+ repairs,
27088
+ rules: {
27089
+ generatedFiles: "do-not-edit",
27090
+ permissions: "explicit-business-input-required",
27091
+ writeRetry: "disabled",
27092
+ fixDefault: "preview"
27093
+ },
27094
+ daily: [
27095
+ ["supacloud", "app", "doctor", "--target", pack.subject, "--format", "json"],
27096
+ ["supacloud", "app", "fix", "--fix", "<reviewed-fix.json>"],
27097
+ ["supacloud", "app", "compile"],
27098
+ ...verification.commands
27099
+ ],
27100
+ release: {
27101
+ first: ["supacloud", "app", "check", "--format", "json"],
27102
+ rule: "Check committed artifacts before any regeneration; never repair drift to make this gate pass",
27103
+ additionalEvidence: verification.releaseOnly
27104
+ },
27105
+ executed: false
27106
+ };
27107
+ return textResult(args.format === "json" ? JSON.stringify({ ...pack, workflow }, null, 2) : [
27108
+ formatModuleContextPack(pack),
27109
+ ` edit files (project-relative): ${workflow.editFiles.join(", ") || "-"}`,
27110
+ ` focused tests: ${verification.tests.join(", ") || "-"}`,
27111
+ ...verification.manual.map((item) => ` manual: ${item}`),
27112
+ " fixes: preview first; business policy requires explicit input",
27113
+ " release: app check before regeneration; local tests do not prove deployment"
27114
+ ].join(`
27115
+ `));
25611
27116
  }
25612
27117
  const project = {
25613
27118
  version: 1,
@@ -25694,8 +27199,10 @@ async function findUnwiredContracts(rootDir) {
25694
27199
  }
25695
27200
  async function runDoctor(args) {
25696
27201
  const { root, outDir, result } = await projectCompileConfig(args);
25697
- const doctor = doctorProject(root, outDir, result.graph, result.upToDate, result.diagnostics);
25698
- const database = checkAppDatabaseSources(root);
27202
+ const target = args.target?.trim();
27203
+ const context = target ? createContextPack({ ...result.graph, diagnostics: result.diagnostics }, target) : undefined;
27204
+ const doctor = doctorProject(root, outDir, { ...result.graph, ...context ? { modules: context.modules } : {}, diagnostics: [] }, result.upToDate, context?.diagnostics ?? result.diagnostics);
27205
+ const database = context ? null : checkAppDatabaseSources(root);
25699
27206
  if (database) {
25700
27207
  doctor.checks.push(database);
25701
27208
  if (!database.ok)
@@ -25705,11 +27212,29 @@ async function runDoctor(args) {
25705
27212
  const autoFixable = fixPlan.filter((repair) => repair.readiness === "preview").length;
25706
27213
  const inputRequired = fixPlan.filter((repair) => repair.readiness === "input-required").length;
25707
27214
  const manualFixes = fixPlan.filter((repair) => repair.readiness === "manual").length;
25708
- const unwiredContracts = await findUnwiredContracts(root);
27215
+ const unwiredContracts = context ? [] : await findUnwiredContracts(root);
27216
+ const scope = context ? {
27217
+ target: context.subject,
27218
+ files: context.files,
27219
+ omittedProjectDiagnostics: result.diagnostics.length - context.diagnostics.length,
27220
+ notChecked: ["unwired contracts outside graph", "additional database source CLI check"],
27221
+ generatedArtifactCheck: "project-wide"
27222
+ } : undefined;
25709
27223
  if (args.format === "json") {
25710
- return textResult(JSON.stringify({ ok: doctor.errors === 0, autoFixable, inputRequired, manualFixes, fixPlan, unwiredContracts, ...doctor }, null, 2), doctor.errors > 0);
27224
+ return textResult(JSON.stringify({
27225
+ ok: doctor.errors === 0,
27226
+ autoFixable,
27227
+ inputRequired,
27228
+ manualFixes,
27229
+ fixPlan,
27230
+ unwiredContracts,
27231
+ ...scope ? { scope } : {},
27232
+ ...doctor
27233
+ }, null, 2), doctor.errors > 0);
25711
27234
  }
25712
27235
  const lines = doctor.checks.map((check) => `${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.detail}`);
27236
+ if (scope)
27237
+ lines.unshift(`TARGET ${scope.target}; omitted project diagnostics=${scope.omittedProjectDiagnostics}; generated drift remains project-wide`);
25713
27238
  for (const diagnostic of doctor.diagnostics ?? []) {
25714
27239
  lines.push(formatDiagnostic(diagnostic));
25715
27240
  }
@@ -26700,6 +28225,21 @@ async function runAppTool(request, options = {}) {
26700
28225
  };
26701
28226
  validateToolArguments2(APPLICATION_TOOL_SCHEMA2, args);
26702
28227
  const result = await delegate(args);
28228
+ if (request.action === "evidence" && format !== "json" && json !== true && !result.isError) {
28229
+ try {
28230
+ const payload = JSON.parse(result.content[0].text);
28231
+ if (payload === null || typeof payload !== "object" || !("project_ref" in payload) || payload.project_ref !== args.ref || !("application_id" in payload) || payload.application_id !== request.id || !("environment_id" in payload) || payload.environment_id !== request.environment_id || !("evidence" in payload))
28232
+ throw new Error;
28233
+ if (payload.evidence === null)
28234
+ return textResult("DEPLOYMENT incomplete: no recorded evidence. Read runtime status and obtain authenticated smoke and recovery evidence.");
28235
+ const evidence = parseDeploymentEvidence(payload.evidence);
28236
+ if (evidence.scope.project_ref !== args.ref || evidence.scope.application_id !== request.id || evidence.scope.environment_id !== request.environment_id)
28237
+ throw new Error;
28238
+ return textResult(formatDeploymentEvidence(evidence));
28239
+ } catch {
28240
+ return textResult("DEPLOYMENT unknown: invalid or mismatched evidence; no mutation was attempted.", true);
28241
+ }
28242
+ }
26703
28243
  if (request.action === "history" && format !== "json" && json !== true) {
26704
28244
  return historyText(result, request, args.ref);
26705
28245
  }
@@ -26792,6 +28332,7 @@ function registerAppTools(server, options = {}) {
26792
28332
  "deploy-plan",
26793
28333
  "diff",
26794
28334
  "status",
28335
+ "evidence",
26795
28336
  "rollback",
26796
28337
  "rollback-plan",
26797
28338
  "history",
@@ -30562,7 +32103,7 @@ async function runProjectDeployment(http, args) {
30562
32103
  // package.json
30563
32104
  var package_default = {
30564
32105
  name: "@supacloud/cli",
30565
- version: "0.101.4",
32106
+ version: "0.103.1",
30566
32107
  description: "Project-scoped CLI for SupaCloud users",
30567
32108
  type: "module",
30568
32109
  main: "./dist/index.js",
@@ -30614,18 +32155,19 @@ var package_default = {
30614
32155
  directory: "packages/cli"
30615
32156
  },
30616
32157
  dependencies: {
30617
- "@supacloud/compiler": "^0.46.4",
30618
- "@supacloud/delivery": "0.18.1",
30619
- "@supacloud/db": "0.17.1",
32158
+ "@supacloud/compiler": "^0.46.5",
32159
+ "@supacloud/delivery": "0.19.0",
32160
+ "@supacloud/db": "0.18.0",
30620
32161
  typebox: "1.3.37"
30621
32162
  },
30622
32163
  overrides: {
30623
- "@supacloud/contracts": "0.11.0",
30624
- "@supacloud/delivery": "0.18.1",
32164
+ "@supacloud/contracts": "0.11.1",
32165
+ "@supacloud/delivery": "0.19.0",
30625
32166
  "@graphql-tools/utils": "^12.0.4"
30626
32167
  },
30627
32168
  devDependencies: {
30628
- "@supacloud/elysia": "0.31.1",
32169
+ "@supabase/supabase-js": "2.117.3",
32170
+ "@supacloud/elysia": "0.33.1",
30629
32171
  "@types/bun": "^1.4.3",
30630
32172
  effect: "4.0.5",
30631
32173
  elysia: "2.0.0-beta.32",
@@ -30891,6 +32433,7 @@ EXAMPLES
30891
32433
  ${preferredCommand} app diff --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid>
30892
32434
  ${preferredCommand} app deploy --ref abc123 --id orders --environment_id test --release_id <sha256> --configuration_id <uuid>
30893
32435
  ${preferredCommand} app status --ref abc123 --id orders --environment_id test
32436
+ ${preferredCommand} app evidence --ref abc123 --id orders --environment_id test
30894
32437
  ${preferredCommand} app diff --ref abc123 --id orders --environment_id production --source_ref staging --source_environment_id staging --source_release_id <sha256>
30895
32438
  ${preferredCommand} --env production app promote-plan --id orders --environment_id production --source_ref staging --source_environment_id staging --source_release_id <sha256>
30896
32439
  ${preferredCommand} app preview-plan --ref abc123 --id orders --environment_id test --release_id <sha256> --branch_ref preview-orders