@supacloud/cli 0.103.7 → 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 +276 -58
- package/dist/shared/tools/app-feature.d.ts +2 -3
- package/package.json +7 -7
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 =
|
|
9408
|
+
var FRONTEND_ARCHIVE_MAX_BYTES = 500 * 1024 * 1024;
|
|
9409
9409
|
var FRONTEND_ARCHIVE_MAX_FILES = 1e4;
|
|
9410
|
-
var FRONTEND_ARCHIVE_MAX_SOURCE_BYTES =
|
|
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 >
|
|
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) >
|
|
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.
|
|
21579
|
+
version: "0.48.0"
|
|
21578
21580
|
},
|
|
21579
21581
|
app: {
|
|
21580
|
-
version: "0.
|
|
21582
|
+
version: "0.29.0",
|
|
21581
21583
|
dependencies: {
|
|
21582
21584
|
rxjs: "7.8.2"
|
|
21583
21585
|
}
|
|
21584
21586
|
},
|
|
21585
21587
|
elysia: {
|
|
21586
|
-
version: "0.
|
|
21588
|
+
version: "0.34.0"
|
|
21587
21589
|
},
|
|
21588
21590
|
commands: {
|
|
21589
|
-
version: "0.
|
|
21591
|
+
version: "0.10.0"
|
|
21590
21592
|
},
|
|
21591
21593
|
contracts: {
|
|
21592
|
-
version: "0.
|
|
21594
|
+
version: "0.12.0"
|
|
21593
21595
|
},
|
|
21594
21596
|
db: {
|
|
21595
|
-
version: "0.
|
|
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.
|
|
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.
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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?:
|
|
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:
|
|
26467
|
-
const service = new ${className}Service(undefined, {
|
|
26468
|
-
async approve(input) {
|
|
26469
|
-
expect(
|
|
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(
|
|
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,7 +26882,7 @@ 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("\\", "/");
|
|
@@ -26708,37 +26907,50 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
|
|
|
26708
26907
|
...(owner.jobs ?? []).flatMap((item) => (item.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])),
|
|
26709
26908
|
...owner.controllers.flatMap((item) => item.routes.flatMap((route) => (route.aspects ?? []).flatMap((aspect) => aspect.file ? [aspect.file] : [])))
|
|
26710
26909
|
].filter((file) => typeof file === "string");
|
|
26711
|
-
const directories = [...new Set(sources.map((file) => dirname7(resolve16(sourceRoot, file))))].sort();
|
|
26712
|
-
const tests = new Set;
|
|
26713
26910
|
const manual = [];
|
|
26714
|
-
const
|
|
26715
|
-
for (const
|
|
26716
|
-
|
|
26717
|
-
|
|
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");
|
|
26718
26916
|
continue;
|
|
26719
26917
|
}
|
|
26720
|
-
const actual = await realpath3(
|
|
26918
|
+
const actual = await realpath3(requested).catch((error) => {
|
|
26721
26919
|
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
26722
26920
|
return null;
|
|
26723
26921
|
throw error;
|
|
26724
26922
|
});
|
|
26725
26923
|
if (actual === null) {
|
|
26726
|
-
manual.push(`Source
|
|
26924
|
+
manual.push(`Source file is missing: ${relative5(requestedRoot, requested).replaceAll("\\", "/")}`);
|
|
26727
26925
|
continue;
|
|
26728
26926
|
}
|
|
26729
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))) {
|
|
26730
26942
|
manual.push("A source directory is outside the project; select its owning project explicitly");
|
|
26731
26943
|
continue;
|
|
26732
26944
|
}
|
|
26733
26945
|
const local = [];
|
|
26734
|
-
for (const entry of await readdir4(
|
|
26946
|
+
for (const entry of await readdir4(directory, { withFileTypes: true })) {
|
|
26735
26947
|
if (entry.isFile() && /\.(test|spec)\.[cm]?[jt]sx?$/.test(entry.name)) {
|
|
26736
|
-
const file = relative5(project, resolve16(
|
|
26948
|
+
const file = relative5(project, resolve16(directory, entry.name)).replaceAll("\\", "/");
|
|
26737
26949
|
tests.add(file);
|
|
26738
26950
|
local.push(file);
|
|
26739
26951
|
}
|
|
26740
26952
|
}
|
|
26741
|
-
const path = relative5(project,
|
|
26953
|
+
const path = relative5(project, directory).replaceAll("\\", "/") || ".";
|
|
26742
26954
|
coverage.push({ directory: path, tests: local.sort(), basis: "colocated-not-proven" });
|
|
26743
26955
|
if (!local.length)
|
|
26744
26956
|
manual.push(`No colocated tests for source directory: ${path}`);
|
|
@@ -26753,7 +26965,7 @@ async function createVerificationPlan(root, graph, target, sourceRoot = root) {
|
|
|
26753
26965
|
source: "declaration",
|
|
26754
26966
|
target: owner.name,
|
|
26755
26967
|
ready: manual.length === 0,
|
|
26756
|
-
files: [...new Set(
|
|
26968
|
+
files: [...new Set(validatedSources.map((file) => relative5(project, file).replaceAll("\\", "/")))].sort(),
|
|
26757
26969
|
tests: selected.slice(0, 32),
|
|
26758
26970
|
commands: selected.length <= 32 ? [
|
|
26759
26971
|
...selected.map((file) => ["bun", "test", `./${file}`]),
|
|
@@ -27289,16 +27501,19 @@ async function projectCompileConfig(args) {
|
|
|
27289
27501
|
}
|
|
27290
27502
|
async function readAllowedDependencies(root) {
|
|
27291
27503
|
const path = join13(root, "package.json");
|
|
27292
|
-
if (!existsSync11(path))
|
|
27293
|
-
return { dependencies: [], devDependencies: [] };
|
|
27504
|
+
if (!existsSync11(path)) {
|
|
27505
|
+
return { dependencies: [], devDependencies: [], optionalDependencies: [], peerDependencies: [] };
|
|
27506
|
+
}
|
|
27294
27507
|
try {
|
|
27295
27508
|
const parsed = JSON.parse(await readFile4(path, "utf8"));
|
|
27296
27509
|
return {
|
|
27297
27510
|
dependencies: Object.keys(parsed.dependencies ?? {}).sort(),
|
|
27298
|
-
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()
|
|
27299
27514
|
};
|
|
27300
27515
|
} catch {
|
|
27301
|
-
return { dependencies: [], devDependencies: [] };
|
|
27516
|
+
return { dependencies: [], devDependencies: [], optionalDependencies: [], peerDependencies: [] };
|
|
27302
27517
|
}
|
|
27303
27518
|
}
|
|
27304
27519
|
function formatModuleContextPack(pack) {
|
|
@@ -27399,6 +27614,9 @@ async function runContext(args) {
|
|
|
27399
27614
|
` modules: ${project.modules.map((module) => module.name).join(", ") || "-"}`,
|
|
27400
27615
|
` external tokens: ${project.externalTokens.join(", ") || "-"}`,
|
|
27401
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(", ") || "-"}`,
|
|
27402
27620
|
...project.diagnostics.map(formatDiagnostic)
|
|
27403
27621
|
].join(`
|
|
27404
27622
|
`));
|
|
@@ -32365,7 +32583,7 @@ async function runProjectDeployment(http, args) {
|
|
|
32365
32583
|
// package.json
|
|
32366
32584
|
var package_default = {
|
|
32367
32585
|
name: "@supacloud/cli",
|
|
32368
|
-
version: "0.
|
|
32586
|
+
version: "0.104.0",
|
|
32369
32587
|
description: "Project-scoped CLI for SupaCloud users",
|
|
32370
32588
|
type: "module",
|
|
32371
32589
|
main: "./dist/index.js",
|
|
@@ -32417,19 +32635,19 @@ var package_default = {
|
|
|
32417
32635
|
directory: "packages/cli"
|
|
32418
32636
|
},
|
|
32419
32637
|
dependencies: {
|
|
32420
|
-
"@supacloud/compiler": "^0.
|
|
32421
|
-
"@supacloud/delivery": "0.
|
|
32422
|
-
"@supacloud/db": "0.
|
|
32638
|
+
"@supacloud/compiler": "^0.48.0",
|
|
32639
|
+
"@supacloud/delivery": "0.20.0",
|
|
32640
|
+
"@supacloud/db": "0.19.0",
|
|
32423
32641
|
typebox: "1.3.37"
|
|
32424
32642
|
},
|
|
32425
32643
|
overrides: {
|
|
32426
|
-
"@supacloud/contracts": "0.
|
|
32427
|
-
"@supacloud/delivery": "0.
|
|
32644
|
+
"@supacloud/contracts": "0.12.0",
|
|
32645
|
+
"@supacloud/delivery": "0.20.0",
|
|
32428
32646
|
"@graphql-tools/utils": "^12.0.4"
|
|
32429
32647
|
},
|
|
32430
32648
|
devDependencies: {
|
|
32431
32649
|
"@supabase/supabase-js": "2.117.3",
|
|
32432
|
-
"@supacloud/elysia": "0.
|
|
32650
|
+
"@supacloud/elysia": "0.34.0",
|
|
32433
32651
|
"@types/bun": "^1.4.3",
|
|
32434
32652
|
effect: "4.0.5",
|
|
32435
32653
|
elysia: "2.0.0-beta.32",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* An editable vertical feature slice.
|
|
3
|
-
*
|
|
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.
|
|
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.
|
|
56
|
-
"@supacloud/delivery": "0.
|
|
57
|
-
"@supacloud/db": "0.
|
|
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.
|
|
62
|
-
"@supacloud/delivery": "0.
|
|
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.
|
|
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",
|