@supacloud/cli 0.95.0 → 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -476,7 +476,7 @@ Example configuration write file:
476
476
  "configuration_id": "01234567-89ab-4def-8123-456789abcdef",
477
477
  "expected_configuration_id": null,
478
478
  "configuration": {
479
- "bun_version": "1.4.2",
479
+ "bun_version": "1.4.3",
480
480
  "targets": [
481
481
  {
482
482
  "name": "api",
package/dist/index.js CHANGED
@@ -9964,22 +9964,28 @@ function validateWorkerExecution(groups) {
9964
9964
  var objectOptions = { additionalProperties: false };
9965
9965
  var reference = String2({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z][A-Za-z0-9_.:/-]*$" });
9966
9966
  var references = _Array_(reference, { uniqueItems: true });
9967
- var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook")]);
9967
+ var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
9968
9968
  var isolation = Union([Literal("shared"), Literal("process")]);
9969
9969
  var DeliveryFilePathSchema = String2({
9970
9970
  maxLength: 512,
9971
9971
  pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
9972
9972
  });
9973
+ var functionTarget = _Object_({
9974
+ slug: String2({ pattern: "^[a-z][a-z0-9-]{0,62}$" }),
9975
+ entrypoint: DeliveryFilePathSchema,
9976
+ verifyJwt: Boolean2()
9977
+ }, objectOptions);
9973
9978
  var DeliveryOptionsSchema = _Object_({
9974
9979
  version: Literal(1),
9975
9980
  execution: Optional(WorkerExecutionSchema),
9976
9981
  targets: Optional(_Array_(_Object_({
9977
9982
  name: String2({ pattern: "^[a-z][a-z0-9-]{0,62}$" }),
9978
9983
  kind,
9979
- modules: _Array_(reference, { minItems: 1, uniqueItems: true }),
9984
+ modules: Optional(_Array_(reference, { minItems: 1, uniqueItems: true })),
9980
9985
  isolation: Optional(isolation),
9981
9986
  capabilities: Optional(references),
9982
- compute: Optional(ComputeResourcesSchema)
9987
+ compute: Optional(ComputeResourcesSchema),
9988
+ function: Optional(functionTarget)
9983
9989
  }, objectOptions))),
9984
9990
  runtime: Optional(_Object_({
9985
9991
  processIsolation: Boolean2(),
@@ -10057,6 +10063,7 @@ var DeliveryTargetSchema = _Object_({
10057
10063
  routes: _Array_(routeSchema),
10058
10064
  jobs: _Array_(jobSchema),
10059
10065
  externalTokens: _Array_(String2()),
10066
+ function: Optional(functionTarget),
10060
10067
  requirements: _Object_({
10061
10068
  processIsolation: Boolean2(),
10062
10069
  durableQueue: Boolean2(),
@@ -21099,7 +21106,7 @@ var starter_metadata_default = {
21099
21106
  generatedBy: "supacloud/starter-metadata-v1",
21100
21107
  packages: {
21101
21108
  compiler: {
21102
- version: "0.45.0"
21109
+ version: "0.46.0"
21103
21110
  },
21104
21111
  app: {
21105
21112
  version: "0.26.1",
@@ -21117,7 +21124,7 @@ var starter_metadata_default = {
21117
21124
  version: "0.8.1"
21118
21125
  },
21119
21126
  db: {
21120
- version: "0.16.2",
21127
+ version: "0.17.0",
21121
21128
  peerDependencies: {
21122
21129
  "drizzle-orm": "1.0.0-rc.5-169397b"
21123
21130
  },
@@ -21126,7 +21133,7 @@ var starter_metadata_default = {
21126
21133
  }
21127
21134
  },
21128
21135
  sdk: {
21129
- version: "0.41.2",
21136
+ version: "0.42.0",
21130
21137
  peerDependencies: {
21131
21138
  "@supabase/supabase-js": "^2.117.3"
21132
21139
  }
@@ -21451,7 +21458,7 @@ function baseFiles(name, sdkDependencies) {
21451
21458
  version: "0.0.0",
21452
21459
  private: true,
21453
21460
  type: "module",
21454
- engines: { bun: ">=1.4.2" },
21461
+ engines: { bun: ">=1.4.3" },
21455
21462
  scripts: {
21456
21463
  compile: "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js compile",
21457
21464
  "check:generated": "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js check",
@@ -21475,7 +21482,7 @@ function baseFiles(name, sdkDependencies) {
21475
21482
  },
21476
21483
  devDependencies: {
21477
21484
  "@supacloud/compiler": `^${compilerMetadata.version}`,
21478
- "@types/bun": "^1.4.2",
21485
+ "@types/bun": "^1.4.3",
21479
21486
  typescript: "^7.0.2"
21480
21487
  }
21481
21488
  }),
@@ -22135,7 +22142,11 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
22135
22142
 
22136
22143
  // src/shared/tools/app-starter-attachment-worker.ts
22137
22144
  var STARTER_ATTACHMENT_WORKER = `import { createWorker, type CompiledModule } from "@supacloud/elysia";
22138
- import type { SupaCloudWorkflowClaim, SupaCloudWorkflowsClient } from "@supacloud/js";
22145
+ import {
22146
+ createSupaCloudWorkflowTransport,
22147
+ type SupaCloudWorkflowClaim,
22148
+ type SupaCloudWorkflowsClient,
22149
+ } from "@supacloud/js";
22139
22150
  import { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from "./review-attachments";
22140
22151
 
22141
22152
  export interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {
@@ -22160,9 +22171,6 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22160
22171
  halted = true;
22161
22172
  failure.reject(new Error("Attachment worker requires recovery"));
22162
22173
  };
22163
- const attempt = (claim: SupaCloudWorkflowClaim) => ({
22164
- stepId: claim.stepId, messageId: claim.messageId, attempt: claim.attempt, workerId: claim.workerId,
22165
- });
22166
22174
  async function ownedClaim(claim: SupaCloudWorkflowClaim) {
22167
22175
  if (claim.workflowName !== "review.verify-attachment" || claim.workflowVersion !== "1"
22168
22176
  || claim.stepKey !== "verify" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {
@@ -22178,6 +22186,9 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22178
22186
  ));
22179
22187
  if (!Array.isArray(bound) || bound.length !== 1) throw new Error("Attachment workflow ownership mismatch");
22180
22188
  }
22189
+ const workflowTransport = createSupaCloudWorkflowTransport({
22190
+ workflows, workerId, visibilityTimeoutSeconds: 120, retryDelaySeconds: 5, acceptClaim: ownedClaim,
22191
+ });
22181
22192
  const worker = createWorker({
22182
22193
  modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,
22183
22194
  pollIntervalMs: options.pollIntervalMs ?? 1000,
@@ -22186,11 +22197,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22186
22197
  async claim(signal): Promise<SupaCloudWorkflowClaim | null> {
22187
22198
  if (halted || signal.aborted) return null;
22188
22199
  try {
22189
- const claim = await workflows.claim({ workerId, visibilityTimeoutSeconds: 120 });
22190
- if (signal.aborted || halted) return null;
22191
- if (!claim || claim.status !== "claimed") return null;
22192
- await ownedClaim(claim);
22193
- return claim;
22200
+ return await workflowTransport.claim(signal);
22194
22201
  } catch {
22195
22202
  if (!signal.aborted) fatal();
22196
22203
  throw new Error("Attachment workflow claim failed");
@@ -22198,9 +22205,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22198
22205
  },
22199
22206
  async ack(claim, output) {
22200
22207
  try {
22201
- if (!output || typeof output !== "object" || Array.isArray(output)) throw new Error("Invalid Job output");
22202
- // The compiled Job validates output; Workflow SDK validates the receipt.
22203
- await workflows.complete({ ...attempt(claim), runOutput: { ...output } });
22208
+ await workflowTransport.ack(claim, output);
22204
22209
  } catch {
22205
22210
  fatal();
22206
22211
  throw new Error("Attachment workflow completion is unconfirmed");
@@ -22208,9 +22213,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
22208
22213
  },
22209
22214
  async fail(claim) {
22210
22215
  try {
22211
- const request: Parameters<typeof workflows.fail>[0] = { ...attempt(claim), errorMessage: "Attachment verification failed" };
22212
- if (claim.attempt < claim.maxAttempts) await workflows.retry({ ...request, delaySeconds: 5 });
22213
- else await workflows.fail(request);
22216
+ await workflowTransport.fail(claim, "Attachment verification failed");
22214
22217
  } catch {
22215
22218
  fatal();
22216
22219
  throw new Error("Attachment workflow failure receipt is unconfirmed");
@@ -22394,9 +22397,11 @@ export class ReviewUploadsController {
22394
22397
  var STARTER_UPLOAD_SCHEMA = `-- Apply explicitly with the migration owner.
22395
22398
  ALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;
22396
22399
  GRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;
22400
+ DROP POLICY IF EXISTS starter_member_read ON public.starter_members;
22397
22401
  CREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated
22398
22402
  USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());
22399
22403
  GRANT SELECT ON public.starter_reviews TO authenticated;
22404
+ DROP POLICY IF EXISTS starter_review_read ON public.starter_reviews;
22400
22405
  CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated
22401
22406
  USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (
22402
22407
  SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id
@@ -22404,11 +22409,14 @@ CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authen
22404
22409
  ));
22405
22410
  INSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)
22406
22411
  VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);
22412
+ DROP POLICY IF EXISTS starter_attachment_read ON storage.objects;
22407
22413
  CREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated
22408
22414
  USING (bucket_id='review-attachments');
22415
+ DROP POLICY IF EXISTS starter_attachment_insert ON storage.objects;
22409
22416
  CREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated
22410
22417
  WITH CHECK (bucket_id='review-attachments');
22411
22418
  -- Restrictive fences also apply when the host already has permissive Storage policies.
22419
+ DROP POLICY IF EXISTS starter_attachment_owner_fence ON storage.objects;
22412
22420
  CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated
22413
22421
  USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (
22414
22422
  SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id
@@ -22421,8 +22429,10 @@ CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE F
22421
22429
  WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')
22422
22430
  AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve
22423
22431
  )));
22432
+ DROP POLICY IF EXISTS starter_attachment_update_fence ON storage.objects;
22424
22433
  CREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated
22425
22434
  USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');
22435
+ DROP POLICY IF EXISTS starter_attachment_delete_fence ON storage.objects;
22426
22436
  CREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated
22427
22437
  USING (bucket_id<>'review-attachments');
22428
22438
  `;
@@ -22570,6 +22580,11 @@ function renderStarterRuntimeRolesSchema(httpRole, workerRole) {
22570
22580
  CREATE ROLE ${httpRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
22571
22581
  CREATE ROLE ${workerRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
22572
22582
  GRANT USAGE ON SCHEMA public TO ${httpRole}, ${workerRole};
22583
+ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
22584
+ ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
22585
+ ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
22586
+ ALTER TABLE public.starter_attachments ENABLE ROW LEVEL SECURITY;
22587
+ ALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;
22573
22588
  GRANT SELECT ON public.starter_application, public.starter_members,
22574
22589
  public.starter_reviews, public.starter_attachments TO ${httpRole}, ${workerRole};
22575
22590
 
@@ -22583,34 +22598,49 @@ GRANT UPDATE(id) ON public.starter_reviews TO ${workerRole};
22583
22598
  GRANT INSERT ON public.starter_attachments TO ${httpRole};
22584
22599
  GRANT SELECT,INSERT ON public.starter_attachment_results TO ${workerRole};
22585
22600
 
22601
+ DROP POLICY IF EXISTS starter_backend_binding_read ON public.starter_application;
22586
22602
  CREATE POLICY starter_backend_binding_read ON public.starter_application FOR SELECT
22587
22603
  TO ${httpRole}, ${workerRole} USING (true);
22604
+ DROP POLICY IF EXISTS starter_backend_binding_lock ON public.starter_application;
22588
22605
  CREATE POLICY starter_backend_binding_lock ON public.starter_application FOR UPDATE
22589
22606
  TO ${httpRole}, ${workerRole} USING (true);
22607
+ DROP POLICY IF EXISTS starter_backend_binding_immutable ON public.starter_application;
22590
22608
  CREATE POLICY starter_backend_binding_immutable ON public.starter_application AS RESTRICTIVE FOR UPDATE
22591
22609
  TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
22610
+ DROP POLICY IF EXISTS starter_backend_member_read ON public.starter_members;
22592
22611
  CREATE POLICY starter_backend_member_read ON public.starter_members FOR SELECT
22593
22612
  TO ${httpRole}, ${workerRole} USING (true);
22613
+ DROP POLICY IF EXISTS starter_backend_member_lock ON public.starter_members;
22594
22614
  CREATE POLICY starter_backend_member_lock ON public.starter_members FOR UPDATE
22595
22615
  TO ${httpRole}, ${workerRole} USING (true);
22616
+ DROP POLICY IF EXISTS starter_backend_member_immutable ON public.starter_members;
22596
22617
  CREATE POLICY starter_backend_member_immutable ON public.starter_members AS RESTRICTIVE FOR UPDATE
22597
22618
  TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
22619
+ DROP POLICY IF EXISTS starter_backend_review_read ON public.starter_reviews;
22598
22620
  CREATE POLICY starter_backend_review_read ON public.starter_reviews FOR SELECT
22599
22621
  TO ${httpRole}, ${workerRole} USING (true);
22622
+ DROP POLICY IF EXISTS starter_backend_review_update ON public.starter_reviews;
22600
22623
  CREATE POLICY starter_backend_review_update ON public.starter_reviews FOR UPDATE
22601
22624
  TO ${httpRole}, ${workerRole} USING (true);
22625
+ DROP POLICY IF EXISTS starter_worker_review_immutable ON public.starter_reviews;
22602
22626
  CREATE POLICY starter_worker_review_immutable ON public.starter_reviews AS RESTRICTIVE FOR UPDATE
22603
22627
  TO ${workerRole} USING (true) WITH CHECK (false);
22628
+ DROP POLICY IF EXISTS starter_backend_attachment_read ON public.starter_attachments;
22604
22629
  CREATE POLICY starter_backend_attachment_read ON public.starter_attachments FOR SELECT
22605
22630
  TO ${httpRole}, ${workerRole} USING (true);
22631
+ DROP POLICY IF EXISTS starter_backend_attachment_lock ON public.starter_attachments;
22606
22632
  CREATE POLICY starter_backend_attachment_lock ON public.starter_attachments FOR UPDATE
22607
22633
  TO ${httpRole}, ${workerRole} USING (true);
22634
+ DROP POLICY IF EXISTS starter_backend_attachment_immutable ON public.starter_attachments;
22608
22635
  CREATE POLICY starter_backend_attachment_immutable ON public.starter_attachments AS RESTRICTIVE FOR UPDATE
22609
22636
  TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
22637
+ DROP POLICY IF EXISTS starter_http_attachment_insert ON public.starter_attachments;
22610
22638
  CREATE POLICY starter_http_attachment_insert ON public.starter_attachments FOR INSERT
22611
22639
  TO ${httpRole} WITH CHECK (true);
22640
+ DROP POLICY IF EXISTS starter_worker_result_read ON public.starter_attachment_results;
22612
22641
  CREATE POLICY starter_worker_result_read ON public.starter_attachment_results FOR SELECT
22613
22642
  TO ${workerRole} USING (true);
22643
+ DROP POLICY IF EXISTS starter_worker_result_insert ON public.starter_attachment_results;
22614
22644
  CREATE POLICY starter_worker_result_insert ON public.starter_attachment_results FOR INSERT
22615
22645
  TO ${workerRole} WITH CHECK (true);
22616
22646
 
@@ -23001,6 +23031,80 @@ export const attachmentResults = pgTable.withRLS("starter_attachment_results", {
23001
23031
  export type ReviewRow = typeof reviews.$inferSelect;
23002
23032
  export type NewReview = typeof reviews.$inferInsert;
23003
23033
  `;
23034
+ var STARTER_DATABASE_MODULES = `import { defineDatabaseModule } from "@supacloud/db";
23035
+ import { getTableConfig } from "drizzle-orm/pg-core";
23036
+ import { application, reviews, members, attachments, attachmentResults } from "./schema";
23037
+
23038
+ const backendPolicies = [
23039
+ ["starter_backend_binding_read", "public.starter_application", "select", ["starter_review_http", "starter_review_worker"]],
23040
+ ["starter_backend_binding_lock", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
23041
+ ["starter_backend_binding_immutable", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
23042
+ ["starter_backend_member_read", "public.starter_members", "select", ["starter_review_http", "starter_review_worker"]],
23043
+ ["starter_backend_member_lock", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
23044
+ ["starter_backend_member_immutable", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
23045
+ ["starter_backend_review_read", "public.starter_reviews", "select", ["starter_review_http", "starter_review_worker"]],
23046
+ ["starter_backend_review_update", "public.starter_reviews", "update", ["starter_review_http"]],
23047
+ ["starter_worker_review_immutable", "public.starter_reviews", "update", ["starter_review_worker"]],
23048
+ ["starter_backend_attachment_read", "public.starter_attachments", "select", ["starter_review_http", "starter_review_worker"]],
23049
+ ["starter_backend_attachment_lock", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
23050
+ ["starter_backend_attachment_immutable", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
23051
+ ["starter_http_attachment_insert", "public.starter_attachments", "insert", ["starter_review_http"]],
23052
+ ["starter_worker_result_read", "public.starter_attachment_results", "select", ["starter_review_worker"]],
23053
+ ["starter_worker_result_insert", "public.starter_attachment_results", "insert", ["starter_review_worker"]],
23054
+ ] as const;
23055
+
23056
+ // The module is the single live-Catalog ownership contract for starter tables.
23057
+ export default defineDatabaseModule({
23058
+ name: "starter",
23059
+ tables: [application, reviews, members, attachments, attachmentResults].map(table => {
23060
+ const config = getTableConfig(table);
23061
+ return (config.schema ?? "public") + "." + config.name;
23062
+ }),
23063
+ policies: [
23064
+ ...backendPolicies.map(([name, table, operation, roles]) => ({
23065
+ name, table, operation, roles: [...roles], source: "migrations/004-review-runtime-roles.sql",
23066
+ tests: ["tests/database-governance.sql"],
23067
+ })),
23068
+ {
23069
+ name: "starter_member_read", table: "public.starter_members", operation: "select",
23070
+ roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
23071
+ tests: ["tests/database-governance.sql"],
23072
+ },
23073
+ {
23074
+ name: "starter_review_read", table: "public.starter_reviews", operation: "select",
23075
+ roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
23076
+ tests: ["tests/database-governance.sql"],
23077
+ },
23078
+ ],
23079
+ });
23080
+ `;
23081
+ var STARTER_DATABASE_TEST = `-- Read-only catalog assertions; behavioral authorization tests remain separate.
23082
+ BEGIN READ ONLY;
23083
+ DO $test$
23084
+ DECLARE
23085
+ owned text[] := ARRAY['starter_application','starter_reviews','starter_members',
23086
+ 'starter_attachments','starter_attachment_results'];
23087
+ BEGIN
23088
+ IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
23089
+ WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN
23090
+ RAISE EXCEPTION 'All starter tables must exist with RLS enabled';
23091
+ END IF;
23092
+ IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid
23093
+ JOIN pg_namespace n ON n.oid=c.relnamespace
23094
+ WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN
23095
+ RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';
23096
+ END IF;
23097
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')
23098
+ AND (rolsuper OR rolbypassrls)) THEN
23099
+ RAISE EXCEPTION 'Starter roles must not bypass row security';
23100
+ END IF;
23101
+ IF (SELECT count(*) FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')) <> 2 THEN
23102
+ RAISE EXCEPTION 'Starter runtime roles are missing';
23103
+ END IF;
23104
+ END
23105
+ $test$;
23106
+ ROLLBACK;
23107
+ `;
23004
23108
  var STARTER_DATABASE_GUIDE = `
23005
23109
  ## Database Source Workflow
23006
23110
 
@@ -23012,13 +23116,14 @@ Opaque JSON still needs runtime decoding.
23012
23116
  After installing dependencies, run \`bun run db:generate\` once and commit db/contracts/.
23013
23117
  The default \`bun run check\` and build fail on stale database contracts before any
23014
23118
  compiler regeneration. After an intentional source change, run db:generate again,
23015
- review the diff, then run check. Ordinary checks are offline.
23016
- The compiler also runs this source-contract gate on compile/check/dev through
23017
- databaseGovernance.mode = "check", including incremental cache hits. Missing
23018
- adoption files or stale contracts fail closed; no contracts are silently regenerated.
23019
- This is not SQL lint or live Catalog reconciliation. Before promotion, declare
23020
- the real database modules, their SQL and tests, then explicitly use mode "enforce"
23021
- with a separately selected read-only local/test database connection.
23119
+ review the diff, then run check.
23120
+ The command starter registers databaseGovernance.moduleFile = "db/modules.ts" and
23121
+ keeps the default \`databaseGovernance.mode = "check"\`: compile/check/build run
23122
+ the offline source-contract gate without a database. Use \`bun run dev:integration\`
23123
+ with the CLI's explicit loopback database URL, or set databaseGovernance.mode to
23124
+ "enforce" with an explicitly supplied \`DATABASE_URL\`, to run SQL lint and live
23125
+ Catalog reconciliation. Missing connections, stale source contracts and Catalog
23126
+ drift fail closed; no contracts or migrations are silently regenerated.
23022
23127
 
23023
23128
  - Maintain one schema-qualified function with explicit grants per db/functions/*.sql.
23024
23129
  - Preserve migrations/ and its existing executor/ledger. Add forward migrations only.
@@ -23042,6 +23147,8 @@ function starterDatabaseFiles() {
23042
23147
  "database.sources.json": JSON.stringify(STARTER_DATABASE_CONFIG, null, 2) + `
23043
23148
  `,
23044
23149
  "db/schema.ts": STARTER_DATABASE_SCHEMA,
23150
+ "db/modules.ts": STARTER_DATABASE_MODULES,
23151
+ "tests/database-governance.sql": STARTER_DATABASE_TEST,
23045
23152
  "db/functions/README.md": `Maintain one schema-qualified function and its explicit EXECUTE ACL per SQL file. No RPC functions are needed by the initial transactional starter.
23046
23153
  `,
23047
23154
  "drizzle.config.ts": `import { defineConfig } from "drizzle-kit";
@@ -23076,7 +23183,7 @@ function appStarterFiles(name) {
23076
23183
  version: "0.0.0",
23077
23184
  private: true,
23078
23185
  type: "module",
23079
- engines: { bun: ">=1.4.2" },
23186
+ engines: { bun: ">=1.4.3" },
23080
23187
  scripts: {
23081
23188
  compile: "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js compile",
23082
23189
  "check:generated": "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js check",
@@ -23112,7 +23219,7 @@ function appStarterFiles(name) {
23112
23219
  devDependencies: {
23113
23220
  "@supacloud/compiler": `^${compilerMetadata2.version}`,
23114
23221
  "drizzle-kit": dbMetadata.devDependencies["drizzle-kit"],
23115
- "@types/bun": "^1.4.2",
23222
+ "@types/bun": "^1.4.3",
23116
23223
  typescript: "^7.0.2"
23117
23224
  }
23118
23225
  }),
@@ -23134,7 +23241,7 @@ function appStarterFiles(name) {
23134
23241
  noEmit: true,
23135
23242
  types: ["bun"]
23136
23243
  },
23137
- include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
23244
+ include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/modules.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
23138
23245
  }),
23139
23246
  "bunfig.toml": `env = false
23140
23247
  `,
@@ -23168,7 +23275,7 @@ export default defineSupacloudConfig({
23168
23275
  outDir: "generated",
23169
23276
  strict: true,
23170
23277
  moduleBoundaryPreset: "modular-monolith",
23171
- databaseGovernance: { mode: "check" },
23278
+ databaseGovernance: { mode: "check", moduleFile: "db/modules.ts", databaseUrlEnv: "DATABASE_URL" },
23172
23279
  effect: {
23173
23280
  requireRouteEffects: true,
23174
23281
  requireErrorMappings: true,
@@ -23182,7 +23289,6 @@ export default defineSupacloudConfig({
23182
23289
  typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
23183
23290
  disallowControllerDirectDb: true,
23184
23291
  detectOrphanModules: true,
23185
- graphql: { schema: "graphql/schema.graphql" },
23186
23292
  commandCapabilities: { permission: true, transaction: true, idempotency: true, audit: true },
23187
23293
  delivery: {
23188
23294
  version: 1,
@@ -23196,50 +23302,6 @@ export default defineSupacloudConfig({
23196
23302
  },
23197
23303
  },
23198
23304
  });
23199
- `,
23200
- "graphql/schema.graphql": `# Offline query-contract example, not a deployed database schema.
23201
- # SYNTHETIC TEST FIXTURE ONLY. Database First is the sole server-schema model.
23202
- # Do not extend this fixture as a server schema; change database declarations and export.
23203
- # Replace using a snapshot exported with the intended caller role before integration.
23204
- type Query {
23205
- reviewCollection(first: Int): ReviewConnection!
23206
- }
23207
- type ReviewConnection {
23208
- edges: [ReviewEdge!]!
23209
- }
23210
- type ReviewEdge {
23211
- node: Review!
23212
- }
23213
- type Review {
23214
- id: ID!
23215
- state: String!
23216
- version: Int!
23217
- }
23218
- `,
23219
- "src/review/reviews.graphql": `query ReviewList($first: Int = 20) {
23220
- reviewCollection(first: $first) {
23221
- edges { node { id state version } }
23222
- }
23223
- }
23224
- `,
23225
- "tests/graphql.test.ts": `import { expect, test } from "bun:test";
23226
- import { createGraphqlClient, type ReviewListQuery } from "../generated/graphql";
23227
-
23228
- test("generated query client preserves its read contract without a live database", async () => {
23229
- const data: ReviewListQuery = {
23230
- reviewCollection: { edges: [{ node: { id: "demo", state: "draft", version: 1 } }] },
23231
- };
23232
- const queries = createGraphqlClient({
23233
- url: "https://project.example.test",
23234
- getAccessToken: async () => "synthetic-user-token",
23235
- fetch: async (_url, request) => {
23236
- expect(new Headers(request?.headers).get("Authorization")).toBe("Bearer synthetic-user-token");
23237
- expect(JSON.parse(String(request?.body)).variables).toEqual({ first: 10 });
23238
- return Response.json({ data });
23239
- },
23240
- });
23241
- expect(await queries.ReviewList({ first: 10 })).toEqual(data);
23242
- });
23243
23305
  `,
23244
23306
  "REACTIVE.md": STARTER_REACTIVE_GUIDE,
23245
23307
  "AGENTS.md": STARTER_REACTIVE_AGENTS + STARTER_DATABASE_GUIDE,
@@ -23249,11 +23311,30 @@ test("generated query client preserves its read contract without a live database
23249
23311
  "scripts/dev.ts": `import { watchProject, compileOptionsFromConfig, loadSupacloudConfig } from "@supacloud/compiler";
23250
23312
 
23251
23313
  if (process.env["APP_ENV"] !== "development") throw new Error("The demo server is development-only");
23314
+ const profile = process.env["SUPACLOUD_DEV_PROFILE"] === "integration" ? "integration" : "fast";
23315
+ if (profile === "integration") {
23316
+ const databaseUrl = process.env["SUPACLOUD_DEV_DATABASE_URL"];
23317
+ if (!databaseUrl) throw new Error("Integration requires an explicit loopback PostgreSQL database");
23318
+ const url = new URL(databaseUrl);
23319
+ if (!["postgres:", "postgresql:"].includes(url.protocol)
23320
+ || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
23321
+ || url.pathname.length <= 1 || url.search || url.hash
23322
+ || process.env["DATABASE_SOCKET_PATH"]
23323
+ || (process.env["DATABASE_URL"] && process.env["DATABASE_URL"] !== databaseUrl)) {
23324
+ throw new Error("Integration requires an explicit loopback PostgreSQL database without conflicting settings");
23325
+ }
23326
+ process.env["DATABASE_URL"] = databaseUrl;
23327
+ }
23328
+ const configured = compileOptionsFromConfig(await loadSupacloudConfig());
23329
+ const governanceMode = profile === "integration" ? "enforce" as const : "check" as const;
23330
+ const options = configured.databaseGovernance
23331
+ ? { ...configured, databaseGovernance: { ...configured.databaseGovernance, mode: governanceMode } }
23332
+ : configured;
23252
23333
  let server: ReturnType<typeof Bun.spawn> | undefined;
23253
23334
  let restarts = Promise.resolve();
23254
23335
  let closing = false;
23255
23336
  const watcher = watchProject({
23256
- ...compileOptionsFromConfig(await loadSupacloudConfig()),
23337
+ ...options,
23257
23338
  writeOnError: false,
23258
23339
  onEvent: (event) => {
23259
23340
  if (event.type === "compile-error") console.error(event.diagnostics);
@@ -23603,7 +23684,6 @@ test("the production composition root rejects missing governance adapters", () =
23603
23684
  \`\`\`sh
23604
23685
  bun install
23605
23686
  bun run db:generate
23606
- bun run check
23607
23687
  bun run dev
23608
23688
  \`\`\`
23609
23689
 
@@ -23639,55 +23719,23 @@ origin. The authenticated transport rejects missing credentials and redirects
23639
23719
  and does not replay a write after 401. Its underlying fetch must also be
23640
23720
  single-attempt.
23641
23721
 
23642
- Reuse the same session resolver for generated/graphql.ts when adopting queries.
23643
23722
  The local memory demo is not this authenticated production integration.
23644
23723
  Never use supacloud.commands or supacloud.workflows from a browser action:
23645
23724
  those RPCs require service-role credentials. Browser business commands use
23646
23725
  application endpoints, optionally composed through @supacloud/js/contracts.
23647
23726
  Management API tokens and service-role keys remain on trusted servers.
23648
23727
 
23649
- ## Typed Queries
23650
-
23651
- This starter uses Database First as its only GraphQL server-schema model.
23652
- Drizzle and reviewed SQL define the database; pg_graphql reflects the migrated
23653
- database under the caller's role. Do not add GraphQL resolver/decorator classes
23654
- or independently authored server SDL. Business writes continue through Commands.
23728
+ ## Reads
23655
23729
 
23656
- GraphQL is preconfigured as the recommended read path. Edit src/review/reviews.graphql;
23657
- compile, check and dev validate it against graphql/schema.graphql and generate
23658
- generated/graphql.ts. Import createGraphqlClient from that file and call
23659
- queries.ReviewList({ first: 20 }). Its variables and selected results are typed.
23660
- Pass a public project key and a getAccessToken callback that reads the current
23661
- user session. Never pass a service-role key to browser code.
23730
+ Ordinary reads use PostgREST through the existing user-scoped
23731
+ @supabase/supabase-js client: client.from("starter_reviews").select("id,state,version").
23732
+ Use the caller's session, RLS and grants; never introduce a second session store.
23733
+ Business writes continue through the governed Command/RPC boundary.
23662
23734
 
23663
- The included schema is a synthetic offline test fixture, not a deployed database.
23664
- Do not hand-edit graphql/schema.graphql to add application fields. Change database
23665
- declarations, apply migrations and export the actual schema. Replace the fixture
23666
- with a snapshot exported under the intended caller role before real integration:
23667
-
23668
- \`\`\`sh
23669
- bun node_modules/@supacloud/compiler/dist/cli.js graphql-schema \\
23670
- --url https://your-project.example \\
23671
- --key-env SUPACLOUD_PUBLISHABLE_KEY --token-env APP_USER_ACCESS_TOKEN
23672
- \`\`\`
23673
-
23674
- After migrations or grants change, and before promotion, run the same command
23675
- with --check --json to detect drift without writing. If it fails, explicitly
23676
- re-export, review the snapshot diff, compile and run application typechecks and
23677
- role/RLS tests. Normal compile/check/dev remain offline; they cannot prove that
23678
- a snapshot came from a database or is current. Keep the exported snapshot and
23679
- generated types in version control.
23680
-
23681
- The flags name environment variables, not credential values. Export requires
23682
- GraphQL and introspection already enabled on the selected development project.
23683
- The compiler does not enable extensions or alter database permissions.
23684
- Production introspection may remain disabled. Queries do not pass through
23685
- application Command/AOP; RLS and grants remain mandatory. Business writes stay
23686
- in Commands. Existing projects without GraphQL configuration remain unaffected.
23687
- Enabled contracts always reject invalid queries, including with strict: false.
23688
- To opt this starter out entirely, set graphql: false and remove its example
23689
- query test and unused generated client; do not replace checked queries with
23690
- untyped calls to bypass validation.
23735
+ GraphQL is not generated or enabled by this starter. For a specific Query Contract,
23736
+ explicitly configure graphql.schema with a role-scoped database export and add
23737
+ checked query documents. Drizzle and append-only migrations remain the database
23738
+ authority; the export is a protocol snapshot, not a second schema source.
23691
23739
 
23692
23740
  ## Environments
23693
23741
 
@@ -23903,8 +23951,8 @@ bunx supacloud-compiler fix ./fix.json --dry-run
23903
23951
  \`\`\`
23904
23952
 
23905
23953
  Context packs include directional dependencies, relevant aspect files, diagnostics
23906
- and static execution plans. Ordinary context can include declared source expressions;
23907
- inspect it before sharing. The optional --events example expects a trusted host
23954
+ and static execution plans, without provider values or source expressions.
23955
+ The optional --events example expects a trusted host
23908
23956
  to capture approved onExecution metadata in { "version": 1, "events": [...] }.
23909
23957
  It selects one opaque request ID, omits payloads and source expressions, and applies
23910
23958
  input/output size limits. It does not read arbitrary logs, prove deployment versions,
@@ -24861,6 +24909,13 @@ async function runContext(args) {
24861
24909
  fix: "supacloud app fix --fix <fix.json>",
24862
24910
  graph: "supacloud app graph --format json",
24863
24911
  explain: "supacloud app explain --target <name>"
24912
+ },
24913
+ defaults: {
24914
+ readTransport: "postgrest-supabase-js",
24915
+ writeTransport: "command-rpc",
24916
+ writeRetry: "disabled",
24917
+ externalOutcome: "receipt-reconcile",
24918
+ databaseSource: "drizzle-append-only"
24864
24919
  }
24865
24920
  };
24866
24921
  if (args.format === "json")
@@ -29210,7 +29265,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
29210
29265
  // package.json
29211
29266
  var package_default = {
29212
29267
  name: "@supacloud/cli",
29213
- version: "0.95.0",
29268
+ version: "0.96.0",
29214
29269
  description: "Project-scoped CLI for SupaCloud users",
29215
29270
  type: "module",
29216
29271
  main: "./dist/index.js",
@@ -29262,19 +29317,19 @@ var package_default = {
29262
29317
  directory: "packages/cli"
29263
29318
  },
29264
29319
  dependencies: {
29265
- "@supacloud/compiler": "^0.45.0",
29266
- "@supacloud/delivery": "0.16.0",
29267
- "@supacloud/db": "0.16.2",
29320
+ "@supacloud/compiler": "^0.46.0",
29321
+ "@supacloud/delivery": "0.17.0",
29322
+ "@supacloud/db": "0.17.0",
29268
29323
  typebox: "1.3.36"
29269
29324
  },
29270
29325
  overrides: {
29271
29326
  "@supacloud/contracts": "0.8.1",
29272
- "@supacloud/delivery": "0.16.0",
29327
+ "@supacloud/delivery": "0.17.0",
29273
29328
  "@graphql-tools/utils": "^12.0.3"
29274
29329
  },
29275
29330
  devDependencies: {
29276
29331
  "@supacloud/elysia": "^0.24.1",
29277
- "@types/bun": "^1.4.2",
29332
+ "@types/bun": "^1.4.3",
29278
29333
  effect: "4.0.2",
29279
29334
  elysia: "2.0.0-beta.21",
29280
29335
  typescript: "^7.0.2"
@@ -1,2 +1,2 @@
1
- export declare const STARTER_ATTACHMENT_WORKER = "import { createWorker, type CompiledModule } from \"@supacloud/elysia\";\nimport type { SupaCloudWorkflowClaim, SupaCloudWorkflowsClient } from \"@supacloud/js\";\nimport { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from \"./review-attachments\";\n\nexport interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {\n modules: readonly CompiledModule[];\n workflows: Pick<SupaCloudWorkflowsClient, \"claim\" | \"complete\" | \"retry\" | \"fail\">;\n workerId: string;\n queueOwnership: \"exclusive-review-attachments\";\n pollIntervalMs?: number;\n}\n\nexport async function createReviewAttachmentWorker(options: ReviewAttachmentWorkerOptions) {\n if (options.queueOwnership !== \"exclusive-review-attachments\") {\n throw new Error(\"Attachment worker requires an exclusively owned Workflow queue\");\n }\n const { store } = await createReviewAttachmentAdapters(options);\n const { workflows, workerId } = options;\n const failure = Promise.withResolvers<never>();\n // The executable attaches its fatal listener after this async factory returns.\n void failure.promise.catch(() => {});\n let halted: boolean = false;\n const fatal = () => {\n halted = true;\n failure.reject(new Error(\"Attachment worker requires recovery\"));\n };\n const attempt = (claim: SupaCloudWorkflowClaim) => ({\n stepId: claim.stepId, messageId: claim.messageId, attempt: claim.attempt, workerId: claim.workerId,\n });\n async function ownedClaim(claim: SupaCloudWorkflowClaim) {\n if (claim.workflowName !== \"review.verify-attachment\" || claim.workflowVersion !== \"1\"\n || claim.stepKey !== \"verify\" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {\n throw new Error(\"Unsupported attachment workflow\");\n }\n const { reviewId, artifactId } = claim.input;\n if (typeof reviewId !== \"string\" || typeof artifactId !== \"string\") {\n throw new Error(\"Invalid attachment workflow input\");\n }\n const bound = await options.database.transaction(tx => tx.query(\n \"SELECT run_id FROM public.starter_attachments WHERE review_id=$1 AND artifact_id=$2 AND run_id=$3\",\n [reviewId, artifactId, claim.runId],\n ));\n if (!Array.isArray(bound) || bound.length !== 1) throw new Error(\"Attachment workflow ownership mismatch\");\n }\n const worker = createWorker({\n modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,\n pollIntervalMs: options.pollIntervalMs ?? 1000,\n onError: fatal,\n transport: {\n async claim(signal): Promise<SupaCloudWorkflowClaim | null> {\n if (halted || signal.aborted) return null;\n try {\n const claim = await workflows.claim({ workerId, visibilityTimeoutSeconds: 120 });\n if (signal.aborted || halted) return null;\n if (!claim || claim.status !== \"claimed\") return null;\n await ownedClaim(claim);\n return claim;\n } catch {\n if (!signal.aborted) fatal();\n throw new Error(\"Attachment workflow claim failed\");\n }\n },\n async ack(claim, output) {\n try {\n if (!output || typeof output !== \"object\" || Array.isArray(output)) throw new Error(\"Invalid Job output\");\n // The compiled Job validates output; Workflow SDK validates the receipt.\n await workflows.complete({ ...attempt(claim), runOutput: { ...output } });\n } catch {\n fatal();\n throw new Error(\"Attachment workflow completion is unconfirmed\");\n }\n },\n async fail(claim) {\n try {\n const request: Parameters<typeof workflows.fail>[0] = { ...attempt(claim), errorMessage: \"Attachment verification failed\" };\n if (claim.attempt < claim.maxAttempts) await workflows.retry({ ...request, delaySeconds: 5 });\n else await workflows.fail(request);\n } catch {\n fatal();\n throw new Error(\"Attachment workflow failure receipt is unconfirmed\");\n }\n },\n },\n mapClaim: (claim: SupaCloudWorkflowClaim) => ({\n id: claim.stepId, jobName: claim.workflowName, input: claim.input, attempt: claim.attempt,\n requestContext: { workerId, jobId: claim.stepId, requestId: claim.runId },\n }),\n });\n if (worker.jobNames.length !== 1 || worker.jobNames[0] !== \"review.verify-attachment\") {\n throw new Error(\"Attachment worker requires its compiled reference Job\");\n }\n let closing: Promise<void> | undefined;\n return {\n failure: failure.promise,\n async start() {\n if (halted) throw new Error(\"Attachment worker is not startable\");\n await worker.start();\n },\n close() {\n halted = true;\n return closing ??= worker.stop();\n },\n };\n}\n";
1
+ export declare const STARTER_ATTACHMENT_WORKER = "import { createWorker, type CompiledModule } from \"@supacloud/elysia\";\nimport {\n createSupaCloudWorkflowTransport,\n type SupaCloudWorkflowClaim,\n type SupaCloudWorkflowsClient,\n} from \"@supacloud/js\";\nimport { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from \"./review-attachments\";\n\nexport interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {\n modules: readonly CompiledModule[];\n workflows: Pick<SupaCloudWorkflowsClient, \"claim\" | \"complete\" | \"retry\" | \"fail\">;\n workerId: string;\n queueOwnership: \"exclusive-review-attachments\";\n pollIntervalMs?: number;\n}\n\nexport async function createReviewAttachmentWorker(options: ReviewAttachmentWorkerOptions) {\n if (options.queueOwnership !== \"exclusive-review-attachments\") {\n throw new Error(\"Attachment worker requires an exclusively owned Workflow queue\");\n }\n const { store } = await createReviewAttachmentAdapters(options);\n const { workflows, workerId } = options;\n const failure = Promise.withResolvers<never>();\n // The executable attaches its fatal listener after this async factory returns.\n void failure.promise.catch(() => {});\n let halted: boolean = false;\n const fatal = () => {\n halted = true;\n failure.reject(new Error(\"Attachment worker requires recovery\"));\n };\n async function ownedClaim(claim: SupaCloudWorkflowClaim) {\n if (claim.workflowName !== \"review.verify-attachment\" || claim.workflowVersion !== \"1\"\n || claim.stepKey !== \"verify\" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {\n throw new Error(\"Unsupported attachment workflow\");\n }\n const { reviewId, artifactId } = claim.input;\n if (typeof reviewId !== \"string\" || typeof artifactId !== \"string\") {\n throw new Error(\"Invalid attachment workflow input\");\n }\n const bound = await options.database.transaction(tx => tx.query(\n \"SELECT run_id FROM public.starter_attachments WHERE review_id=$1 AND artifact_id=$2 AND run_id=$3\",\n [reviewId, artifactId, claim.runId],\n ));\n if (!Array.isArray(bound) || bound.length !== 1) throw new Error(\"Attachment workflow ownership mismatch\");\n }\n const workflowTransport = createSupaCloudWorkflowTransport({\n workflows, workerId, visibilityTimeoutSeconds: 120, retryDelaySeconds: 5, acceptClaim: ownedClaim,\n });\n const worker = createWorker({\n modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,\n pollIntervalMs: options.pollIntervalMs ?? 1000,\n onError: fatal,\n transport: {\n async claim(signal): Promise<SupaCloudWorkflowClaim | null> {\n if (halted || signal.aborted) return null;\n try {\n return await workflowTransport.claim(signal);\n } catch {\n if (!signal.aborted) fatal();\n throw new Error(\"Attachment workflow claim failed\");\n }\n },\n async ack(claim, output) {\n try {\n await workflowTransport.ack(claim, output);\n } catch {\n fatal();\n throw new Error(\"Attachment workflow completion is unconfirmed\");\n }\n },\n async fail(claim) {\n try {\n await workflowTransport.fail(claim, \"Attachment verification failed\");\n } catch {\n fatal();\n throw new Error(\"Attachment workflow failure receipt is unconfirmed\");\n }\n },\n },\n mapClaim: (claim: SupaCloudWorkflowClaim) => ({\n id: claim.stepId, jobName: claim.workflowName, input: claim.input, attempt: claim.attempt,\n requestContext: { workerId, jobId: claim.stepId, requestId: claim.runId },\n }),\n });\n if (worker.jobNames.length !== 1 || worker.jobNames[0] !== \"review.verify-attachment\") {\n throw new Error(\"Attachment worker requires its compiled reference Job\");\n }\n let closing: Promise<void> | undefined;\n return {\n failure: failure.promise,\n async start() {\n if (halted) throw new Error(\"Attachment worker is not startable\");\n await worker.start();\n },\n close() {\n halted = true;\n return closing ??= worker.stop();\n },\n };\n}\n";
2
2
  export declare const STARTER_ATTACHMENT_DELIVERY_WORKER = "import { SQL } from \"bun\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { SupaCloudWorkflowsClient } from \"@supacloud/js\";\nimport { createBunCommandDatabase } from \"@supacloud/db/bun\";\nimport type { CompiledModule } from \"@supacloud/elysia\";\nimport { createReviewAttachmentWorker } from \"./host/review-attachment-worker\";\n\nfunction required(name: string): string {\n const value = process.env[name];\n if (!value?.trim()) throw new Error(\"Missing required worker setting: \" + name);\n return value;\n}\n\nfunction databaseConnection(): SQL.Options {\n const socket = process.env[\"DATABASE_SOCKET_PATH\"];\n if (socket && process.env[\"DATABASE_URL\"]) throw new Error(\"Conflicting database connection settings\");\n if (socket) {\n return { adapter: \"postgres\", path: socket, database: required(\"DATABASE_NAME\"), username: required(\"DATABASE_USER\") };\n }\n const url = new URL(required(\"DATABASE_URL\"));\n if (url.protocol !== \"postgres:\" && url.protocol !== \"postgresql:\") throw new Error(\"Invalid database URL\");\n return {\n adapter: \"postgres\", url: url.href, hostname: url.hostname.replace(/^\\[|\\]$/g, \"\"), port: Number(url.port || 5432),\n username: decodeURIComponent(url.username), password: decodeURIComponent(url.password),\n database: decodeURIComponent(url.pathname.slice(1)),\n };\n}\n\nexport async function createDeliveryWorker(modules: CompiledModule[], lifecycle: { signal: AbortSignal }) {\n lifecycle.signal.throwIfAborted();\n if (required(\"REVIEW_QUEUE_OWNERSHIP\") !== \"exclusive-review-attachments\") {\n throw new Error(\"Attachment worker requires an exclusively owned Workflow queue\");\n }\n const connection = databaseConnection();\n const settings = {\n projectId: required(\"SUPACLOUD_PROJECT_ID\"), tenantId: required(\"APP_TENANT_ID\"),\n workerId: required(\"REVIEW_WORKER_ID\"), url: required(\"SUPACLOUD_URL\"), key: required(\"SUPACLOUD_SERVICE_ROLE_KEY\"),\n };\n const service = createClient(settings.url, settings.key, {\n auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n });\n const pool = new SQL({ ...connection, max: 4, connectionTimeout: 5 });\n let closing: Promise<void> | undefined;\n const closePool = () => closing ??= pool.close({ timeout: 5 });\n try {\n const worker = await createReviewAttachmentWorker({\n database: createBunCommandDatabase(pool), service, projectId: settings.projectId,\n tenantId: settings.tenantId, modules, workerId: settings.workerId,\n workflows: new SupaCloudWorkflowsClient(service), queueOwnership: \"exclusive-review-attachments\",\n });\n lifecycle.signal.throwIfAborted();\n return {\n start: () => worker.start(), failure: worker.failure,\n async close() { try { await worker.close(); } finally { await closePool(); } },\n };\n } catch (error) {\n await closePool();\n const code = error !== null && typeof error === \"object\" && \"code\" in error\n && typeof error.code === \"string\" && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : \"UNKNOWN\";\n console.error(JSON.stringify({ event: \"review-worker-initialization-failed\", code }));\n throw new Error(\"Attachment worker initialization failed\");\n }\n}\n";
@@ -10,5 +10,7 @@ export declare const STARTER_DATABASE_CONFIG: {
10
10
  role: string;
11
11
  };
12
12
  export declare const STARTER_DATABASE_SCHEMA = "import { sql } from \"drizzle-orm\";\nimport { boolean, check, integer, jsonb, pgTable, primaryKey, text, uuid } from \"drizzle-orm/pg-core\";\n\n// Structure for the reviewed starter migrations. Grants/policies stay in those migrations.\nexport const application = pgTable.withRLS(\"starter_application\", {\n singleton: boolean().default(true).primaryKey(),\n projectId: text(\"project_id\").notNull(),\n tenantId: text(\"tenant_id\").notNull(),\n}, table => [\n check(\"starter_application_singleton_check\", sql`${table.singleton}`),\n check(\"starter_application_project_id_check\", sql`length(${table.projectId}) > 0`),\n check(\"starter_application_tenant_id_check\", sql`length(${table.tenantId}) > 0`),\n]);\n\nexport const reviews = pgTable.withRLS(\"starter_reviews\", {\n id: text().primaryKey(),\n ownerId: text(\"owner_id\").notNull(),\n state: text({ enum: [\"draft\", \"approved\"] }).notNull(),\n version: integer().notNull(),\n}, table => [\n check(\"starter_reviews_state_check\", sql`${table.state} IN ('draft', 'approved')`),\n check(\"starter_reviews_version_check\", sql`${table.version} > 0`),\n]);\n\nexport const members = pgTable.withRLS(\"starter_members\", {\n subject: text().primaryKey(),\n enabled: boolean().default(true).notNull(),\n canApprove: boolean(\"can_approve\").default(false).notNull(),\n storageSubject: uuid(\"storage_subject\").unique(),\n});\n\nexport const attachments = pgTable.withRLS(\"starter_attachments\", {\n reviewId: text(\"review_id\").primaryKey().references(() => reviews.id),\n artifactId: uuid(\"artifact_id\").notNull().unique(),\n ownerId: text(\"owner_id\").notNull(),\n runId: uuid(\"run_id\").notNull().unique(),\n objectPath: text(\"object_path\").notNull().unique(),\n});\n\nexport const attachmentResults = pgTable.withRLS(\"starter_attachment_results\", {\n reviewId: text(\"review_id\").notNull().references(() => reviews.id),\n version: integer().notNull(),\n artifactId: uuid(\"artifact_id\").notNull(),\n result: jsonb().$type<unknown>().notNull(),\n}, table => [\n primaryKey({ columns: [table.reviewId, table.version, table.artifactId] }),\n check(\"starter_attachment_results_version_check\", sql`${table.version} >= 2`),\n]);\n\nexport type ReviewRow = typeof reviews.$inferSelect;\nexport type NewReview = typeof reviews.$inferInsert;\n";
13
- export declare const STARTER_DATABASE_GUIDE = "\n## Database Source Workflow\n\nDrizzle owns table declarations and inferred row types in db/schema.ts.\nUse the existing @supacloud/db/drizzle-bun adapters for typed queries and governed\ntransactions; never replace permission, idempotency or audit checks with plain CRUD.\nOpaque JSON still needs runtime decoding.\n\nAfter installing dependencies, run `bun run db:generate` once and commit db/contracts/.\nThe default `bun run check` and build fail on stale database contracts before any\ncompiler regeneration. After an intentional source change, run db:generate again,\nreview the diff, then run check. Ordinary checks are offline.\nThe compiler also runs this source-contract gate on compile/check/dev through\ndatabaseGovernance.mode = \"check\", including incremental cache hits. Missing\nadoption files or stale contracts fail closed; no contracts are silently regenerated.\nThis is not SQL lint or live Catalog reconciliation. Before promotion, declare\nthe real database modules, their SQL and tests, then explicitly use mode \"enforce\"\nwith a separately selected read-only local/test database connection.\n\n- Maintain one schema-qualified function with explicit grants per db/functions/*.sql.\n- Preserve migrations/ and its existing executor/ledger. Add forward migrations only.\n- `bun run db:diff` generates Drizzle drafts under db/migration-candidates/, never\n into the deployment directory. The starter has existing SQL history, so establish\n and review a matching Drizzle baseline before promoting any draft; the first\n draft is not automatically an incremental production migration.\n- `bun run db:pull` requires an explicitly selected DATABASE_URL and writes only\n output/database-audit/drizzle-candidate/. Review candidates; never overwrite source blindly.\n- Full SQL dumps belong under the ignored output/database-audit/ directory. They are\n audit/recovery artifacts, never ordinary type, application or test inputs.\n- `bun run db:assess` explains adoption/findings without writing or connecting.\n- GraphQL/PostgREST exports remain legitimate protocol contracts. This check is not\n proof of deployed schema, effective role permissions or complete RLS equivalence.\n\nDo not introduce any/@ts-ignore to bypass a contract. Use Drizzle-inferred types,\npreserve bigint/numeric precision and decode unknown results at the boundary.\n";
13
+ export declare const STARTER_DATABASE_MODULES = "import { defineDatabaseModule } from \"@supacloud/db\";\nimport { getTableConfig } from \"drizzle-orm/pg-core\";\nimport { application, reviews, members, attachments, attachmentResults } from \"./schema\";\n\nconst backendPolicies = [\n [\"starter_backend_binding_read\", \"public.starter_application\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_binding_lock\", \"public.starter_application\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_binding_immutable\", \"public.starter_application\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_member_read\", \"public.starter_members\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_member_lock\", \"public.starter_members\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_member_immutable\", \"public.starter_members\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_review_read\", \"public.starter_reviews\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_review_update\", \"public.starter_reviews\", \"update\", [\"starter_review_http\"]],\n [\"starter_worker_review_immutable\", \"public.starter_reviews\", \"update\", [\"starter_review_worker\"]],\n [\"starter_backend_attachment_read\", \"public.starter_attachments\", \"select\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_attachment_lock\", \"public.starter_attachments\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_backend_attachment_immutable\", \"public.starter_attachments\", \"update\", [\"starter_review_http\", \"starter_review_worker\"]],\n [\"starter_http_attachment_insert\", \"public.starter_attachments\", \"insert\", [\"starter_review_http\"]],\n [\"starter_worker_result_read\", \"public.starter_attachment_results\", \"select\", [\"starter_review_worker\"]],\n [\"starter_worker_result_insert\", \"public.starter_attachment_results\", \"insert\", [\"starter_review_worker\"]],\n] as const;\n\n// The module is the single live-Catalog ownership contract for starter tables.\nexport default defineDatabaseModule({\n name: \"starter\",\n tables: [application, reviews, members, attachments, attachmentResults].map(table => {\n const config = getTableConfig(table);\n return (config.schema ?? \"public\") + \".\" + config.name;\n }),\n policies: [\n ...backendPolicies.map(([name, table, operation, roles]) => ({\n name, table, operation, roles: [...roles], source: \"migrations/004-review-runtime-roles.sql\",\n tests: [\"tests/database-governance.sql\"],\n })),\n {\n name: \"starter_member_read\", table: \"public.starter_members\", operation: \"select\",\n roles: [\"authenticated\"], source: \"migrations/003-review-uploads.sql\",\n tests: [\"tests/database-governance.sql\"],\n },\n {\n name: \"starter_review_read\", table: \"public.starter_reviews\", operation: \"select\",\n roles: [\"authenticated\"], source: \"migrations/003-review-uploads.sql\",\n tests: [\"tests/database-governance.sql\"],\n },\n ],\n});\n";
14
+ export declare const STARTER_DATABASE_TEST = "-- Read-only catalog assertions; behavioral authorization tests remain separate.\nBEGIN READ ONLY;\nDO $test$\nDECLARE\n owned text[] := ARRAY['starter_application','starter_reviews','starter_members',\n 'starter_attachments','starter_attachment_results'];\nBEGIN\n IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace\n WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN\n RAISE EXCEPTION 'All starter tables must exist with RLS enabled';\n END IF;\n IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid\n JOIN pg_namespace n ON n.oid=c.relnamespace\n WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN\n RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';\n END IF;\n IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')\n AND (rolsuper OR rolbypassrls)) THEN\n RAISE EXCEPTION 'Starter roles must not bypass row security';\n END IF;\n IF (SELECT count(*) FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')) <> 2 THEN\n RAISE EXCEPTION 'Starter runtime roles are missing';\n END IF;\nEND\n$test$;\nROLLBACK;\n";
15
+ export declare const STARTER_DATABASE_GUIDE = "\n## Database Source Workflow\n\nDrizzle owns table declarations and inferred row types in db/schema.ts.\nUse the existing @supacloud/db/drizzle-bun adapters for typed queries and governed\ntransactions; never replace permission, idempotency or audit checks with plain CRUD.\nOpaque JSON still needs runtime decoding.\n\nAfter installing dependencies, run `bun run db:generate` once and commit db/contracts/.\nThe default `bun run check` and build fail on stale database contracts before any\ncompiler regeneration. After an intentional source change, run db:generate again,\nreview the diff, then run check.\nThe command starter registers databaseGovernance.moduleFile = \"db/modules.ts\" and\nkeeps the default `databaseGovernance.mode = \"check\"`: compile/check/build run\nthe offline source-contract gate without a database. Use `bun run dev:integration`\nwith the CLI's explicit loopback database URL, or set databaseGovernance.mode to\n\"enforce\" with an explicitly supplied `DATABASE_URL`, to run SQL lint and live\nCatalog reconciliation. Missing connections, stale source contracts and Catalog\ndrift fail closed; no contracts or migrations are silently regenerated.\n\n- Maintain one schema-qualified function with explicit grants per db/functions/*.sql.\n- Preserve migrations/ and its existing executor/ledger. Add forward migrations only.\n- `bun run db:diff` generates Drizzle drafts under db/migration-candidates/, never\n into the deployment directory. The starter has existing SQL history, so establish\n and review a matching Drizzle baseline before promoting any draft; the first\n draft is not automatically an incremental production migration.\n- `bun run db:pull` requires an explicitly selected DATABASE_URL and writes only\n output/database-audit/drizzle-candidate/. Review candidates; never overwrite source blindly.\n- Full SQL dumps belong under the ignored output/database-audit/ directory. They are\n audit/recovery artifacts, never ordinary type, application or test inputs.\n- `bun run db:assess` explains adoption/findings without writing or connecting.\n- GraphQL/PostgREST exports remain legitimate protocol contracts. This check is not\n proof of deployed schema, effective role permissions or complete RLS equivalence.\n\nDo not introduce any/@ts-ignore to bypass a contract. Use Drizzle-inferred types,\npreserve bigint/numeric precision and decode unknown results at the boundary.\n";
14
16
  export declare function starterDatabaseFiles(): Record<string, string>;
@@ -1,3 +1,3 @@
1
1
  export declare const STARTER_UPLOAD_FEATURE = "import { Effect } from \"effect\";\nimport { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from \"@supacloud/app\";\nimport { ApplicationError } from \"@supacloud/elysia\";\nimport { UploadBody, UploadLocation, UploadParams, UploadResult } from \"./contracts\";\n\nexport interface ReviewUploadInput { reviewId: string; artifactId: string; expectedVersion: number }\nexport interface ReviewUploadLocation { artifactId: string; bucketId: string; objectPath: string }\nexport interface ReviewUploadResult { artifactId: string; runId: string; objectPath: string; sha256: string; bytes: number }\ntype PreviewUploadError =\n | { readonly _tag: \"AUTHENTICATION_REQUIRED\" }\n | { readonly _tag: \"UPLOAD_INPUT_INVALID\" }\n | { readonly _tag: \"UPLOAD_DENIED\" }\n | { readonly _tag: \"UPLOADS_UNAVAILABLE\" };\ntype RegisterUploadError =\n | PreviewUploadError\n | { readonly _tag: \"UPLOAD_CONTENT_INVALID\" }\n | { readonly _tag: \"COMMAND_IDEMPOTENCY_CONFLICT\" }\n | { readonly _tag: \"COMMAND_REJECTED\" }\n | { readonly _tag: \"UPLOAD_OUTCOME_UNKNOWN\" };\nexport interface ReviewUploadPort {\n previewUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadLocation>;\n registerUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadResult>;\n}\n@Injectable({ scope: \"request\" })\nexport class ReviewUploads {\n constructor(@Inject(DB_CLIENT) private readonly store: Partial<ReviewUploadPort>,\n @Inject(REQUEST_CONTEXT) private readonly context: unknown) {}\n\n preview(input: ReviewUploadInput): Promise<ReviewUploadLocation> {\n if (!this.store.previewUpload) throw new ApplicationError(\"Attachment uploads are not configured\", { status: 501, code: \"UPLOADS_UNAVAILABLE\" });\n return this.store.previewUpload(this.context, input);\n }\n register(input: ReviewUploadInput): Promise<ReviewUploadResult> {\n if (!this.store.registerUpload) throw new ApplicationError(\"Attachment uploads are not configured\", { status: 501, code: \"UPLOADS_UNAVAILABLE\" });\n return this.store.registerUpload(this.context, input);\n }\n}\n\n@Controller(\"/reviews\")\nexport class ReviewUploadsController {\n constructor(@Inject(ReviewUploads) private readonly uploads: ReviewUploads) {}\n\n @Post(\"/:id/attachment-upload\", {\n params: UploadParams,\n body: UploadBody,\n responses: { 200: UploadLocation },\n effect: {\n required: true,\n dependencies: [],\n errors: [\n { tag: \"AUTHENTICATION_REQUIRED\", status: 401, code: \"AUTHENTICATION_REQUIRED\" },\n { tag: \"UPLOAD_INPUT_INVALID\", status: 400, code: \"UPLOAD_INPUT_INVALID\" },\n { tag: \"UPLOAD_DENIED\", status: 403, code: \"UPLOAD_DENIED\" },\n { tag: \"UPLOADS_UNAVAILABLE\", status: 501, code: \"UPLOADS_UNAVAILABLE\" },\n ],\n retry: \"none\",\n },\n })\n preview(@Param(\"id\") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadLocation, PreviewUploadError, never> {\n return Effect.promise(() => this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));\n }\n @Post(\"/:id/attachment-registration\", {\n params: UploadParams,\n body: UploadBody,\n responses: { 200: UploadResult },\n effect: {\n required: true,\n dependencies: [],\n errors: [\n { tag: \"AUTHENTICATION_REQUIRED\", status: 401, code: \"AUTHENTICATION_REQUIRED\" },\n { tag: \"UPLOAD_INPUT_INVALID\", status: 400, code: \"UPLOAD_INPUT_INVALID\" },\n { tag: \"UPLOAD_DENIED\", status: 403, code: \"UPLOAD_DENIED\" },\n { tag: \"UPLOAD_CONTENT_INVALID\", status: 400, code: \"UPLOAD_CONTENT_INVALID\" },\n { tag: \"COMMAND_IDEMPOTENCY_CONFLICT\", status: 409, code: \"COMMAND_IDEMPOTENCY_CONFLICT\" },\n { tag: \"COMMAND_REJECTED\", status: 403, code: \"COMMAND_REJECTED\" },\n { tag: \"UPLOADS_UNAVAILABLE\", status: 501, code: \"UPLOADS_UNAVAILABLE\" },\n { tag: \"UPLOAD_OUTCOME_UNKNOWN\", status: 503, code: \"UPLOAD_OUTCOME_UNKNOWN\" },\n ],\n retry: \"none\",\n },\n })\n register(@Param(\"id\") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadResult, RegisterUploadError, never> {\n return Effect.promise(() => this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));\n }\n}\n";
2
- export declare const STARTER_UPLOAD_SCHEMA = "-- Apply explicitly with the migration owner.\nALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;\nGRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;\nCREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated\n USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());\nGRANT SELECT ON public.starter_reviews TO authenticated;\nCREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated\n USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (\n SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id\n AND m.enabled AND m.can_approve AND m.storage_subject=auth.uid()\n ));\nINSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)\n VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);\nCREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated\n USING (bucket_id='review-attachments');\nCREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated\n WITH CHECK (bucket_id='review-attachments');\n-- Restrictive fences also apply when the host already has permissive Storage policies.\nCREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated\n USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )))\n WITH CHECK (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text\n AND name ~ '^[a-f0-9-]{36}/[a-f0-9-]{36}/[a-f0-9-]{36}[.]txt$' AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )));\nCREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated\n USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');\nCREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated\n USING (bucket_id<>'review-attachments');\n";
2
+ export declare const STARTER_UPLOAD_SCHEMA = "-- Apply explicitly with the migration owner.\nALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;\nGRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;\nDROP POLICY IF EXISTS starter_member_read ON public.starter_members;\nCREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated\n USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());\nGRANT SELECT ON public.starter_reviews TO authenticated;\nDROP POLICY IF EXISTS starter_review_read ON public.starter_reviews;\nCREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated\n USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (\n SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id\n AND m.enabled AND m.can_approve AND m.storage_subject=auth.uid()\n ));\nINSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)\n VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);\nDROP POLICY IF EXISTS starter_attachment_read ON storage.objects;\nCREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated\n USING (bucket_id='review-attachments');\nDROP POLICY IF EXISTS starter_attachment_insert ON storage.objects;\nCREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated\n WITH CHECK (bucket_id='review-attachments');\n-- Restrictive fences also apply when the host already has permissive Storage policies.\nDROP POLICY IF EXISTS starter_attachment_owner_fence ON storage.objects;\nCREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated\n USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )))\n WITH CHECK (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text\n AND name ~ '^[a-f0-9-]{36}/[a-f0-9-]{36}/[a-f0-9-]{36}[.]txt$' AND EXISTS (\n SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id\n WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')\n AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve\n )));\nDROP POLICY IF EXISTS starter_attachment_update_fence ON storage.objects;\nCREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated\n USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');\nDROP POLICY IF EXISTS starter_attachment_delete_fence ON storage.objects;\nCREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated\n USING (bucket_id<>'review-attachments');\n";
3
3
  export declare const STARTER_UPLOAD_ADAPTER = "import { MIMEType } from \"node:util\";\nimport { SupaCloudArtifactsClient } from \"@supacloud/js\";\nimport { createTransactionalCommand, plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandTransaction } from \"@supacloud/db\";\nimport { ApplicationError, requireTrustedIdentity } from \"@supacloud/elysia\";\nimport type { ReviewAttachmentOptions } from \"./review-attachments\";\nimport type { ReviewUploadInput, ReviewUploadPort, ReviewUploadResult } from \"../review/uploads\";\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\nfunction record(value: unknown): Record<string, unknown> {\n if (!isRecord(value)) throw new TypeError(\"Invalid upload record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid upload database result\");\n return value.map(record);\n}\nfunction capture(value: unknown): ReviewUploadInput {\n const item = record(value), uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n if (typeof item[\"reviewId\"] !== \"string\" || !uuid.test(item[\"reviewId\"])\n || typeof item[\"artifactId\"] !== \"string\" || !uuid.test(item[\"artifactId\"])\n || typeof item[\"expectedVersion\"] !== \"number\" || !Number.isSafeInteger(item[\"expectedVersion\"]) || item[\"expectedVersion\"] < 1) {\n throw new ApplicationError(\"Invalid upload input\", { status: 400, code: \"UPLOAD_INPUT_INVALID\" });\n }\n return { reviewId: item[\"reviewId\"].toLowerCase(), artifactId: item[\"artifactId\"].toLowerCase(), expectedVersion: item[\"expectedVersion\"] };\n}\nfunction result(value: unknown): ReviewUploadResult {\n const item = record(value);\n if (typeof item[\"artifactId\"] !== \"string\" || typeof item[\"runId\"] !== \"string\" || typeof item[\"objectPath\"] !== \"string\"\n || typeof item[\"sha256\"] !== \"string\" || typeof item[\"bytes\"] !== \"number\") throw new TypeError(\"Invalid upload receipt\");\n return { artifactId: item[\"artifactId\"], runId: item[\"runId\"], objectPath: item[\"objectPath\"], sha256: item[\"sha256\"], bytes: item[\"bytes\"] };\n}\n\nexport async function createReviewUploadAdapters(options: ReviewAttachmentOptions): Promise<ReviewUploadPort> {\n const { database, service, projectId, tenantId } = options;\n const binding = rows(await database.transaction(tx => tx.query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n )));\n if (!projectId.trim() || !tenantId.trim() || binding.length !== 1\n || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Upload database project/tenant binding mismatch\");\n }\n const artifacts = new SupaCloudArtifactsClient(service);\n function actor(context: unknown): string {\n const who = requireTrustedIdentity(context);\n const access = record(record(context)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Upload permission denied\", { status: 403, code: \"UPLOAD_DENIED\" });\n }\n return who.subject;\n }\n async function authorize(tx: CommandTransaction, subject: string, input: ReviewUploadInput, replay = false) {\n const allowed = rows(await tx.query(\n \"SELECT m.storage_subject::text AS storage_subject FROM public.starter_reviews r \" +\n \"JOIN public.starter_members m ON m.subject=r.owner_id CROSS JOIN public.starter_application p \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND ((r.state='draft' AND r.version=$3) OR \" +\n \"($6::boolean AND r.state='approved' AND r.version=$3+1 AND EXISTS (\" +\n \"SELECT 1 FROM public.starter_attachments b WHERE b.review_id=r.id AND b.artifact_id=$7::uuid AND b.owner_id=$2 \" +\n \"AND b.object_path=m.storage_subject::text || '/' || r.id || '/' || $7::text || '.txt'))) \" +\n \"AND m.enabled AND m.can_approve AND m.storage_subject IS NOT NULL \" +\n \"AND p.singleton AND p.project_id=$4 AND p.tenant_id=$5 FOR UPDATE OF r,m FOR SHARE OF p\",\n [input.reviewId, subject, input.expectedVersion, projectId, tenantId, replay, input.artifactId],\n ));\n if (allowed.length !== 1 || typeof allowed[0]?.[\"storage_subject\"] !== \"string\") {\n throw new ApplicationError(\"Upload ownership, permission or revision changed\", { status: 403, code: \"UPLOAD_DENIED\" });\n }\n return allowed[0][\"storage_subject\"] + \"/\" + input.reviewId + \"/\" + input.artifactId + \".txt\";\n }\n return {\n async previewUpload(context, value) {\n const input = capture(value), subject = actor(context);\n const objectPath = await database.transaction(tx => authorize(tx, subject, input));\n return { artifactId: input.artifactId, bucketId: \"review-attachments\", objectPath };\n },\n async registerUpload(context, value) {\n const input = capture(value), subject = actor(context);\n const objectPath = await database.transaction(tx => authorize(tx, subject, input, true));\n try {\n const downloaded = await service.storage.from(\"review-attachments\").download(objectPath);\n if (downloaded.error || !downloaded.data) throw new Error(\"Upload unavailable\");\n const blob = downloaded.data;\n if (blob.size < 1 || blob.size > 1048576 || new MIMEType(blob.type).essence !== \"text/plain\") {\n throw new ApplicationError(\"Unsupported upload\", { status: 400, code: \"UPLOAD_CONTENT_INVALID\" });\n }\n const sha256 = Array.from(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", await blob.arrayBuffer())),\n byte => byte.toString(16).padStart(2, \"0\")).join(\"\");\n await artifacts.register({ artifactId: input.artifactId, bucketId: \"review-attachments\", objectPath,\n artifactType: \"review.attachment\", sha256, sizeBytes: blob.size, mimeType: \"text/plain\" });\n const command = createTransactionalCommand({\n name: \"review.attach\", store: createPostgresCommandStore(database), inputCodec: plaintextCommandInput,\n input: capture, result,\n async authorize(who, request, tx) {\n try {\n const currentPath = await authorize(tx, who.actorId, request, true);\n return currentPath === objectPath ? \"allow\" : \"deny\";\n } catch (error) {\n if (error instanceof ApplicationError && error.code === \"UPLOAD_DENIED\") return \"deny\";\n throw error;\n }\n },\n async execute(tx, request) {\n const existing = rows(await tx.query(\"SELECT artifact_id FROM public.starter_attachments WHERE review_id=$1\", [request.reviewId]));\n if (existing.length) throw new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\");\n const bound: ReviewUploadResult = { artifactId: request.artifactId, runId: request.artifactId, objectPath, sha256, bytes: blob.size };\n await tx.query(\"INSERT INTO public.starter_attachments VALUES($1,$2,$3,$4,$5)\",\n [request.reviewId, bound.artifactId, subject, bound.runId, bound.objectPath]);\n return bound;\n },\n audit: { event: \"review.attachment-bound\", details: request => ({ reviewId: request.reviewId, artifactId: request.artifactId }) },\n });\n const receipt = await command.execute({ actorId: subject, tenantId }, input.artifactId, input);\n if (receipt.status !== \"confirmed\") throw new CommandError(\"COMMAND_OUTCOME_UNKNOWN\");\n return receipt.result;\n } catch (error) {\n if (error instanceof ApplicationError) throw error;\n if (error instanceof CommandError && error.code === \"COMMAND_IDEMPOTENCY_CONFLICT\") {\n throw new ApplicationError(\"Conflicting attachment binding\", { status: 409, code: error.code });\n }\n if (error instanceof CommandError && error.code === \"COMMAND_REJECTED\") {\n throw new ApplicationError(\"Upload permission denied\", { status: 403, code: error.code });\n }\n // Registration may have committed even when binding failed; never delete immutable evidence here.\n throw new ApplicationError(\"Upload registration could not be confirmed\", { status: 503, code: \"UPLOAD_OUTCOME_UNKNOWN\" });\n }\n },\n };\n}\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.95.0",
3
+ "version": "0.96.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -52,19 +52,19 @@
52
52
  "directory": "packages/cli"
53
53
  },
54
54
  "dependencies": {
55
- "@supacloud/compiler": "^0.45.0",
56
- "@supacloud/delivery": "0.16.0",
57
- "@supacloud/db": "0.16.2",
55
+ "@supacloud/compiler": "^0.46.0",
56
+ "@supacloud/delivery": "0.17.0",
57
+ "@supacloud/db": "0.17.0",
58
58
  "typebox": "1.3.36"
59
59
  },
60
60
  "overrides": {
61
61
  "@supacloud/contracts": "0.8.1",
62
- "@supacloud/delivery": "0.16.0",
62
+ "@supacloud/delivery": "0.17.0",
63
63
  "@graphql-tools/utils": "^12.0.3"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@supacloud/elysia": "^0.24.1",
67
- "@types/bun": "^1.4.2",
67
+ "@types/bun": "^1.4.3",
68
68
  "effect": "4.0.2",
69
69
  "elysia": "2.0.0-beta.21",
70
70
  "typescript": "^7.0.2"