@supacloud/cli 0.103.6 → 0.104.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/dist/index.js CHANGED
@@ -9405,9 +9405,9 @@ var $pack = require_pack();
9405
9405
  var FRONTEND_ARCHIVE_FORMAT = "tar.zst";
9406
9406
  var FRONTEND_ARCHIVE_RELEASE_SCHEMA = "supacloud.frontend-release.v2";
9407
9407
  var FRONTEND_ARCHIVE_CONTENT_TYPE = "application/vnd.supacloud.frontend.tar+zstd";
9408
- var FRONTEND_ARCHIVE_MAX_BYTES = 100 * 1024 * 1024;
9408
+ var FRONTEND_ARCHIVE_MAX_BYTES = 500 * 1024 * 1024;
9409
9409
  var FRONTEND_ARCHIVE_MAX_FILES = 1e4;
9410
- var FRONTEND_ARCHIVE_MAX_SOURCE_BYTES = 300 * 1024 * 1024;
9410
+ var FRONTEND_ARCHIVE_MAX_SOURCE_BYTES = 500 * 1024 * 1024;
9411
9411
  var FRONTEND_ARCHIVE_MAX_TAR_BYTES = FRONTEND_ARCHIVE_MAX_SOURCE_BYTES + FRONTEND_ARCHIVE_MAX_FILES * 2048 + 1024;
9412
9412
  function assertFrontendArchivePath(path) {
9413
9413
  if (!path || path.length > 1024 || Buffer.byteLength(path) > 4096 || path.startsWith("/") || path.includes("\\") || /[\u0000-\u001f\u007f]/u.test(path) || path.split("/").some((segment) => !segment || segment === "." || segment === "..") || /^[A-Za-z]:/u.test(path))
@@ -10285,6 +10285,8 @@ function parseDeliveryBuildManifest(value) {
10285
10285
 
10286
10286
  // node_modules/@supacloud/delivery/src/delivery-artifact.ts
10287
10287
  var JSON_LIMIT = 1048576;
10288
+ var DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
10289
+ var DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
10288
10290
  async function readDeliveryManifest(manifestPath) {
10289
10291
  const selected = resolve4(manifestPath);
10290
10292
  const root = await realpath2(dirname2(selected));
@@ -10298,7 +10300,7 @@ async function readDeliverySelection(manifestPath, target) {
10298
10300
  return { root, object, planned };
10299
10301
  }
10300
10302
  async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
10301
- if (object.files.length > 1024 || object.files.some((file) => file.bytes > 64 * 1024 * 1024) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > 128 * 1024 * 1024) {
10303
+ if (object.files.length > 1024 || object.files.some((file) => file.bytes > DELIVERY_ARTIFACT_MAX_FILE_BYTES) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
10302
10304
  throw new Error("Artifact inventory exceeds limits.");
10303
10305
  }
10304
10306
  const objectRoot = join2(root, "objects", object.objectId);
@@ -11420,7 +11422,7 @@ function buildDeliveryMigrationPlan(archive, inventory, ref) {
11420
11422
  // node_modules/@supacloud/delivery/src/index.ts
11421
11423
  async function readDeliveryExecutableArchive(manifestPath) {
11422
11424
  const { root, manifest } = await readDeliveryManifest(manifestPath);
11423
- if (manifest.objects.length === 0 || manifest.objects.length > 32 || manifest.objects.some((object) => object.entryKind === "compiled-module-factory") || manifest.objects.reduce((total, object) => total + object.files.reduce((bytes, file) => bytes + file.bytes, 0), 0) > 128 * 1024 * 1024) {
11425
+ if (manifest.objects.length === 0 || manifest.objects.length > 32 || manifest.objects.some((object) => object.entryKind === "compiled-module-factory") || manifest.objects.reduce((total, object) => total + object.files.reduce((bytes, file) => bytes + file.bytes, 0), 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
11424
11426
  throw new Error("Invalid executable delivery inventory.");
11425
11427
  }
11426
11428
  const objects = [];
@@ -21574,25 +21576,25 @@ var starter_metadata_default = {
21574
21576
  generatedBy: "supacloud/starter-metadata-v1",
21575
21577
  packages: {
21576
21578
  compiler: {
21577
- version: "0.47.2"
21579
+ version: "0.48.0"
21578
21580
  },
21579
21581
  app: {
21580
- version: "0.28.2",
21582
+ version: "0.29.0",
21581
21583
  dependencies: {
21582
21584
  rxjs: "7.8.2"
21583
21585
  }
21584
21586
  },
21585
21587
  elysia: {
21586
- version: "0.33.4"
21588
+ version: "0.34.0"
21587
21589
  },
21588
21590
  commands: {
21589
- version: "0.9.6"
21591
+ version: "0.10.0"
21590
21592
  },
21591
21593
  contracts: {
21592
- version: "0.11.4"
21594
+ version: "0.12.0"
21593
21595
  },
21594
21596
  db: {
21595
- version: "0.18.3",
21597
+ version: "0.19.0",
21596
21598
  peerDependencies: {
21597
21599
  "drizzle-orm": "1.0.0-rc.5-5935859"
21598
21600
  },
@@ -21601,7 +21603,7 @@ var starter_metadata_default = {
21601
21603
  }
21602
21604
  },
21603
21605
  sdk: {
21604
- version: "0.43.3",
21606
+ version: "0.44.0",
21605
21607
  peerDependencies: {
21606
21608
  "@supabase/supabase-js": "^2.117.3"
21607
21609
  }
@@ -26247,7 +26249,7 @@ export const ${className}Params = t.Object({
26247
26249
  id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
26248
26250
  });
26249
26251
  export const ${className}ListQuery = t.Object({
26250
- state: t.Optional(t.String({ minLength: 1, maxLength: 64 })),
26252
+ state: t.Optional(t.Union([t.Literal("draft"), t.Literal("approved")])),
26251
26253
  });
26252
26254
  export const ${className}ApproveBody = t.Object({
26253
26255
  expectedVersion: t.Integer({ minimum: 1 }),
@@ -26259,12 +26261,19 @@ export const ${className}Row = t.Object({
26259
26261
  version: t.Integer({ minimum: 1 }),
26260
26262
  });
26261
26263
  export const ${className}ListResponse = t.Array(${className}Row);
26264
+ export const ${className}RefreshInput = t.Object({
26265
+ id: t.String({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z0-9_-]+$" }),
26266
+ version: t.Integer({ minimum: 1 }),
26267
+ });
26262
26268
  `,
26263
- [`${name}.model.ts`]: `import { InjectionToken } from "@supacloud/app/core";
26269
+ [`${name}.model.ts`]: `import {
26270
+ InjectionToken, type FeatureRequestContext,
26271
+ } from "@supacloud/app/core";
26264
26272
  import type {
26265
26273
  ${className}ApproveBody,
26266
26274
  ${className}ListQuery,
26267
26275
  ${className}Row,
26276
+ ${className}RefreshInput,
26268
26277
  } from "./${name}.contracts";
26269
26278
  import type { StaticDecode } from "typebox";
26270
26279
 
@@ -26272,14 +26281,15 @@ export type ${className}ListInput = StaticDecode<typeof ${className}ListQuery>;
26272
26281
  export type ${className}ApproveInput = StaticDecode<typeof ${className}ApproveBody>;
26273
26282
  export type ${className}ApproveCommand = ${className}ApproveInput & { id: string };
26274
26283
  export type ${className}Result = StaticDecode<typeof ${className}Row>;
26284
+ export type ${className}RefreshJobInput = StaticDecode<typeof ${className}RefreshInput>;
26275
26285
 
26276
26286
  export interface ${className}ReadPort {
26277
- list(input: ${className}ListInput): Promise<${className}Result[]>;
26278
- detail(id: string): Promise<${className}Result>;
26287
+ list(context: FeatureRequestContext, input: ${className}ListInput): Promise<${className}Result[]>;
26288
+ detail(context: FeatureRequestContext, id: string): Promise<${className}Result>;
26279
26289
  }
26280
26290
 
26281
26291
  export interface ${className}WritePort {
26282
- approve(input: ${className}ApproveCommand): Promise<${className}Result>;
26292
+ approve(context: FeatureRequestContext, input: ${className}ApproveCommand): Promise<${className}Result>;
26283
26293
  }
26284
26294
 
26285
26295
  export const ${className}Reader = new InjectionToken<${className}ReadPort>(
@@ -26289,7 +26299,10 @@ export const ${className}Writer = new InjectionToken<${className}WritePort>(
26289
26299
  "${name}.writer", { scope: "request" },
26290
26300
  );
26291
26301
  `,
26292
- [`${name}.service.ts`]: `import { Inject, Injectable, Optional } from "@supacloud/app/core";
26302
+ [`${name}.service.ts`]: `import {
26303
+ Inject, Injectable, Optional, REQUEST_CONTEXT, requireFeatureRequestContext,
26304
+ type FeatureRequestContext,
26305
+ } from "@supacloud/app/core";
26293
26306
  import {
26294
26307
  ${className}Reader,
26295
26308
  ${className}Writer,
@@ -26302,49 +26315,67 @@ import {
26302
26315
  @Injectable({ scope: "request" })
26303
26316
  export class ${className}Service {
26304
26317
  constructor(
26318
+ @Inject(REQUEST_CONTEXT) private readonly requestContext: unknown,
26305
26319
  @Inject(${className}Reader) @Optional() private readonly reader?: ${className}ReadPort,
26306
26320
  @Inject(${className}Writer) @Optional() private readonly writer?: ${className}WritePort,
26307
26321
  ) {}
26308
26322
 
26309
- async list(input: Parameters<${className}ReadPort["list"]>[0]): Promise<${className}Result[]> {
26323
+ private context(): FeatureRequestContext {
26324
+ return requireFeatureRequestContext(this.requestContext);
26325
+ }
26326
+
26327
+ async list(input: Parameters<${className}ReadPort["list"]>[1]): Promise<${className}Result[]> {
26310
26328
  if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
26311
- return this.reader.list(input);
26329
+ return this.reader.list(this.context(), input);
26312
26330
  }
26313
26331
 
26314
26332
  async detail(id: string): Promise<${className}Result> {
26315
26333
  if (!this.reader) throw new Error("Bind ${className}Reader with verified identity and tenant scope");
26316
- return this.reader.detail(id);
26334
+ return this.reader.detail(this.context(), id);
26317
26335
  }
26318
26336
 
26319
26337
  async approve(id: string, input: ${className}ApproveInput): Promise<${className}Result> {
26320
26338
  if (!this.writer) throw new Error("Bind ${className}Writer with durable command governance");
26321
- return this.writer.approve({ ...input, id });
26339
+ return this.writer.approve(this.context(), { ...input, id });
26322
26340
  }
26323
26341
  }
26324
26342
  `,
26325
26343
  [`${name}.feature.ts`]: `import { Effect } from "effect";
26326
26344
  import {
26327
- Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, Get, Inject,
26328
- Injectable, Job, Param, Post, Query, type FeatureState,
26345
+ Body, Command, Controller, defineFeatureSlice, defineFeatureSpec, Get, Inject, JOB_CONTEXT,
26346
+ Injectable, Job, Param, Post, Query, requireFeatureRequestContext, type FeatureRequestContext,
26347
+ type FeatureState,
26329
26348
  } from "@supacloud/app/core";
26330
26349
  import {
26331
26350
  ${className}ApproveBody,
26332
26351
  ${className}ListQuery,
26333
26352
  ${className}ListResponse,
26334
26353
  ${className}Params,
26354
+ ${className}RefreshInput,
26335
26355
  ${className}Row,
26336
26356
  } from "./${name}.contracts";
26337
26357
  import { ${className}Service } from "./${name}.service";
26338
- import type { ${className}ApproveInput, ${className}ListInput, ${className}Result } from "./${name}.model";
26358
+ import type {
26359
+ ${className}ApproveInput,
26360
+ ${className}ListInput,
26361
+ ${className}RefreshJobInput,
26362
+ ${className}Result,
26363
+ } from "./${name}.model";
26339
26364
 
26340
26365
  export const featureSpec = defineFeatureSpec({
26341
26366
  name: ${JSON.stringify(name)},
26342
26367
  states: ["draft", "approved"],
26368
+ tenantField: "tenant_id",
26343
26369
  transitions: {
26344
26370
  approve: {
26345
26371
  from: "draft", to: "approved", command: "Approve${className}Command",
26346
26372
  permission: ${JSON.stringify(permission)}, transaction: "required",
26347
26373
  idempotency: "required", audit: ${JSON.stringify(`${name}.approved`)},
26374
+ job: ${JSON.stringify(`${name}.refresh`)},
26375
+ persistence: {
26376
+ boundary: "database", optimisticLock: "required", versionField: "version", tenantField: "tenant_id", stateField: "state",
26377
+ receipt: "required", reconciliation: "none",
26378
+ },
26348
26379
  route: "POST /${name}/:id/approve",
26349
26380
  },
26350
26381
  },
@@ -26352,7 +26383,7 @@ export const featureSpec = defineFeatureSpec({
26352
26383
  export type ${className}State = FeatureState<typeof featureSpec>;
26353
26384
 
26354
26385
  @Injectable({ scope: "request" })
26355
- @Query({ name: "${name}.list" })
26386
+ @Query({ name: "${name}.list", tenantScope: "required" })
26356
26387
  export class List${className}Query {
26357
26388
  constructor(@Inject(${className}Service) private readonly service: ${className}Service) {}
26358
26389
  execute(input: ${className}ListInput): Promise<${className}Result[]> {
@@ -26367,6 +26398,7 @@ export class List${className}Query {
26367
26398
  transaction: "required",
26368
26399
  audit: ${JSON.stringify(`${name}.approved`)},
26369
26400
  idempotency: "required",
26401
+ tenantScope: "required",
26370
26402
  rpc: ${JSON.stringify(rpc)},
26371
26403
  })
26372
26404
  export class Approve${className}Command {
@@ -26378,11 +26410,17 @@ export class Approve${className}Command {
26378
26410
 
26379
26411
  @Job({
26380
26412
  name: ${JSON.stringify(`${name}.refresh`)},
26413
+ input: ${className}RefreshInput,
26381
26414
  mode: "task",
26382
26415
  idempotency: "required",
26416
+ tenantScope: "required",
26383
26417
  })
26384
26418
  export class Refresh${className}Job {
26385
- run(_input: { id: string; version: number }): Promise<void> {
26419
+ constructor(@Inject(JOB_CONTEXT) private readonly jobContext: unknown) {}
26420
+
26421
+ async run(_input: ${className}RefreshJobInput): Promise<void> {
26422
+ const context: FeatureRequestContext = requireFeatureRequestContext(this.jobContext);
26423
+ void context;
26386
26424
  throw new Error("Bind the existing Workflow/Queue adapter before dispatching ${name}.refresh");
26387
26425
  }
26388
26426
  }
@@ -26400,7 +26438,7 @@ export class ${className}Controller {
26400
26438
  responses: { 200: ${className}ListResponse },
26401
26439
  effect: { required: true, dependencies: [], errors: [], retry: "none" },
26402
26440
  })
26403
- list(@Query("state") state?: string): Effect.Effect<${className}Result[], never, never> {
26441
+ list(@Query("state") state?: ${className}State): Effect.Effect<${className}Result[], never, never> {
26404
26442
  return Effect.promise(() => this.listQuery.execute(state === undefined ? {} : { state }));
26405
26443
  }
26406
26444
 
@@ -26438,10 +26476,27 @@ export const ${className}Feature = defineFeatureSlice({
26438
26476
  });
26439
26477
  `,
26440
26478
  [`${name}.feature.test.ts`]: `import { expect, test } from "bun:test";
26479
+ import { Refresh${className}Job } from "./${name}.feature";
26441
26480
  import { ${className}Service } from "./${name}.service";
26481
+ import { ${className}Fixture, ${className}RequestContext, ${className}RequestEnvelope } from "./${name}.fixture";
26482
+
26483
+ test("${name} rejects missing trusted tenant context before adapter execution", async () => {
26484
+ let calls = 0;
26485
+ const service = new ${className}Service(undefined, {
26486
+ list: async () => { calls++; return []; },
26487
+ detail: async () => { calls++; return { id: "example", state: "draft", version: 1 }; },
26488
+ });
26489
+ await expect(service.list({})).rejects.toThrow("FEATURE_CONTEXT_REQUIRED");
26490
+ expect(calls).toBe(0);
26491
+ });
26492
+
26493
+ test("${name} rejects missing trusted job context before dispatch", async () => {
26494
+ const job = new Refresh${className}Job({});
26495
+ await expect(job.run({ id: "example", version: 1 })).rejects.toThrow("FEATURE_CONTEXT_REQUIRED");
26496
+ });
26442
26497
 
26443
26498
  test("${name} fails closed until verified read and durable write ports are bound", async () => {
26444
- const service = new ${className}Service();
26499
+ const service = new ${className}Service(${className}RequestEnvelope);
26445
26500
  await expect(service.list({})).rejects.toThrow("Bind ${className}Reader");
26446
26501
  await expect(service.detail("example")).rejects.toThrow("Bind ${className}Reader");
26447
26502
  await expect(service.approve("example", { expectedVersion: 1 })).rejects.toThrow("Bind ${className}Writer");
@@ -26450,7 +26505,7 @@ test("${name} fails closed until verified read and durable write ports are bound
26450
26505
  test("${name} preserves denied reads and rejected writes without fallback or retry", async () => {
26451
26506
  const denied = new Error("denied");
26452
26507
  let attempts = 0;
26453
- const service = new ${className}Service({
26508
+ const service = new ${className}Service(${className}RequestEnvelope, {
26454
26509
  list: async () => { throw denied; },
26455
26510
  detail: async () => { throw denied; },
26456
26511
  }, {
@@ -26463,14 +26518,150 @@ test("${name} preserves denied reads and rejected writes without fallback or ret
26463
26518
  });
26464
26519
 
26465
26520
  test("${name} forwards the authoritative version and result without inventing persistence", async () => {
26466
- const row = { id: "example", state: "approved", version: 2 };
26467
- const service = new ${className}Service(undefined, {
26468
- async approve(input) {
26469
- expect(input).toEqual({ id: "example", expectedVersion: 1, reason: "reviewed" });
26521
+ const row = { id: ${className}Fixture.id, state: "approved", version: 2 };
26522
+ const service = new ${className}Service(${className}RequestEnvelope, undefined, {
26523
+ async approve(requestContext, input) {
26524
+ expect(requestContext).toMatchObject(${className}RequestContext);
26525
+ expect(input).toEqual({ id: ${className}Fixture.id, ...${className}Fixture.approve });
26470
26526
  return row;
26471
26527
  },
26472
26528
  });
26473
- expect(await service.approve("example", { expectedVersion: 1, reason: "reviewed" })).toBe(row);
26529
+ expect(await service.approve(${className}Fixture.id, ${className}Fixture.approve)).toBe(row);
26530
+ });
26531
+ `,
26532
+ [`${name}.fixture.ts`]: `import type { ${className}ApproveInput, ${className}Result } from "./${name}.model";
26533
+
26534
+ /**
26535
+ * Test data only. Production adapters must load authoritative rows and
26536
+ * identity from the database/authentication boundary.
26537
+ */
26538
+ export const ${className}Fixture = {
26539
+ id: "example",
26540
+ approve: { expectedVersion: 1, reason: "reviewed" } satisfies ${className}ApproveInput,
26541
+ row: { id: "example", state: "draft", version: 1 } satisfies ${className}Result,
26542
+ } as const;
26543
+
26544
+ export const ${className}RequestContext = {
26545
+ requestId: "request-example",
26546
+ tenantId: "tenant-example",
26547
+ actorId: "reviewer-example",
26548
+ permissions: [${JSON.stringify(permission)}],
26549
+ } as const;
26550
+
26551
+ export const ${className}RequestEnvelope = {
26552
+ ...${className}RequestContext,
26553
+ identity: { authenticated: true, subject: "reviewer-example" },
26554
+ access: { tenantId: "tenant-example", permissions: [${JSON.stringify(permission)}] },
26555
+ } as const satisfies Record<string, unknown>;
26556
+ `,
26557
+ [`${name}.e2e.test.ts`]: `import { expect, test } from "bun:test";
26558
+ import { ${className}Fixture } from "./${name}.fixture";
26559
+
26560
+ const baseUrl = process.env["SUPACLOUD_FEATURE_E2E_URL"];
26561
+ const authorization = process.env["SUPACLOUD_FEATURE_E2E_AUTHORIZATION"];
26562
+ const otherAuthorization = process.env["SUPACLOUD_FEATURE_E2E_OTHER_AUTHORIZATION"];
26563
+ const fixtureId = process.env["SUPACLOUD_FEATURE_E2E_ID"] ?? ${className}Fixture.id;
26564
+ const idempotencyKey = process.env["SUPACLOUD_FEATURE_E2E_IDEMPOTENCY_KEY"];
26565
+ const enabled = Boolean(baseUrl && authorization && idempotencyKey);
26566
+ const tenantIsolationEnabled = Boolean(enabled && otherAuthorization);
26567
+
26568
+ function requireConfig(): { baseUrl: string; authorization: string; idempotencyKey: string } {
26569
+ if (!baseUrl || !authorization || !idempotencyKey) {
26570
+ throw new Error(
26571
+ "Set SUPACLOUD_FEATURE_E2E_URL, SUPACLOUD_FEATURE_E2E_AUTHORIZATION and " +
26572
+ "SUPACLOUD_FEATURE_E2E_IDEMPOTENCY_KEY before running this generated E2E test.",
26573
+ );
26574
+ }
26575
+ return { baseUrl, authorization, idempotencyKey };
26576
+ }
26577
+
26578
+ async function json(response: Response): Promise<unknown> {
26579
+ const text = await response.text();
26580
+ if (!text) return undefined;
26581
+ try {
26582
+ return JSON.parse(text) as unknown;
26583
+ } catch {
26584
+ return text;
26585
+ }
26586
+ }
26587
+
26588
+ function containsFixture(value: unknown): boolean {
26589
+ return JSON.stringify(value).includes(fixtureId);
26590
+ }
26591
+
26592
+ test.skipIf(!enabled)("${name} authenticated list/detail/approval E2E", async () => {
26593
+ const config = requireConfig();
26594
+ const headers = { authorization: config.authorization, "content-type": "application/json" };
26595
+ const list = await fetch(new URL("/${name}", config.baseUrl), { headers });
26596
+ expect(list.ok).toBe(true);
26597
+ const detail = await fetch(
26598
+ new URL("/${name}/" + encodeURIComponent(fixtureId), config.baseUrl),
26599
+ { headers },
26600
+ );
26601
+ expect(detail.ok).toBe(true);
26602
+ const approve = (key: string, body = ${className}Fixture.approve) => fetch(
26603
+ new URL("/${name}/" + encodeURIComponent(fixtureId) + "/approve", config.baseUrl),
26604
+ {
26605
+ method: "POST",
26606
+ headers: { ...headers, "idempotency-key": key },
26607
+ body: JSON.stringify(body),
26608
+ },
26609
+ );
26610
+ const first = await approve(config.idempotencyKey);
26611
+ const firstBody = await json(first);
26612
+ expect([200, 409, 422]).toContain(first.status);
26613
+
26614
+ // Replaying an identical command may return the original success or a
26615
+ // domain conflict, but it must never create a second successful transition.
26616
+ const replay = await approve(config.idempotencyKey);
26617
+ const replayBody = await json(replay);
26618
+ expect(replay.status).toBe(first.status);
26619
+ expect(replayBody).toEqual(firstBody);
26620
+
26621
+ // Reusing a key with a different payload must be rejected by the receipt
26622
+ // boundary instead of being treated as a new approval.
26623
+ const conflicting = await approve(
26624
+ config.idempotencyKey,
26625
+ { ...${className}Fixture.approve, reason: "conflicting replay" },
26626
+ );
26627
+ expect([400, 409, 422]).toContain(conflicting.status);
26628
+
26629
+ // Two distinct keys racing on one version may commit at most one state
26630
+ // transition. A fixture that is already terminal may reject both.
26631
+ const [raceA, raceB] = await Promise.all([
26632
+ approve(config.idempotencyKey + "-race-a"),
26633
+ approve(config.idempotencyKey + "-race-b"),
26634
+ ]);
26635
+ expect([raceA.status, raceB.status].filter((status) => status >= 200 && status < 300).length)
26636
+ .toBeLessThanOrEqual(1);
26637
+ });
26638
+
26639
+ test.skipIf(!tenantIsolationEnabled)("${name} rejects cross-tenant access", async () => {
26640
+ const config = requireConfig();
26641
+ if (!otherAuthorization) throw new Error("Missing SUPACLOUD_FEATURE_E2E_OTHER_AUTHORIZATION");
26642
+ const headers = {
26643
+ authorization: otherAuthorization,
26644
+ "content-type": "application/json",
26645
+ };
26646
+ const list = await fetch(new URL("/${name}", config.baseUrl), { headers });
26647
+ expect(list.ok).toBe(true);
26648
+ expect(containsFixture(await json(list))).toBe(false);
26649
+
26650
+ const detail = await fetch(
26651
+ new URL("/${name}/" + encodeURIComponent(fixtureId), config.baseUrl),
26652
+ { headers },
26653
+ );
26654
+ expect([403, 404]).toContain(detail.status);
26655
+
26656
+ const approval = await fetch(
26657
+ new URL("/${name}/" + encodeURIComponent(fixtureId) + "/approve", config.baseUrl),
26658
+ {
26659
+ method: "POST",
26660
+ headers: { ...headers, "idempotency-key": config.idempotencyKey + "-other-tenant" },
26661
+ body: JSON.stringify(${className}Fixture.approve),
26662
+ },
26663
+ );
26664
+ expect([403, 404]).toContain(approval.status);
26474
26665
  });
26475
26666
  `,
26476
26667
  [`${name}.deployment.md`]: `# ${className} feature delivery checklist
@@ -26485,6 +26676,9 @@ test("${name} forwards the authoritative version and result without inventing pe
26485
26676
 
26486
26677
  The scaffold does not create migrations, infer permissions, or provide an in-memory
26487
26678
  production fallback. Database migrations remain the database source of truth.
26679
+ Every adapter receives a trusted framework context whose tenant and actor values
26680
+ come from verified identity/access data. Do not add tenant, actor, or permission
26681
+ fields to the request body as a substitute for this context.
26488
26682
  The HTTP schemas describe the boundary, not a second database model. Bind their
26489
26683
  fields to the existing database-source contract checker before delivery.
26490
26684
  The Writer input is derived from the approval body contract plus the route ID.
@@ -26492,6 +26686,11 @@ Add request fields in the contract, not a second handwritten Writer interface.
26492
26686
  Typecheck application adapters after changing a field type; the derived Port
26493
26687
  must expose incompatible assumptions in business code.
26494
26688
 
26689
+ For authenticated tenant-isolation acceptance, also provide
26690
+ \`SUPACLOUD_FEATURE_E2E_OTHER_AUTHORIZATION\` for a principal from another tenant.
26691
+ The generated E2E then verifies that the fixture is absent from list results and that
26692
+ cross-tenant detail and approval are rejected.
26693
+
26495
26694
  The named RPC adapter must invoke the continuation in the same database transaction
26496
26695
  used by the request-scoped Writer, or own the complete command instead; never execute
26497
26696
  both paths. Lock/check the version and state, reauthorize replays, and commit the
@@ -26683,14 +26882,15 @@ async function runLocalDevelopment(options) {
26683
26882
  }
26684
26883
 
26685
26884
  // src/shared/tools/app-verification-plan.ts
26686
- import { readdir as readdir4, realpath as realpath3 } from "node:fs/promises";
26885
+ import { readdir as readdir4, realpath as realpath3, stat } from "node:fs/promises";
26687
26886
  import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative5, resolve as resolve16 } from "node:path";
26688
26887
  function inside2(root, file) {
26689
26888
  const path = relative5(root, file).replaceAll("\\", "/");
26690
26889
  return path !== ".." && !path.startsWith("../") && !isAbsolute4(path);
26691
26890
  }
26692
26891
  async function createVerificationPlan(root, graph, target, sourceRoot = root) {
26693
- const owners = graph.modules.filter((module) => module.name === target || module.className === target || [...module.providers, ...module.controllers, ...module.commands, ...module.jobs ?? [], ...module.queries ?? []].some((item) => ("className" in item) && item.className === target || ("name" in item) && item.name === target || ("token" in item) && item.token === target || ("useClass" in item) && item.useClass === target));
26892
+ const exact = graph.modules.filter((module) => module.name === target);
26893
+ const owners = exact.length > 0 ? exact : graph.modules.filter((module) => module.className === target || [...module.providers, ...module.controllers, ...module.commands, ...module.jobs ?? [], ...module.queries ?? []].some((item) => ("className" in item) && item.className === target || ("name" in item) && item.name === target || ("token" in item) && item.token === target || ("useClass" in item) && item.useClass === target || ("serviceKey" in item) && item.serviceKey === target));
26694
26894
  if (owners.length !== 1)
26695
26895
  throw new Error("Select one unambiguous module or owned symbol for verification");
26696
26896
  const owner = owners[0];
@@ -26707,37 +26907,50 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
26707
26907
  ...(owner.jobs ?? []).flatMap((item) => (item.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])),
26708
26908
  ...owner.controllers.flatMap((item) => item.routes.flatMap((route) => (route.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])))
26709
26909
  ].filter((file) => typeof file === "string");
26710
- const directories = [...new Set(sources.map((file) => dirname7(resolve16(sourceRoot, file))))].sort();
26711
- const tests = new Set;
26712
26910
  const manual = [];
26713
- const coverage = [];
26714
- for (const directory of directories) {
26715
- if (!inside2(requestedRoot, directory)) {
26716
- manual.push("A source directory is outside the project; select its owning project explicitly");
26911
+ const validatedSources = [];
26912
+ for (const source of new Set(sources)) {
26913
+ const requested = resolve16(sourceRoot, source);
26914
+ if (!inside2(requestedRoot, requested)) {
26915
+ manual.push("A source file is outside the project; select its owning project explicitly");
26717
26916
  continue;
26718
26917
  }
26719
- const actual = await realpath3(directory).catch((error) => {
26918
+ const actual = await realpath3(requested).catch((error) => {
26720
26919
  if (error instanceof Error && "code" in error && error.code === "ENOENT")
26721
26920
  return null;
26722
26921
  throw error;
26723
26922
  });
26724
26923
  if (actual === null) {
26725
- manual.push(`Source directory is missing: ${relative5(requestedRoot, directory).replaceAll("\\", "/")}`);
26924
+ manual.push(`Source file is missing: ${relative5(requestedRoot, requested).replaceAll("\\", "/")}`);
26726
26925
  continue;
26727
26926
  }
26728
26927
  if (!inside2(project, actual)) {
26928
+ manual.push("A source file is outside the project; select its owning project explicitly");
26929
+ continue;
26930
+ }
26931
+ if (!(await stat(actual)).isFile()) {
26932
+ manual.push(`Source is not a regular file: ${relative5(requestedRoot, requested).replaceAll("\\", "/")}`);
26933
+ continue;
26934
+ }
26935
+ validatedSources.push(actual);
26936
+ }
26937
+ const directories = [...new Set(validatedSources.map((file) => dirname7(file)))].sort();
26938
+ const tests = new Set;
26939
+ const coverage = [];
26940
+ for (const directory of directories) {
26941
+ if (!inside2(project, await realpath3(directory))) {
26729
26942
  manual.push("A source directory is outside the project; select its owning project explicitly");
26730
26943
  continue;
26731
26944
  }
26732
26945
  const local = [];
26733
- for (const entry of await readdir4(actual, { withFileTypes: true })) {
26946
+ for (const entry of await readdir4(directory, { withFileTypes: true })) {
26734
26947
  if (entry.isFile() && /\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
26735
- const file = relative5(project, resolve16(actual, entry.name)).replaceAll("\\", "/");
26948
+ const file = relative5(project, resolve16(directory, entry.name)).replaceAll("\\", "/");
26736
26949
  tests.add(file);
26737
26950
  local.push(file);
26738
26951
  }
26739
26952
  }
26740
- const path = relative5(project, actual).replaceAll("\\", "/") || ".";
26953
+ const path = relative5(project, directory).replaceAll("\\", "/") || ".";
26741
26954
  coverage.push({ directory: path, tests: local.sort(), basis: "colocated-not-proven" });
26742
26955
  if (!local.length)
26743
26956
  manual.push(`No colocated tests for source directory: ${path}`);
@@ -26752,7 +26965,7 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
26752
26965
  source: "declaration",
26753
26966
  target: owner.name,
26754
26967
  ready: manual.length === 0,
26755
- files: [...new Set(sources.map((file) => relative5(requestedRoot, resolve16(sourceRoot, file)).replaceAll("\\", "/")))].sort(),
26968
+ files: [...new Set(validatedSources.map((file) => relative5(project, file).replaceAll("\\", "/")))].sort(),
26756
26969
  tests: selected.slice(0, 32),
26757
26970
  commands: selected.length <= 32 ? [
26758
26971
  ...selected.map((file) => ["bun", "test", `./${file}`]),
@@ -27288,16 +27501,19 @@ async function projectCompileConfig(args) {
27288
27501
  }
27289
27502
  async function readAllowedDependencies(root) {
27290
27503
  const path = join13(root, "package.json");
27291
- if (!existsSync11(path))
27292
- return { dependencies: [], devDependencies: [] };
27504
+ if (!existsSync11(path)) {
27505
+ return { dependencies: [], devDependencies: [], optionalDependencies: [], peerDependencies: [] };
27506
+ }
27293
27507
  try {
27294
27508
  const parsed = JSON.parse(await readFile4(path, "utf8"));
27295
27509
  return {
27296
27510
  dependencies: Object.keys(parsed.dependencies ?? {}).sort(),
27297
- devDependencies: Object.keys(parsed.devDependencies ?? {}).sort()
27511
+ devDependencies: Object.keys(parsed.devDependencies ?? {}).sort(),
27512
+ optionalDependencies: Object.keys(parsed.optionalDependencies ?? {}).sort(),
27513
+ peerDependencies: Object.keys(parsed.peerDependencies ?? {}).sort()
27298
27514
  };
27299
27515
  } catch {
27300
- return { dependencies: [], devDependencies: [] };
27516
+ return { dependencies: [], devDependencies: [], optionalDependencies: [], peerDependencies: [] };
27301
27517
  }
27302
27518
  }
27303
27519
  function formatModuleContextPack(pack) {
@@ -27398,6 +27614,9 @@ async function runContext(args) {
27398
27614
  ` modules: ${project.modules.map((module) => module.name).join(", ") || "-"}`,
27399
27615
  ` external tokens: ${project.externalTokens.join(", ") || "-"}`,
27400
27616
  ` dependencies: ${project.allowedDependencies.dependencies.join(", ") || "-"}`,
27617
+ ` dev dependencies: ${project.allowedDependencies.devDependencies.join(", ") || "-"}`,
27618
+ ` optional dependencies: ${project.allowedDependencies.optionalDependencies.join(", ") || "-"}`,
27619
+ ` peer dependencies: ${project.allowedDependencies.peerDependencies.join(", ") || "-"}`,
27401
27620
  ...project.diagnostics.map(formatDiagnostic)
27402
27621
  ].join(`
27403
27622
  `));
@@ -32364,7 +32583,7 @@ async function runProjectDeployment(http, args) {
32364
32583
  // package.json
32365
32584
  var package_default = {
32366
32585
  name: "@supacloud/cli",
32367
- version: "0.103.6",
32586
+ version: "0.104.0",
32368
32587
  description: "Project-scoped CLI for SupaCloud users",
32369
32588
  type: "module",
32370
32589
  main: "./dist/index.js",
@@ -32416,19 +32635,19 @@ var package_default = {
32416
32635
  directory: "packages/cli"
32417
32636
  },
32418
32637
  dependencies: {
32419
- "@supacloud/compiler": "^0.47.2",
32420
- "@supacloud/delivery": "0.19.3",
32421
- "@supacloud/db": "0.18.3",
32638
+ "@supacloud/compiler": "^0.48.0",
32639
+ "@supacloud/delivery": "0.20.0",
32640
+ "@supacloud/db": "0.19.0",
32422
32641
  typebox: "1.3.37"
32423
32642
  },
32424
32643
  overrides: {
32425
- "@supacloud/contracts": "0.11.4",
32426
- "@supacloud/delivery": "0.19.3",
32644
+ "@supacloud/contracts": "0.12.0",
32645
+ "@supacloud/delivery": "0.20.0",
32427
32646
  "@graphql-tools/utils": "^12.0.4"
32428
32647
  },
32429
32648
  devDependencies: {
32430
32649
  "@supabase/supabase-js": "2.117.3",
32431
- "@supacloud/elysia": "0.33.4",
32650
+ "@supacloud/elysia": "0.34.0",
32432
32651
  "@types/bun": "^1.4.3",
32433
32652
  effect: "4.0.5",
32434
32653
  elysia: "2.0.0-beta.32",
@@ -1,7 +1,6 @@
1
1
  /**
2
- * An editable vertical feature slice. The generated ports are deliberately
3
- * fail-closed: database access, authorization and durable command adapters
4
- * remain application-owned and cannot be fabricated by the scaffold.
2
+ * An editable vertical feature slice. Persistence remains application-owned,
3
+ * while every generated port receives the trusted framework request context.
5
4
  */
6
5
  export declare function featureScaffold(name: string, className: string, governance: {
7
6
  permission: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.103.6",
3
+ "version": "0.104.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.47.2",
56
- "@supacloud/delivery": "0.19.3",
57
- "@supacloud/db": "0.18.3",
55
+ "@supacloud/compiler": "^0.48.0",
56
+ "@supacloud/delivery": "0.20.0",
57
+ "@supacloud/db": "0.19.0",
58
58
  "typebox": "1.3.37"
59
59
  },
60
60
  "overrides": {
61
- "@supacloud/contracts": "0.11.4",
62
- "@supacloud/delivery": "0.19.3",
61
+ "@supacloud/contracts": "0.12.0",
62
+ "@supacloud/delivery": "0.20.0",
63
63
  "@graphql-tools/utils": "^12.0.4"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@supabase/supabase-js": "2.117.3",
67
- "@supacloud/elysia": "0.33.4",
67
+ "@supacloud/elysia": "0.34.0",
68
68
  "@types/bun": "^1.4.3",
69
69
  "effect": "4.0.5",
70
70
  "elysia": "2.0.0-beta.32",