@supacloud/cli 0.99.0 → 0.101.2

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
@@ -3177,23 +3177,22 @@ function InstanceKeys(value) {
3177
3177
  function IsIEEE754(value) {
3178
3178
  return typeof value === "number";
3179
3179
  }
3180
- var ByteMarker;
3181
- (function(ByteMarker) {
3182
- ByteMarker[ByteMarker["Array"] = 0] = "Array";
3183
- ByteMarker[ByteMarker["BigInt"] = 1] = "BigInt";
3184
- ByteMarker[ByteMarker["Boolean"] = 2] = "Boolean";
3185
- ByteMarker[ByteMarker["Date"] = 3] = "Date";
3186
- ByteMarker[ByteMarker["Constructor"] = 4] = "Constructor";
3187
- ByteMarker[ByteMarker["Function"] = 5] = "Function";
3188
- ByteMarker[ByteMarker["Null"] = 6] = "Null";
3189
- ByteMarker[ByteMarker["Number"] = 7] = "Number";
3190
- ByteMarker[ByteMarker["Object"] = 8] = "Object";
3191
- ByteMarker[ByteMarker["RegExp"] = 9] = "RegExp";
3192
- ByteMarker[ByteMarker["String"] = 10] = "String";
3193
- ByteMarker[ByteMarker["Symbol"] = 11] = "Symbol";
3194
- ByteMarker[ByteMarker["TypeArray"] = 12] = "TypeArray";
3195
- ByteMarker[ByteMarker["Undefined"] = 13] = "Undefined";
3196
- })(ByteMarker || (ByteMarker = {}));
3180
+ var ByteMarker = {
3181
+ Array: 0,
3182
+ BigInt: 1,
3183
+ Boolean: 2,
3184
+ Date: 3,
3185
+ Constructor: 4,
3186
+ Function: 5,
3187
+ Null: 6,
3188
+ Number: 7,
3189
+ Object: 8,
3190
+ RegExp: 9,
3191
+ String: 10,
3192
+ Symbol: 11,
3193
+ TypeArray: 12,
3194
+ Undefined: 13
3195
+ };
3197
3196
  var Accumulator = BigInt("14695981039346656037");
3198
3197
  var [Prime, Size] = [BigInt("1099511628211"), BigInt("18446744073709551616")];
3199
3198
  var Bytes = Array.from({ length: 256 }).map((_, i) => BigInt(i));
@@ -15196,7 +15195,7 @@ function scaffoldFiles(framework, slug) {
15196
15195
  "package.json": JSON.stringify({
15197
15196
  private: true,
15198
15197
  type: "module",
15199
- dependencies: { elysia: "2.0.0-beta.21" }
15198
+ dependencies: { elysia: "2.0.0-beta.32" }
15200
15199
  }, null, 2) + `
15201
15200
  `,
15202
15201
  "index.ts": `import { Elysia } from "elysia";
@@ -21389,34 +21388,34 @@ var starter_metadata_default = {
21389
21388
  generatedBy: "supacloud/starter-metadata-v1",
21390
21389
  packages: {
21391
21390
  compiler: {
21392
- version: "0.46.1"
21391
+ version: "0.46.4"
21393
21392
  },
21394
21393
  app: {
21395
- version: "0.27.0",
21394
+ version: "0.27.1",
21396
21395
  dependencies: {
21397
21396
  rxjs: "7.8.2"
21398
21397
  }
21399
21398
  },
21400
21399
  elysia: {
21401
- version: "0.30.0"
21400
+ version: "0.31.1"
21402
21401
  },
21403
21402
  commands: {
21404
- version: "0.9.2"
21403
+ version: "0.9.3"
21405
21404
  },
21406
21405
  contracts: {
21407
- version: "0.9.0"
21406
+ version: "0.10.0"
21408
21407
  },
21409
21408
  db: {
21410
- version: "0.17.0",
21409
+ version: "0.17.1",
21411
21410
  peerDependencies: {
21412
- "drizzle-orm": "1.0.0-rc.5-169397b"
21411
+ "drizzle-orm": "1.0.0-rc.5-5935859"
21413
21412
  },
21414
21413
  devDependencies: {
21415
- "drizzle-kit": "1.0.0-rc.5-ab785fc"
21414
+ "drizzle-kit": "1.0.0-rc.5-5935859"
21416
21415
  }
21417
21416
  },
21418
21417
  sdk: {
21419
- version: "0.42.0",
21418
+ version: "0.43.0",
21420
21419
  peerDependencies: {
21421
21420
  "@supabase/supabase-js": "^2.117.3"
21422
21421
  }
@@ -21759,8 +21758,8 @@ function baseFiles(name, sdkDependencies) {
21759
21758
  dependencies: {
21760
21759
  "@supacloud/app": `^${appMetadata.version}`,
21761
21760
  "@supacloud/elysia": `^${elysiaMetadata.version}`,
21762
- effect: "4.0.2",
21763
- elysia: "2.0.0-beta.21",
21761
+ effect: "4.0.5",
21762
+ elysia: "2.0.0-beta.32",
21764
21763
  ...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
21765
21764
  },
21766
21765
  devDependencies: {
@@ -22223,6 +22222,8 @@ export type ReviewAttachmentInput = Static<typeof AttachmentInput>;
22223
22222
  export type ReviewAttachmentResult = Static<typeof AttachmentResult>;
22224
22223
 
22225
22224
  export interface ReviewAttachmentStore {
22225
+ // Reauthorize before returning a committed result; null means execution is still needed.
22226
+ readResult(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult | null>;
22226
22227
  // Resolve the immutable artifact under the worker's own authorization and current review revision.
22227
22228
  readAttachment(input: ReviewAttachmentInput): Promise<{ body: Uint8Array; sha256: string }>;
22228
22229
  // Revalidate revision/ownership and persist idempotently in a transaction; return the durable result.
@@ -22238,6 +22239,8 @@ export class VerifyReviewAttachment {
22238
22239
  constructor(@Inject(DB_CLIENT) private readonly store: ReviewAttachmentStore) {}
22239
22240
 
22240
22241
  async run(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult> {
22242
+ const committed = await this.store.readResult(input);
22243
+ if (committed !== null) return committed;
22241
22244
  const source = await this.store.readAttachment(input);
22242
22245
  if (source.body.byteLength < 1 || source.body.byteLength > 1048576) {
22243
22246
  throw new ApplicationError("Attachment size is outside the supported range", { code: "ATTACHMENT_SIZE_INVALID" });
@@ -22262,6 +22265,7 @@ const sha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", b
22262
22265
  test("attachment job awaits the authorized reader and the committed result", async () => {
22263
22266
  let recorded: ReviewAttachmentResult | undefined;
22264
22267
  const job = new VerifyReviewAttachment({
22268
+ async readResult() { return null; },
22265
22269
  async readAttachment() { return { body, sha256 }; },
22266
22270
  async recordAttachment(_input, result) { await Promise.resolve(); recorded = result; return result; },
22267
22271
  });
@@ -22278,16 +22282,33 @@ test("missing authorization, corrupted bytes and persistence failures are not su
22278
22282
  async () => ({ body: new Uint8Array(1048577), sha256 }),
22279
22283
  ]) {
22280
22284
  const job = new VerifyReviewAttachment({
22285
+ async readResult() { return null; },
22281
22286
  readAttachment, async recordAttachment(_input, result) { records++; return result; },
22282
22287
  });
22283
22288
  await expect(job.run(input)).rejects.toThrow();
22284
22289
  }
22285
22290
  expect(records).toBe(0);
22286
22291
  await expect(new VerifyReviewAttachment({
22292
+ async readResult() { return null; },
22287
22293
  async readAttachment() { return { body, sha256 }; },
22288
22294
  async recordAttachment() { throw new Error("Commit failed"); },
22289
22295
  }).run(input)).rejects.toThrow("Commit failed");
22290
22296
  });
22297
+
22298
+ test("a reauthorized committed result avoids another download or write", async () => {
22299
+ const result = { ...input, sha256, bytes: body.byteLength };
22300
+ const job = new VerifyReviewAttachment({
22301
+ async readResult() { return result; },
22302
+ async readAttachment() { throw new Error("Must not download again"); },
22303
+ async recordAttachment() { throw new Error("Must not write again"); },
22304
+ });
22305
+ expect(await job.run(input)).toEqual(result);
22306
+ await expect(new VerifyReviewAttachment({
22307
+ async readResult() { throw new Error("Access revoked"); },
22308
+ async readAttachment() { throw new Error("Must not download"); },
22309
+ async recordAttachment() { throw new Error("Must not write"); },
22310
+ }).run(input)).rejects.toThrow("Access revoked");
22311
+ });
22291
22312
  `;
22292
22313
 
22293
22314
  // src/shared/tools/app-starter-attachments.ts
@@ -22308,7 +22329,8 @@ ALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;
22308
22329
  var STARTER_ATTACHMENT_POSTGRES = `import { SupaCloudArtifactsClient } from "@supacloud/js";
22309
22330
  import type { SupabaseClient } from "@supabase/supabase-js";
22310
22331
  import type { CommandDatabase, CommandTransaction } from "@supacloud/db";
22311
- import type { ReviewAttachmentInput, ReviewAttachmentResult, ReviewAttachmentStore } from "../review/attachment";
22332
+ import { createSchemaDecoder } from "@supacloud/elysia";
22333
+ import { AttachmentResult, type ReviewAttachmentInput, type ReviewAttachmentResult, type ReviewAttachmentStore } from "../review/attachment";
22312
22334
 
22313
22335
  export interface ReviewAttachmentOptions {
22314
22336
  database: CommandDatabase;
@@ -22329,14 +22351,13 @@ function record(value: unknown): Record<string, unknown> {
22329
22351
  if (!isRecord(value)) throw new TypeError("Invalid attachment record");
22330
22352
  return value;
22331
22353
  }
22332
- function durableResult(value: unknown): ReviewAttachmentResult {
22333
- const item = record(value);
22334
- if (typeof item["reviewId"] !== "string" || typeof item["artifactId"] !== "string"
22335
- || typeof item["version"] !== "number" || typeof item["sha256"] !== "string"
22336
- || typeof item["bytes"] !== "number" || Object.keys(item).length !== 5) {
22337
- throw new Error("Invalid durable attachment result");
22338
- }
22339
- return { reviewId: item["reviewId"], artifactId: item["artifactId"], version: item["version"], sha256: item["sha256"], bytes: item["bytes"] };
22354
+ const durableResult = createSchemaDecoder(AttachmentResult, { normalize: false });
22355
+
22356
+ function resultFor(input: ReviewAttachmentInput, value: unknown): ReviewAttachmentResult {
22357
+ const result = durableResult(value);
22358
+ if (result.reviewId !== input.reviewId || result.version !== input.version
22359
+ || result.artifactId !== input.artifactId) throw new Error("Conflicting durable attachment result");
22360
+ return result;
22340
22361
  }
22341
22362
 
22342
22363
  export async function createReviewAttachmentAdapters(options: ReviewAttachmentOptions): Promise<{
@@ -22382,6 +22403,16 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22382
22403
  ]);
22383
22404
  },
22384
22405
  store: {
22406
+ async readResult(input) {
22407
+ return database.transaction(async tx => {
22408
+ await authorize(tx, input);
22409
+ const saved = rows(await tx.query(
22410
+ "SELECT result FROM public.starter_attachment_results WHERE review_id=$1 AND version=$2 AND artifact_id=$3",
22411
+ [input.reviewId, input.version, input.artifactId],
22412
+ ));
22413
+ return saved.length === 0 ? null : resultFor(input, saved[0]?.["result"]);
22414
+ });
22415
+ },
22385
22416
  async readAttachment(input) {
22386
22417
  const allowed = await database.transaction(tx => authorize(tx, input));
22387
22418
  const artifact = await artifacts.get(input.artifactId);
@@ -22395,24 +22426,19 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22395
22426
  return { body: new Uint8Array(await downloaded.data.arrayBuffer()), sha256: artifact.sha256 };
22396
22427
  },
22397
22428
  async recordAttachment(input, result) {
22398
- if (result.reviewId !== input.reviewId || result.version !== input.version
22399
- || result.artifactId !== input.artifactId || !/^[a-f0-9]{64}$/.test(result.sha256)
22400
- || !Number.isSafeInteger(result.bytes) || result.bytes < 1 || result.bytes > 1048576) {
22401
- throw new Error("Invalid attachment result");
22402
- }
22429
+ const decoded = resultFor(input, result);
22403
22430
  return database.transaction(async tx => {
22404
22431
  await authorize(tx, input);
22405
22432
  await tx.query(
22406
22433
  "INSERT INTO public.starter_attachment_results VALUES($1,$2,$3,$4::text::jsonb) ON CONFLICT DO NOTHING",
22407
- [input.reviewId, input.version, input.artifactId, JSON.stringify(result)],
22434
+ [input.reviewId, input.version, input.artifactId, JSON.stringify(decoded)],
22408
22435
  );
22409
22436
  const saved = rows(await tx.query(
22410
22437
  "SELECT result FROM public.starter_attachment_results WHERE review_id=$1 AND version=$2 AND artifact_id=$3",
22411
22438
  [input.reviewId, input.version, input.artifactId],
22412
22439
  ))[0]?.["result"];
22413
- const value = durableResult(saved);
22414
- if (value.reviewId !== result.reviewId || value.version !== result.version || value.artifactId !== result.artifactId
22415
- || value.sha256 !== result.sha256 || value.bytes !== result.bytes) {
22440
+ const value = resultFor(input, saved);
22441
+ if (value.sha256 !== decoded.sha256 || value.bytes !== decoded.bytes) {
22416
22442
  throw new Error("Conflicting durable attachment result");
22417
22443
  }
22418
22444
  return value;
@@ -22958,20 +22984,21 @@ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
22958
22984
  ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
22959
22985
  ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
22960
22986
  `;
22961
- var STARTER_REVIEW_POSTGRES = `import { AsyncLocalStorage } from "node:async_hooks";
22962
- import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
22987
+ var STARTER_REVIEW_POSTGRES = `import { plaintextCommandInput } from "@supacloud/commands";
22963
22988
  import { CommandError } from "@supacloud/contracts";
22964
22989
  import { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from "@supacloud/db";
22965
22990
  import {
22966
- ApplicationError, createSupAuthRequestContext, requireTrustedIdentity, requireIdempotencyKey,
22991
+ ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,
22992
+ createTransactionalCommandGovernance,
22967
22993
  type CommandInvocation, type SupAuthContextOptions,
22968
22994
  } from "@supacloud/elysia";
22969
22995
  import type { AppAdapters } from "../application";
22970
22996
  import type { ReviewStore } from "../review/review";
22971
22997
  import type { ReviewUploadPort } from "../review/uploads";
22998
+ import { ApprovalInput, ReviewResult } from "../review/contracts";
22972
22999
 
22973
- interface Approval { id: string; expectedVersion: number }
22974
- interface Review { state: "draft" | "approved"; version: number }
23000
+ const input = createSchemaDecoder(ApprovalInput, { normalize: false });
23001
+ const result = createSchemaDecoder(ReviewResult, { normalize: false });
22975
23002
 
22976
23003
  function isRecord(value: unknown): value is Record<string, unknown> {
22977
23004
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -22988,19 +23015,6 @@ function isPublicConflict(value: unknown): boolean {
22988
23015
  return value instanceof ApplicationError && value.status === 409
22989
23016
  || isRecord(value) && value["expose"] === true && value["status"] === 409 && typeof value["code"] === "string";
22990
23017
  }
22991
- function input(value: unknown): Approval {
22992
- const item = record(value);
22993
- if (typeof item["id"] !== "string" || !item["id"].length || typeof item["expectedVersion"] !== "number"
22994
- || !Number.isSafeInteger(item["expectedVersion"]) || item["expectedVersion"] < 1) throw new TypeError("Invalid approval input");
22995
- return { id: item["id"], expectedVersion: item["expectedVersion"] };
22996
- }
22997
- function result(value: unknown): Review {
22998
- const item = record(value);
22999
- if ((item["state"] !== "draft" && item["state"] !== "approved") || typeof item["version"] !== "number"
23000
- || !Number.isSafeInteger(item["version"]) || item["version"] < 1) throw new TypeError("Invalid review result");
23001
- return { state: item["state"], version: item["version"] };
23002
- }
23003
-
23004
23018
  export interface ReviewPostgresOptions {
23005
23019
  database: CommandDatabase;
23006
23020
  tenantId: string;
@@ -23038,12 +23052,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23038
23052
  } : null;
23039
23053
  },
23040
23054
  });
23041
- const scope = new AsyncLocalStorage<CommandTransaction>();
23042
- const transaction = () => {
23043
- const tx = scope.getStore();
23044
- if (!tx) throw new Error("Review storage requires its durable command transaction");
23045
- return tx;
23046
- };
23055
+ const transaction = () => commandBinding.transaction();
23047
23056
  const store: ReviewStore = {
23048
23057
  ...options.uploads,
23049
23058
  async get(table, id) {
@@ -23072,64 +23081,30 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
23072
23081
  }
23073
23082
  return who.subject;
23074
23083
  }
23075
- const commandStore = createPostgresCommandStore(database);
23076
- return {
23077
- deps: { dbClient: store }, requestContext,
23078
- commandGovernance: {
23079
- authorize(invocation) {
23080
- const command = invocation.command;
23081
- if (command.name !== "review.approve" || command.permission !== "review.approve"
23082
- || command.transaction !== "required" || command.idempotency !== "required"
23083
- || command.audit !== "review.approved" || command.rpc !== undefined) {
23084
- throw new ApplicationError("Unsupported review command", { status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" });
23085
- }
23086
- actor(invocation);
23087
- },
23088
- async idempotency(invocation, next) {
23089
- const command = createTransactionalCommand({
23090
- name: invocation.command.name, store: commandStore,
23091
- inputCodec: plaintextCommandInput, input, result,
23092
- async authorize(who, request, tx) {
23093
- const found = rows(await tx.query(
23094
- "SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
23095
- "CROSS JOIN public.starter_application a " +
23096
- "WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
23097
- "AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
23098
- [request.id, who.actorId, projectId, who.tenantId],
23099
- ));
23100
- return found.length === 1 ? "allow" : "deny";
23101
- },
23102
- execute: tx => scope.run(tx, async () => {
23103
- try { return await next(); }
23104
- catch (error) {
23105
- if (isPublicConflict(error)) {
23106
- throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
23107
- }
23108
- throw error;
23109
- }
23110
- }),
23111
- audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23112
- });
23113
- try {
23114
- const receipt = await command.execute(
23115
- { actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),
23116
- { id: invocation.input.params["id"], expectedVersion: record(invocation.input.body)["expectedVersion"] },
23117
- invocation.request.signal,
23118
- );
23119
- if (receipt.status !== "confirmed") throw new CommandError("COMMAND_OUTCOME_UNKNOWN");
23120
- return receipt.result;
23121
- } catch (error) {
23122
- if (!(error instanceof CommandError)) throw error;
23123
- const status = error.code === "COMMAND_REJECTED" ? 403
23124
- : error.code === "COMMAND_IDEMPOTENCY_CONFLICT" ? 409 : 503;
23125
- throw new ApplicationError(error.code, { code: error.code, status });
23126
- }
23084
+ const commandBinding = createTransactionalCommandGovernance({
23085
+ permission: "review.approve",
23086
+ identity: invocation => ({ actorId: actor(invocation), tenantId }),
23087
+ input: invocation => ({
23088
+ ...record(invocation.input.body), id: invocation.input.params["id"],
23089
+ }),
23090
+ rejection: error => isPublicConflict(error) ? new CommandError("COMMAND_IDEMPOTENCY_CONFLICT") : undefined,
23091
+ definition: {
23092
+ name: "review.approve", store: createPostgresCommandStore(database),
23093
+ 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";
23127
23103
  },
23128
- transaction(_invocation, next) { transaction(); return next(); },
23129
- // The durable command owns the authoritative audit and receipt in the same transaction.
23130
- audit: { succeeded() { transaction(); }, failed() {} },
23104
+ audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
23131
23105
  },
23132
- };
23106
+ });
23107
+ return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };
23133
23108
  }
23134
23109
  `;
23135
23110
  var STARTER_REVIEW_POSTGRES_TEST = `import { expect, test } from "bun:test";
@@ -23166,9 +23141,9 @@ test("PostgreSQL host never falls back to demo identity or unbound business stor
23166
23141
  await expect(adapters.requestContext(new Request("http://localhost/reviews/health")))
23167
23142
  .rejects.toMatchObject({ code: "AUTHENTICATION_REQUIRED" });
23168
23143
  const store = adapters.deps["dbClient"] as ReviewStore;
23169
- await expect(store.get("reviews", "example")).rejects.toThrow("durable command transaction");
23144
+ await expect(store.get("reviews", "example")).rejects.toThrow("not active");
23170
23145
  await expect(store.set("reviews", "example", { state: "approved", version: 2 }))
23171
- .rejects.toThrow("durable command transaction");
23146
+ .rejects.toThrow("not active");
23172
23147
  expect(db.queries).toHaveLength(1);
23173
23148
  });
23174
23149
  `;
@@ -23454,6 +23429,58 @@ export default defineConfig({
23454
23429
  };
23455
23430
  }
23456
23431
 
23432
+ // src/shared/tools/app-starter-query.ts
23433
+ var STARTER_REVIEW_QUERY = `import type { SupabaseClient } from "@supabase/supabase-js";
23434
+ import type { ReviewRow } from "../../db/schema";
23435
+
23436
+ // This read projection uses SQL column names shared with the Drizzle source.
23437
+ // Do not expose camelCase Drizzle property names as PostgREST column names.
23438
+ export type ReviewSummary = Pick<ReviewRow, "id" | "state" | "version">;
23439
+ export type ReviewReadDatabase = {
23440
+ public: {
23441
+ Tables: {
23442
+ starter_reviews: {
23443
+ Row: ReviewSummary;
23444
+ Insert: never;
23445
+ Update: never;
23446
+ Relationships: [];
23447
+ };
23448
+ };
23449
+ Views: Record<never, never>;
23450
+ Functions: Record<never, never>;
23451
+ };
23452
+ };
23453
+
23454
+ export interface ReviewPageOptions {
23455
+ page?: number;
23456
+ pageSize?: number;
23457
+ signal?: AbortSignal;
23458
+ }
23459
+
23460
+ // Reuse the application's user-scoped client. Identity verification and row
23461
+ // authorization belong to the gateway and database, never a caller-supplied owner.
23462
+ export async function listReviews(
23463
+ client: SupabaseClient<ReviewReadDatabase>,
23464
+ { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},
23465
+ ) {
23466
+ if (!Number.isSafeInteger(page) || page < 1
23467
+ || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100
23468
+ || !Number.isSafeInteger(page * pageSize)) {
23469
+ throw new RangeError("Invalid review page: page >= 1 and pageSize between 1 and 100 are required");
23470
+ }
23471
+ signal?.throwIfAborted();
23472
+ const start = (page - 1) * pageSize;
23473
+ const query = client.from("starter_reviews")
23474
+ .select("id,state,version", { count: "exact" })
23475
+ .order("id", { ascending: true })
23476
+ .range(start, start + pageSize - 1);
23477
+ const { data, error, count } = await (signal ? query.abortSignal(signal) : query);
23478
+ if (error) throw error;
23479
+ if (data === null || count === null) throw new Error("Incomplete review page response");
23480
+ return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };
23481
+ }
23482
+ `;
23483
+
23457
23484
  // src/shared/tools/app-starter.ts
23458
23485
  var { compiler: compilerMetadata2, app: appMetadata2, elysia: elysiaMetadata2, commands: commandsMetadata, contracts: contractsMetadata, db: dbMetadata, sdk: sdkMetadata } = starter_metadata_default.packages;
23459
23486
  var json2 = (value) => `${JSON.stringify(value, null, 2)}
@@ -23461,6 +23488,7 @@ var json2 = (value) => `${JSON.stringify(value, null, 2)}
23461
23488
  function appStarterFiles(name) {
23462
23489
  return {
23463
23490
  ...starterDatabaseFiles(),
23491
+ "src/review/list-reviews.ts": STARTER_REVIEW_QUERY,
23464
23492
  "package.json": json2({
23465
23493
  name,
23466
23494
  version: "0.0.0",
@@ -23495,8 +23523,8 @@ function appStarterFiles(name) {
23495
23523
  "drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
23496
23524
  "@supacloud/js": `^${sdkMetadata.version}`,
23497
23525
  "@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
23498
- effect: "4.0.2",
23499
- elysia: "2.0.0-beta.21",
23526
+ effect: "4.0.5",
23527
+ elysia: "2.0.0-beta.32",
23500
23528
  rxjs: appMetadata2.dependencies.rxjs
23501
23529
  },
23502
23530
  devDependencies: {
@@ -23750,11 +23778,10 @@ import {
23750
23778
  Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
23751
23779
  defineFeatureSlice, defineFeatureSpec, type Aspect,
23752
23780
  } from "@supacloud/app";
23753
- import { ApplicationError, assertFeatureTransition } from "@supacloud/elysia";
23754
- import { ApproveBody, HealthResult, Params, ReviewResult } from "./contracts";
23781
+ import { ApplicationError, assertFeatureTransition, createSchemaDecoder } from "@supacloud/elysia";
23782
+ import { ApproveBody, HealthResult, Params, ReviewResult, type ApprovalBody, type Review } from "./contracts";
23755
23783
  import { VerifyReviewAttachment } from "./attachment";
23756
23784
  import { ReviewUploads, ReviewUploadsController, type ReviewUploadPort } from "./uploads";
23757
- import type { ReviewRow } from "../../db/schema";
23758
23785
 
23759
23786
  export const reviewSpec = defineFeatureSpec({
23760
23787
  name: "review",
@@ -23769,7 +23796,6 @@ export const reviewSpec = defineFeatureSpec({
23769
23796
  },
23770
23797
  });
23771
23798
 
23772
- type Review = Pick<ReviewRow, "state" | "version">;
23773
23799
  // Production implementations must bind both operations to the current transaction.
23774
23800
  export interface ReviewStore extends Partial<ReviewUploadPort> {
23775
23801
  get(table: string, key: string): unknown;
@@ -23782,13 +23808,12 @@ export const requireRequest: Aspect = (context, next) => {
23782
23808
  return next();
23783
23809
  };
23784
23810
 
23811
+ const decodeReview = createSchemaDecoder(ReviewResult, { normalize: false });
23785
23812
  function readReview(value: unknown): Review {
23786
- if (typeof value !== "object" || value === null ||
23787
- !("state" in value) || (value.state !== "draft" && value.state !== "approved") ||
23788
- !("version" in value) || typeof value.version !== "number") {
23813
+ try { return decodeReview(value); }
23814
+ catch {
23789
23815
  throw new ApplicationError("Review not found", { status: 404, code: "REVIEW_NOT_FOUND" });
23790
23816
  }
23791
- return { state: value.state, version: value.version };
23792
23817
  }
23793
23818
 
23794
23819
  @Command({
@@ -23820,7 +23845,7 @@ type ReviewError =
23820
23845
  | { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
23821
23846
  | { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
23822
23847
  | { readonly _tag: "COMMAND_REJECTED" }
23823
- | { readonly _tag: "REVIEW_COMMAND_UNSUPPORTED" }
23848
+ | { readonly _tag: "COMMAND_BINDING_MISMATCH" }
23824
23849
  | { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" };
23825
23850
 
23826
23851
  @Controller("/reviews")
@@ -23849,13 +23874,13 @@ export class ReviewController {
23849
23874
  { tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
23850
23875
  { tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
23851
23876
  { tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
23852
- { tag: "REVIEW_COMMAND_UNSUPPORTED", status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" },
23877
+ { tag: "COMMAND_BINDING_MISMATCH", status: 501, code: "COMMAND_BINDING_MISMATCH" },
23853
23878
  { tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
23854
23879
  ],
23855
23880
  retry: "none",
23856
23881
  },
23857
23882
  })
23858
- approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Effect.Effect<Review, ReviewError, never> {
23883
+ approve(@Param("id") id: string, @Body() body: ApprovalBody): Effect.Effect<Review, ReviewError, never> {
23859
23884
  return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
23860
23885
  }
23861
23886
  }
@@ -23867,13 +23892,16 @@ export const ReviewFeature = defineFeatureSlice({
23867
23892
  });
23868
23893
  `,
23869
23894
  "src/review/attachment.ts": STARTER_REVIEW_JOB,
23870
- "src/review/contracts.ts": `import { t } from "elysia";
23895
+ "src/review/contracts.ts": `import { t, type UnwrapSchema } from "elysia";
23871
23896
  export const Params = t.Object({ id: t.String({ minLength: 1 }) });
23872
- export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1 }) });
23897
+ export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1, maximum: 9007199254740991 }) });
23898
+ export const ApprovalInput = t.Object({ ...Params.properties, ...ApproveBody.properties }, { additionalProperties: false });
23899
+ export type ApprovalBody = UnwrapSchema<typeof ApproveBody>;
23873
23900
  export const ReviewResult = t.Object({
23874
23901
  state: t.Union([t.Literal("draft"), t.Literal("approved")]),
23875
- version: t.Integer({ minimum: 1 }),
23902
+ version: t.Integer({ minimum: 1, maximum: 9007199254740991 }),
23876
23903
  });
23904
+ export type Review = UnwrapSchema<typeof ReviewResult>;
23877
23905
  export const HealthResult = t.Object({ ok: t.Boolean() });
23878
23906
  export const UploadParams = t.Object({ id: t.String({ format: "uuid" }) });
23879
23907
  export const UploadBody = t.Object({
@@ -24015,6 +24043,18 @@ Ordinary reads use PostgREST through the existing user-scoped
24015
24043
  Use the caller's session, RLS and grants; never introduce a second session store.
24016
24044
  Business writes continue through the governed Command/RPC boundary.
24017
24045
 
24046
+ Use listReviews from src/review/list-reviews.ts for the paginated review list.
24047
+ Type the existing client with ReviewReadDatabase (a read-only projection derived
24048
+ from db/schema.ts), then call listReviews(client, { page: 1, pageSize: 25, signal }).
24049
+ The helper returns data, total, page, pageSize and hasNextPage, orders by the unique
24050
+ id and preserves PostgREST errors. Pages are one-based and limited to 100 rows.
24051
+ Exact counts obey RLS but may be expensive on large tables; offset pages are not
24052
+ a cross-request snapshot when records change. No owner filter replaces RLS.
24053
+ Do not create a service-role client or a second auth session for this helper.
24054
+ Existing starters can add this file without a new migration only if their reviewed
24055
+ review-table grants and identity-mapping RLS from migration 003 are already applied.
24056
+ The projection is not a full database export and grants no write access.
24057
+
24018
24058
  GraphQL is not generated or enabled by this starter. For a specific Query Contract,
24019
24059
  explicitly configure graphql.schema with a role-scoped database export and add
24020
24060
  checked query documents. Drizzle and append-only migrations remain the database
@@ -24169,6 +24209,15 @@ the existing Workflow and invokes the compiled Job with a ReviewAttachmentStore.
24169
24209
  Its reader authorizes the worker independently; its writer rechecks the review
24170
24210
  revision/artifact binding and persists the result idempotently before acknowledgement.
24171
24211
  After a lost acknowledgement, reconcile that result instead of repeating effects.
24212
+ The generated Job first calls store.readResult. The PostgreSQL adapter rechecks
24213
+ current permission, ownership, revision and project/tenant binding inside the
24214
+ read transaction, then validates a stored result with AttachmentResult. A valid
24215
+ committed result skips Storage download and result insertion on redelivery.
24216
+ Revoked access or a malformed/conflicting result fails closed, not as a cache miss.
24217
+ Existing custom ReviewAttachmentStore implementations must add readResult with
24218
+ these same authorization and validation guarantees when adopting this Job.
24219
+ This does not automatically restart a halted worker, reset the attempt budget,
24220
+ or make external side effects atomic with the database.
24172
24221
 
24173
24222
  src/host/review-attachments.ts supplies createReviewAttachmentAdapters with a
24174
24223
  durable store and enqueue callback for the approval adapter's afterApproved hook.
@@ -30513,7 +30562,7 @@ async function runProjectDeployment(http, args) {
30513
30562
  // package.json
30514
30563
  var package_default = {
30515
30564
  name: "@supacloud/cli",
30516
- version: "0.99.0",
30565
+ version: "0.101.2",
30517
30566
  description: "Project-scoped CLI for SupaCloud users",
30518
30567
  type: "module",
30519
30568
  main: "./dist/index.js",
@@ -30565,21 +30614,21 @@ var package_default = {
30565
30614
  directory: "packages/cli"
30566
30615
  },
30567
30616
  dependencies: {
30568
- "@supacloud/compiler": "^0.46.1",
30569
- "@supacloud/delivery": "0.18.0",
30570
- "@supacloud/db": "0.17.0",
30571
- typebox: "1.3.36"
30617
+ "@supacloud/compiler": "^0.46.4",
30618
+ "@supacloud/delivery": "0.18.1",
30619
+ "@supacloud/db": "0.17.1",
30620
+ typebox: "1.3.37"
30572
30621
  },
30573
30622
  overrides: {
30574
- "@supacloud/contracts": "0.9.0",
30575
- "@supacloud/delivery": "0.18.0",
30576
- "@graphql-tools/utils": "^12.0.3"
30623
+ "@supacloud/contracts": "0.10.0",
30624
+ "@supacloud/delivery": "0.18.1",
30625
+ "@graphql-tools/utils": "^12.0.4"
30577
30626
  },
30578
30627
  devDependencies: {
30579
- "@supacloud/elysia": "^0.24.1",
30628
+ "@supacloud/elysia": "0.31.1",
30580
30629
  "@types/bun": "^1.4.3",
30581
- effect: "4.0.2",
30582
- elysia: "2.0.0-beta.21",
30630
+ effect: "4.0.5",
30631
+ elysia: "2.0.0-beta.32",
30583
30632
  typescript: "^7.0.2"
30584
30633
  }
30585
30634
  };
@@ -1,2 +1,2 @@
1
1
  export declare const STARTER_ATTACHMENT_SCHEMA = "-- Apply with the migration owner, never from an application process.\nCREATE TABLE public.starter_attachments (\n review_id text PRIMARY KEY REFERENCES public.starter_reviews(id),\n artifact_id uuid NOT NULL UNIQUE, owner_id text NOT NULL, run_id uuid NOT NULL UNIQUE,\n object_path text NOT NULL UNIQUE\n);\nCREATE TABLE public.starter_attachment_results (\n review_id text NOT NULL REFERENCES public.starter_reviews(id),\n version integer NOT NULL CHECK (version >= 2), artifact_id uuid NOT NULL,\n result jsonb NOT NULL, PRIMARY KEY(review_id,version,artifact_id)\n);\nALTER TABLE public.starter_attachments ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;\n";
2
- export declare const STARTER_ATTACHMENT_POSTGRES = "import { SupaCloudArtifactsClient } from \"@supacloud/js\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { CommandDatabase, CommandTransaction } from \"@supacloud/db\";\nimport type { ReviewAttachmentInput, ReviewAttachmentResult, ReviewAttachmentStore } from \"../review/attachment\";\n\nexport interface ReviewAttachmentOptions {\n database: CommandDatabase;\n // Provision this client for the same project as the database; never use a caller's bearer token.\n service: SupabaseClient;\n projectId: string;\n tenantId: string;\n}\n\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid attachment database result\");\n return value.map(record);\n}\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid attachment record\");\n return value;\n}\nfunction durableResult(value: unknown): ReviewAttachmentResult {\n const item = record(value);\n if (typeof item[\"reviewId\"] !== \"string\" || typeof item[\"artifactId\"] !== \"string\"\n || typeof item[\"version\"] !== \"number\" || typeof item[\"sha256\"] !== \"string\"\n || typeof item[\"bytes\"] !== \"number\" || Object.keys(item).length !== 5) {\n throw new Error(\"Invalid durable attachment result\");\n }\n return { reviewId: item[\"reviewId\"], artifactId: item[\"artifactId\"], version: item[\"version\"], sha256: item[\"sha256\"], bytes: item[\"bytes\"] };\n}\n\nexport async function createReviewAttachmentAdapters(options: ReviewAttachmentOptions): Promise<{\n store: ReviewAttachmentStore;\n enqueue(tx: CommandTransaction, reviewId: string, version: number): Promise<void>;\n}> {\n const { database, service, projectId, tenantId } = options;\n if (!projectId.trim() || !tenantId.trim()) throw new TypeError(\"Attachment project/tenant is required\");\n const artifacts = new SupaCloudArtifactsClient(service);\n const binding = rows(await database.transaction(tx => tx.query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n )));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Attachment database project/tenant binding mismatch\");\n }\n async function authorize(tx: CommandTransaction, input: ReviewAttachmentInput) {\n const allowed = rows(await tx.query(\n \"SELECT a.object_path,a.run_id FROM public.starter_attachments a \" +\n \"JOIN public.starter_reviews r ON r.id=a.review_id AND r.owner_id=a.owner_id \" +\n \"JOIN public.starter_members m ON m.subject=a.owner_id AND m.enabled AND m.can_approve \" +\n \"CROSS JOIN public.starter_application p \" +\n \"WHERE a.review_id=$1 AND a.artifact_id=$2 AND r.version=$3 AND r.state='approved' \" +\n \"AND p.singleton AND p.project_id=$4 AND p.tenant_id=$5 \" +\n \"FOR UPDATE OF r,a,m FOR SHARE OF p\",\n [input.reviewId, input.artifactId, input.version, projectId, tenantId],\n ));\n if (allowed.length !== 1 || typeof allowed[0]?.[\"object_path\"] !== \"string\") {\n throw new Error(\"Attachment worker authorization or review revision is no longer valid\");\n }\n return allowed[0];\n }\n return {\n async enqueue(tx, reviewId, version) {\n const found = rows(await tx.query(\n \"SELECT artifact_id FROM public.starter_attachments WHERE review_id=$1\", [reviewId],\n ))[0];\n if (!found) return;\n const input = { reviewId, version, artifactId: String(found[\"artifact_id\"]) };\n const allowed = await authorize(tx, input);\n // Bind serialized JSON as text so drivers cannot encode the string as a JSON scalar.\n await tx.query(\"SELECT supacloud_workflows.start_run($1::uuid,$2,'1','verify',$3::text::jsonb,3)\", [\n String(allowed[\"run_id\"]), \"review.verify-attachment\", JSON.stringify(input),\n ]);\n },\n store: {\n async readAttachment(input) {\n const allowed = await database.transaction(tx => authorize(tx, input));\n const artifact = await artifacts.get(input.artifactId);\n if (!artifact || artifact.bucketId !== \"review-attachments\"\n || artifact.objectPath !== allowed[\"object_path\"] || artifact.artifactType !== \"review.attachment\"\n || artifact.mimeType !== \"text/plain\" || !/^[1-9][0-9]*$/.test(artifact.sizeBytes)\n || BigInt(artifact.sizeBytes) > 1048576n) throw new Error(\"Invalid attachment artifact\");\n const downloaded = await service.storage.from(artifact.bucketId).download(artifact.objectPath);\n if (downloaded.error || !downloaded.data) throw new Error(\"Attachment download failed\");\n if (downloaded.data.size !== Number(artifact.sizeBytes)) throw new Error(\"Attachment size mismatch\");\n return { body: new Uint8Array(await downloaded.data.arrayBuffer()), sha256: artifact.sha256 };\n },\n async recordAttachment(input, result) {\n if (result.reviewId !== input.reviewId || result.version !== input.version\n || result.artifactId !== input.artifactId || !/^[a-f0-9]{64}$/.test(result.sha256)\n || !Number.isSafeInteger(result.bytes) || result.bytes < 1 || result.bytes > 1048576) {\n throw new Error(\"Invalid attachment result\");\n }\n return database.transaction(async tx => {\n await authorize(tx, input);\n await tx.query(\n \"INSERT INTO public.starter_attachment_results VALUES($1,$2,$3,$4::text::jsonb) ON CONFLICT DO NOTHING\",\n [input.reviewId, input.version, input.artifactId, JSON.stringify(result)],\n );\n const saved = rows(await tx.query(\n \"SELECT result FROM public.starter_attachment_results WHERE review_id=$1 AND version=$2 AND artifact_id=$3\",\n [input.reviewId, input.version, input.artifactId],\n ))[0]?.[\"result\"];\n const value = durableResult(saved);\n if (value.reviewId !== result.reviewId || value.version !== result.version || value.artifactId !== result.artifactId\n || value.sha256 !== result.sha256 || value.bytes !== result.bytes) {\n throw new Error(\"Conflicting durable attachment result\");\n }\n return value;\n });\n },\n },\n };\n}\n";
2
+ export declare const STARTER_ATTACHMENT_POSTGRES = "import { SupaCloudArtifactsClient } from \"@supacloud/js\";\nimport type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { CommandDatabase, CommandTransaction } from \"@supacloud/db\";\nimport { createSchemaDecoder } from \"@supacloud/elysia\";\nimport { AttachmentResult, type ReviewAttachmentInput, type ReviewAttachmentResult, type ReviewAttachmentStore } from \"../review/attachment\";\n\nexport interface ReviewAttachmentOptions {\n database: CommandDatabase;\n // Provision this client for the same project as the database; never use a caller's bearer token.\n service: SupabaseClient;\n projectId: string;\n tenantId: string;\n}\n\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid attachment database result\");\n return value.map(record);\n}\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid attachment record\");\n return value;\n}\nconst durableResult = createSchemaDecoder(AttachmentResult, { normalize: false });\n\nfunction resultFor(input: ReviewAttachmentInput, value: unknown): ReviewAttachmentResult {\n const result = durableResult(value);\n if (result.reviewId !== input.reviewId || result.version !== input.version\n || result.artifactId !== input.artifactId) throw new Error(\"Conflicting durable attachment result\");\n return result;\n}\n\nexport async function createReviewAttachmentAdapters(options: ReviewAttachmentOptions): Promise<{\n store: ReviewAttachmentStore;\n enqueue(tx: CommandTransaction, reviewId: string, version: number): Promise<void>;\n}> {\n const { database, service, projectId, tenantId } = options;\n if (!projectId.trim() || !tenantId.trim()) throw new TypeError(\"Attachment project/tenant is required\");\n const artifacts = new SupaCloudArtifactsClient(service);\n const binding = rows(await database.transaction(tx => tx.query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n )));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Attachment database project/tenant binding mismatch\");\n }\n async function authorize(tx: CommandTransaction, input: ReviewAttachmentInput) {\n const allowed = rows(await tx.query(\n \"SELECT a.object_path,a.run_id FROM public.starter_attachments a \" +\n \"JOIN public.starter_reviews r ON r.id=a.review_id AND r.owner_id=a.owner_id \" +\n \"JOIN public.starter_members m ON m.subject=a.owner_id AND m.enabled AND m.can_approve \" +\n \"CROSS JOIN public.starter_application p \" +\n \"WHERE a.review_id=$1 AND a.artifact_id=$2 AND r.version=$3 AND r.state='approved' \" +\n \"AND p.singleton AND p.project_id=$4 AND p.tenant_id=$5 \" +\n \"FOR UPDATE OF r,a,m FOR SHARE OF p\",\n [input.reviewId, input.artifactId, input.version, projectId, tenantId],\n ));\n if (allowed.length !== 1 || typeof allowed[0]?.[\"object_path\"] !== \"string\") {\n throw new Error(\"Attachment worker authorization or review revision is no longer valid\");\n }\n return allowed[0];\n }\n return {\n async enqueue(tx, reviewId, version) {\n const found = rows(await tx.query(\n \"SELECT artifact_id FROM public.starter_attachments WHERE review_id=$1\", [reviewId],\n ))[0];\n if (!found) return;\n const input = { reviewId, version, artifactId: String(found[\"artifact_id\"]) };\n const allowed = await authorize(tx, input);\n // Bind serialized JSON as text so drivers cannot encode the string as a JSON scalar.\n await tx.query(\"SELECT supacloud_workflows.start_run($1::uuid,$2,'1','verify',$3::text::jsonb,3)\", [\n String(allowed[\"run_id\"]), \"review.verify-attachment\", JSON.stringify(input),\n ]);\n },\n store: {\n async readResult(input) {\n return database.transaction(async tx => {\n await authorize(tx, input);\n const saved = rows(await tx.query(\n \"SELECT result FROM public.starter_attachment_results WHERE review_id=$1 AND version=$2 AND artifact_id=$3\",\n [input.reviewId, input.version, input.artifactId],\n ));\n return saved.length === 0 ? null : resultFor(input, saved[0]?.[\"result\"]);\n });\n },\n async readAttachment(input) {\n const allowed = await database.transaction(tx => authorize(tx, input));\n const artifact = await artifacts.get(input.artifactId);\n if (!artifact || artifact.bucketId !== \"review-attachments\"\n || artifact.objectPath !== allowed[\"object_path\"] || artifact.artifactType !== \"review.attachment\"\n || artifact.mimeType !== \"text/plain\" || !/^[1-9][0-9]*$/.test(artifact.sizeBytes)\n || BigInt(artifact.sizeBytes) > 1048576n) throw new Error(\"Invalid attachment artifact\");\n const downloaded = await service.storage.from(artifact.bucketId).download(artifact.objectPath);\n if (downloaded.error || !downloaded.data) throw new Error(\"Attachment download failed\");\n if (downloaded.data.size !== Number(artifact.sizeBytes)) throw new Error(\"Attachment size mismatch\");\n return { body: new Uint8Array(await downloaded.data.arrayBuffer()), sha256: artifact.sha256 };\n },\n async recordAttachment(input, result) {\n const decoded = resultFor(input, result);\n return database.transaction(async tx => {\n await authorize(tx, input);\n await tx.query(\n \"INSERT INTO public.starter_attachment_results VALUES($1,$2,$3,$4::text::jsonb) ON CONFLICT DO NOTHING\",\n [input.reviewId, input.version, input.artifactId, JSON.stringify(decoded)],\n );\n const saved = rows(await tx.query(\n \"SELECT result FROM public.starter_attachment_results WHERE review_id=$1 AND version=$2 AND artifact_id=$3\",\n [input.reviewId, input.version, input.artifactId],\n ))[0]?.[\"result\"];\n const value = resultFor(input, saved);\n if (value.sha256 !== decoded.sha256 || value.bytes !== decoded.bytes) {\n throw new Error(\"Conflicting durable attachment result\");\n }\n return value;\n });\n },\n },\n };\n}\n";
@@ -1,4 +1,4 @@
1
1
  export declare const STARTER_REVIEW_SCHEMA = "-- Apply with the platform migration owner, never from the HTTP process.\n-- One application database belongs to one explicitly provisioned project/tenant.\nCREATE TABLE public.starter_application (\n singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),\n project_id text NOT NULL CHECK (length(project_id) > 0),\n tenant_id text NOT NULL CHECK (length(tenant_id) > 0)\n);\nCREATE TABLE public.starter_reviews (\n id text PRIMARY KEY, owner_id text NOT NULL,\n state text NOT NULL CHECK (state IN ('draft','approved')),\n version integer NOT NULL CHECK (version > 0)\n);\nCREATE TABLE public.starter_members (\n subject text PRIMARY KEY, enabled boolean NOT NULL DEFAULT true,\n can_approve boolean NOT NULL DEFAULT false\n);\nALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;\n";
2
- export declare const STARTER_REVIEW_POSTGRES = "import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { createTransactionalCommand, plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSupAuthRequestContext, requireTrustedIdentity, requireIdempotencyKey,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\n\ninterface Approval { id: string; expectedVersion: number }\ninterface Review { state: \"draft\" | \"approved\"; version: number }\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nfunction input(value: unknown): Approval {\n const item = record(value);\n if (typeof item[\"id\"] !== \"string\" || !item[\"id\"].length || typeof item[\"expectedVersion\"] !== \"number\"\n || !Number.isSafeInteger(item[\"expectedVersion\"]) || item[\"expectedVersion\"] < 1) throw new TypeError(\"Invalid approval input\");\n return { id: item[\"id\"], expectedVersion: item[\"expectedVersion\"] };\n}\nfunction result(value: unknown): Review {\n const item = record(value);\n if ((item[\"state\"] !== \"draft\" && item[\"state\"] !== \"approved\") || typeof item[\"version\"] !== \"number\"\n || !Number.isSafeInteger(item[\"version\"]) || item[\"version\"] < 1) throw new TypeError(\"Invalid review result\");\n return { state: item[\"state\"], version: item[\"version\"] };\n}\n\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const scope = new AsyncLocalStorage<CommandTransaction>();\n const transaction = () => {\n const tx = scope.getStore();\n if (!tx) throw new Error(\"Review storage requires its durable command transaction\");\n return tx;\n };\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandStore = createPostgresCommandStore(database);\n return {\n deps: { dbClient: store }, requestContext,\n commandGovernance: {\n authorize(invocation) {\n const command = invocation.command;\n if (command.name !== \"review.approve\" || command.permission !== \"review.approve\"\n || command.transaction !== \"required\" || command.idempotency !== \"required\"\n || command.audit !== \"review.approved\" || command.rpc !== undefined) {\n throw new ApplicationError(\"Unsupported review command\", { status: 501, code: \"REVIEW_COMMAND_UNSUPPORTED\" });\n }\n actor(invocation);\n },\n async idempotency(invocation, next) {\n const command = createTransactionalCommand({\n name: invocation.command.name, store: commandStore,\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n execute: tx => scope.run(tx, async () => {\n try { return await next(); }\n catch (error) {\n if (isPublicConflict(error)) {\n throw new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\");\n }\n throw error;\n }\n }),\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n });\n try {\n const receipt = await command.execute(\n { actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),\n { id: invocation.input.params[\"id\"], expectedVersion: record(invocation.input.body)[\"expectedVersion\"] },\n invocation.request.signal,\n );\n if (receipt.status !== \"confirmed\") throw new CommandError(\"COMMAND_OUTCOME_UNKNOWN\");\n return receipt.result;\n } catch (error) {\n if (!(error instanceof CommandError)) throw error;\n const status = error.code === \"COMMAND_REJECTED\" ? 403\n : error.code === \"COMMAND_IDEMPOTENCY_CONFLICT\" ? 409 : 503;\n throw new ApplicationError(error.code, { code: error.code, status });\n }\n },\n transaction(_invocation, next) { transaction(); return next(); },\n // The durable command owns the authoritative audit and receipt in the same transaction.\n audit: { succeeded() { transaction(); }, failed() {} },\n },\n };\n}\n";
3
- export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"durable command transaction\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"durable command transaction\");\n expect(db.queries).toHaveLength(1);\n});\n";
2
+ export declare const STARTER_REVIEW_POSTGRES = "import { plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,\n createTransactionalCommandGovernance,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\nimport { ApprovalInput, ReviewResult } from \"../review/contracts\";\n\nconst input = createSchemaDecoder(ApprovalInput, { normalize: false });\nconst result = createSchemaDecoder(ReviewResult, { normalize: false });\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const transaction = () => commandBinding.transaction();\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandBinding = createTransactionalCommandGovernance({\n permission: \"review.approve\",\n identity: invocation => ({ actorId: actor(invocation), tenantId }),\n input: invocation => ({\n ...record(invocation.input.body), id: invocation.input.params[\"id\"],\n }),\n rejection: error => isPublicConflict(error) ? new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\") : undefined,\n definition: {\n name: \"review.approve\", store: createPostgresCommandStore(database),\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n },\n });\n return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };\n}\n";
3
+ export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"not active\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"not active\");\n expect(db.queries).toHaveLength(1);\n});\n";
4
4
  export declare const STARTER_REVIEW_DELIVERY_HOST = "import { SQL } from \"bun\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { createBunCommandDatabase } from \"@supacloud/db/bun\";\nimport { createApplication, type CompiledModule } from \"@supacloud/elysia\";\nimport { createReviewPostgresAdapters } from \"./host/review-postgres\";\nimport { createReviewAttachmentAdapters } from \"./host/review-attachments\";\nimport { createReviewUploadAdapters } from \"./host/review-uploads\";\n\nfunction required(name: string): string {\n const value = process.env[name];\n if (!value?.trim()) throw new Error(\"Missing required host setting: \" + name);\n return value;\n}\n\nfunction databaseConnection(): SQL.Options {\n const socket = process.env[\"DATABASE_SOCKET_PATH\"];\n if (socket && process.env[\"DATABASE_URL\"]) throw new Error(\"Conflicting database connection settings\");\n if (socket) {\n return { adapter: \"postgres\", path: socket, database: required(\"DATABASE_NAME\"), username: required(\"DATABASE_USER\") };\n }\n const url = new URL(required(\"DATABASE_URL\"));\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") throw new Error(\"Invalid database URL\");\n return {\n adapter: \"postgres\", url: url.href, hostname: url.hostname.replace(/^\\[|\\]$/g, \"\"), port: Number(url.port || 5432),\n username: decodeURIComponent(url.username), password: decodeURIComponent(url.password),\n database: decodeURIComponent(url.pathname.slice(1)),\n };\n}\n\nexport async function createDeliveryApplication(modules: CompiledModule[], lifecycle: { signal: AbortSignal }) {\n lifecycle.signal.throwIfAborted();\n const connection = databaseConnection();\n const attachments = process.env[\"REVIEW_ATTACHMENTS\"];\n if (attachments !== undefined && attachments !== \"enabled\") throw new Error(\"Invalid attachment configuration\");\n const settings = {\n tenantId: required(\"APP_TENANT_ID\"),\n identity: {\n issuer: required(\"SUPAUTH_ISSUER\"), audience: required(\"SUPAUTH_AUDIENCE\"),\n clientId: required(\"SUPAUTH_CLIENT_ID\"), projectId: required(\"SUPACLOUD_PROJECT_ID\"),\n jwksUrl: required(\"SUPAUTH_JWKS_URL\"),\n },\n };\n const pool = new SQL({ ...connection, max: 12, connectionTimeout: 5 });\n let closing: Promise<void> | undefined;\n const close = () => closing ??= pool.close({ timeout: 5 });\n let stage: \"attachments\" | \"uploads\" | \"postgres\" | \"application\" = \"attachments\";\n try {\n const database = createBunCommandDatabase(pool);\n const attachmentOptions = attachments === \"enabled\" ? {\n database, tenantId: settings.tenantId, projectId: settings.identity.projectId,\n service: createClient(required(\"SUPACLOUD_URL\"), required(\"SUPACLOUD_SERVICE_ROLE_KEY\"), {\n auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n }),\n } : undefined;\n const durable = attachmentOptions ? await createReviewAttachmentAdapters(attachmentOptions) : undefined;\n stage = \"uploads\";\n const uploads = attachmentOptions ? await createReviewUploadAdapters(attachmentOptions) : undefined;\n stage = \"postgres\";\n const adapters = await createReviewPostgresAdapters({\n database, tenantId: settings.tenantId, identity: settings.identity,\n ...(uploads ? { uploads } : {}),\n ...(durable ? { afterApproved: durable.enqueue } : {}),\n });\n lifecycle.signal.throwIfAborted();\n stage = \"application\";\n const app = createApplication({ name: \"review\", modules, ...adapters });\n return { fetch: (request: Request) => app.handle(request), close };\n } catch (error) {\n await close();\n const code = error !== null && typeof error === \"object\" && \"code\" in error\n && typeof error.code === \"string\" && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : \"UNKNOWN\";\n console.error(JSON.stringify({ event: \"review-host-initialization-failed\", stage, code }));\n // Database/identity errors can contain connection details; never echo the underlying cause.\n throw new Error(\"Review host initialization failed\");\n }\n}\n";
@@ -0,0 +1 @@
1
+ export declare const STARTER_REVIEW_QUERY = "import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { ReviewRow } from \"../../db/schema\";\n\n// This read projection uses SQL column names shared with the Drizzle source.\n// Do not expose camelCase Drizzle property names as PostgREST column names.\nexport type ReviewSummary = Pick<ReviewRow, \"id\" | \"state\" | \"version\">;\nexport type ReviewReadDatabase = {\n public: {\n Tables: {\n starter_reviews: {\n Row: ReviewSummary;\n Insert: never;\n Update: never;\n Relationships: [];\n };\n };\n Views: Record<never, never>;\n Functions: Record<never, never>;\n };\n};\n\nexport interface ReviewPageOptions {\n page?: number;\n pageSize?: number;\n signal?: AbortSignal;\n}\n\n// Reuse the application's user-scoped client. Identity verification and row\n// authorization belong to the gateway and database, never a caller-supplied owner.\nexport async function listReviews(\n client: SupabaseClient<ReviewReadDatabase>,\n { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},\n) {\n if (!Number.isSafeInteger(page) || page < 1\n || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100\n || !Number.isSafeInteger(page * pageSize)) {\n throw new RangeError(\"Invalid review page: page >= 1 and pageSize between 1 and 100 are required\");\n }\n signal?.throwIfAborted();\n const start = (page - 1) * pageSize;\n const query = client.from(\"starter_reviews\")\n .select(\"id,state,version\", { count: \"exact\" })\n .order(\"id\", { ascending: true })\n .range(start, start + pageSize - 1);\n const { data, error, count } = await (signal ? query.abortSignal(signal) : query);\n if (error) throw error;\n if (data === null || count === null) throw new Error(\"Incomplete review page response\");\n return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };\n}\n";
@@ -1,2 +1,2 @@
1
- export declare const STARTER_REVIEW_JOB = "import { DB_CLIENT, Inject, Injectable, Job } from \"@supacloud/app\";\nimport { ApplicationError } from \"@supacloud/elysia\";\nimport { t } from \"elysia\";\nimport type { Static } from \"elysia/type\";\n\nexport const AttachmentInput = t.Object({\n reviewId: t.String({ minLength: 1, maxLength: 100 }),\n version: t.Integer({ minimum: 2 }),\n artifactId: t.String({ format: \"uuid\" }),\n}, { additionalProperties: false });\n\nexport const AttachmentResult = t.Object({\n ...AttachmentInput.properties,\n sha256: t.String({ pattern: \"^[a-f0-9]{64}$\" }),\n bytes: t.Integer({ minimum: 1, maximum: 1048576 }),\n}, { additionalProperties: false });\n\nexport type ReviewAttachmentInput = Static<typeof AttachmentInput>;\nexport type ReviewAttachmentResult = Static<typeof AttachmentResult>;\n\nexport interface ReviewAttachmentStore {\n // Resolve the immutable artifact under the worker's own authorization and current review revision.\n readAttachment(input: ReviewAttachmentInput): Promise<{ body: Uint8Array; sha256: string }>;\n // Revalidate revision/ownership and persist idempotently in a transaction; return the durable result.\n recordAttachment(input: ReviewAttachmentInput, result: ReviewAttachmentResult): Promise<ReviewAttachmentResult>;\n}\n\n@Injectable({ scope: \"job\" })\n@Job({\n name: \"review.verify-attachment\", mode: \"task\", idempotency: \"required\",\n timeoutSec: 30, maxAttempts: 3, input: AttachmentInput, output: AttachmentResult,\n})\nexport class VerifyReviewAttachment {\n constructor(@Inject(DB_CLIENT) private readonly store: ReviewAttachmentStore) {}\n\n async run(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult> {\n const source = await this.store.readAttachment(input);\n if (source.body.byteLength < 1 || source.body.byteLength > 1048576) {\n throw new ApplicationError(\"Attachment size is outside the supported range\", { code: \"ATTACHMENT_SIZE_INVALID\" });\n }\n const digest = await crypto.subtle.digest(\"SHA-256\", new Uint8Array(source.body));\n const sha256 = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n if (sha256 !== source.sha256) {\n throw new ApplicationError(\"Attachment integrity check failed\", { code: \"ATTACHMENT_INTEGRITY_FAILED\" });\n }\n return this.store.recordAttachment(input, { ...input, sha256, bytes: source.body.byteLength });\n }\n}\n";
2
- export declare const STARTER_REVIEW_JOB_TEST = "import { expect, test } from \"bun:test\";\nimport { VerifyReviewAttachment, type ReviewAttachmentResult } from \"../src/review/attachment\";\n\nconst input = { reviewId: \"review\", version: 2, artifactId: \"11111111-1111-4111-8111-111111111111\" };\nconst body = new TextEncoder().encode(\"review attachment\");\nconst sha256 = Array.from(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", body)),\n byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\ntest(\"attachment job awaits the authorized reader and the committed result\", async () => {\n let recorded: ReviewAttachmentResult | undefined;\n const job = new VerifyReviewAttachment({\n async readAttachment() { return { body, sha256 }; },\n async recordAttachment(_input, result) { await Promise.resolve(); recorded = result; return result; },\n });\n expect(await job.run(input)).toEqual({ ...input, sha256, bytes: body.byteLength });\n expect(recorded).toEqual({ ...input, sha256, bytes: body.byteLength });\n});\n\ntest(\"missing authorization, corrupted bytes and persistence failures are not successful jobs\", async () => {\n let records = 0;\n for (const readAttachment of [\n async () => { throw new Error(\"Access denied\"); },\n async () => ({ body, sha256: \"0\".repeat(64) }),\n async () => ({ body: new Uint8Array(), sha256 }),\n async () => ({ body: new Uint8Array(1048577), sha256 }),\n ]) {\n const job = new VerifyReviewAttachment({\n readAttachment, async recordAttachment(_input, result) { records++; return result; },\n });\n await expect(job.run(input)).rejects.toThrow();\n }\n expect(records).toBe(0);\n await expect(new VerifyReviewAttachment({\n async readAttachment() { return { body, sha256 }; },\n async recordAttachment() { throw new Error(\"Commit failed\"); },\n }).run(input)).rejects.toThrow(\"Commit failed\");\n});\n";
1
+ export declare const STARTER_REVIEW_JOB = "import { DB_CLIENT, Inject, Injectable, Job } from \"@supacloud/app\";\nimport { ApplicationError } from \"@supacloud/elysia\";\nimport { t } from \"elysia\";\nimport type { Static } from \"elysia/type\";\n\nexport const AttachmentInput = t.Object({\n reviewId: t.String({ minLength: 1, maxLength: 100 }),\n version: t.Integer({ minimum: 2 }),\n artifactId: t.String({ format: \"uuid\" }),\n}, { additionalProperties: false });\n\nexport const AttachmentResult = t.Object({\n ...AttachmentInput.properties,\n sha256: t.String({ pattern: \"^[a-f0-9]{64}$\" }),\n bytes: t.Integer({ minimum: 1, maximum: 1048576 }),\n}, { additionalProperties: false });\n\nexport type ReviewAttachmentInput = Static<typeof AttachmentInput>;\nexport type ReviewAttachmentResult = Static<typeof AttachmentResult>;\n\nexport interface ReviewAttachmentStore {\n // Reauthorize before returning a committed result; null means execution is still needed.\n readResult(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult | null>;\n // Resolve the immutable artifact under the worker's own authorization and current review revision.\n readAttachment(input: ReviewAttachmentInput): Promise<{ body: Uint8Array; sha256: string }>;\n // Revalidate revision/ownership and persist idempotently in a transaction; return the durable result.\n recordAttachment(input: ReviewAttachmentInput, result: ReviewAttachmentResult): Promise<ReviewAttachmentResult>;\n}\n\n@Injectable({ scope: \"job\" })\n@Job({\n name: \"review.verify-attachment\", mode: \"task\", idempotency: \"required\",\n timeoutSec: 30, maxAttempts: 3, input: AttachmentInput, output: AttachmentResult,\n})\nexport class VerifyReviewAttachment {\n constructor(@Inject(DB_CLIENT) private readonly store: ReviewAttachmentStore) {}\n\n async run(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult> {\n const committed = await this.store.readResult(input);\n if (committed !== null) return committed;\n const source = await this.store.readAttachment(input);\n if (source.body.byteLength < 1 || source.body.byteLength > 1048576) {\n throw new ApplicationError(\"Attachment size is outside the supported range\", { code: \"ATTACHMENT_SIZE_INVALID\" });\n }\n const digest = await crypto.subtle.digest(\"SHA-256\", new Uint8Array(source.body));\n const sha256 = Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n if (sha256 !== source.sha256) {\n throw new ApplicationError(\"Attachment integrity check failed\", { code: \"ATTACHMENT_INTEGRITY_FAILED\" });\n }\n return this.store.recordAttachment(input, { ...input, sha256, bytes: source.body.byteLength });\n }\n}\n";
2
+ export declare const STARTER_REVIEW_JOB_TEST = "import { expect, test } from \"bun:test\";\nimport { VerifyReviewAttachment, type ReviewAttachmentResult } from \"../src/review/attachment\";\n\nconst input = { reviewId: \"review\", version: 2, artifactId: \"11111111-1111-4111-8111-111111111111\" };\nconst body = new TextEncoder().encode(\"review attachment\");\nconst sha256 = Array.from(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", body)),\n byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n\ntest(\"attachment job awaits the authorized reader and the committed result\", async () => {\n let recorded: ReviewAttachmentResult | undefined;\n const job = new VerifyReviewAttachment({\n async readResult() { return null; },\n async readAttachment() { return { body, sha256 }; },\n async recordAttachment(_input, result) { await Promise.resolve(); recorded = result; return result; },\n });\n expect(await job.run(input)).toEqual({ ...input, sha256, bytes: body.byteLength });\n expect(recorded).toEqual({ ...input, sha256, bytes: body.byteLength });\n});\n\ntest(\"missing authorization, corrupted bytes and persistence failures are not successful jobs\", async () => {\n let records = 0;\n for (const readAttachment of [\n async () => { throw new Error(\"Access denied\"); },\n async () => ({ body, sha256: \"0\".repeat(64) }),\n async () => ({ body: new Uint8Array(), sha256 }),\n async () => ({ body: new Uint8Array(1048577), sha256 }),\n ]) {\n const job = new VerifyReviewAttachment({\n async readResult() { return null; },\n readAttachment, async recordAttachment(_input, result) { records++; return result; },\n });\n await expect(job.run(input)).rejects.toThrow();\n }\n expect(records).toBe(0);\n await expect(new VerifyReviewAttachment({\n async readResult() { return null; },\n async readAttachment() { return { body, sha256 }; },\n async recordAttachment() { throw new Error(\"Commit failed\"); },\n }).run(input)).rejects.toThrow(\"Commit failed\");\n});\n\ntest(\"a reauthorized committed result avoids another download or write\", async () => {\n const result = { ...input, sha256, bytes: body.byteLength };\n const job = new VerifyReviewAttachment({\n async readResult() { return result; },\n async readAttachment() { throw new Error(\"Must not download again\"); },\n async recordAttachment() { throw new Error(\"Must not write again\"); },\n });\n expect(await job.run(input)).toEqual(result);\n await expect(new VerifyReviewAttachment({\n async readResult() { throw new Error(\"Access revoked\"); },\n async readAttachment() { throw new Error(\"Must not download\"); },\n async recordAttachment() { throw new Error(\"Must not write\"); },\n }).run(input)).rejects.toThrow(\"Access revoked\");\n});\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.99.0",
3
+ "version": "0.101.2",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,21 +52,21 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.46.1",
56
- "@supacloud/delivery": "0.18.0",
57
- "@supacloud/db": "0.17.0",
58
- "typebox": "1.3.36"
55
+ "@supacloud/compiler": "^0.46.4",
56
+ "@supacloud/delivery": "0.18.1",
57
+ "@supacloud/db": "0.17.1",
58
+ "typebox": "1.3.37"
59
59
  },
60
60
  "overrides": {
61
- "@supacloud/contracts": "0.9.0",
62
- "@supacloud/delivery": "0.18.0",
63
- "@graphql-tools/utils": "^12.0.3"
61
+ "@supacloud/contracts": "0.10.0",
62
+ "@supacloud/delivery": "0.18.1",
63
+ "@graphql-tools/utils": "^12.0.4"
64
64
  },
65
65
  "devDependencies": {
66
- "@supacloud/elysia": "^0.24.1",
66
+ "@supacloud/elysia": "0.31.1",
67
67
  "@types/bun": "^1.4.3",
68
- "effect": "4.0.2",
69
- "elysia": "2.0.0-beta.21",
68
+ "effect": "4.0.5",
69
+ "elysia": "2.0.0-beta.32",
70
70
  "typescript": "^7.0.2"
71
71
  }
72
72
  }