@supacloud/cli 0.95.1 → 0.96.1
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 +13 -4
- package/dist/index.js +318 -136
- package/dist/shared/deployment-upgrade.d.ts +1 -1
- package/dist/shared/tools/app-starter-attachment-worker.d.ts +1 -1
- package/dist/shared/tools/app-starter-database.d.ts +3 -1
- package/dist/shared/tools/app-starter-upload.d.ts +1 -1
- package/dist/shared/tools/deploy-tools.d.ts +1 -0
- package/dist/shared/tools/developer-experience-tools.d.ts +1 -1
- package/dist/shared/tools/frontend-release-control.d.ts +1 -0
- package/dist/tool-runtime.js +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -26,11 +26,15 @@ supacloud --env test deploy
|
|
|
26
26
|
overwriting an existing project configuration. `env list` and `env check` are
|
|
27
27
|
local-only, redact credentials, and provide stable JSON with `--json` for CI or
|
|
28
28
|
AI tooling. The real `.env.supacloud.*` files are added to `.gitignore`.
|
|
29
|
-
Fill in
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
Fill in the Management credentials and project/environment in the selected profile.
|
|
30
|
+
Static frontends need only `frontend` build configuration; no application binding
|
|
31
|
+
or backend delivery build is required. Set `SUPACLOUD_APPLICATION_ID` only for
|
|
32
|
+
executable application delivery. Upgrade the CLI with
|
|
32
33
|
`npm install -g @supacloud/cli@latest` and upgrade the SupaCloud server together.
|
|
33
34
|
`deploy` checks the server protocol before building or uploading.
|
|
35
|
+
Use `frontend.name` to select exactly one named site in each environment when
|
|
36
|
+
deployment IDs differ. A missing or ambiguous name fails before upload; it never
|
|
37
|
+
falls back to another site.
|
|
34
38
|
|
|
35
39
|
## Application Starter
|
|
36
40
|
|
|
@@ -645,7 +649,12 @@ FA-owned business features are documented in
|
|
|
645
649
|
and `--dry_run` are removed. Put build settings in `supacloud.json`; use
|
|
646
650
|
`app deploy-plan` for read-only review. Historical project-specific manifests
|
|
647
651
|
and standalone Function targets must move to application delivery configuration.
|
|
648
|
-
The CLI rejects them rather than falling back to a partial Web deployment.
|
|
652
|
+
The CLI rejects them rather than silently falling back to a partial Web deployment.
|
|
653
|
+
For an intentional Web-only release in a mixed repository, use
|
|
654
|
+
`supacloud --env test deploy --assets_only`. This preserves historical manifests,
|
|
655
|
+
Functions, schedules and migrations without invoking application compilation.
|
|
656
|
+
Static publication reuses immutable archives, pre-upload CAS, mutation receipts
|
|
657
|
+
and exact post-activation readback. It never retries a conflicting activation.
|
|
649
658
|
|
|
650
659
|
The legacy `.env` fallback is unclassified and therefore does not enable the
|
|
651
660
|
production confirmation gate. Production automation must select a `prod` or
|
package/dist/index.js
CHANGED
|
@@ -8767,7 +8767,7 @@ function outcomeUnknownGuidance(text) {
|
|
|
8767
8767
|
}
|
|
8768
8768
|
|
|
8769
8769
|
// src/shared/deployment-upgrade.ts
|
|
8770
|
-
var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment
|
|
8770
|
+
var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
|
|
8771
8771
|
var removedFrontendActions = new Set([
|
|
8772
8772
|
"deploy_git",
|
|
8773
8773
|
"deploy_upload",
|
|
@@ -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(),
|
|
@@ -16343,6 +16350,39 @@ async function getFrontendRelease(http, projectRef, deploymentId, releaseId) {
|
|
|
16343
16350
|
}
|
|
16344
16351
|
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
|
|
16345
16352
|
}
|
|
16353
|
+
async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath) {
|
|
16354
|
+
const endpoint = releaseEndpoint(projectRef, deploymentId);
|
|
16355
|
+
const archive = await verifiedArchive(archivePath);
|
|
16356
|
+
let response;
|
|
16357
|
+
try {
|
|
16358
|
+
response = await http.postBinary(endpoint, {
|
|
16359
|
+
stream: archiveStream(archive),
|
|
16360
|
+
byteLength: archive.sizeBytes
|
|
16361
|
+
}, {
|
|
16362
|
+
contentType: FRONTEND_ARCHIVE_CONTENT_TYPE,
|
|
16363
|
+
contentLength: archive.sizeBytes,
|
|
16364
|
+
contentSha256: archive.sha256,
|
|
16365
|
+
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
16366
|
+
timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
|
|
16367
|
+
responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
|
|
16368
|
+
});
|
|
16369
|
+
} finally {
|
|
16370
|
+
await archive.handle.close();
|
|
16371
|
+
}
|
|
16372
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
|
|
16373
|
+
if (!release) {
|
|
16374
|
+
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
16375
|
+
return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
|
|
16376
|
+
}
|
|
16377
|
+
return uploadReadback(http, projectRef, deploymentId, archive.sha256, response.status);
|
|
16378
|
+
}
|
|
16379
|
+
const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
16380
|
+
const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef, deploymentId, releaseId: release.release_id }) : null;
|
|
16381
|
+
if (!verified || verified.tree_sha256 !== release.tree_sha256) {
|
|
16382
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", readback.status);
|
|
16383
|
+
}
|
|
16384
|
+
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release: verified });
|
|
16385
|
+
}
|
|
16346
16386
|
async function uploadProjectWebRelease(http, projectRef, environmentId, deploymentId, archivePath) {
|
|
16347
16387
|
const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/environments/${encodeURIComponent(environmentId)}/deployments/web-release?deployment_id=${encodeURIComponent(deploymentId)}`;
|
|
16348
16388
|
const archive = await verifiedArchive(archivePath);
|
|
@@ -16397,6 +16437,16 @@ async function uploadProjectWebRelease(http, projectRef, environmentId, deployme
|
|
|
16397
16437
|
await archive.handle.close();
|
|
16398
16438
|
}
|
|
16399
16439
|
}
|
|
16440
|
+
async function uploadReadback(http, projectRef, deploymentId, releaseId, uploadStatus) {
|
|
16441
|
+
const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
|
|
16442
|
+
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
16443
|
+
});
|
|
16444
|
+
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
|
|
16445
|
+
if (!release) {
|
|
16446
|
+
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
|
|
16447
|
+
}
|
|
16448
|
+
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
|
|
16449
|
+
}
|
|
16400
16450
|
function stableJson(candidate) {
|
|
16401
16451
|
if (candidate === null || typeof candidate !== "object")
|
|
16402
16452
|
return JSON.stringify(candidate);
|
|
@@ -16516,6 +16566,57 @@ async function getActiveFrontendRelease(http, projectRef, deploymentId) {
|
|
|
16516
16566
|
const snapshot = await readActiveFrontendRelease(http, projectRef, deploymentId);
|
|
16517
16567
|
return "failure" in snapshot ? snapshot.failure : toolResponse(snapshot.inventory);
|
|
16518
16568
|
}
|
|
16569
|
+
async function publishFrontendRelease(http, projectRef, deploymentId, archivePath, expectedTreeSha256) {
|
|
16570
|
+
const authority = await readActiveFrontendRelease(http, projectRef, deploymentId);
|
|
16571
|
+
if ("failure" in authority)
|
|
16572
|
+
return authority.failure;
|
|
16573
|
+
const inventory = authority.inventory;
|
|
16574
|
+
const current = inventory.releases[0];
|
|
16575
|
+
if (current?.tree_sha256 === expectedTreeSha256) {
|
|
16576
|
+
return toolResponse({
|
|
16577
|
+
operation: "frontend.deploy",
|
|
16578
|
+
project_ref: projectRef,
|
|
16579
|
+
deployment_id: deploymentId,
|
|
16580
|
+
active_release_id: inventory.active_release_id,
|
|
16581
|
+
activation_id: inventory.active_activation_id,
|
|
16582
|
+
release: current,
|
|
16583
|
+
unchanged: true
|
|
16584
|
+
});
|
|
16585
|
+
}
|
|
16586
|
+
const uploaded = await uploadFrontendRelease(http, projectRef, deploymentId, archivePath);
|
|
16587
|
+
if (uploaded.isError)
|
|
16588
|
+
return uploaded;
|
|
16589
|
+
const value = JSON.parse(uploaded.content[0].text);
|
|
16590
|
+
const envelope = value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
16591
|
+
const candidate = envelope?.release;
|
|
16592
|
+
const release = candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
16593
|
+
if (!release || release.tree_sha256 !== expectedTreeSha256 || typeof release.release_id !== "string" || !RELEASE_ID_PATTERN.test(release.release_id)) {
|
|
16594
|
+
return releaseFailure("frontend.deploy", "INVALID_RESPONSE", 200);
|
|
16595
|
+
}
|
|
16596
|
+
const mutationId = crypto.randomUUID();
|
|
16597
|
+
const activated = await activateFrontendRelease(http, {
|
|
16598
|
+
projectRef,
|
|
16599
|
+
deploymentId,
|
|
16600
|
+
releaseId: release.release_id,
|
|
16601
|
+
mutationId,
|
|
16602
|
+
expectedActiveReleaseId: inventory.active_release_id ?? "absent",
|
|
16603
|
+
expectedActivationId: inventory.active_activation_id ?? "absent"
|
|
16604
|
+
});
|
|
16605
|
+
const activatedPayload = JSON.parse(activated.content[0].text);
|
|
16606
|
+
const active = activatedPayload.release;
|
|
16607
|
+
const treeMismatch = !activated.isError && (!active || typeof active !== "object" || !("tree_sha256" in active) || active.tree_sha256 !== expectedTreeSha256);
|
|
16608
|
+
return {
|
|
16609
|
+
...toolResponse({
|
|
16610
|
+
...treeMismatch ? { ok: false, error: { code: "OUTCOME_UNKNOWN", http_status: 200 } } : activatedPayload,
|
|
16611
|
+
operation: "frontend.deploy",
|
|
16612
|
+
mutation_id: mutationId,
|
|
16613
|
+
target_release_id: release.release_id,
|
|
16614
|
+
previous_release_id: inventory.active_release_id,
|
|
16615
|
+
previous_activation_id: inventory.active_activation_id
|
|
16616
|
+
}),
|
|
16617
|
+
...activated.isError || treeMismatch ? { isError: true } : {}
|
|
16618
|
+
};
|
|
16619
|
+
}
|
|
16519
16620
|
async function rollbackFrontendRelease(http, projectRef, deploymentId, releaseId) {
|
|
16520
16621
|
if (releaseId !== undefined && !RELEASE_ID_PATTERN.test(releaseId)) {
|
|
16521
16622
|
throw new Error("'release_id' must be a SHA-256 digest");
|
|
@@ -21099,7 +21200,7 @@ var starter_metadata_default = {
|
|
|
21099
21200
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
21100
21201
|
packages: {
|
|
21101
21202
|
compiler: {
|
|
21102
|
-
version: "0.
|
|
21203
|
+
version: "0.46.0"
|
|
21103
21204
|
},
|
|
21104
21205
|
app: {
|
|
21105
21206
|
version: "0.26.1",
|
|
@@ -21117,7 +21218,7 @@ var starter_metadata_default = {
|
|
|
21117
21218
|
version: "0.8.1"
|
|
21118
21219
|
},
|
|
21119
21220
|
db: {
|
|
21120
|
-
version: "0.
|
|
21221
|
+
version: "0.17.0",
|
|
21121
21222
|
peerDependencies: {
|
|
21122
21223
|
"drizzle-orm": "1.0.0-rc.5-169397b"
|
|
21123
21224
|
},
|
|
@@ -21126,7 +21227,7 @@ var starter_metadata_default = {
|
|
|
21126
21227
|
}
|
|
21127
21228
|
},
|
|
21128
21229
|
sdk: {
|
|
21129
|
-
version: "0.
|
|
21230
|
+
version: "0.42.0",
|
|
21130
21231
|
peerDependencies: {
|
|
21131
21232
|
"@supabase/supabase-js": "^2.117.3"
|
|
21132
21233
|
}
|
|
@@ -22135,7 +22236,11 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
|
|
|
22135
22236
|
|
|
22136
22237
|
// src/shared/tools/app-starter-attachment-worker.ts
|
|
22137
22238
|
var STARTER_ATTACHMENT_WORKER = `import { createWorker, type CompiledModule } from "@supacloud/elysia";
|
|
22138
|
-
import
|
|
22239
|
+
import {
|
|
22240
|
+
createSupaCloudWorkflowTransport,
|
|
22241
|
+
type SupaCloudWorkflowClaim,
|
|
22242
|
+
type SupaCloudWorkflowsClient,
|
|
22243
|
+
} from "@supacloud/js";
|
|
22139
22244
|
import { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from "./review-attachments";
|
|
22140
22245
|
|
|
22141
22246
|
export interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {
|
|
@@ -22160,9 +22265,6 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22160
22265
|
halted = true;
|
|
22161
22266
|
failure.reject(new Error("Attachment worker requires recovery"));
|
|
22162
22267
|
};
|
|
22163
|
-
const attempt = (claim: SupaCloudWorkflowClaim) => ({
|
|
22164
|
-
stepId: claim.stepId, messageId: claim.messageId, attempt: claim.attempt, workerId: claim.workerId,
|
|
22165
|
-
});
|
|
22166
22268
|
async function ownedClaim(claim: SupaCloudWorkflowClaim) {
|
|
22167
22269
|
if (claim.workflowName !== "review.verify-attachment" || claim.workflowVersion !== "1"
|
|
22168
22270
|
|| claim.stepKey !== "verify" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {
|
|
@@ -22178,6 +22280,9 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22178
22280
|
));
|
|
22179
22281
|
if (!Array.isArray(bound) || bound.length !== 1) throw new Error("Attachment workflow ownership mismatch");
|
|
22180
22282
|
}
|
|
22283
|
+
const workflowTransport = createSupaCloudWorkflowTransport({
|
|
22284
|
+
workflows, workerId, visibilityTimeoutSeconds: 120, retryDelaySeconds: 5, acceptClaim: ownedClaim,
|
|
22285
|
+
});
|
|
22181
22286
|
const worker = createWorker({
|
|
22182
22287
|
modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,
|
|
22183
22288
|
pollIntervalMs: options.pollIntervalMs ?? 1000,
|
|
@@ -22186,11 +22291,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22186
22291
|
async claim(signal): Promise<SupaCloudWorkflowClaim | null> {
|
|
22187
22292
|
if (halted || signal.aborted) return null;
|
|
22188
22293
|
try {
|
|
22189
|
-
|
|
22190
|
-
if (signal.aborted || halted) return null;
|
|
22191
|
-
if (!claim || claim.status !== "claimed") return null;
|
|
22192
|
-
await ownedClaim(claim);
|
|
22193
|
-
return claim;
|
|
22294
|
+
return await workflowTransport.claim(signal);
|
|
22194
22295
|
} catch {
|
|
22195
22296
|
if (!signal.aborted) fatal();
|
|
22196
22297
|
throw new Error("Attachment workflow claim failed");
|
|
@@ -22198,9 +22299,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22198
22299
|
},
|
|
22199
22300
|
async ack(claim, output) {
|
|
22200
22301
|
try {
|
|
22201
|
-
|
|
22202
|
-
// The compiled Job validates output; Workflow SDK validates the receipt.
|
|
22203
|
-
await workflows.complete({ ...attempt(claim), runOutput: { ...output } });
|
|
22302
|
+
await workflowTransport.ack(claim, output);
|
|
22204
22303
|
} catch {
|
|
22205
22304
|
fatal();
|
|
22206
22305
|
throw new Error("Attachment workflow completion is unconfirmed");
|
|
@@ -22208,9 +22307,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22208
22307
|
},
|
|
22209
22308
|
async fail(claim) {
|
|
22210
22309
|
try {
|
|
22211
|
-
|
|
22212
|
-
if (claim.attempt < claim.maxAttempts) await workflows.retry({ ...request, delaySeconds: 5 });
|
|
22213
|
-
else await workflows.fail(request);
|
|
22310
|
+
await workflowTransport.fail(claim, "Attachment verification failed");
|
|
22214
22311
|
} catch {
|
|
22215
22312
|
fatal();
|
|
22216
22313
|
throw new Error("Attachment workflow failure receipt is unconfirmed");
|
|
@@ -22394,9 +22491,11 @@ export class ReviewUploadsController {
|
|
|
22394
22491
|
var STARTER_UPLOAD_SCHEMA = `-- Apply explicitly with the migration owner.
|
|
22395
22492
|
ALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;
|
|
22396
22493
|
GRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;
|
|
22494
|
+
DROP POLICY IF EXISTS starter_member_read ON public.starter_members;
|
|
22397
22495
|
CREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated
|
|
22398
22496
|
USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());
|
|
22399
22497
|
GRANT SELECT ON public.starter_reviews TO authenticated;
|
|
22498
|
+
DROP POLICY IF EXISTS starter_review_read ON public.starter_reviews;
|
|
22400
22499
|
CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated
|
|
22401
22500
|
USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (
|
|
22402
22501
|
SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id
|
|
@@ -22404,11 +22503,14 @@ CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authen
|
|
|
22404
22503
|
));
|
|
22405
22504
|
INSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)
|
|
22406
22505
|
VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);
|
|
22506
|
+
DROP POLICY IF EXISTS starter_attachment_read ON storage.objects;
|
|
22407
22507
|
CREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated
|
|
22408
22508
|
USING (bucket_id='review-attachments');
|
|
22509
|
+
DROP POLICY IF EXISTS starter_attachment_insert ON storage.objects;
|
|
22409
22510
|
CREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated
|
|
22410
22511
|
WITH CHECK (bucket_id='review-attachments');
|
|
22411
22512
|
-- Restrictive fences also apply when the host already has permissive Storage policies.
|
|
22513
|
+
DROP POLICY IF EXISTS starter_attachment_owner_fence ON storage.objects;
|
|
22412
22514
|
CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated
|
|
22413
22515
|
USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (
|
|
22414
22516
|
SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id
|
|
@@ -22421,8 +22523,10 @@ CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE F
|
|
|
22421
22523
|
WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')
|
|
22422
22524
|
AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve
|
|
22423
22525
|
)));
|
|
22526
|
+
DROP POLICY IF EXISTS starter_attachment_update_fence ON storage.objects;
|
|
22424
22527
|
CREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated
|
|
22425
22528
|
USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');
|
|
22529
|
+
DROP POLICY IF EXISTS starter_attachment_delete_fence ON storage.objects;
|
|
22426
22530
|
CREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated
|
|
22427
22531
|
USING (bucket_id<>'review-attachments');
|
|
22428
22532
|
`;
|
|
@@ -22570,6 +22674,11 @@ function renderStarterRuntimeRolesSchema(httpRole, workerRole) {
|
|
|
22570
22674
|
CREATE ROLE ${httpRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
|
22571
22675
|
CREATE ROLE ${workerRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
|
22572
22676
|
GRANT USAGE ON SCHEMA public TO ${httpRole}, ${workerRole};
|
|
22677
|
+
ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
|
|
22678
|
+
ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
|
|
22679
|
+
ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
|
|
22680
|
+
ALTER TABLE public.starter_attachments ENABLE ROW LEVEL SECURITY;
|
|
22681
|
+
ALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;
|
|
22573
22682
|
GRANT SELECT ON public.starter_application, public.starter_members,
|
|
22574
22683
|
public.starter_reviews, public.starter_attachments TO ${httpRole}, ${workerRole};
|
|
22575
22684
|
|
|
@@ -22583,34 +22692,49 @@ GRANT UPDATE(id) ON public.starter_reviews TO ${workerRole};
|
|
|
22583
22692
|
GRANT INSERT ON public.starter_attachments TO ${httpRole};
|
|
22584
22693
|
GRANT SELECT,INSERT ON public.starter_attachment_results TO ${workerRole};
|
|
22585
22694
|
|
|
22695
|
+
DROP POLICY IF EXISTS starter_backend_binding_read ON public.starter_application;
|
|
22586
22696
|
CREATE POLICY starter_backend_binding_read ON public.starter_application FOR SELECT
|
|
22587
22697
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22698
|
+
DROP POLICY IF EXISTS starter_backend_binding_lock ON public.starter_application;
|
|
22588
22699
|
CREATE POLICY starter_backend_binding_lock ON public.starter_application FOR UPDATE
|
|
22589
22700
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22701
|
+
DROP POLICY IF EXISTS starter_backend_binding_immutable ON public.starter_application;
|
|
22590
22702
|
CREATE POLICY starter_backend_binding_immutable ON public.starter_application AS RESTRICTIVE FOR UPDATE
|
|
22591
22703
|
TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
|
|
22704
|
+
DROP POLICY IF EXISTS starter_backend_member_read ON public.starter_members;
|
|
22592
22705
|
CREATE POLICY starter_backend_member_read ON public.starter_members FOR SELECT
|
|
22593
22706
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22707
|
+
DROP POLICY IF EXISTS starter_backend_member_lock ON public.starter_members;
|
|
22594
22708
|
CREATE POLICY starter_backend_member_lock ON public.starter_members FOR UPDATE
|
|
22595
22709
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22710
|
+
DROP POLICY IF EXISTS starter_backend_member_immutable ON public.starter_members;
|
|
22596
22711
|
CREATE POLICY starter_backend_member_immutable ON public.starter_members AS RESTRICTIVE FOR UPDATE
|
|
22597
22712
|
TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
|
|
22713
|
+
DROP POLICY IF EXISTS starter_backend_review_read ON public.starter_reviews;
|
|
22598
22714
|
CREATE POLICY starter_backend_review_read ON public.starter_reviews FOR SELECT
|
|
22599
22715
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22716
|
+
DROP POLICY IF EXISTS starter_backend_review_update ON public.starter_reviews;
|
|
22600
22717
|
CREATE POLICY starter_backend_review_update ON public.starter_reviews FOR UPDATE
|
|
22601
22718
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22719
|
+
DROP POLICY IF EXISTS starter_worker_review_immutable ON public.starter_reviews;
|
|
22602
22720
|
CREATE POLICY starter_worker_review_immutable ON public.starter_reviews AS RESTRICTIVE FOR UPDATE
|
|
22603
22721
|
TO ${workerRole} USING (true) WITH CHECK (false);
|
|
22722
|
+
DROP POLICY IF EXISTS starter_backend_attachment_read ON public.starter_attachments;
|
|
22604
22723
|
CREATE POLICY starter_backend_attachment_read ON public.starter_attachments FOR SELECT
|
|
22605
22724
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22725
|
+
DROP POLICY IF EXISTS starter_backend_attachment_lock ON public.starter_attachments;
|
|
22606
22726
|
CREATE POLICY starter_backend_attachment_lock ON public.starter_attachments FOR UPDATE
|
|
22607
22727
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22728
|
+
DROP POLICY IF EXISTS starter_backend_attachment_immutable ON public.starter_attachments;
|
|
22608
22729
|
CREATE POLICY starter_backend_attachment_immutable ON public.starter_attachments AS RESTRICTIVE FOR UPDATE
|
|
22609
22730
|
TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
|
|
22731
|
+
DROP POLICY IF EXISTS starter_http_attachment_insert ON public.starter_attachments;
|
|
22610
22732
|
CREATE POLICY starter_http_attachment_insert ON public.starter_attachments FOR INSERT
|
|
22611
22733
|
TO ${httpRole} WITH CHECK (true);
|
|
22734
|
+
DROP POLICY IF EXISTS starter_worker_result_read ON public.starter_attachment_results;
|
|
22612
22735
|
CREATE POLICY starter_worker_result_read ON public.starter_attachment_results FOR SELECT
|
|
22613
22736
|
TO ${workerRole} USING (true);
|
|
22737
|
+
DROP POLICY IF EXISTS starter_worker_result_insert ON public.starter_attachment_results;
|
|
22614
22738
|
CREATE POLICY starter_worker_result_insert ON public.starter_attachment_results FOR INSERT
|
|
22615
22739
|
TO ${workerRole} WITH CHECK (true);
|
|
22616
22740
|
|
|
@@ -23001,6 +23125,80 @@ export const attachmentResults = pgTable.withRLS("starter_attachment_results", {
|
|
|
23001
23125
|
export type ReviewRow = typeof reviews.$inferSelect;
|
|
23002
23126
|
export type NewReview = typeof reviews.$inferInsert;
|
|
23003
23127
|
`;
|
|
23128
|
+
var STARTER_DATABASE_MODULES = `import { defineDatabaseModule } from "@supacloud/db";
|
|
23129
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
23130
|
+
import { application, reviews, members, attachments, attachmentResults } from "./schema";
|
|
23131
|
+
|
|
23132
|
+
const backendPolicies = [
|
|
23133
|
+
["starter_backend_binding_read", "public.starter_application", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23134
|
+
["starter_backend_binding_lock", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23135
|
+
["starter_backend_binding_immutable", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23136
|
+
["starter_backend_member_read", "public.starter_members", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23137
|
+
["starter_backend_member_lock", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23138
|
+
["starter_backend_member_immutable", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23139
|
+
["starter_backend_review_read", "public.starter_reviews", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23140
|
+
["starter_backend_review_update", "public.starter_reviews", "update", ["starter_review_http"]],
|
|
23141
|
+
["starter_worker_review_immutable", "public.starter_reviews", "update", ["starter_review_worker"]],
|
|
23142
|
+
["starter_backend_attachment_read", "public.starter_attachments", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23143
|
+
["starter_backend_attachment_lock", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23144
|
+
["starter_backend_attachment_immutable", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23145
|
+
["starter_http_attachment_insert", "public.starter_attachments", "insert", ["starter_review_http"]],
|
|
23146
|
+
["starter_worker_result_read", "public.starter_attachment_results", "select", ["starter_review_worker"]],
|
|
23147
|
+
["starter_worker_result_insert", "public.starter_attachment_results", "insert", ["starter_review_worker"]],
|
|
23148
|
+
] as const;
|
|
23149
|
+
|
|
23150
|
+
// The module is the single live-Catalog ownership contract for starter tables.
|
|
23151
|
+
export default defineDatabaseModule({
|
|
23152
|
+
name: "starter",
|
|
23153
|
+
tables: [application, reviews, members, attachments, attachmentResults].map(table => {
|
|
23154
|
+
const config = getTableConfig(table);
|
|
23155
|
+
return (config.schema ?? "public") + "." + config.name;
|
|
23156
|
+
}),
|
|
23157
|
+
policies: [
|
|
23158
|
+
...backendPolicies.map(([name, table, operation, roles]) => ({
|
|
23159
|
+
name, table, operation, roles: [...roles], source: "migrations/004-review-runtime-roles.sql",
|
|
23160
|
+
tests: ["tests/database-governance.sql"],
|
|
23161
|
+
})),
|
|
23162
|
+
{
|
|
23163
|
+
name: "starter_member_read", table: "public.starter_members", operation: "select",
|
|
23164
|
+
roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
|
|
23165
|
+
tests: ["tests/database-governance.sql"],
|
|
23166
|
+
},
|
|
23167
|
+
{
|
|
23168
|
+
name: "starter_review_read", table: "public.starter_reviews", operation: "select",
|
|
23169
|
+
roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
|
|
23170
|
+
tests: ["tests/database-governance.sql"],
|
|
23171
|
+
},
|
|
23172
|
+
],
|
|
23173
|
+
});
|
|
23174
|
+
`;
|
|
23175
|
+
var STARTER_DATABASE_TEST = `-- Read-only catalog assertions; behavioral authorization tests remain separate.
|
|
23176
|
+
BEGIN READ ONLY;
|
|
23177
|
+
DO $test$
|
|
23178
|
+
DECLARE
|
|
23179
|
+
owned text[] := ARRAY['starter_application','starter_reviews','starter_members',
|
|
23180
|
+
'starter_attachments','starter_attachment_results'];
|
|
23181
|
+
BEGIN
|
|
23182
|
+
IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
23183
|
+
WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN
|
|
23184
|
+
RAISE EXCEPTION 'All starter tables must exist with RLS enabled';
|
|
23185
|
+
END IF;
|
|
23186
|
+
IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid
|
|
23187
|
+
JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
23188
|
+
WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN
|
|
23189
|
+
RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';
|
|
23190
|
+
END IF;
|
|
23191
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')
|
|
23192
|
+
AND (rolsuper OR rolbypassrls)) THEN
|
|
23193
|
+
RAISE EXCEPTION 'Starter roles must not bypass row security';
|
|
23194
|
+
END IF;
|
|
23195
|
+
IF (SELECT count(*) FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')) <> 2 THEN
|
|
23196
|
+
RAISE EXCEPTION 'Starter runtime roles are missing';
|
|
23197
|
+
END IF;
|
|
23198
|
+
END
|
|
23199
|
+
$test$;
|
|
23200
|
+
ROLLBACK;
|
|
23201
|
+
`;
|
|
23004
23202
|
var STARTER_DATABASE_GUIDE = `
|
|
23005
23203
|
## Database Source Workflow
|
|
23006
23204
|
|
|
@@ -23012,13 +23210,14 @@ Opaque JSON still needs runtime decoding.
|
|
|
23012
23210
|
After installing dependencies, run \`bun run db:generate\` once and commit db/contracts/.
|
|
23013
23211
|
The default \`bun run check\` and build fail on stale database contracts before any
|
|
23014
23212
|
compiler regeneration. After an intentional source change, run db:generate again,
|
|
23015
|
-
review the diff, then run check.
|
|
23016
|
-
The
|
|
23017
|
-
databaseGovernance.mode = "check"
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23213
|
+
review the diff, then run check.
|
|
23214
|
+
The command starter registers databaseGovernance.moduleFile = "db/modules.ts" and
|
|
23215
|
+
keeps the default \`databaseGovernance.mode = "check"\`: compile/check/build run
|
|
23216
|
+
the offline source-contract gate without a database. Use \`bun run dev:integration\`
|
|
23217
|
+
with the CLI's explicit loopback database URL, or set databaseGovernance.mode to
|
|
23218
|
+
"enforce" with an explicitly supplied \`DATABASE_URL\`, to run SQL lint and live
|
|
23219
|
+
Catalog reconciliation. Missing connections, stale source contracts and Catalog
|
|
23220
|
+
drift fail closed; no contracts or migrations are silently regenerated.
|
|
23022
23221
|
|
|
23023
23222
|
- Maintain one schema-qualified function with explicit grants per db/functions/*.sql.
|
|
23024
23223
|
- Preserve migrations/ and its existing executor/ledger. Add forward migrations only.
|
|
@@ -23042,6 +23241,8 @@ function starterDatabaseFiles() {
|
|
|
23042
23241
|
"database.sources.json": JSON.stringify(STARTER_DATABASE_CONFIG, null, 2) + `
|
|
23043
23242
|
`,
|
|
23044
23243
|
"db/schema.ts": STARTER_DATABASE_SCHEMA,
|
|
23244
|
+
"db/modules.ts": STARTER_DATABASE_MODULES,
|
|
23245
|
+
"tests/database-governance.sql": STARTER_DATABASE_TEST,
|
|
23045
23246
|
"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
23247
|
`,
|
|
23047
23248
|
"drizzle.config.ts": `import { defineConfig } from "drizzle-kit";
|
|
@@ -23134,7 +23335,7 @@ function appStarterFiles(name) {
|
|
|
23134
23335
|
noEmit: true,
|
|
23135
23336
|
types: ["bun"]
|
|
23136
23337
|
},
|
|
23137
|
-
include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
|
|
23338
|
+
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
23339
|
}),
|
|
23139
23340
|
"bunfig.toml": `env = false
|
|
23140
23341
|
`,
|
|
@@ -23168,7 +23369,7 @@ export default defineSupacloudConfig({
|
|
|
23168
23369
|
outDir: "generated",
|
|
23169
23370
|
strict: true,
|
|
23170
23371
|
moduleBoundaryPreset: "modular-monolith",
|
|
23171
|
-
databaseGovernance: { mode: "check" },
|
|
23372
|
+
databaseGovernance: { mode: "check", moduleFile: "db/modules.ts", databaseUrlEnv: "DATABASE_URL" },
|
|
23172
23373
|
effect: {
|
|
23173
23374
|
requireRouteEffects: true,
|
|
23174
23375
|
requireErrorMappings: true,
|
|
@@ -23182,7 +23383,6 @@ export default defineSupacloudConfig({
|
|
|
23182
23383
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
23183
23384
|
disallowControllerDirectDb: true,
|
|
23184
23385
|
detectOrphanModules: true,
|
|
23185
|
-
graphql: { schema: "graphql/schema.graphql" },
|
|
23186
23386
|
commandCapabilities: { permission: true, transaction: true, idempotency: true, audit: true },
|
|
23187
23387
|
delivery: {
|
|
23188
23388
|
version: 1,
|
|
@@ -23196,50 +23396,6 @@ export default defineSupacloudConfig({
|
|
|
23196
23396
|
},
|
|
23197
23397
|
},
|
|
23198
23398
|
});
|
|
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
23399
|
`,
|
|
23244
23400
|
"REACTIVE.md": STARTER_REACTIVE_GUIDE,
|
|
23245
23401
|
"AGENTS.md": STARTER_REACTIVE_AGENTS + STARTER_DATABASE_GUIDE,
|
|
@@ -23249,11 +23405,30 @@ test("generated query client preserves its read contract without a live database
|
|
|
23249
23405
|
"scripts/dev.ts": `import { watchProject, compileOptionsFromConfig, loadSupacloudConfig } from "@supacloud/compiler";
|
|
23250
23406
|
|
|
23251
23407
|
if (process.env["APP_ENV"] !== "development") throw new Error("The demo server is development-only");
|
|
23408
|
+
const profile = process.env["SUPACLOUD_DEV_PROFILE"] === "integration" ? "integration" : "fast";
|
|
23409
|
+
if (profile === "integration") {
|
|
23410
|
+
const databaseUrl = process.env["SUPACLOUD_DEV_DATABASE_URL"];
|
|
23411
|
+
if (!databaseUrl) throw new Error("Integration requires an explicit loopback PostgreSQL database");
|
|
23412
|
+
const url = new URL(databaseUrl);
|
|
23413
|
+
if (!["postgres:", "postgresql:"].includes(url.protocol)
|
|
23414
|
+
|| !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
23415
|
+
|| url.pathname.length <= 1 || url.search || url.hash
|
|
23416
|
+
|| process.env["DATABASE_SOCKET_PATH"]
|
|
23417
|
+
|| (process.env["DATABASE_URL"] && process.env["DATABASE_URL"] !== databaseUrl)) {
|
|
23418
|
+
throw new Error("Integration requires an explicit loopback PostgreSQL database without conflicting settings");
|
|
23419
|
+
}
|
|
23420
|
+
process.env["DATABASE_URL"] = databaseUrl;
|
|
23421
|
+
}
|
|
23422
|
+
const configured = compileOptionsFromConfig(await loadSupacloudConfig());
|
|
23423
|
+
const governanceMode = profile === "integration" ? "enforce" as const : "check" as const;
|
|
23424
|
+
const options = configured.databaseGovernance
|
|
23425
|
+
? { ...configured, databaseGovernance: { ...configured.databaseGovernance, mode: governanceMode } }
|
|
23426
|
+
: configured;
|
|
23252
23427
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
23253
23428
|
let restarts = Promise.resolve();
|
|
23254
23429
|
let closing = false;
|
|
23255
23430
|
const watcher = watchProject({
|
|
23256
|
-
...
|
|
23431
|
+
...options,
|
|
23257
23432
|
writeOnError: false,
|
|
23258
23433
|
onEvent: (event) => {
|
|
23259
23434
|
if (event.type === "compile-error") console.error(event.diagnostics);
|
|
@@ -23603,7 +23778,6 @@ test("the production composition root rejects missing governance adapters", () =
|
|
|
23603
23778
|
\`\`\`sh
|
|
23604
23779
|
bun install
|
|
23605
23780
|
bun run db:generate
|
|
23606
|
-
bun run check
|
|
23607
23781
|
bun run dev
|
|
23608
23782
|
\`\`\`
|
|
23609
23783
|
|
|
@@ -23639,55 +23813,23 @@ origin. The authenticated transport rejects missing credentials and redirects
|
|
|
23639
23813
|
and does not replay a write after 401. Its underlying fetch must also be
|
|
23640
23814
|
single-attempt.
|
|
23641
23815
|
|
|
23642
|
-
Reuse the same session resolver for generated/graphql.ts when adopting queries.
|
|
23643
23816
|
The local memory demo is not this authenticated production integration.
|
|
23644
23817
|
Never use supacloud.commands or supacloud.workflows from a browser action:
|
|
23645
23818
|
those RPCs require service-role credentials. Browser business commands use
|
|
23646
23819
|
application endpoints, optionally composed through @supacloud/js/contracts.
|
|
23647
23820
|
Management API tokens and service-role keys remain on trusted servers.
|
|
23648
23821
|
|
|
23649
|
-
##
|
|
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.
|
|
23822
|
+
## Reads
|
|
23655
23823
|
|
|
23656
|
-
|
|
23657
|
-
|
|
23658
|
-
|
|
23659
|
-
|
|
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.
|
|
23824
|
+
Ordinary reads use PostgREST through the existing user-scoped
|
|
23825
|
+
@supabase/supabase-js client: client.from("starter_reviews").select("id,state,version").
|
|
23826
|
+
Use the caller's session, RLS and grants; never introduce a second session store.
|
|
23827
|
+
Business writes continue through the governed Command/RPC boundary.
|
|
23662
23828
|
|
|
23663
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
|
|
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.
|
|
23829
|
+
GraphQL is not generated or enabled by this starter. For a specific Query Contract,
|
|
23830
|
+
explicitly configure graphql.schema with a role-scoped database export and add
|
|
23831
|
+
checked query documents. Drizzle and append-only migrations remain the database
|
|
23832
|
+
authority; the export is a protocol snapshot, not a second schema source.
|
|
23691
23833
|
|
|
23692
23834
|
## Environments
|
|
23693
23835
|
|
|
@@ -23903,8 +24045,8 @@ bunx supacloud-compiler fix ./fix.json --dry-run
|
|
|
23903
24045
|
\`\`\`
|
|
23904
24046
|
|
|
23905
24047
|
Context packs include directional dependencies, relevant aspect files, diagnostics
|
|
23906
|
-
and static execution plans
|
|
23907
|
-
|
|
24048
|
+
and static execution plans, without provider values or source expressions.
|
|
24049
|
+
The optional --events example expects a trusted host
|
|
23908
24050
|
to capture approved onExecution metadata in { "version": 1, "events": [...] }.
|
|
23909
24051
|
It selects one opaque request ID, omits payloads and source expressions, and applies
|
|
23910
24052
|
input/output size limits. It does not read arbitrary logs, prove deployment versions,
|
|
@@ -24861,6 +25003,13 @@ async function runContext(args) {
|
|
|
24861
25003
|
fix: "supacloud app fix --fix <fix.json>",
|
|
24862
25004
|
graph: "supacloud app graph --format json",
|
|
24863
25005
|
explain: "supacloud app explain --target <name>"
|
|
25006
|
+
},
|
|
25007
|
+
defaults: {
|
|
25008
|
+
readTransport: "postgrest-supabase-js",
|
|
25009
|
+
writeTransport: "command-rpc",
|
|
25010
|
+
writeRetry: "disabled",
|
|
25011
|
+
externalOutcome: "receipt-reconcile",
|
|
25012
|
+
databaseSource: "drizzle-append-only"
|
|
24864
25013
|
}
|
|
24865
25014
|
};
|
|
24866
25015
|
if (args.format === "json")
|
|
@@ -28131,6 +28280,7 @@ var deployToolSchema = {
|
|
|
28131
28280
|
ref: optional(String2(), "Project ref (defaults to the linked project)"),
|
|
28132
28281
|
cwd: optional(String2(), "Project directory (default: current directory)"),
|
|
28133
28282
|
skip_build: optional(Boolean2(), "Use an existing output directory without building"),
|
|
28283
|
+
assets_only: optional(Boolean2(), "Publish only configured static assets; leave backend Functions, schedules and migrations unchanged"),
|
|
28134
28284
|
json: optional(Boolean2(), "Print only the final JSON result")
|
|
28135
28285
|
};
|
|
28136
28286
|
function record4(candidate) {
|
|
@@ -28225,6 +28375,7 @@ function deployTargetConfig(candidate, name) {
|
|
|
28225
28375
|
type: value.type,
|
|
28226
28376
|
root: optionalText(value.root, `targets.${name}.root`),
|
|
28227
28377
|
id: optionalText(value.id, `targets.${name}.id`),
|
|
28378
|
+
name: optionalText(value.name, `targets.${name}.name`),
|
|
28228
28379
|
slug: optionalText(value.slug, `targets.${name}.slug`),
|
|
28229
28380
|
buildCommand: optionalText(value.buildCommand, `targets.${name}.buildCommand`),
|
|
28230
28381
|
outputDirectory: optionalText(value.outputDirectory, `targets.${name}.outputDirectory`),
|
|
@@ -28275,6 +28426,7 @@ async function readDeployConfig(configRoot) {
|
|
|
28275
28426
|
targets: targetValues ? Object.fromEntries(Object.entries(targetValues).map(([name, value]) => [name, deployTargetConfig(value, name)])) : undefined,
|
|
28276
28427
|
frontend: frontend ? {
|
|
28277
28428
|
id: optionalText(frontend.id, "frontend.id"),
|
|
28429
|
+
name: optionalText(frontend.name, "frontend.name"),
|
|
28278
28430
|
root: optionalText(frontend.root, "frontend.root"),
|
|
28279
28431
|
buildCommand: optionalText(frontend.buildCommand, "frontend.buildCommand"),
|
|
28280
28432
|
outputDirectory: optionalText(frontend.outputDirectory, "frontend.outputDirectory"),
|
|
@@ -28337,13 +28489,22 @@ function deploymentList(candidate) {
|
|
|
28337
28489
|
throw new Error("Invalid frontend deployment list response");
|
|
28338
28490
|
return values.map(frontendDeployment);
|
|
28339
28491
|
}
|
|
28340
|
-
function selectDeployment(deployments, requestedId, projectName) {
|
|
28492
|
+
function selectDeployment(deployments, requestedId, projectName, requestedName) {
|
|
28341
28493
|
if (requestedId) {
|
|
28342
28494
|
const selected = deployments.find((deployment) => deployment.id === requestedId);
|
|
28343
28495
|
if (!selected)
|
|
28344
28496
|
throw new Error(`Frontend deployment '${requestedId}' was not found`);
|
|
28497
|
+
if (requestedName && selected.name !== requestedName) {
|
|
28498
|
+
throw new Error(`Frontend deployment '${requestedId}' does not match name '${requestedName}'`);
|
|
28499
|
+
}
|
|
28345
28500
|
return selected;
|
|
28346
28501
|
}
|
|
28502
|
+
if (requestedName) {
|
|
28503
|
+
const matches = deployments.filter((deployment) => deployment.name === requestedName);
|
|
28504
|
+
if (matches.length !== 1)
|
|
28505
|
+
throw new Error(`Expected exactly one frontend deployment named '${requestedName}'`);
|
|
28506
|
+
return matches[0];
|
|
28507
|
+
}
|
|
28347
28508
|
if (deployments.length === 1)
|
|
28348
28509
|
return deployments[0];
|
|
28349
28510
|
if (projectName) {
|
|
@@ -28361,6 +28522,7 @@ function selectFrontendTarget(configRoot, config) {
|
|
|
28361
28522
|
type: "frontend",
|
|
28362
28523
|
root: config.frontend?.root,
|
|
28363
28524
|
id: config.frontend?.id,
|
|
28525
|
+
name: config.frontend?.name,
|
|
28364
28526
|
buildCommand: config.frontend?.buildCommand,
|
|
28365
28527
|
outputDirectory: config.frontend?.outputDirectory,
|
|
28366
28528
|
buildEnv: config.frontend?.buildEnv
|
|
@@ -28525,7 +28687,7 @@ function payload(response) {
|
|
|
28525
28687
|
throw new Error("Deployment operation returned an invalid response");
|
|
28526
28688
|
return parsed;
|
|
28527
28689
|
}
|
|
28528
|
-
async function stageApplicationWebRelease(http, projectRef, environmentId, configRoot, skipBuild, json, environmentVariables) {
|
|
28690
|
+
async function stageApplicationWebRelease(http, projectRef, environmentId, configRoot, skipBuild, json, environmentVariables, assetsOnly = false) {
|
|
28529
28691
|
const target = selectFrontendTarget(configRoot, await readDeployConfig(configRoot));
|
|
28530
28692
|
if (!target)
|
|
28531
28693
|
return null;
|
|
@@ -28538,7 +28700,7 @@ async function stageApplicationWebRelease(http, projectRef, environmentId, confi
|
|
|
28538
28700
|
const listResponse = await http.get(`/v1/projects/${encodeURIComponent(projectRef)}/frontend/deployments`);
|
|
28539
28701
|
if (!listResponse.ok)
|
|
28540
28702
|
throw new Error(`Failed to list frontend deployments (HTTP ${listResponse.status})`);
|
|
28541
|
-
const selected = selectDeployment(deploymentList(listResponse.data), requestedId, await packageName(projectDirectory));
|
|
28703
|
+
const selected = selectDeployment(deploymentList(listResponse.data), requestedId, await packageName(projectDirectory), target.config.name);
|
|
28542
28704
|
const configuredBuildCommand = String(target.config.buildCommand || selected.buildCommand || "").trim();
|
|
28543
28705
|
const buildCommand = configuredBuildCommand || (skipBuild ? "" : await defaultBuildCommand(projectDirectory, configRoot));
|
|
28544
28706
|
const configuredOutput = String(target.config.outputDirectory || "").trim();
|
|
@@ -28551,6 +28713,9 @@ async function stageApplicationWebRelease(http, projectRef, environmentId, confi
|
|
|
28551
28713
|
}
|
|
28552
28714
|
const archive = await createFrontendArchive(outputDirectory);
|
|
28553
28715
|
try {
|
|
28716
|
+
if (assetsOnly) {
|
|
28717
|
+
return { result: await publishFrontendRelease(http, projectRef, selected.id, archive.archivePath, archive.treeSha256) };
|
|
28718
|
+
}
|
|
28554
28719
|
const uploaded = payload(await uploadProjectWebRelease(http, projectRef, environmentId, selected.id, archive.archivePath));
|
|
28555
28720
|
const release = record4(uploaded.release);
|
|
28556
28721
|
const releaseId = requiredString(release?.release_id, "release.release_id");
|
|
@@ -28580,7 +28745,23 @@ function registerDeployTools(server, http, options = {}) {
|
|
|
28580
28745
|
...config.application,
|
|
28581
28746
|
environmentId: options.environment || config.application.environmentId
|
|
28582
28747
|
} : undefined;
|
|
28583
|
-
|
|
28748
|
+
const legacyManifest = await hasProjectReleaseManifest(configRoot);
|
|
28749
|
+
const assetsOnly = args.assets_only === true || !application?.id && selectFrontendTarget(configRoot, config) !== null && !legacyManifest && !Object.values(config.targets ?? {}).some((target) => target.type === "edge_function");
|
|
28750
|
+
if (assetsOnly) {
|
|
28751
|
+
if (!selectFrontendTarget(configRoot, config)) {
|
|
28752
|
+
throw new Error("Static asset deployment requires a frontend configuration in supacloud.json");
|
|
28753
|
+
}
|
|
28754
|
+
const environmentId = options.environment?.trim() || application?.environmentId?.trim();
|
|
28755
|
+
if (!environmentId || !Check2(ApplicationIdSchema, environmentId)) {
|
|
28756
|
+
throw new Error("Static asset deployment requires SUPACLOUD_ENV");
|
|
28757
|
+
}
|
|
28758
|
+
await requireProjectDeploymentProtocol(http, projectRef, environmentId);
|
|
28759
|
+
const staged = await stageApplicationWebRelease(http, projectRef, environmentId, configRoot, args.skip_build === true, args.json === true, options.environmentVariables, true);
|
|
28760
|
+
if (!staged?.result)
|
|
28761
|
+
throw new Error("Static asset deployment did not produce a receipt");
|
|
28762
|
+
return staged.result;
|
|
28763
|
+
}
|
|
28764
|
+
if (legacyManifest) {
|
|
28584
28765
|
throw new Error(`${DEPLOYMENT_UPGRADE} Replace deploy-manifest.json with the current application delivery configuration; no remote mutation was performed.`);
|
|
28585
28766
|
}
|
|
28586
28767
|
if (application?.id) {
|
|
@@ -28908,7 +29089,7 @@ var TEST_PROFILE = [
|
|
|
28908
29089
|
"SUPACLOUD_API_URL=https://management.test.example.com",
|
|
28909
29090
|
"SUPACLOUD_API_TOKEN=<test-management-api-token>",
|
|
28910
29091
|
"SUPACLOUD_PROJECT_REF=<test-project-ref>",
|
|
28911
|
-
"SUPACLOUD_APPLICATION_ID=<application-id>",
|
|
29092
|
+
"# Optional for executable application delivery: SUPACLOUD_APPLICATION_ID=<application-id>",
|
|
28912
29093
|
""
|
|
28913
29094
|
].join(`
|
|
28914
29095
|
`);
|
|
@@ -28917,7 +29098,7 @@ var PRODUCTION_PROFILE = [
|
|
|
28917
29098
|
"SUPACLOUD_API_URL=https://management.example.com",
|
|
28918
29099
|
"SUPACLOUD_API_TOKEN=<production-management-api-token>",
|
|
28919
29100
|
"SUPACLOUD_PROJECT_REF=<production-project-ref>",
|
|
28920
|
-
"SUPACLOUD_APPLICATION_ID=<application-id>",
|
|
29101
|
+
"# Optional for executable application delivery: SUPACLOUD_APPLICATION_ID=<application-id>",
|
|
28921
29102
|
""
|
|
28922
29103
|
].join(`
|
|
28923
29104
|
`);
|
|
@@ -29041,7 +29222,7 @@ async function initializeProject(request, options) {
|
|
|
29041
29222
|
},
|
|
29042
29223
|
next: [
|
|
29043
29224
|
"cp .env.supacloud.test.example .env.supacloud.test",
|
|
29044
|
-
"
|
|
29225
|
+
"Configure frontend assets; set SUPACLOUD_APPLICATION_ID only for executable application delivery.",
|
|
29045
29226
|
"supacloud env check",
|
|
29046
29227
|
"supacloud --env test deploy"
|
|
29047
29228
|
]
|
|
@@ -29089,12 +29270,12 @@ function statusMissing(context) {
|
|
|
29089
29270
|
}
|
|
29090
29271
|
return ["SUPACLOUD_API_URL", "SUPACLOUD_API_TOKEN", "SUPACLOUD_PROJECT_REF"];
|
|
29091
29272
|
}
|
|
29092
|
-
function deployMissing(context, application) {
|
|
29273
|
+
function deployMissing(context, application, hasFrontend = false) {
|
|
29093
29274
|
const missing = statusMissing(context);
|
|
29094
29275
|
if (context.credentialScope !== "management") {
|
|
29095
29276
|
return [...missing, "Management API context"];
|
|
29096
29277
|
}
|
|
29097
|
-
if (!Check2(ApplicationIdSchema, context.applicationId || application?.id)) {
|
|
29278
|
+
if (!(hasFrontend && !context.applicationId && !application) && !Check2(ApplicationIdSchema, context.applicationId || application?.id)) {
|
|
29098
29279
|
missing.push("valid SUPACLOUD_APPLICATION_ID or supacloud.json application.id");
|
|
29099
29280
|
}
|
|
29100
29281
|
if (!Check2(ApplicationIdSchema, context.applicationEnvironmentId || application?.environmentId)) {
|
|
@@ -29114,7 +29295,8 @@ async function inspectProfile(profile) {
|
|
|
29114
29295
|
throw error;
|
|
29115
29296
|
});
|
|
29116
29297
|
const config = record5(JSON.parse(configText));
|
|
29117
|
-
const
|
|
29298
|
+
const hasFrontend = record5(config?.frontend) !== null || Object.values(record5(config?.targets) ?? {}).some((target) => record5(target)?.type === "frontend");
|
|
29299
|
+
const deploy = deployMissing(context, record5(config?.application), hasFrontend);
|
|
29118
29300
|
return {
|
|
29119
29301
|
name: profile.name,
|
|
29120
29302
|
path: profile.path,
|
|
@@ -29210,7 +29392,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
|
|
|
29210
29392
|
// package.json
|
|
29211
29393
|
var package_default = {
|
|
29212
29394
|
name: "@supacloud/cli",
|
|
29213
|
-
version: "0.
|
|
29395
|
+
version: "0.96.1",
|
|
29214
29396
|
description: "Project-scoped CLI for SupaCloud users",
|
|
29215
29397
|
type: "module",
|
|
29216
29398
|
main: "./dist/index.js",
|
|
@@ -29262,14 +29444,14 @@ var package_default = {
|
|
|
29262
29444
|
directory: "packages/cli"
|
|
29263
29445
|
},
|
|
29264
29446
|
dependencies: {
|
|
29265
|
-
"@supacloud/compiler": "^0.
|
|
29266
|
-
"@supacloud/delivery": "0.
|
|
29267
|
-
"@supacloud/db": "0.
|
|
29447
|
+
"@supacloud/compiler": "^0.46.0",
|
|
29448
|
+
"@supacloud/delivery": "0.17.0",
|
|
29449
|
+
"@supacloud/db": "0.17.0",
|
|
29268
29450
|
typebox: "1.3.36"
|
|
29269
29451
|
},
|
|
29270
29452
|
overrides: {
|
|
29271
29453
|
"@supacloud/contracts": "0.8.1",
|
|
29272
|
-
"@supacloud/delivery": "0.
|
|
29454
|
+
"@supacloud/delivery": "0.17.0",
|
|
29273
29455
|
"@graphql-tools/utils": "^12.0.3"
|
|
29274
29456
|
},
|
|
29275
29457
|
devDependencies: {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment
|
|
1
|
+
export declare const DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
|
|
2
2
|
export declare function assertCurrentDeploymentInput(tool: string, input: unknown): void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const STARTER_ATTACHMENT_WORKER = "import { createWorker, type CompiledModule } from \"@supacloud/elysia\";\nimport type
|
|
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
|
|
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";
|
|
@@ -26,6 +26,7 @@ export declare const deployToolSchema: {
|
|
|
26
26
|
ref: Type.TOptional<Type.TString>;
|
|
27
27
|
cwd: Type.TOptional<Type.TString>;
|
|
28
28
|
skip_build: Type.TOptional<Type.TBoolean>;
|
|
29
|
+
assets_only: Type.TOptional<Type.TBoolean>;
|
|
29
30
|
json: Type.TOptional<Type.TBoolean>;
|
|
30
31
|
};
|
|
31
32
|
export declare function findDeployConfigRoot(startDirectory: string, environmentName?: string): Promise<string>;
|
|
@@ -15,7 +15,7 @@ export declare const environmentToolSchema: ToolSchema;
|
|
|
15
15
|
declare function outputDirectoryFor(manifest: PackageManifest): string;
|
|
16
16
|
declare function profileNameFromFile(name: string): string | null;
|
|
17
17
|
declare function statusMissing(context: ResolvedContext): string[];
|
|
18
|
-
declare function deployMissing(context: ResolvedContext, application?: Record<string, unknown> | null): string[];
|
|
18
|
+
declare function deployMissing(context: ResolvedContext, application?: Record<string, unknown> | null, hasFrontend?: boolean): string[];
|
|
19
19
|
export declare function registerDeveloperExperienceTools(server: ToolServer, options?: DeveloperExperienceOptions): void;
|
|
20
20
|
export declare const __internal: {
|
|
21
21
|
outputDirectoryFor: typeof outputDirectoryFor;
|
|
@@ -46,6 +46,7 @@ export declare function uploadProjectWebRelease(http: FrontendReleaseHttp, proje
|
|
|
46
46
|
export declare function uploadFrontendSource(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, archivePath: string): Promise<ToolResponse>;
|
|
47
47
|
export declare function getActiveFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string): Promise<ToolResponse>;
|
|
48
48
|
export declare function readFrontendReleaseAuthority(http: FrontendReleaseHttp, projectRef: string, deploymentId: string): Promise<ToolResponse>;
|
|
49
|
+
export declare function publishFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, archivePath: string, expectedTreeSha256: string): Promise<ToolResponse>;
|
|
49
50
|
export declare function rollbackFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, releaseId?: string): Promise<ToolResponse>;
|
|
50
51
|
export declare function activateFrontendRelease(http: FrontendReleaseHttp, input: {
|
|
51
52
|
projectRef: string;
|
package/dist/tool-runtime.js
CHANGED
|
@@ -131,7 +131,7 @@ import { Type as Type2 } from "typebox";
|
|
|
131
131
|
import { Value as Value2 } from "typebox/value";
|
|
132
132
|
|
|
133
133
|
// src/shared/deployment-upgrade.ts
|
|
134
|
-
var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment
|
|
134
|
+
var DEPLOYMENT_UPGRADE = "DEPLOYMENT_UPGRADE_REQUIRED: Legacy deployment commands are no longer supported. Upgrade with `npm install -g @supacloud/cli@latest`, configure frontend in supacloud.json for static assets, or application.id and application.environmentId for executable application delivery, then run `supacloud deploy`. Use `supacloud deploy --assets_only` to publish only Web assets in a mixed repository. SupaCloud owns activation; do not retry with legacy commands.";
|
|
135
135
|
var removedFrontendActions = new Set([
|
|
136
136
|
"deploy_git",
|
|
137
137
|
"deploy_upload",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.1",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,14 +52,14 @@
|
|
|
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.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.
|
|
62
|
+
"@supacloud/delivery": "0.17.0",
|
|
63
63
|
"@graphql-tools/utils": "^12.0.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|