@supacloud/cli 0.101.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
@@ -21388,7 +21388,7 @@ var starter_metadata_default = {
21388
21388
  generatedBy: "supacloud/starter-metadata-v1",
21389
21389
  packages: {
21390
21390
  compiler: {
21391
- version: "0.46.3"
21391
+ version: "0.46.4"
21392
21392
  },
21393
21393
  app: {
21394
21394
  version: "0.27.1",
@@ -21397,13 +21397,13 @@ var starter_metadata_default = {
21397
21397
  }
21398
21398
  },
21399
21399
  elysia: {
21400
- version: "0.31.0"
21400
+ version: "0.31.1"
21401
21401
  },
21402
21402
  commands: {
21403
21403
  version: "0.9.3"
21404
21404
  },
21405
21405
  contracts: {
21406
- version: "0.9.0"
21406
+ version: "0.10.0"
21407
21407
  },
21408
21408
  db: {
21409
21409
  version: "0.17.1",
@@ -22222,6 +22222,8 @@ export type ReviewAttachmentInput = Static<typeof AttachmentInput>;
22222
22222
  export type ReviewAttachmentResult = Static<typeof AttachmentResult>;
22223
22223
 
22224
22224
  export interface ReviewAttachmentStore {
22225
+ // Reauthorize before returning a committed result; null means execution is still needed.
22226
+ readResult(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult | null>;
22225
22227
  // Resolve the immutable artifact under the worker's own authorization and current review revision.
22226
22228
  readAttachment(input: ReviewAttachmentInput): Promise<{ body: Uint8Array; sha256: string }>;
22227
22229
  // Revalidate revision/ownership and persist idempotently in a transaction; return the durable result.
@@ -22237,6 +22239,8 @@ export class VerifyReviewAttachment {
22237
22239
  constructor(@Inject(DB_CLIENT) private readonly store: ReviewAttachmentStore) {}
22238
22240
 
22239
22241
  async run(input: ReviewAttachmentInput): Promise<ReviewAttachmentResult> {
22242
+ const committed = await this.store.readResult(input);
22243
+ if (committed !== null) return committed;
22240
22244
  const source = await this.store.readAttachment(input);
22241
22245
  if (source.body.byteLength < 1 || source.body.byteLength > 1048576) {
22242
22246
  throw new ApplicationError("Attachment size is outside the supported range", { code: "ATTACHMENT_SIZE_INVALID" });
@@ -22261,6 +22265,7 @@ const sha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", b
22261
22265
  test("attachment job awaits the authorized reader and the committed result", async () => {
22262
22266
  let recorded: ReviewAttachmentResult | undefined;
22263
22267
  const job = new VerifyReviewAttachment({
22268
+ async readResult() { return null; },
22264
22269
  async readAttachment() { return { body, sha256 }; },
22265
22270
  async recordAttachment(_input, result) { await Promise.resolve(); recorded = result; return result; },
22266
22271
  });
@@ -22277,16 +22282,33 @@ test("missing authorization, corrupted bytes and persistence failures are not su
22277
22282
  async () => ({ body: new Uint8Array(1048577), sha256 }),
22278
22283
  ]) {
22279
22284
  const job = new VerifyReviewAttachment({
22285
+ async readResult() { return null; },
22280
22286
  readAttachment, async recordAttachment(_input, result) { records++; return result; },
22281
22287
  });
22282
22288
  await expect(job.run(input)).rejects.toThrow();
22283
22289
  }
22284
22290
  expect(records).toBe(0);
22285
22291
  await expect(new VerifyReviewAttachment({
22292
+ async readResult() { return null; },
22286
22293
  async readAttachment() { return { body, sha256 }; },
22287
22294
  async recordAttachment() { throw new Error("Commit failed"); },
22288
22295
  }).run(input)).rejects.toThrow("Commit failed");
22289
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
+ });
22290
22312
  `;
22291
22313
 
22292
22314
  // src/shared/tools/app-starter-attachments.ts
@@ -22307,7 +22329,8 @@ ALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;
22307
22329
  var STARTER_ATTACHMENT_POSTGRES = `import { SupaCloudArtifactsClient } from "@supacloud/js";
22308
22330
  import type { SupabaseClient } from "@supabase/supabase-js";
22309
22331
  import type { CommandDatabase, CommandTransaction } from "@supacloud/db";
22310
- 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";
22311
22334
 
22312
22335
  export interface ReviewAttachmentOptions {
22313
22336
  database: CommandDatabase;
@@ -22328,14 +22351,13 @@ function record(value: unknown): Record<string, unknown> {
22328
22351
  if (!isRecord(value)) throw new TypeError("Invalid attachment record");
22329
22352
  return value;
22330
22353
  }
22331
- function durableResult(value: unknown): ReviewAttachmentResult {
22332
- const item = record(value);
22333
- if (typeof item["reviewId"] !== "string" || typeof item["artifactId"] !== "string"
22334
- || typeof item["version"] !== "number" || typeof item["sha256"] !== "string"
22335
- || typeof item["bytes"] !== "number" || Object.keys(item).length !== 5) {
22336
- throw new Error("Invalid durable attachment result");
22337
- }
22338
- 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;
22339
22361
  }
22340
22362
 
22341
22363
  export async function createReviewAttachmentAdapters(options: ReviewAttachmentOptions): Promise<{
@@ -22381,6 +22403,16 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22381
22403
  ]);
22382
22404
  },
22383
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
+ },
22384
22416
  async readAttachment(input) {
22385
22417
  const allowed = await database.transaction(tx => authorize(tx, input));
22386
22418
  const artifact = await artifacts.get(input.artifactId);
@@ -22394,24 +22426,19 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22394
22426
  return { body: new Uint8Array(await downloaded.data.arrayBuffer()), sha256: artifact.sha256 };
22395
22427
  },
22396
22428
  async recordAttachment(input, result) {
22397
- if (result.reviewId !== input.reviewId || result.version !== input.version
22398
- || result.artifactId !== input.artifactId || !/^[a-f0-9]{64}$/.test(result.sha256)
22399
- || !Number.isSafeInteger(result.bytes) || result.bytes < 1 || result.bytes > 1048576) {
22400
- throw new Error("Invalid attachment result");
22401
- }
22429
+ const decoded = resultFor(input, result);
22402
22430
  return database.transaction(async tx => {
22403
22431
  await authorize(tx, input);
22404
22432
  await tx.query(
22405
22433
  "INSERT INTO public.starter_attachment_results VALUES($1,$2,$3,$4::text::jsonb) ON CONFLICT DO NOTHING",
22406
- [input.reviewId, input.version, input.artifactId, JSON.stringify(result)],
22434
+ [input.reviewId, input.version, input.artifactId, JSON.stringify(decoded)],
22407
22435
  );
22408
22436
  const saved = rows(await tx.query(
22409
22437
  "SELECT result FROM public.starter_attachment_results WHERE review_id=$1 AND version=$2 AND artifact_id=$3",
22410
22438
  [input.reviewId, input.version, input.artifactId],
22411
22439
  ))[0]?.["result"];
22412
- const value = durableResult(saved);
22413
- if (value.reviewId !== result.reviewId || value.version !== result.version || value.artifactId !== result.artifactId
22414
- || value.sha256 !== result.sha256 || value.bytes !== result.bytes) {
22440
+ const value = resultFor(input, saved);
22441
+ if (value.sha256 !== decoded.sha256 || value.bytes !== decoded.bytes) {
22415
22442
  throw new Error("Conflicting durable attachment result");
22416
22443
  }
22417
22444
  return value;
@@ -24182,6 +24209,15 @@ the existing Workflow and invokes the compiled Job with a ReviewAttachmentStore.
24182
24209
  Its reader authorizes the worker independently; its writer rechecks the review
24183
24210
  revision/artifact binding and persists the result idempotently before acknowledgement.
24184
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.
24185
24221
 
24186
24222
  src/host/review-attachments.ts supplies createReviewAttachmentAdapters with a
24187
24223
  durable store and enqueue callback for the approval adapter's afterApproved hook.
@@ -30526,7 +30562,7 @@ async function runProjectDeployment(http, args) {
30526
30562
  // package.json
30527
30563
  var package_default = {
30528
30564
  name: "@supacloud/cli",
30529
- version: "0.101.0",
30565
+ version: "0.101.2",
30530
30566
  description: "Project-scoped CLI for SupaCloud users",
30531
30567
  type: "module",
30532
30568
  main: "./dist/index.js",
@@ -30578,18 +30614,18 @@ var package_default = {
30578
30614
  directory: "packages/cli"
30579
30615
  },
30580
30616
  dependencies: {
30581
- "@supacloud/compiler": "^0.46.3",
30617
+ "@supacloud/compiler": "^0.46.4",
30582
30618
  "@supacloud/delivery": "0.18.1",
30583
30619
  "@supacloud/db": "0.17.1",
30584
30620
  typebox: "1.3.37"
30585
30621
  },
30586
30622
  overrides: {
30587
- "@supacloud/contracts": "0.9.0",
30623
+ "@supacloud/contracts": "0.10.0",
30588
30624
  "@supacloud/delivery": "0.18.1",
30589
30625
  "@graphql-tools/utils": "^12.0.4"
30590
30626
  },
30591
30627
  devDependencies: {
30592
- "@supacloud/elysia": "0.31.0",
30628
+ "@supacloud/elysia": "0.31.1",
30593
30629
  "@types/bun": "^1.4.3",
30594
30630
  effect: "4.0.5",
30595
30631
  elysia: "2.0.0-beta.32",
@@ -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,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.101.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,18 +52,18 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.46.3",
55
+ "@supacloud/compiler": "^0.46.4",
56
56
  "@supacloud/delivery": "0.18.1",
57
57
  "@supacloud/db": "0.17.1",
58
58
  "typebox": "1.3.37"
59
59
  },
60
60
  "overrides": {
61
- "@supacloud/contracts": "0.9.0",
61
+ "@supacloud/contracts": "0.10.0",
62
62
  "@supacloud/delivery": "0.18.1",
63
63
  "@graphql-tools/utils": "^12.0.4"
64
64
  },
65
65
  "devDependencies": {
66
- "@supacloud/elysia": "0.31.0",
66
+ "@supacloud/elysia": "0.31.1",
67
67
  "@types/bun": "^1.4.3",
68
68
  "effect": "4.0.5",
69
69
  "elysia": "2.0.0-beta.32",