@supacloud/cli 0.98.0 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -6
- package/dist/index.js +272 -254
- package/dist/shared/deployment-upgrade.d.ts +1 -1
- package/dist/shared/tools/app-starter-postgres.d.ts +2 -2
- package/dist/shared/tools/app-starter-query.d.ts +1 -0
- package/dist/shared/tools/deploy-tools.d.ts +2 -0
- package/dist/shared/tools/frontend-release-control.d.ts +4 -1
- package/dist/shared/tools/project-deployment.d.ts +1 -0
- package/dist/tool-runtime.js +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -659,12 +659,19 @@ FA-owned business features are documented in
|
|
|
659
659
|
|
|
660
660
|
`deploy --target`, `--id`, `--build_command`, `--output_dir`, `--plan`, `--diff`
|
|
661
661
|
and `--dry_run` are removed. Put build settings in `supacloud.json`; use
|
|
662
|
-
`app deploy-plan` for read-only review.
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
`supacloud --env test deploy --
|
|
667
|
-
|
|
662
|
+
`app deploy-plan` for read-only review. With a frontend configured, `supacloud deploy`
|
|
663
|
+
publishes only its static assets, even in a mixed repository. `--assets_only`
|
|
664
|
+
remains an explicit alias. This preserves historical manifests, Functions,
|
|
665
|
+
Storage, schedules and migrations without invoking application compilation.
|
|
666
|
+
Use `supacloud --env test deploy --backend` to publish the configured Application
|
|
667
|
+
independently; it never builds or uploads Web. Unsupported historical backend
|
|
668
|
+
manifests and standalone Function targets still fail closed on that Application path.
|
|
669
|
+
|
|
670
|
+
Application deployments return `accepted` after durable queue insertion, not a
|
|
671
|
+
successful activation. Inspect the receipt with the read-only
|
|
672
|
+
`supacloud --env test deploy --status <deployment-id>`. Queue delivery survives
|
|
673
|
+
restart; interrupted side effects require reconciliation rather than blind retry.
|
|
674
|
+
|
|
668
675
|
Static publication reuses immutable archives, pre-upload CAS, mutation receipts
|
|
669
676
|
and exact post-activation readback. It never retries a conflicting activation.
|
|
670
677
|
|
package/dist/index.js
CHANGED
|
@@ -3177,23 +3177,22 @@ function InstanceKeys(value) {
|
|
|
3177
3177
|
function IsIEEE754(value) {
|
|
3178
3178
|
return typeof value === "number";
|
|
3179
3179
|
}
|
|
3180
|
-
var ByteMarker
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
})(ByteMarker || (ByteMarker = {}));
|
|
3180
|
+
var ByteMarker = {
|
|
3181
|
+
Array: 0,
|
|
3182
|
+
BigInt: 1,
|
|
3183
|
+
Boolean: 2,
|
|
3184
|
+
Date: 3,
|
|
3185
|
+
Constructor: 4,
|
|
3186
|
+
Function: 5,
|
|
3187
|
+
Null: 6,
|
|
3188
|
+
Number: 7,
|
|
3189
|
+
Object: 8,
|
|
3190
|
+
RegExp: 9,
|
|
3191
|
+
String: 10,
|
|
3192
|
+
Symbol: 11,
|
|
3193
|
+
TypeArray: 12,
|
|
3194
|
+
Undefined: 13
|
|
3195
|
+
};
|
|
3197
3196
|
var Accumulator = BigInt("14695981039346656037");
|
|
3198
3197
|
var [Prime, Size] = [BigInt("1099511628211"), BigInt("18446744073709551616")];
|
|
3199
3198
|
var Bytes = Array.from({ length: 256 }).map((_, i) => BigInt(i));
|
|
@@ -8786,7 +8785,7 @@ function outcomeUnknownGuidance(text) {
|
|
|
8786
8785
|
}
|
|
8787
8786
|
|
|
8788
8787
|
// src/shared/deployment-upgrade.ts
|
|
8789
|
-
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`.
|
|
8788
|
+
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`. With a frontend configured, deploy publishes only Web assets; use `supacloud deploy --backend` for the application backend. `supacloud deploy --assets_only` remains an explicit Web-only option. SupaCloud owns activation; do not retry with legacy commands.";
|
|
8790
8789
|
var removedFrontendActions = new Set([
|
|
8791
8790
|
"deploy_git",
|
|
8792
8791
|
"deploy_upload",
|
|
@@ -9339,6 +9338,8 @@ function declaredMode(moduleName, action) {
|
|
|
9339
9338
|
return;
|
|
9340
9339
|
}
|
|
9341
9340
|
function executionMode(moduleName, action, args) {
|
|
9341
|
+
if (moduleName === "deploy" && typeof args.status === "string")
|
|
9342
|
+
return "read";
|
|
9342
9343
|
if (moduleName === "deploy" && (args.dry_run === true || args.plan === true || args.diff === true))
|
|
9343
9344
|
return "read";
|
|
9344
9345
|
if (moduleName === "database" && ["push_migrations", "baseline_migrations"].includes(action) && args.dry_run === true)
|
|
@@ -15194,7 +15195,7 @@ function scaffoldFiles(framework, slug) {
|
|
|
15194
15195
|
"package.json": JSON.stringify({
|
|
15195
15196
|
private: true,
|
|
15196
15197
|
type: "module",
|
|
15197
|
-
dependencies: { elysia: "2.0.0-beta.
|
|
15198
|
+
dependencies: { elysia: "2.0.0-beta.32" }
|
|
15198
15199
|
}, null, 2) + `
|
|
15199
15200
|
`,
|
|
15200
15201
|
"index.ts": `import { Elysia } from "elysia";
|
|
@@ -16763,7 +16764,7 @@ async function getFrontendRelease(http, projectRef, deploymentId, releaseId) {
|
|
|
16763
16764
|
}
|
|
16764
16765
|
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
|
|
16765
16766
|
}
|
|
16766
|
-
async function
|
|
16767
|
+
async function uploadFrontendReleaseWithReadback(http, projectRef, deploymentId, archivePath, verifyReadback) {
|
|
16767
16768
|
const endpoint = releaseEndpoint(projectRef, deploymentId);
|
|
16768
16769
|
const archive = await verifiedArchive(archivePath);
|
|
16769
16770
|
let response;
|
|
@@ -16782,13 +16783,15 @@ async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath
|
|
|
16782
16783
|
} finally {
|
|
16783
16784
|
await archive.handle.close();
|
|
16784
16785
|
}
|
|
16785
|
-
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
|
|
16786
|
+
const release = response.ok && (response.status === 200 || response.status === 201) ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId: archive.sha256 }) : null;
|
|
16786
16787
|
if (!release) {
|
|
16787
16788
|
if (response.status >= 400 && response.status < 500 && response.status !== 408 && !response.transportError) {
|
|
16788
16789
|
return releaseFailure("frontend.upload_release", "HTTP_ERROR", response.status);
|
|
16789
16790
|
}
|
|
16790
16791
|
return uploadReadback(http, projectRef, deploymentId, archive.sha256, response.status);
|
|
16791
16792
|
}
|
|
16793
|
+
if (!verifyReadback)
|
|
16794
|
+
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release });
|
|
16792
16795
|
const readback = await http.get(`${endpoint}/${release.release_id}`, { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
16793
16796
|
const verified = readback.ok ? releaseEnvelope(readback.data, { projectRef, deploymentId, releaseId: release.release_id }) : null;
|
|
16794
16797
|
if (!verified || verified.tree_sha256 !== release.tree_sha256) {
|
|
@@ -16796,65 +16799,11 @@ async function uploadFrontendRelease(http, projectRef, deploymentId, archivePath
|
|
|
16796
16799
|
}
|
|
16797
16800
|
return toolResponse({ project_ref: projectRef, deployment_id: deploymentId, release: verified });
|
|
16798
16801
|
}
|
|
16799
|
-
async function uploadProjectWebRelease(http, projectRef, environmentId, deploymentId, archivePath) {
|
|
16800
|
-
const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/environments/${encodeURIComponent(environmentId)}/deployments/web-release?deployment_id=${encodeURIComponent(deploymentId)}`;
|
|
16801
|
-
const archive = await verifiedArchive(archivePath);
|
|
16802
|
-
const successPayload = (value) => {
|
|
16803
|
-
const release = value ? releaseRecord(value.release) : null;
|
|
16804
|
-
if (!value || value.ok !== true || value.project_ref !== projectRef || value.environment_id !== environmentId || value.deployment_id !== deploymentId || value.release_id !== archive.sha256 || release?.release_id !== archive.sha256 || release.tree_sha256.length !== 64)
|
|
16805
|
-
return null;
|
|
16806
|
-
return toolResponse({
|
|
16807
|
-
project_ref: projectRef,
|
|
16808
|
-
environment_id: environmentId,
|
|
16809
|
-
deployment_id: deploymentId,
|
|
16810
|
-
release_id: archive.sha256,
|
|
16811
|
-
release
|
|
16812
|
-
});
|
|
16813
|
-
};
|
|
16814
|
-
try {
|
|
16815
|
-
const response = await http.postBinary(endpoint, {
|
|
16816
|
-
stream: archiveStream(archive),
|
|
16817
|
-
byteLength: archive.sizeBytes
|
|
16818
|
-
}, {
|
|
16819
|
-
contentType: FRONTEND_ARCHIVE_CONTENT_TYPE,
|
|
16820
|
-
contentLength: archive.sizeBytes,
|
|
16821
|
-
contentSha256: archive.sha256,
|
|
16822
|
-
maxJsonBytes: RESPONSE_MAX_BYTES,
|
|
16823
|
-
timeoutMs: UPLOAD_REQUEST_TIMEOUT_MS,
|
|
16824
|
-
responseTimeoutMs: MUTATION_RESPONSE_TIMEOUT_MS
|
|
16825
|
-
});
|
|
16826
|
-
const value = response.data && typeof response.data === "object" && !Array.isArray(response.data) ? response.data : null;
|
|
16827
|
-
if (response.ok && response.status === 201) {
|
|
16828
|
-
const success = successPayload(value);
|
|
16829
|
-
if (success)
|
|
16830
|
-
return success;
|
|
16831
|
-
}
|
|
16832
|
-
if (response.transportError || response.responseReadError || response.status >= 500 || response.status === 202) {
|
|
16833
|
-
const readback = await getFrontendRelease(http, projectRef, deploymentId, archive.sha256);
|
|
16834
|
-
if (!readback.isError) {
|
|
16835
|
-
const readbackValue = JSON.parse(readback.content[0]?.text ?? "{}");
|
|
16836
|
-
const release = readbackValue.release;
|
|
16837
|
-
if (release && typeof release === "object" && !Array.isArray(release) && release.release_id === archive.sha256) {
|
|
16838
|
-
return toolResponse({
|
|
16839
|
-
project_ref: projectRef,
|
|
16840
|
-
environment_id: environmentId,
|
|
16841
|
-
deployment_id: deploymentId,
|
|
16842
|
-
release_id: archive.sha256,
|
|
16843
|
-
release
|
|
16844
|
-
});
|
|
16845
|
-
}
|
|
16846
|
-
}
|
|
16847
|
-
}
|
|
16848
|
-
return releaseFailure("frontend.upload_project_web_release", response.status >= 400 && response.status < 500 && !response.transportError ? "HTTP_ERROR" : "OUTCOME_UNKNOWN", response.status);
|
|
16849
|
-
} finally {
|
|
16850
|
-
await archive.handle.close();
|
|
16851
|
-
}
|
|
16852
|
-
}
|
|
16853
16802
|
async function uploadReadback(http, projectRef, deploymentId, releaseId, uploadStatus) {
|
|
16854
16803
|
const response = await http.get(releasePath(projectRef, deploymentId, releaseId), {
|
|
16855
16804
|
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
16856
16805
|
});
|
|
16857
|
-
const release = response.ok ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
|
|
16806
|
+
const release = response.ok && response.status === 200 ? releaseEnvelope(response.data, { projectRef, deploymentId, releaseId }) : null;
|
|
16858
16807
|
if (!release) {
|
|
16859
16808
|
return releaseFailure("frontend.upload_release", "OUTCOME_UNKNOWN", uploadStatus);
|
|
16860
16809
|
}
|
|
@@ -16913,13 +16862,9 @@ function activationReceipt(candidate, input) {
|
|
|
16913
16862
|
return release;
|
|
16914
16863
|
}
|
|
16915
16864
|
async function activeReleaseReadback(http, identity) {
|
|
16916
|
-
const snapshot = await
|
|
16917
|
-
if (
|
|
16918
|
-
return
|
|
16919
|
-
const inventory = snapshot.inventory;
|
|
16920
|
-
if (inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
|
|
16921
|
-
return { release: null, status: 200 };
|
|
16922
|
-
}
|
|
16865
|
+
const snapshot = await activeReleaseSnapshotReadback(http, identity);
|
|
16866
|
+
if (!snapshot.release)
|
|
16867
|
+
return snapshot;
|
|
16923
16868
|
const releaseRead = await http.get(releasePath(identity.projectRef, identity.deploymentId, identity.releaseId), { maxJsonBytes: RESPONSE_MAX_BYTES });
|
|
16924
16869
|
const release = releaseRead.ok && releaseRead.status === 200 ? releaseEnvelope(releaseRead.data, {
|
|
16925
16870
|
projectRef: identity.projectRef,
|
|
@@ -16928,6 +16873,16 @@ async function activeReleaseReadback(http, identity) {
|
|
|
16928
16873
|
}) : null;
|
|
16929
16874
|
return { release, status: releaseRead.status };
|
|
16930
16875
|
}
|
|
16876
|
+
async function activeReleaseSnapshotReadback(http, identity) {
|
|
16877
|
+
const snapshot = await readActiveFrontendRelease(http, identity.projectRef, identity.deploymentId);
|
|
16878
|
+
if ("failure" in snapshot)
|
|
16879
|
+
return { release: null, status: snapshot.status };
|
|
16880
|
+
const inventory = snapshot.inventory;
|
|
16881
|
+
if (inventory.active_release_id !== identity.releaseId || inventory.active_activation_id !== identity.mutationId) {
|
|
16882
|
+
return { release: null, status: 200 };
|
|
16883
|
+
}
|
|
16884
|
+
return { release: inventory.releases[0] ?? null, status: 200 };
|
|
16885
|
+
}
|
|
16931
16886
|
async function readActiveFrontendRelease(http, projectRef, deploymentId) {
|
|
16932
16887
|
const response = await http.get(`${deploymentEndpoint(projectRef, deploymentId)}/active-release`, {
|
|
16933
16888
|
maxJsonBytes: RESPONSE_MAX_BYTES
|
|
@@ -16962,7 +16917,7 @@ async function getActiveFrontendRelease(http, projectRef, deploymentId) {
|
|
|
16962
16917
|
const snapshot = await readActiveFrontendRelease(http, projectRef, deploymentId);
|
|
16963
16918
|
return "failure" in snapshot ? snapshot.failure : toolResponse(snapshot.inventory);
|
|
16964
16919
|
}
|
|
16965
|
-
async function publishFrontendRelease(http, projectRef, deploymentId,
|
|
16920
|
+
async function publishFrontendRelease(http, projectRef, deploymentId, archiveSource, expectedTreeSha256) {
|
|
16966
16921
|
const authority = await readActiveFrontendRelease(http, projectRef, deploymentId);
|
|
16967
16922
|
if ("failure" in authority)
|
|
16968
16923
|
return authority.failure;
|
|
@@ -16979,7 +16934,14 @@ async function publishFrontendRelease(http, projectRef, deploymentId, archivePat
|
|
|
16979
16934
|
unchanged: true
|
|
16980
16935
|
});
|
|
16981
16936
|
}
|
|
16982
|
-
const
|
|
16937
|
+
const archive = typeof archiveSource === "string" ? { archivePath: archiveSource } : await archiveSource();
|
|
16938
|
+
let uploaded;
|
|
16939
|
+
try {
|
|
16940
|
+
uploaded = await uploadFrontendReleaseWithReadback(http, projectRef, deploymentId, archive.archivePath, false);
|
|
16941
|
+
} finally {
|
|
16942
|
+
if ("cleanup" in archive)
|
|
16943
|
+
await archive.cleanup();
|
|
16944
|
+
}
|
|
16983
16945
|
if (uploaded.isError)
|
|
16984
16946
|
return uploaded;
|
|
16985
16947
|
const value = JSON.parse(uploaded.content[0].text);
|
|
@@ -16990,14 +16952,14 @@ async function publishFrontendRelease(http, projectRef, deploymentId, archivePat
|
|
|
16990
16952
|
return releaseFailure("frontend.deploy", "INVALID_RESPONSE", 200);
|
|
16991
16953
|
}
|
|
16992
16954
|
const mutationId = crypto.randomUUID();
|
|
16993
|
-
const activated = await
|
|
16955
|
+
const activated = await activateFrontendReleaseWithReadback(http, {
|
|
16994
16956
|
projectRef,
|
|
16995
16957
|
deploymentId,
|
|
16996
16958
|
releaseId: release.release_id,
|
|
16997
16959
|
mutationId,
|
|
16998
16960
|
expectedActiveReleaseId: inventory.active_release_id ?? "absent",
|
|
16999
16961
|
expectedActivationId: inventory.active_activation_id ?? "absent"
|
|
17000
|
-
});
|
|
16962
|
+
}, activeReleaseSnapshotReadback);
|
|
17001
16963
|
const activatedPayload = JSON.parse(activated.content[0].text);
|
|
17002
16964
|
const active = activatedPayload.release;
|
|
17003
16965
|
const treeMismatch = !activated.isError && (!active || typeof active !== "object" || !("tree_sha256" in active) || active.tree_sha256 !== expectedTreeSha256);
|
|
@@ -17176,6 +17138,9 @@ async function readPreviousFrontendRelease(http, projectRef, deploymentId) {
|
|
|
17176
17138
|
};
|
|
17177
17139
|
}
|
|
17178
17140
|
async function activateFrontendRelease(http, input) {
|
|
17141
|
+
return activateFrontendReleaseWithReadback(http, input, activeReleaseReadback);
|
|
17142
|
+
}
|
|
17143
|
+
async function activateFrontendReleaseWithReadback(http, input, readActive) {
|
|
17179
17144
|
const mutationId = input.mutationId;
|
|
17180
17145
|
if (!MUTATION_ID_PATTERN2.test(mutationId))
|
|
17181
17146
|
throw new Error("'mutation_id' must be a UUIDv4");
|
|
@@ -17201,7 +17166,7 @@ async function activateFrontendRelease(http, input) {
|
|
|
17201
17166
|
}
|
|
17202
17167
|
return activationReadback(http, input, mutationId, response.status);
|
|
17203
17168
|
}
|
|
17204
|
-
const readback = await
|
|
17169
|
+
const readback = await readActive(http, { ...input, mutationId });
|
|
17205
17170
|
if (!readback.release || readback.release.tree_sha256 !== release.tree_sha256) {
|
|
17206
17171
|
return releaseFailure("frontend.activate_release", "OUTCOME_UNKNOWN", readback.status);
|
|
17207
17172
|
}
|
|
@@ -21423,34 +21388,34 @@ var starter_metadata_default = {
|
|
|
21423
21388
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
21424
21389
|
packages: {
|
|
21425
21390
|
compiler: {
|
|
21426
|
-
version: "0.46.
|
|
21391
|
+
version: "0.46.3"
|
|
21427
21392
|
},
|
|
21428
21393
|
app: {
|
|
21429
|
-
version: "0.27.
|
|
21394
|
+
version: "0.27.1",
|
|
21430
21395
|
dependencies: {
|
|
21431
21396
|
rxjs: "7.8.2"
|
|
21432
21397
|
}
|
|
21433
21398
|
},
|
|
21434
21399
|
elysia: {
|
|
21435
|
-
version: "0.
|
|
21400
|
+
version: "0.31.0"
|
|
21436
21401
|
},
|
|
21437
21402
|
commands: {
|
|
21438
|
-
version: "0.9.
|
|
21403
|
+
version: "0.9.3"
|
|
21439
21404
|
},
|
|
21440
21405
|
contracts: {
|
|
21441
21406
|
version: "0.9.0"
|
|
21442
21407
|
},
|
|
21443
21408
|
db: {
|
|
21444
|
-
version: "0.17.
|
|
21409
|
+
version: "0.17.1",
|
|
21445
21410
|
peerDependencies: {
|
|
21446
|
-
"drizzle-orm": "1.0.0-rc.5-
|
|
21411
|
+
"drizzle-orm": "1.0.0-rc.5-5935859"
|
|
21447
21412
|
},
|
|
21448
21413
|
devDependencies: {
|
|
21449
|
-
"drizzle-kit": "1.0.0-rc.5-
|
|
21414
|
+
"drizzle-kit": "1.0.0-rc.5-5935859"
|
|
21450
21415
|
}
|
|
21451
21416
|
},
|
|
21452
21417
|
sdk: {
|
|
21453
|
-
version: "0.
|
|
21418
|
+
version: "0.43.0",
|
|
21454
21419
|
peerDependencies: {
|
|
21455
21420
|
"@supabase/supabase-js": "^2.117.3"
|
|
21456
21421
|
}
|
|
@@ -21793,8 +21758,8 @@ function baseFiles(name, sdkDependencies) {
|
|
|
21793
21758
|
dependencies: {
|
|
21794
21759
|
"@supacloud/app": `^${appMetadata.version}`,
|
|
21795
21760
|
"@supacloud/elysia": `^${elysiaMetadata.version}`,
|
|
21796
|
-
effect: "4.0.
|
|
21797
|
-
elysia: "2.0.0-beta.
|
|
21761
|
+
effect: "4.0.5",
|
|
21762
|
+
elysia: "2.0.0-beta.32",
|
|
21798
21763
|
...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
|
|
21799
21764
|
},
|
|
21800
21765
|
devDependencies: {
|
|
@@ -22992,20 +22957,21 @@ ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
|
|
|
22992
22957
|
ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
|
|
22993
22958
|
ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
|
|
22994
22959
|
`;
|
|
22995
|
-
var STARTER_REVIEW_POSTGRES = `import {
|
|
22996
|
-
import { createTransactionalCommand, plaintextCommandInput } from "@supacloud/commands";
|
|
22960
|
+
var STARTER_REVIEW_POSTGRES = `import { plaintextCommandInput } from "@supacloud/commands";
|
|
22997
22961
|
import { CommandError } from "@supacloud/contracts";
|
|
22998
22962
|
import { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from "@supacloud/db";
|
|
22999
22963
|
import {
|
|
23000
|
-
ApplicationError, createSupAuthRequestContext, requireTrustedIdentity,
|
|
22964
|
+
ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,
|
|
22965
|
+
createTransactionalCommandGovernance,
|
|
23001
22966
|
type CommandInvocation, type SupAuthContextOptions,
|
|
23002
22967
|
} from "@supacloud/elysia";
|
|
23003
22968
|
import type { AppAdapters } from "../application";
|
|
23004
22969
|
import type { ReviewStore } from "../review/review";
|
|
23005
22970
|
import type { ReviewUploadPort } from "../review/uploads";
|
|
22971
|
+
import { ApprovalInput, ReviewResult } from "../review/contracts";
|
|
23006
22972
|
|
|
23007
|
-
|
|
23008
|
-
|
|
22973
|
+
const input = createSchemaDecoder(ApprovalInput, { normalize: false });
|
|
22974
|
+
const result = createSchemaDecoder(ReviewResult, { normalize: false });
|
|
23009
22975
|
|
|
23010
22976
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
23011
22977
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -23022,19 +22988,6 @@ function isPublicConflict(value: unknown): boolean {
|
|
|
23022
22988
|
return value instanceof ApplicationError && value.status === 409
|
|
23023
22989
|
|| isRecord(value) && value["expose"] === true && value["status"] === 409 && typeof value["code"] === "string";
|
|
23024
22990
|
}
|
|
23025
|
-
function input(value: unknown): Approval {
|
|
23026
|
-
const item = record(value);
|
|
23027
|
-
if (typeof item["id"] !== "string" || !item["id"].length || typeof item["expectedVersion"] !== "number"
|
|
23028
|
-
|| !Number.isSafeInteger(item["expectedVersion"]) || item["expectedVersion"] < 1) throw new TypeError("Invalid approval input");
|
|
23029
|
-
return { id: item["id"], expectedVersion: item["expectedVersion"] };
|
|
23030
|
-
}
|
|
23031
|
-
function result(value: unknown): Review {
|
|
23032
|
-
const item = record(value);
|
|
23033
|
-
if ((item["state"] !== "draft" && item["state"] !== "approved") || typeof item["version"] !== "number"
|
|
23034
|
-
|| !Number.isSafeInteger(item["version"]) || item["version"] < 1) throw new TypeError("Invalid review result");
|
|
23035
|
-
return { state: item["state"], version: item["version"] };
|
|
23036
|
-
}
|
|
23037
|
-
|
|
23038
22991
|
export interface ReviewPostgresOptions {
|
|
23039
22992
|
database: CommandDatabase;
|
|
23040
22993
|
tenantId: string;
|
|
@@ -23072,12 +23025,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
|
|
|
23072
23025
|
} : null;
|
|
23073
23026
|
},
|
|
23074
23027
|
});
|
|
23075
|
-
const
|
|
23076
|
-
const transaction = () => {
|
|
23077
|
-
const tx = scope.getStore();
|
|
23078
|
-
if (!tx) throw new Error("Review storage requires its durable command transaction");
|
|
23079
|
-
return tx;
|
|
23080
|
-
};
|
|
23028
|
+
const transaction = () => commandBinding.transaction();
|
|
23081
23029
|
const store: ReviewStore = {
|
|
23082
23030
|
...options.uploads,
|
|
23083
23031
|
async get(table, id) {
|
|
@@ -23106,64 +23054,30 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
|
|
|
23106
23054
|
}
|
|
23107
23055
|
return who.subject;
|
|
23108
23056
|
}
|
|
23109
|
-
const
|
|
23110
|
-
|
|
23111
|
-
|
|
23112
|
-
|
|
23113
|
-
|
|
23114
|
-
|
|
23115
|
-
|
|
23116
|
-
|
|
23117
|
-
|
|
23118
|
-
|
|
23119
|
-
|
|
23120
|
-
|
|
23121
|
-
|
|
23122
|
-
|
|
23123
|
-
|
|
23124
|
-
|
|
23125
|
-
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
|
|
23129
|
-
"CROSS JOIN public.starter_application a " +
|
|
23130
|
-
"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
|
|
23131
|
-
"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
|
|
23132
|
-
[request.id, who.actorId, projectId, who.tenantId],
|
|
23133
|
-
));
|
|
23134
|
-
return found.length === 1 ? "allow" : "deny";
|
|
23135
|
-
},
|
|
23136
|
-
execute: tx => scope.run(tx, async () => {
|
|
23137
|
-
try { return await next(); }
|
|
23138
|
-
catch (error) {
|
|
23139
|
-
if (isPublicConflict(error)) {
|
|
23140
|
-
throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
|
|
23141
|
-
}
|
|
23142
|
-
throw error;
|
|
23143
|
-
}
|
|
23144
|
-
}),
|
|
23145
|
-
audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
|
|
23146
|
-
});
|
|
23147
|
-
try {
|
|
23148
|
-
const receipt = await command.execute(
|
|
23149
|
-
{ actorId: actor(invocation), tenantId }, requireIdempotencyKey(invocation),
|
|
23150
|
-
{ id: invocation.input.params["id"], expectedVersion: record(invocation.input.body)["expectedVersion"] },
|
|
23151
|
-
invocation.request.signal,
|
|
23152
|
-
);
|
|
23153
|
-
if (receipt.status !== "confirmed") throw new CommandError("COMMAND_OUTCOME_UNKNOWN");
|
|
23154
|
-
return receipt.result;
|
|
23155
|
-
} catch (error) {
|
|
23156
|
-
if (!(error instanceof CommandError)) throw error;
|
|
23157
|
-
const status = error.code === "COMMAND_REJECTED" ? 403
|
|
23158
|
-
: error.code === "COMMAND_IDEMPOTENCY_CONFLICT" ? 409 : 503;
|
|
23159
|
-
throw new ApplicationError(error.code, { code: error.code, status });
|
|
23160
|
-
}
|
|
23057
|
+
const commandBinding = createTransactionalCommandGovernance({
|
|
23058
|
+
permission: "review.approve",
|
|
23059
|
+
identity: invocation => ({ actorId: actor(invocation), tenantId }),
|
|
23060
|
+
input: invocation => ({
|
|
23061
|
+
...record(invocation.input.body), id: invocation.input.params["id"],
|
|
23062
|
+
}),
|
|
23063
|
+
rejection: error => isPublicConflict(error) ? new CommandError("COMMAND_IDEMPOTENCY_CONFLICT") : undefined,
|
|
23064
|
+
definition: {
|
|
23065
|
+
name: "review.approve", store: createPostgresCommandStore(database),
|
|
23066
|
+
inputCodec: plaintextCommandInput, input, result,
|
|
23067
|
+
async authorize(who, request, tx) {
|
|
23068
|
+
const found = rows(await tx.query(
|
|
23069
|
+
"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id " +
|
|
23070
|
+
"CROSS JOIN public.starter_application a " +
|
|
23071
|
+
"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve " +
|
|
23072
|
+
"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a",
|
|
23073
|
+
[request.id, who.actorId, projectId, who.tenantId],
|
|
23074
|
+
));
|
|
23075
|
+
return found.length === 1 ? "allow" : "deny";
|
|
23161
23076
|
},
|
|
23162
|
-
|
|
23163
|
-
// The durable command owns the authoritative audit and receipt in the same transaction.
|
|
23164
|
-
audit: { succeeded() { transaction(); }, failed() {} },
|
|
23077
|
+
audit: { event: "review.approved", details: request => ({ reviewId: request.id }) },
|
|
23165
23078
|
},
|
|
23166
|
-
};
|
|
23079
|
+
});
|
|
23080
|
+
return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };
|
|
23167
23081
|
}
|
|
23168
23082
|
`;
|
|
23169
23083
|
var STARTER_REVIEW_POSTGRES_TEST = `import { expect, test } from "bun:test";
|
|
@@ -23200,9 +23114,9 @@ test("PostgreSQL host never falls back to demo identity or unbound business stor
|
|
|
23200
23114
|
await expect(adapters.requestContext(new Request("http://localhost/reviews/health")))
|
|
23201
23115
|
.rejects.toMatchObject({ code: "AUTHENTICATION_REQUIRED" });
|
|
23202
23116
|
const store = adapters.deps["dbClient"] as ReviewStore;
|
|
23203
|
-
await expect(store.get("reviews", "example")).rejects.toThrow("
|
|
23117
|
+
await expect(store.get("reviews", "example")).rejects.toThrow("not active");
|
|
23204
23118
|
await expect(store.set("reviews", "example", { state: "approved", version: 2 }))
|
|
23205
|
-
.rejects.toThrow("
|
|
23119
|
+
.rejects.toThrow("not active");
|
|
23206
23120
|
expect(db.queries).toHaveLength(1);
|
|
23207
23121
|
});
|
|
23208
23122
|
`;
|
|
@@ -23488,6 +23402,58 @@ export default defineConfig({
|
|
|
23488
23402
|
};
|
|
23489
23403
|
}
|
|
23490
23404
|
|
|
23405
|
+
// src/shared/tools/app-starter-query.ts
|
|
23406
|
+
var STARTER_REVIEW_QUERY = `import type { SupabaseClient } from "@supabase/supabase-js";
|
|
23407
|
+
import type { ReviewRow } from "../../db/schema";
|
|
23408
|
+
|
|
23409
|
+
// This read projection uses SQL column names shared with the Drizzle source.
|
|
23410
|
+
// Do not expose camelCase Drizzle property names as PostgREST column names.
|
|
23411
|
+
export type ReviewSummary = Pick<ReviewRow, "id" | "state" | "version">;
|
|
23412
|
+
export type ReviewReadDatabase = {
|
|
23413
|
+
public: {
|
|
23414
|
+
Tables: {
|
|
23415
|
+
starter_reviews: {
|
|
23416
|
+
Row: ReviewSummary;
|
|
23417
|
+
Insert: never;
|
|
23418
|
+
Update: never;
|
|
23419
|
+
Relationships: [];
|
|
23420
|
+
};
|
|
23421
|
+
};
|
|
23422
|
+
Views: Record<never, never>;
|
|
23423
|
+
Functions: Record<never, never>;
|
|
23424
|
+
};
|
|
23425
|
+
};
|
|
23426
|
+
|
|
23427
|
+
export interface ReviewPageOptions {
|
|
23428
|
+
page?: number;
|
|
23429
|
+
pageSize?: number;
|
|
23430
|
+
signal?: AbortSignal;
|
|
23431
|
+
}
|
|
23432
|
+
|
|
23433
|
+
// Reuse the application's user-scoped client. Identity verification and row
|
|
23434
|
+
// authorization belong to the gateway and database, never a caller-supplied owner.
|
|
23435
|
+
export async function listReviews(
|
|
23436
|
+
client: SupabaseClient<ReviewReadDatabase>,
|
|
23437
|
+
{ page = 1, pageSize = 25, signal }: ReviewPageOptions = {},
|
|
23438
|
+
) {
|
|
23439
|
+
if (!Number.isSafeInteger(page) || page < 1
|
|
23440
|
+
|| !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100
|
|
23441
|
+
|| !Number.isSafeInteger(page * pageSize)) {
|
|
23442
|
+
throw new RangeError("Invalid review page: page >= 1 and pageSize between 1 and 100 are required");
|
|
23443
|
+
}
|
|
23444
|
+
signal?.throwIfAborted();
|
|
23445
|
+
const start = (page - 1) * pageSize;
|
|
23446
|
+
const query = client.from("starter_reviews")
|
|
23447
|
+
.select("id,state,version", { count: "exact" })
|
|
23448
|
+
.order("id", { ascending: true })
|
|
23449
|
+
.range(start, start + pageSize - 1);
|
|
23450
|
+
const { data, error, count } = await (signal ? query.abortSignal(signal) : query);
|
|
23451
|
+
if (error) throw error;
|
|
23452
|
+
if (data === null || count === null) throw new Error("Incomplete review page response");
|
|
23453
|
+
return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };
|
|
23454
|
+
}
|
|
23455
|
+
`;
|
|
23456
|
+
|
|
23491
23457
|
// src/shared/tools/app-starter.ts
|
|
23492
23458
|
var { compiler: compilerMetadata2, app: appMetadata2, elysia: elysiaMetadata2, commands: commandsMetadata, contracts: contractsMetadata, db: dbMetadata, sdk: sdkMetadata } = starter_metadata_default.packages;
|
|
23493
23459
|
var json2 = (value) => `${JSON.stringify(value, null, 2)}
|
|
@@ -23495,6 +23461,7 @@ var json2 = (value) => `${JSON.stringify(value, null, 2)}
|
|
|
23495
23461
|
function appStarterFiles(name) {
|
|
23496
23462
|
return {
|
|
23497
23463
|
...starterDatabaseFiles(),
|
|
23464
|
+
"src/review/list-reviews.ts": STARTER_REVIEW_QUERY,
|
|
23498
23465
|
"package.json": json2({
|
|
23499
23466
|
name,
|
|
23500
23467
|
version: "0.0.0",
|
|
@@ -23529,8 +23496,8 @@ function appStarterFiles(name) {
|
|
|
23529
23496
|
"drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
|
|
23530
23497
|
"@supacloud/js": `^${sdkMetadata.version}`,
|
|
23531
23498
|
"@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
|
|
23532
|
-
effect: "4.0.
|
|
23533
|
-
elysia: "2.0.0-beta.
|
|
23499
|
+
effect: "4.0.5",
|
|
23500
|
+
elysia: "2.0.0-beta.32",
|
|
23534
23501
|
rxjs: appMetadata2.dependencies.rxjs
|
|
23535
23502
|
},
|
|
23536
23503
|
devDependencies: {
|
|
@@ -23784,11 +23751,10 @@ import {
|
|
|
23784
23751
|
Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
|
|
23785
23752
|
defineFeatureSlice, defineFeatureSpec, type Aspect,
|
|
23786
23753
|
} from "@supacloud/app";
|
|
23787
|
-
import { ApplicationError, assertFeatureTransition } from "@supacloud/elysia";
|
|
23788
|
-
import { ApproveBody, HealthResult, Params, ReviewResult } from "./contracts";
|
|
23754
|
+
import { ApplicationError, assertFeatureTransition, createSchemaDecoder } from "@supacloud/elysia";
|
|
23755
|
+
import { ApproveBody, HealthResult, Params, ReviewResult, type ApprovalBody, type Review } from "./contracts";
|
|
23789
23756
|
import { VerifyReviewAttachment } from "./attachment";
|
|
23790
23757
|
import { ReviewUploads, ReviewUploadsController, type ReviewUploadPort } from "./uploads";
|
|
23791
|
-
import type { ReviewRow } from "../../db/schema";
|
|
23792
23758
|
|
|
23793
23759
|
export const reviewSpec = defineFeatureSpec({
|
|
23794
23760
|
name: "review",
|
|
@@ -23803,7 +23769,6 @@ export const reviewSpec = defineFeatureSpec({
|
|
|
23803
23769
|
},
|
|
23804
23770
|
});
|
|
23805
23771
|
|
|
23806
|
-
type Review = Pick<ReviewRow, "state" | "version">;
|
|
23807
23772
|
// Production implementations must bind both operations to the current transaction.
|
|
23808
23773
|
export interface ReviewStore extends Partial<ReviewUploadPort> {
|
|
23809
23774
|
get(table: string, key: string): unknown;
|
|
@@ -23816,13 +23781,12 @@ export const requireRequest: Aspect = (context, next) => {
|
|
|
23816
23781
|
return next();
|
|
23817
23782
|
};
|
|
23818
23783
|
|
|
23784
|
+
const decodeReview = createSchemaDecoder(ReviewResult, { normalize: false });
|
|
23819
23785
|
function readReview(value: unknown): Review {
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
!("version" in value) || typeof value.version !== "number") {
|
|
23786
|
+
try { return decodeReview(value); }
|
|
23787
|
+
catch {
|
|
23823
23788
|
throw new ApplicationError("Review not found", { status: 404, code: "REVIEW_NOT_FOUND" });
|
|
23824
23789
|
}
|
|
23825
|
-
return { state: value.state, version: value.version };
|
|
23826
23790
|
}
|
|
23827
23791
|
|
|
23828
23792
|
@Command({
|
|
@@ -23854,7 +23818,7 @@ type ReviewError =
|
|
|
23854
23818
|
| { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
|
|
23855
23819
|
| { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
|
|
23856
23820
|
| { readonly _tag: "COMMAND_REJECTED" }
|
|
23857
|
-
| { readonly _tag: "
|
|
23821
|
+
| { readonly _tag: "COMMAND_BINDING_MISMATCH" }
|
|
23858
23822
|
| { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" };
|
|
23859
23823
|
|
|
23860
23824
|
@Controller("/reviews")
|
|
@@ -23883,13 +23847,13 @@ export class ReviewController {
|
|
|
23883
23847
|
{ tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
|
|
23884
23848
|
{ tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
|
|
23885
23849
|
{ tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
|
|
23886
|
-
{ tag: "
|
|
23850
|
+
{ tag: "COMMAND_BINDING_MISMATCH", status: 501, code: "COMMAND_BINDING_MISMATCH" },
|
|
23887
23851
|
{ tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
|
|
23888
23852
|
],
|
|
23889
23853
|
retry: "none",
|
|
23890
23854
|
},
|
|
23891
23855
|
})
|
|
23892
|
-
approve(@Param("id") id: string, @Body() body:
|
|
23856
|
+
approve(@Param("id") id: string, @Body() body: ApprovalBody): Effect.Effect<Review, ReviewError, never> {
|
|
23893
23857
|
return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
|
|
23894
23858
|
}
|
|
23895
23859
|
}
|
|
@@ -23901,13 +23865,16 @@ export const ReviewFeature = defineFeatureSlice({
|
|
|
23901
23865
|
});
|
|
23902
23866
|
`,
|
|
23903
23867
|
"src/review/attachment.ts": STARTER_REVIEW_JOB,
|
|
23904
|
-
"src/review/contracts.ts": `import { t } from "elysia";
|
|
23868
|
+
"src/review/contracts.ts": `import { t, type UnwrapSchema } from "elysia";
|
|
23905
23869
|
export const Params = t.Object({ id: t.String({ minLength: 1 }) });
|
|
23906
|
-
export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1 }) });
|
|
23870
|
+
export const ApproveBody = t.Object({ expectedVersion: t.Integer({ minimum: 1, maximum: 9007199254740991 }) });
|
|
23871
|
+
export const ApprovalInput = t.Object({ ...Params.properties, ...ApproveBody.properties }, { additionalProperties: false });
|
|
23872
|
+
export type ApprovalBody = UnwrapSchema<typeof ApproveBody>;
|
|
23907
23873
|
export const ReviewResult = t.Object({
|
|
23908
23874
|
state: t.Union([t.Literal("draft"), t.Literal("approved")]),
|
|
23909
|
-
version: t.Integer({ minimum: 1 }),
|
|
23875
|
+
version: t.Integer({ minimum: 1, maximum: 9007199254740991 }),
|
|
23910
23876
|
});
|
|
23877
|
+
export type Review = UnwrapSchema<typeof ReviewResult>;
|
|
23911
23878
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
23912
23879
|
export const UploadParams = t.Object({ id: t.String({ format: "uuid" }) });
|
|
23913
23880
|
export const UploadBody = t.Object({
|
|
@@ -24049,6 +24016,18 @@ Ordinary reads use PostgREST through the existing user-scoped
|
|
|
24049
24016
|
Use the caller's session, RLS and grants; never introduce a second session store.
|
|
24050
24017
|
Business writes continue through the governed Command/RPC boundary.
|
|
24051
24018
|
|
|
24019
|
+
Use listReviews from src/review/list-reviews.ts for the paginated review list.
|
|
24020
|
+
Type the existing client with ReviewReadDatabase (a read-only projection derived
|
|
24021
|
+
from db/schema.ts), then call listReviews(client, { page: 1, pageSize: 25, signal }).
|
|
24022
|
+
The helper returns data, total, page, pageSize and hasNextPage, orders by the unique
|
|
24023
|
+
id and preserves PostgREST errors. Pages are one-based and limited to 100 rows.
|
|
24024
|
+
Exact counts obey RLS but may be expensive on large tables; offset pages are not
|
|
24025
|
+
a cross-request snapshot when records change. No owner filter replaces RLS.
|
|
24026
|
+
Do not create a service-role client or a second auth session for this helper.
|
|
24027
|
+
Existing starters can add this file without a new migration only if their reviewed
|
|
24028
|
+
review-table grants and identity-mapping RLS from migration 003 are already applied.
|
|
24029
|
+
The projection is not a full database export and grants no write access.
|
|
24030
|
+
|
|
24052
24031
|
GraphQL is not generated or enabled by this starter. For a specific Query Contract,
|
|
24053
24032
|
explicitly configure graphql.schema with a role-scoped database export and add
|
|
24054
24033
|
checked query documents. Drizzle and append-only migrations remain the database
|
|
@@ -26359,6 +26338,7 @@ async function runAppRelease(request, options, delegate) {
|
|
|
26359
26338
|
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
26360
26339
|
ok: true,
|
|
26361
26340
|
operation: "applications.release",
|
|
26341
|
+
...deployReceipt.status === "accepted" ? { status: "accepted" } : {},
|
|
26362
26342
|
project_ref: ref,
|
|
26363
26343
|
application_id: id,
|
|
26364
26344
|
environment_id: environmentId,
|
|
@@ -26369,16 +26349,17 @@ async function runAppRelease(request, options, delegate) {
|
|
|
26369
26349
|
build: buildReceipt ? completed.build : { skipped: true, manifest_path: manifestPath },
|
|
26370
26350
|
phases: completed,
|
|
26371
26351
|
no_op: deployReceipt.action === "no-op" || deployReceipt.no_op === true,
|
|
26372
|
-
execution_performed: deployReceipt.execution_performed === true,
|
|
26352
|
+
...deployReceipt.status === "accepted" ? {} : { execution_performed: deployReceipt.execution_performed === true },
|
|
26373
26353
|
...typeof deployReceipt.activation_id === "string" ? { activation_id: deployReceipt.activation_id } : {},
|
|
26374
26354
|
...deployReceipt.web === undefined ? {} : { web: deployReceipt.web }
|
|
26375
26355
|
};
|
|
26376
26356
|
return request.format === "json" || request.json === true ? textResult(JSON.stringify(result, null, 2)) : textResult([
|
|
26377
|
-
`${ref}/${id}/${environmentId}: ${result.no_op ? "no-op" : "released"}`,
|
|
26357
|
+
`${ref}/${id}/${environmentId}: ${deployReceipt.status === "accepted" ? "accepted" : result.no_op ? "no-op" : "released"}`,
|
|
26378
26358
|
`Release: ${releaseId}`,
|
|
26379
26359
|
`Configuration: ${configurationId}`,
|
|
26380
26360
|
`Activation: ${result.activation_id ?? "not performed"}`,
|
|
26381
26361
|
...result.deployment_id === undefined ? [] : [`Deployment: ${result.deployment_id}`],
|
|
26362
|
+
...deployReceipt.status === "accepted" && result.deployment_id !== undefined ? [`Status: supacloud deploy --status ${result.deployment_id}`] : [],
|
|
26382
26363
|
`Build: ${request.skip_build ? "skipped" : "completed"}`
|
|
26383
26364
|
].join(`
|
|
26384
26365
|
`));
|
|
@@ -29057,11 +29038,22 @@ async function requireProjectDeploymentProtocol(http, projectRef, environmentId)
|
|
|
29057
29038
|
throw new Error("DEPLOYMENT_PROTOCOL_MISMATCH: Upgrade the SupaCloud server and CLI together (`npm install -g @supacloud/cli@latest`), then retry `supacloud deploy`. No upload or deployment was attempted; legacy endpoints will not be used.");
|
|
29058
29039
|
}
|
|
29059
29040
|
}
|
|
29041
|
+
var operation2 = "applications.project_deploy";
|
|
29060
29042
|
var scopeSchema = _Object_({
|
|
29061
29043
|
project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
|
|
29062
29044
|
application_id: ApplicationIdSchema,
|
|
29063
29045
|
environment_id: ApplicationIdSchema
|
|
29064
29046
|
});
|
|
29047
|
+
var acceptedSchema = _Object_({
|
|
29048
|
+
schema: Literal("supacloud.project-deployment-accepted.v1"),
|
|
29049
|
+
ok: Literal(true),
|
|
29050
|
+
status: Literal("accepted"),
|
|
29051
|
+
deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
|
|
29052
|
+
...scopeSchema.properties,
|
|
29053
|
+
release_id: ApplicationDeployRequestSchema.properties.release_id,
|
|
29054
|
+
request_sha256: String2({ pattern: "^[a-f0-9]{64}$" }),
|
|
29055
|
+
replayed: Boolean2()
|
|
29056
|
+
}, { additionalProperties: false });
|
|
29065
29057
|
var statusSchema = _Object_({
|
|
29066
29058
|
schema: Literal("supacloud.project-deployment-status.v1"),
|
|
29067
29059
|
deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
|
|
@@ -29086,6 +29078,27 @@ var statusSchema = _Object_({
|
|
|
29086
29078
|
receipt: Optional(Unknown()),
|
|
29087
29079
|
failure_code: Optional(String2({ pattern: "^[A-Z][A-Z0-9_]{0,63}$" }))
|
|
29088
29080
|
}, { additionalProperties: false });
|
|
29081
|
+
async function readProjectDeploymentStatus(http, projectRef, environmentId, deploymentId) {
|
|
29082
|
+
if (!/^[a-z0-9-]{1,20}$/.test(projectRef) || !Check2(ApplicationIdSchema, environmentId) || !Check2(ApplicationDeployRequestSchema.properties.mutation_id, deploymentId)) {
|
|
29083
|
+
return releaseControlFailure(operation2, "INVALID_RESPONSE", null);
|
|
29084
|
+
}
|
|
29085
|
+
const path = `/v1/projects/${encodeURIComponent(projectRef)}/environments/${encodeURIComponent(environmentId)}/deployments/${deploymentId}`;
|
|
29086
|
+
const response = await http.get(path, { retry: false, totalTimeoutMs: 1e4, maxJsonBytes: 131072 });
|
|
29087
|
+
try {
|
|
29088
|
+
const state = response.data;
|
|
29089
|
+
if (!response.ok || response.status !== 200 || !Check2(statusSchema, state) || state.project_ref !== projectRef || state.environment_id !== environmentId || state.deployment_id !== deploymentId)
|
|
29090
|
+
throw new Error;
|
|
29091
|
+
if (state.status === "succeeded") {
|
|
29092
|
+
const receipt = parseApplicationDeployResult(state.receipt);
|
|
29093
|
+
if (state.phase !== "verifying" || receipt.project_ref !== projectRef || receipt.environment_id !== environmentId || receipt.application_id !== state.application_id || receipt.mutation_id !== deploymentId)
|
|
29094
|
+
throw new Error;
|
|
29095
|
+
} else if (state.receipt !== undefined)
|
|
29096
|
+
throw new Error;
|
|
29097
|
+
return state.status === "outcome_unknown" || state.status === "failed_terminal" ? releaseControlFailure(operation2, state.status === "outcome_unknown" ? "OUTCOME_UNKNOWN" : "MUTATION_NOT_SUCCEEDED", response.status, { ...state }) : releaseControlSuccess(operation2, { ...state });
|
|
29098
|
+
} catch {
|
|
29099
|
+
return releaseControlFailure(operation2, "INVALID_RESPONSE", response.status);
|
|
29100
|
+
}
|
|
29101
|
+
}
|
|
29089
29102
|
|
|
29090
29103
|
// src/shared/tools/deploy-tools.ts
|
|
29091
29104
|
var MAX_SOURCE_BYTES = 256 * 1024 * 1024;
|
|
@@ -29102,6 +29115,8 @@ var deployToolSchema = {
|
|
|
29102
29115
|
cwd: optional(String2(), "Project directory (default: current directory)"),
|
|
29103
29116
|
skip_build: optional(Boolean2(), "Use an existing output directory without building"),
|
|
29104
29117
|
assets_only: optional(Boolean2(), "Publish only configured static assets; leave backend Functions, schedules and migrations unchanged"),
|
|
29118
|
+
backend: optional(Boolean2(), "Publish the application backend independently; do not build or activate frontend assets"),
|
|
29119
|
+
status: optional(String2({ format: "uuid" }), "Read a backend deployment status without publishing"),
|
|
29105
29120
|
json: optional(Boolean2(), "Print only the final JSON result")
|
|
29106
29121
|
};
|
|
29107
29122
|
function record4(candidate) {
|
|
@@ -29477,8 +29492,7 @@ async function collectArchiveFiles(root) {
|
|
|
29477
29492
|
}
|
|
29478
29493
|
return { files, bytes, count: paths.length, treeSha256: tree.digest("hex") };
|
|
29479
29494
|
}
|
|
29480
|
-
async function
|
|
29481
|
-
const collected = await collectArchiveFiles(outputDirectory);
|
|
29495
|
+
async function createCollectedFrontendArchive(outputDirectory, collected) {
|
|
29482
29496
|
const temporaryDirectory = await mkdtemp2(join18(tmpdir3(), "supacloud-deploy-"));
|
|
29483
29497
|
const archivePath = join18(temporaryDirectory, `${basename8(outputDirectory)}.tar.zst`);
|
|
29484
29498
|
try {
|
|
@@ -29499,16 +29513,7 @@ async function createFrontendArchive(outputDirectory) {
|
|
|
29499
29513
|
throw error;
|
|
29500
29514
|
}
|
|
29501
29515
|
}
|
|
29502
|
-
function
|
|
29503
|
-
if (response.isError)
|
|
29504
|
-
throw new Error(response.content[0]?.text || "Deployment operation failed");
|
|
29505
|
-
const text = response.content.find((chunk) => chunk.type === "text")?.text;
|
|
29506
|
-
const parsed = text ? record4(JSON.parse(text)) : null;
|
|
29507
|
-
if (!parsed)
|
|
29508
|
-
throw new Error("Deployment operation returned an invalid response");
|
|
29509
|
-
return parsed;
|
|
29510
|
-
}
|
|
29511
|
-
async function stageApplicationWebRelease(http, projectRef, environmentId, configRoot, skipBuild, json, environmentVariables, assetsOnly = false) {
|
|
29516
|
+
async function publishConfiguredFrontend(http, projectRef, configRoot, skipBuild, json, environmentVariables) {
|
|
29512
29517
|
const target = selectFrontendTarget(configRoot, await readDeployConfig(configRoot));
|
|
29513
29518
|
if (!target)
|
|
29514
29519
|
return null;
|
|
@@ -29532,27 +29537,8 @@ async function stageApplicationWebRelease(http, projectRef, environmentId, confi
|
|
|
29532
29537
|
if (!skipBuild) {
|
|
29533
29538
|
await runBuild(buildCommand, projectDirectory, build.env, json);
|
|
29534
29539
|
}
|
|
29535
|
-
const
|
|
29536
|
-
|
|
29537
|
-
if (assetsOnly) {
|
|
29538
|
-
return { result: await publishFrontendRelease(http, projectRef, selected.id, archive.archivePath, archive.treeSha256) };
|
|
29539
|
-
}
|
|
29540
|
-
const uploaded = payload(await uploadProjectWebRelease(http, projectRef, environmentId, selected.id, archive.archivePath));
|
|
29541
|
-
const release = record4(uploaded.release);
|
|
29542
|
-
const releaseId = requiredString(release?.release_id, "release.release_id");
|
|
29543
|
-
if (releaseId !== archive.sha256)
|
|
29544
|
-
throw new Error("Uploaded Web release identity does not match the local archive");
|
|
29545
|
-
if (release?.tree_sha256 !== archive.treeSha256)
|
|
29546
|
-
throw new Error("Uploaded Web release tree does not match the local build");
|
|
29547
|
-
return {
|
|
29548
|
-
request: {
|
|
29549
|
-
deployment_id: selected.id,
|
|
29550
|
-
release_id: releaseId
|
|
29551
|
-
}
|
|
29552
|
-
};
|
|
29553
|
-
} finally {
|
|
29554
|
-
await archive.cleanup();
|
|
29555
|
-
}
|
|
29540
|
+
const collected = await collectArchiveFiles(outputDirectory);
|
|
29541
|
+
return publishFrontendRelease(http, projectRef, selected.id, () => createCollectedFrontendArchive(outputDirectory, collected), collected.treeSha256);
|
|
29556
29542
|
}
|
|
29557
29543
|
function registerDeployTools(server, http, options = {}) {
|
|
29558
29544
|
registerTool(server, "deploy", "Build and publish the linked project with one command", deployToolSchema, async (args) => {
|
|
@@ -29560,6 +29546,16 @@ function registerDeployTools(server, http, options = {}) {
|
|
|
29560
29546
|
const projectRef = String(args.ref || options.projectRef || "").trim();
|
|
29561
29547
|
if (!projectRef)
|
|
29562
29548
|
throw new Error("A linked project ref is required. Set SUPACLOUD_PROJECT_REF or pass --ref");
|
|
29549
|
+
if (args.assets_only === true && args.backend === true)
|
|
29550
|
+
throw new Error("Choose either frontend assets or backend deployment");
|
|
29551
|
+
if (typeof args.status === "string") {
|
|
29552
|
+
if (args.assets_only === true || args.backend === true)
|
|
29553
|
+
throw new Error("Status cannot be combined with publishing options");
|
|
29554
|
+
const environmentId = options.environment?.trim() || options.application?.environmentId?.trim();
|
|
29555
|
+
if (!environmentId)
|
|
29556
|
+
throw new Error("Deployment status requires SUPACLOUD_ENV");
|
|
29557
|
+
return readProjectDeploymentStatus(http, projectRef, environmentId, args.status);
|
|
29558
|
+
}
|
|
29563
29559
|
const configRoot = await findDeployConfigRoot(invocationDirectory, options.environment ?? undefined);
|
|
29564
29560
|
const config = await readDeployConfig(configRoot);
|
|
29565
29561
|
const application = options.application?.id ? options.application : config.application ? {
|
|
@@ -29567,7 +29563,7 @@ function registerDeployTools(server, http, options = {}) {
|
|
|
29567
29563
|
environmentId: options.environment || config.application.environmentId
|
|
29568
29564
|
} : undefined;
|
|
29569
29565
|
const legacyManifest = await hasProjectReleaseManifest(configRoot);
|
|
29570
|
-
const assetsOnly = args.assets_only === true ||
|
|
29566
|
+
const assetsOnly = args.assets_only === true || args.backend !== true && selectFrontendTarget(configRoot, config) !== null;
|
|
29571
29567
|
if (assetsOnly) {
|
|
29572
29568
|
if (!selectFrontendTarget(configRoot, config)) {
|
|
29573
29569
|
throw new Error("Static asset deployment requires a frontend configuration in supacloud.json");
|
|
@@ -29577,10 +29573,10 @@ function registerDeployTools(server, http, options = {}) {
|
|
|
29577
29573
|
throw new Error("Static asset deployment requires SUPACLOUD_ENV");
|
|
29578
29574
|
}
|
|
29579
29575
|
await requireProjectDeploymentProtocol(http, projectRef, environmentId);
|
|
29580
|
-
const
|
|
29581
|
-
if (!
|
|
29576
|
+
const result = await publishConfiguredFrontend(http, projectRef, configRoot, args.skip_build === true, args.json === true, options.environmentVariables);
|
|
29577
|
+
if (!result)
|
|
29582
29578
|
throw new Error("Static asset deployment did not produce a receipt");
|
|
29583
|
-
return
|
|
29579
|
+
return result;
|
|
29584
29580
|
}
|
|
29585
29581
|
if (legacyManifest) {
|
|
29586
29582
|
throw new Error(`${DEPLOYMENT_UPGRADE} Replace deploy-manifest.json with the current application delivery configuration; no remote mutation was performed.`);
|
|
@@ -29602,7 +29598,6 @@ function registerDeployTools(server, http, options = {}) {
|
|
|
29602
29598
|
throw new Error("Application profile must set only one of SUPACLOUD_APPLICATION_CONFIGURATION_PATH or SUPACLOUD_APPLICATION_CONFIGURATION_ID");
|
|
29603
29599
|
}
|
|
29604
29600
|
await requireProjectDeploymentProtocol(http, projectRef, environmentId);
|
|
29605
|
-
const stagedWeb = await stageApplicationWebRelease(http, projectRef, environmentId, configRoot, args.skip_build === true, args.json === true, options.environmentVariables);
|
|
29606
29601
|
const result = await options.applicationRelease({
|
|
29607
29602
|
action: "release",
|
|
29608
29603
|
ref: projectRef,
|
|
@@ -29613,7 +29608,6 @@ function registerDeployTools(server, http, options = {}) {
|
|
|
29613
29608
|
...configurationId ? { configuration_id: configurationId } : {},
|
|
29614
29609
|
...application.manifestPath ? { manifest_path: application.manifestPath } : {},
|
|
29615
29610
|
...args.skip_build === true ? { skip_build: true } : {},
|
|
29616
|
-
...stagedWeb?.request === undefined ? {} : { web_release: stagedWeb.request },
|
|
29617
29611
|
...args.json === true ? { format: "json", json: true } : {}
|
|
29618
29612
|
});
|
|
29619
29613
|
return result;
|
|
@@ -30408,12 +30402,22 @@ function registerDeveloperExperienceTools(server, options = {}) {
|
|
|
30408
30402
|
}
|
|
30409
30403
|
|
|
30410
30404
|
// src/shared/tools/project-deployment.ts
|
|
30411
|
-
var
|
|
30405
|
+
var operation3 = "applications.project_deploy";
|
|
30412
30406
|
var scopeSchema2 = _Object_({
|
|
30413
30407
|
project_ref: String2({ pattern: "^[a-z0-9-]{1,20}$" }),
|
|
30414
30408
|
application_id: ApplicationIdSchema,
|
|
30415
30409
|
environment_id: ApplicationIdSchema
|
|
30416
30410
|
});
|
|
30411
|
+
var acceptedSchema2 = _Object_({
|
|
30412
|
+
schema: Literal("supacloud.project-deployment-accepted.v1"),
|
|
30413
|
+
ok: Literal(true),
|
|
30414
|
+
status: Literal("accepted"),
|
|
30415
|
+
deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
|
|
30416
|
+
...scopeSchema2.properties,
|
|
30417
|
+
release_id: ApplicationDeployRequestSchema.properties.release_id,
|
|
30418
|
+
request_sha256: String2({ pattern: "^[a-f0-9]{64}$" }),
|
|
30419
|
+
replayed: Boolean2()
|
|
30420
|
+
}, { additionalProperties: false });
|
|
30417
30421
|
var statusSchema2 = _Object_({
|
|
30418
30422
|
schema: Literal("supacloud.project-deployment-status.v1"),
|
|
30419
30423
|
deployment_id: ApplicationDeployRequestSchema.properties.mutation_id,
|
|
@@ -30457,7 +30461,7 @@ async function runProjectDeployment(http, args) {
|
|
|
30457
30461
|
...args.web_release === undefined ? {} : { web_release: args.web_release }
|
|
30458
30462
|
};
|
|
30459
30463
|
if (!Check2(scopeSchema2, scope) || !Check2(ApplicationDeployRequestSchema, request) || request.configuration_id !== undefined && request.configuration_write !== undefined && request.configuration_id !== request.configuration_write.configuration_id) {
|
|
30460
|
-
return releaseControlFailure(
|
|
30464
|
+
return releaseControlFailure(operation3, "INVALID_RESPONSE", null);
|
|
30461
30465
|
}
|
|
30462
30466
|
const expected = { ...scope, request };
|
|
30463
30467
|
const identity = {
|
|
@@ -30473,15 +30477,29 @@ async function runProjectDeployment(http, args) {
|
|
|
30473
30477
|
deployment_id: mutation_id,
|
|
30474
30478
|
application_id: scope.application_id
|
|
30475
30479
|
}, { timeoutMs: 120000, maxJsonBytes: 131072, responseTimeoutMs: 30000 });
|
|
30476
|
-
if (result.ok && result.status ===
|
|
30480
|
+
if (result.ok && result.status === 202) {
|
|
30481
|
+
const accepted = result.data;
|
|
30482
|
+
if (Check2(acceptedSchema2, accepted) && accepted.deployment_id === mutation_id && accepted.project_ref === scope.project_ref && accepted.application_id === scope.application_id && accepted.environment_id === scope.environment_id && accepted.release_id === request.release_id && accepted.request_sha256 === mutationRequestFingerprint(request)) {
|
|
30483
|
+
return releaseControlSuccess(operation3, {
|
|
30484
|
+
status: "accepted",
|
|
30485
|
+
deployment_id: mutation_id,
|
|
30486
|
+
project_ref: scope.project_ref,
|
|
30487
|
+
application_id: scope.application_id,
|
|
30488
|
+
environment_id: scope.environment_id,
|
|
30489
|
+
release_id: request.release_id,
|
|
30490
|
+
replayed: accepted.replayed,
|
|
30491
|
+
status_path: `${endpoint}/${mutation_id}`
|
|
30492
|
+
});
|
|
30493
|
+
}
|
|
30494
|
+
} else if (result.ok && result.status === 200) {
|
|
30477
30495
|
try {
|
|
30478
|
-
return releaseControlSuccess(
|
|
30496
|
+
return releaseControlSuccess(operation3, {
|
|
30479
30497
|
...responseReceipt(result.data, expected),
|
|
30480
30498
|
deployment_id: mutation_id
|
|
30481
30499
|
});
|
|
30482
30500
|
} catch {}
|
|
30483
30501
|
} else if (!result.ok && !result.transportError && !result.responseReadError && result.status < 500 && result.status !== 408) {
|
|
30484
|
-
return releaseControlMutationFailure(
|
|
30502
|
+
return releaseControlMutationFailure(operation3, result, identity);
|
|
30485
30503
|
}
|
|
30486
30504
|
const observed = await http.get(`${endpoint}/${encodeURIComponent(mutation_id)}`, {
|
|
30487
30505
|
retry: false,
|
|
@@ -30495,20 +30513,20 @@ async function runProjectDeployment(http, args) {
|
|
|
30495
30513
|
if (state.deployment_id !== mutation_id || state.project_ref !== scope.project_ref || state.application_id !== scope.application_id || state.environment_id !== scope.environment_id || state.status !== "succeeded" || state.phase !== "verifying")
|
|
30496
30514
|
throw new Error;
|
|
30497
30515
|
const receipt = parseApplicationDeployResult(state.receipt, expected);
|
|
30498
|
-
return releaseControlSuccess(
|
|
30516
|
+
return releaseControlSuccess(operation3, {
|
|
30499
30517
|
...receipt,
|
|
30500
30518
|
deployment_id: mutation_id,
|
|
30501
30519
|
replayed: true,
|
|
30502
30520
|
execution_performed: false
|
|
30503
30521
|
});
|
|
30504
30522
|
} catch {
|
|
30505
|
-
return releaseControlFailure(
|
|
30523
|
+
return releaseControlFailure(operation3, "OUTCOME_UNKNOWN", result.transportError ? null : result.status, identity);
|
|
30506
30524
|
}
|
|
30507
30525
|
}
|
|
30508
30526
|
// package.json
|
|
30509
30527
|
var package_default = {
|
|
30510
30528
|
name: "@supacloud/cli",
|
|
30511
|
-
version: "0.
|
|
30529
|
+
version: "0.101.0",
|
|
30512
30530
|
description: "Project-scoped CLI for SupaCloud users",
|
|
30513
30531
|
type: "module",
|
|
30514
30532
|
main: "./dist/index.js",
|
|
@@ -30560,21 +30578,21 @@ var package_default = {
|
|
|
30560
30578
|
directory: "packages/cli"
|
|
30561
30579
|
},
|
|
30562
30580
|
dependencies: {
|
|
30563
|
-
"@supacloud/compiler": "^0.46.
|
|
30564
|
-
"@supacloud/delivery": "0.18.
|
|
30565
|
-
"@supacloud/db": "0.17.
|
|
30566
|
-
typebox: "1.3.
|
|
30581
|
+
"@supacloud/compiler": "^0.46.3",
|
|
30582
|
+
"@supacloud/delivery": "0.18.1",
|
|
30583
|
+
"@supacloud/db": "0.17.1",
|
|
30584
|
+
typebox: "1.3.37"
|
|
30567
30585
|
},
|
|
30568
30586
|
overrides: {
|
|
30569
30587
|
"@supacloud/contracts": "0.9.0",
|
|
30570
|
-
"@supacloud/delivery": "0.18.
|
|
30571
|
-
"@graphql-tools/utils": "^12.0.
|
|
30588
|
+
"@supacloud/delivery": "0.18.1",
|
|
30589
|
+
"@graphql-tools/utils": "^12.0.4"
|
|
30572
30590
|
},
|
|
30573
30591
|
devDependencies: {
|
|
30574
|
-
"@supacloud/elysia": "
|
|
30592
|
+
"@supacloud/elysia": "0.31.0",
|
|
30575
30593
|
"@types/bun": "^1.4.3",
|
|
30576
|
-
effect: "4.0.
|
|
30577
|
-
elysia: "2.0.0-beta.
|
|
30594
|
+
effect: "4.0.5",
|
|
30595
|
+
elysia: "2.0.0-beta.32",
|
|
30578
30596
|
typescript: "^7.0.2"
|
|
30579
30597
|
}
|
|
30580
30598
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
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`.
|
|
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`. With a frontend configured, deploy publishes only Web assets; use `supacloud deploy --backend` for the application backend. `supacloud deploy --assets_only` remains an explicit Web-only option. SupaCloud owns activation; do not retry with legacy commands.";
|
|
2
2
|
export declare function assertCurrentDeploymentInput(tool: string, input: unknown): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export declare const STARTER_REVIEW_SCHEMA = "-- Apply with the platform migration owner, never from the HTTP process.\n-- One application database belongs to one explicitly provisioned project/tenant.\nCREATE TABLE public.starter_application (\n singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),\n project_id text NOT NULL CHECK (length(project_id) > 0),\n tenant_id text NOT NULL CHECK (length(tenant_id) > 0)\n);\nCREATE TABLE public.starter_reviews (\n id text PRIMARY KEY, owner_id text NOT NULL,\n state text NOT NULL CHECK (state IN ('draft','approved')),\n version integer NOT NULL CHECK (version > 0)\n);\nCREATE TABLE public.starter_members (\n subject text PRIMARY KEY, enabled boolean NOT NULL DEFAULT true,\n can_approve boolean NOT NULL DEFAULT false\n);\nALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;\nALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;\n";
|
|
2
|
-
export declare const STARTER_REVIEW_POSTGRES = "import {
|
|
3
|
-
export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"
|
|
2
|
+
export declare const STARTER_REVIEW_POSTGRES = "import { plaintextCommandInput } from \"@supacloud/commands\";\nimport { CommandError } from \"@supacloud/contracts\";\nimport { createPostgresCommandStore, type CommandDatabase, type CommandTransaction } from \"@supacloud/db\";\nimport {\n ApplicationError, createSchemaDecoder, createSupAuthRequestContext, requireTrustedIdentity,\n createTransactionalCommandGovernance,\n type CommandInvocation, type SupAuthContextOptions,\n} from \"@supacloud/elysia\";\nimport type { AppAdapters } from \"../application\";\nimport type { ReviewStore } from \"../review/review\";\nimport type { ReviewUploadPort } from \"../review/uploads\";\nimport { ApprovalInput, ReviewResult } from \"../review/contracts\";\n\nconst input = createSchemaDecoder(ApprovalInput, { normalize: false });\nconst result = createSchemaDecoder(ReviewResult, { normalize: false });\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 review record\");\n return value;\n}\nfunction rows(value: unknown): Record<string, unknown>[] {\n if (!Array.isArray(value)) throw new TypeError(\"Invalid database result\");\n return value.map(record);\n}\nfunction isPublicConflict(value: unknown): boolean {\n return value instanceof ApplicationError && value.status === 409\n || isRecord(value) && value[\"expose\"] === true && value[\"status\"] === 409 && typeof value[\"code\"] === \"string\";\n}\nexport interface ReviewPostgresOptions {\n database: CommandDatabase;\n tenantId: string;\n identity: Omit<SupAuthContextOptions, \"resolveAccess\">;\n uploads?: ReviewUploadPort;\n // Only database effects here: they commit/rollback with the approval and receipt.\n afterApproved?(tx: CommandTransaction, id: string, version: number): Promise<void>;\n}\n\nexport async function createReviewPostgresAdapters(options: ReviewPostgresOptions): Promise<\n AppAdapters & { requestContext: ReturnType<typeof createSupAuthRequestContext> }\n> {\n const { database, tenantId, identity } = options;\n const projectId = identity.projectId;\n const afterApproved = options.afterApproved;\n if (!tenantId.trim()) throw new TypeError(\"Review tenantId is required\");\n const query = (text: string, parameters: readonly string[] = []) =>\n database.transaction(tx => tx.query(text, parameters));\n const binding = rows(await query(\n \"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\",\n ));\n if (binding.length !== 1 || binding[0]?.[\"project_id\"] !== projectId || binding[0]?.[\"tenant_id\"] !== tenantId) {\n throw new Error(\"Review database project/tenant binding mismatch\");\n }\n const requestContext = createSupAuthRequestContext({\n ...identity,\n async resolveAccess(who) {\n const found = rows(await query(\n \"SELECT m.can_approve FROM public.starter_members m,public.starter_application a \" +\n \"WHERE m.subject=$1 AND m.enabled AND a.singleton AND a.project_id=$2 AND a.tenant_id=$3\",\n [who.subject, projectId, tenantId],\n ));\n return found.length === 1 ? { projectId, tenantId,\n permissions: found[0]?.[\"can_approve\"] === true ? [\"review.approve\"] : [],\n } : null;\n },\n });\n const transaction = () => commandBinding.transaction();\n const store: ReviewStore = {\n ...options.uploads,\n async get(table, id) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n return rows(await transaction().query(\n \"SELECT state,version FROM public.starter_reviews WHERE id=$1 FOR UPDATE\", [id],\n ))[0];\n },\n async set(table, id, value) {\n if (table !== \"reviews\") throw new Error(\"Unsupported review table\");\n const review = result(value);\n const updated = rows(await transaction().query(\n \"UPDATE public.starter_reviews SET state=$2,version=$3 WHERE id=$1 RETURNING id\",\n [id, review.state, review.version],\n ));\n if (updated.length !== 1) throw new Error(\"Review write did not update one record\");\n await afterApproved?.(transaction(), id, review.version);\n },\n };\n function actor(invocation: CommandInvocation): string {\n const who = requireTrustedIdentity(invocation.requestContext);\n const access = record(record(invocation.requestContext)[\"access\"]);\n if (access[\"projectId\"] !== projectId || access[\"tenantId\"] !== tenantId\n || !Array.isArray(access[\"permissions\"]) || !access[\"permissions\"].includes(\"review.approve\")) {\n throw new ApplicationError(\"Command permission denied\", { status: 403, code: \"REVIEW_PERMISSION_DENIED\" });\n }\n return who.subject;\n }\n const commandBinding = createTransactionalCommandGovernance({\n permission: \"review.approve\",\n identity: invocation => ({ actorId: actor(invocation), tenantId }),\n input: invocation => ({\n ...record(invocation.input.body), id: invocation.input.params[\"id\"],\n }),\n rejection: error => isPublicConflict(error) ? new CommandError(\"COMMAND_IDEMPOTENCY_CONFLICT\") : undefined,\n definition: {\n name: \"review.approve\", store: createPostgresCommandStore(database),\n inputCodec: plaintextCommandInput, input, result,\n async authorize(who, request, tx) {\n const found = rows(await tx.query(\n \"SELECT r.id FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id \" +\n \"CROSS JOIN public.starter_application a \" +\n \"WHERE r.id=$1 AND r.owner_id=$2 AND m.enabled AND m.can_approve \" +\n \"AND a.singleton AND a.project_id=$3 AND a.tenant_id=$4 FOR UPDATE OF r,m FOR SHARE OF a\",\n [request.id, who.actorId, projectId, who.tenantId],\n ));\n return found.length === 1 ? \"allow\" : \"deny\";\n },\n audit: { event: \"review.approved\", details: request => ({ reviewId: request.id }) },\n },\n });\n return { deps: { dbClient: store }, requestContext, commandGovernance: commandBinding.governance };\n}\n";
|
|
3
|
+
export declare const STARTER_REVIEW_POSTGRES_TEST = "import { expect, test } from \"bun:test\";\nimport { createReviewPostgresAdapters } from \"../src/host/review-postgres\";\nimport type { ReviewStore } from \"../src/review/review\";\n\nconst identity = {\n issuer: \"https://identity.example.test\", audience: \"review-test\", clientId: \"review-client\",\n projectId: \"review-test\", jwksUrl: \"https://identity.example.test/jwks\",\n};\nfunction database(binding: unknown) {\n const queries: string[] = [];\n return {\n queries,\n async transaction<T>(run: (tx: { query(text: string): Promise<unknown> }) => Promise<T>): Promise<T> {\n return run({ async query(text) { queries.push(text); return binding; } });\n },\n };\n}\n\ntest(\"PostgreSQL host rejects missing or mismatched application ownership without creating data\", async () => {\n for (const binding of [[], [{ project_id: \"other\", tenant_id: \"review-test\" }],\n [{ project_id: \"review-test\", tenant_id: \"other\" }]]) {\n const db = database(binding);\n await expect(createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" }))\n .rejects.toThrow(\"binding mismatch\");\n expect(db.queries).toEqual([\"SELECT project_id,tenant_id FROM public.starter_application WHERE singleton\"]);\n }\n});\n\ntest(\"PostgreSQL host never falls back to demo identity or unbound business storage\", async () => {\n const db = database([{ project_id: \"review-test\", tenant_id: \"review-test\" }]);\n const adapters = await createReviewPostgresAdapters({ database: db, identity, tenantId: \"review-test\" });\n await expect(adapters.requestContext(new Request(\"http://localhost/reviews/health\")))\n .rejects.toMatchObject({ code: \"AUTHENTICATION_REQUIRED\" });\n const store = adapters.deps[\"dbClient\"] as ReviewStore;\n await expect(store.get(\"reviews\", \"example\")).rejects.toThrow(\"not active\");\n await expect(store.set(\"reviews\", \"example\", { state: \"approved\", version: 2 }))\n .rejects.toThrow(\"not active\");\n expect(db.queries).toHaveLength(1);\n});\n";
|
|
4
4
|
export declare const STARTER_REVIEW_DELIVERY_HOST = "import { SQL } from \"bun\";\nimport { createClient } from \"@supabase/supabase-js\";\nimport { createBunCommandDatabase } from \"@supacloud/db/bun\";\nimport { createApplication, type CompiledModule } from \"@supacloud/elysia\";\nimport { createReviewPostgresAdapters } from \"./host/review-postgres\";\nimport { createReviewAttachmentAdapters } from \"./host/review-attachments\";\nimport { createReviewUploadAdapters } from \"./host/review-uploads\";\n\nfunction required(name: string): string {\n const value = process.env[name];\n if (!value?.trim()) throw new Error(\"Missing required host 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 createDeliveryApplication(modules: CompiledModule[], lifecycle: { signal: AbortSignal }) {\n lifecycle.signal.throwIfAborted();\n const connection = databaseConnection();\n const attachments = process.env[\"REVIEW_ATTACHMENTS\"];\n if (attachments !== undefined && attachments !== \"enabled\") throw new Error(\"Invalid attachment configuration\");\n const settings = {\n tenantId: required(\"APP_TENANT_ID\"),\n identity: {\n issuer: required(\"SUPAUTH_ISSUER\"), audience: required(\"SUPAUTH_AUDIENCE\"),\n clientId: required(\"SUPAUTH_CLIENT_ID\"), projectId: required(\"SUPACLOUD_PROJECT_ID\"),\n jwksUrl: required(\"SUPAUTH_JWKS_URL\"),\n },\n };\n const pool = new SQL({ ...connection, max: 12, connectionTimeout: 5 });\n let closing: Promise<void> | undefined;\n const close = () => closing ??= pool.close({ timeout: 5 });\n let stage: \"attachments\" | \"uploads\" | \"postgres\" | \"application\" = \"attachments\";\n try {\n const database = createBunCommandDatabase(pool);\n const attachmentOptions = attachments === \"enabled\" ? {\n database, tenantId: settings.tenantId, projectId: settings.identity.projectId,\n service: createClient(required(\"SUPACLOUD_URL\"), required(\"SUPACLOUD_SERVICE_ROLE_KEY\"), {\n auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n }),\n } : undefined;\n const durable = attachmentOptions ? await createReviewAttachmentAdapters(attachmentOptions) : undefined;\n stage = \"uploads\";\n const uploads = attachmentOptions ? await createReviewUploadAdapters(attachmentOptions) : undefined;\n stage = \"postgres\";\n const adapters = await createReviewPostgresAdapters({\n database, tenantId: settings.tenantId, identity: settings.identity,\n ...(uploads ? { uploads } : {}),\n ...(durable ? { afterApproved: durable.enqueue } : {}),\n });\n lifecycle.signal.throwIfAborted();\n stage = \"application\";\n const app = createApplication({ name: \"review\", modules, ...adapters });\n return { fetch: (request: Request) => app.handle(request), close };\n } catch (error) {\n await close();\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-host-initialization-failed\", stage, code }));\n // Database/identity errors can contain connection details; never echo the underlying cause.\n throw new Error(\"Review host initialization failed\");\n }\n}\n";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STARTER_REVIEW_QUERY = "import type { SupabaseClient } from \"@supabase/supabase-js\";\nimport type { ReviewRow } from \"../../db/schema\";\n\n// This read projection uses SQL column names shared with the Drizzle source.\n// Do not expose camelCase Drizzle property names as PostgREST column names.\nexport type ReviewSummary = Pick<ReviewRow, \"id\" | \"state\" | \"version\">;\nexport type ReviewReadDatabase = {\n public: {\n Tables: {\n starter_reviews: {\n Row: ReviewSummary;\n Insert: never;\n Update: never;\n Relationships: [];\n };\n };\n Views: Record<never, never>;\n Functions: Record<never, never>;\n };\n};\n\nexport interface ReviewPageOptions {\n page?: number;\n pageSize?: number;\n signal?: AbortSignal;\n}\n\n// Reuse the application's user-scoped client. Identity verification and row\n// authorization belong to the gateway and database, never a caller-supplied owner.\nexport async function listReviews(\n client: SupabaseClient<ReviewReadDatabase>,\n { page = 1, pageSize = 25, signal }: ReviewPageOptions = {},\n) {\n if (!Number.isSafeInteger(page) || page < 1\n || !Number.isSafeInteger(pageSize) || pageSize < 1 || pageSize > 100\n || !Number.isSafeInteger(page * pageSize)) {\n throw new RangeError(\"Invalid review page: page >= 1 and pageSize between 1 and 100 are required\");\n }\n signal?.throwIfAborted();\n const start = (page - 1) * pageSize;\n const query = client.from(\"starter_reviews\")\n .select(\"id,state,version\", { count: \"exact\" })\n .order(\"id\", { ascending: true })\n .range(start, start + pageSize - 1);\n const { data, error, count } = await (signal ? query.abortSignal(signal) : query);\n if (error) throw error;\n if (data === null || count === null) throw new Error(\"Incomplete review page response\");\n return { data, total: count, page, pageSize, hasNextPage: start + data.length < count };\n}\n";
|
|
@@ -27,6 +27,8 @@ export declare const deployToolSchema: {
|
|
|
27
27
|
cwd: Type.TOptional<Type.TString>;
|
|
28
28
|
skip_build: Type.TOptional<Type.TBoolean>;
|
|
29
29
|
assets_only: Type.TOptional<Type.TBoolean>;
|
|
30
|
+
backend: Type.TOptional<Type.TBoolean>;
|
|
31
|
+
status: Type.TOptional<Type.TString>;
|
|
30
32
|
json: Type.TOptional<Type.TBoolean>;
|
|
31
33
|
};
|
|
32
34
|
export declare function findDeployConfigRoot(startDirectory: string, environmentName?: string): Promise<string>;
|
|
@@ -46,7 +46,10 @@ 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,
|
|
49
|
+
export declare function publishFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, archiveSource: string | (() => Promise<{
|
|
50
|
+
archivePath: string;
|
|
51
|
+
cleanup(): Promise<void>;
|
|
52
|
+
}>), expectedTreeSha256: string): Promise<ToolResponse>;
|
|
50
53
|
export declare function rollbackFrontendRelease(http: FrontendReleaseHttp, projectRef: string, deploymentId: string, releaseId?: string, options?: {
|
|
51
54
|
expectedActiveReleaseId?: string;
|
|
52
55
|
expectedActivationId?: string;
|
|
@@ -2,3 +2,4 @@ import type { ToolResult } from "../tool-server";
|
|
|
2
2
|
import type { HttpTransport } from "../transports/http";
|
|
3
3
|
export declare function requireProjectDeploymentProtocol(http: Pick<HttpTransport, "get">, projectRef: string, environmentId: string): Promise<void>;
|
|
4
4
|
export declare function runProjectDeployment(http: Pick<HttpTransport, "post" | "get">, args: Record<string, unknown>): Promise<ToolResult>;
|
|
5
|
+
export declare function readProjectDeploymentStatus(http: Pick<HttpTransport, "get">, projectRef: string, environmentId: string, deploymentId: string): Promise<ToolResult>;
|
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 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`.
|
|
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`. With a frontend configured, deploy publishes only Web assets; use `supacloud deploy --backend` for the application backend. `supacloud deploy --assets_only` remains an explicit Web-only option. 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.101.0",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,21 +52,21 @@
|
|
|
52
52
|
"directory": "packages/cli"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@supacloud/compiler": "^0.46.
|
|
56
|
-
"@supacloud/delivery": "0.18.
|
|
57
|
-
"@supacloud/db": "0.17.
|
|
58
|
-
"typebox": "1.3.
|
|
55
|
+
"@supacloud/compiler": "^0.46.3",
|
|
56
|
+
"@supacloud/delivery": "0.18.1",
|
|
57
|
+
"@supacloud/db": "0.17.1",
|
|
58
|
+
"typebox": "1.3.37"
|
|
59
59
|
},
|
|
60
60
|
"overrides": {
|
|
61
61
|
"@supacloud/contracts": "0.9.0",
|
|
62
|
-
"@supacloud/delivery": "0.18.
|
|
63
|
-
"@graphql-tools/utils": "^12.0.
|
|
62
|
+
"@supacloud/delivery": "0.18.1",
|
|
63
|
+
"@graphql-tools/utils": "^12.0.4"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@supacloud/elysia": "
|
|
66
|
+
"@supacloud/elysia": "0.31.0",
|
|
67
67
|
"@types/bun": "^1.4.3",
|
|
68
|
-
"effect": "4.0.
|
|
69
|
-
"elysia": "2.0.0-beta.
|
|
68
|
+
"effect": "4.0.5",
|
|
69
|
+
"elysia": "2.0.0-beta.32",
|
|
70
70
|
"typescript": "^7.0.2"
|
|
71
71
|
}
|
|
72
72
|
}
|