@supacloud/cli 0.95.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -476,7 +476,7 @@ Example configuration write file:
|
|
|
476
476
|
"configuration_id": "01234567-89ab-4def-8123-456789abcdef",
|
|
477
477
|
"expected_configuration_id": null,
|
|
478
478
|
"configuration": {
|
|
479
|
-
"bun_version": "1.4.
|
|
479
|
+
"bun_version": "1.4.3",
|
|
480
480
|
"targets": [
|
|
481
481
|
{
|
|
482
482
|
"name": "api",
|
package/dist/index.js
CHANGED
|
@@ -9964,22 +9964,28 @@ function validateWorkerExecution(groups) {
|
|
|
9964
9964
|
var objectOptions = { additionalProperties: false };
|
|
9965
9965
|
var reference = String2({ minLength: 1, maxLength: 128, pattern: "^[A-Za-z][A-Za-z0-9_.:/-]*$" });
|
|
9966
9966
|
var references = _Array_(reference, { uniqueItems: true });
|
|
9967
|
-
var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook")]);
|
|
9967
|
+
var kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
9968
9968
|
var isolation = Union([Literal("shared"), Literal("process")]);
|
|
9969
9969
|
var DeliveryFilePathSchema = String2({
|
|
9970
9970
|
maxLength: 512,
|
|
9971
9971
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
9972
9972
|
});
|
|
9973
|
+
var functionTarget = _Object_({
|
|
9974
|
+
slug: String2({ pattern: "^[a-z][a-z0-9-]{0,62}$" }),
|
|
9975
|
+
entrypoint: DeliveryFilePathSchema,
|
|
9976
|
+
verifyJwt: Boolean2()
|
|
9977
|
+
}, objectOptions);
|
|
9973
9978
|
var DeliveryOptionsSchema = _Object_({
|
|
9974
9979
|
version: Literal(1),
|
|
9975
9980
|
execution: Optional(WorkerExecutionSchema),
|
|
9976
9981
|
targets: Optional(_Array_(_Object_({
|
|
9977
9982
|
name: String2({ pattern: "^[a-z][a-z0-9-]{0,62}$" }),
|
|
9978
9983
|
kind,
|
|
9979
|
-
modules: _Array_(reference, { minItems: 1, uniqueItems: true }),
|
|
9984
|
+
modules: Optional(_Array_(reference, { minItems: 1, uniqueItems: true })),
|
|
9980
9985
|
isolation: Optional(isolation),
|
|
9981
9986
|
capabilities: Optional(references),
|
|
9982
|
-
compute: Optional(ComputeResourcesSchema)
|
|
9987
|
+
compute: Optional(ComputeResourcesSchema),
|
|
9988
|
+
function: Optional(functionTarget)
|
|
9983
9989
|
}, objectOptions))),
|
|
9984
9990
|
runtime: Optional(_Object_({
|
|
9985
9991
|
processIsolation: Boolean2(),
|
|
@@ -10057,6 +10063,7 @@ var DeliveryTargetSchema = _Object_({
|
|
|
10057
10063
|
routes: _Array_(routeSchema),
|
|
10058
10064
|
jobs: _Array_(jobSchema),
|
|
10059
10065
|
externalTokens: _Array_(String2()),
|
|
10066
|
+
function: Optional(functionTarget),
|
|
10060
10067
|
requirements: _Object_({
|
|
10061
10068
|
processIsolation: Boolean2(),
|
|
10062
10069
|
durableQueue: Boolean2(),
|
|
@@ -21099,7 +21106,7 @@ var starter_metadata_default = {
|
|
|
21099
21106
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
21100
21107
|
packages: {
|
|
21101
21108
|
compiler: {
|
|
21102
|
-
version: "0.
|
|
21109
|
+
version: "0.46.0"
|
|
21103
21110
|
},
|
|
21104
21111
|
app: {
|
|
21105
21112
|
version: "0.26.1",
|
|
@@ -21117,7 +21124,7 @@ var starter_metadata_default = {
|
|
|
21117
21124
|
version: "0.8.1"
|
|
21118
21125
|
},
|
|
21119
21126
|
db: {
|
|
21120
|
-
version: "0.
|
|
21127
|
+
version: "0.17.0",
|
|
21121
21128
|
peerDependencies: {
|
|
21122
21129
|
"drizzle-orm": "1.0.0-rc.5-169397b"
|
|
21123
21130
|
},
|
|
@@ -21126,7 +21133,7 @@ var starter_metadata_default = {
|
|
|
21126
21133
|
}
|
|
21127
21134
|
},
|
|
21128
21135
|
sdk: {
|
|
21129
|
-
version: "0.
|
|
21136
|
+
version: "0.42.0",
|
|
21130
21137
|
peerDependencies: {
|
|
21131
21138
|
"@supabase/supabase-js": "^2.117.3"
|
|
21132
21139
|
}
|
|
@@ -21451,7 +21458,7 @@ function baseFiles(name, sdkDependencies) {
|
|
|
21451
21458
|
version: "0.0.0",
|
|
21452
21459
|
private: true,
|
|
21453
21460
|
type: "module",
|
|
21454
|
-
engines: { bun: ">=1.4.
|
|
21461
|
+
engines: { bun: ">=1.4.3" },
|
|
21455
21462
|
scripts: {
|
|
21456
21463
|
compile: "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js compile",
|
|
21457
21464
|
"check:generated": "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js check",
|
|
@@ -21475,7 +21482,7 @@ function baseFiles(name, sdkDependencies) {
|
|
|
21475
21482
|
},
|
|
21476
21483
|
devDependencies: {
|
|
21477
21484
|
"@supacloud/compiler": `^${compilerMetadata.version}`,
|
|
21478
|
-
"@types/bun": "^1.4.
|
|
21485
|
+
"@types/bun": "^1.4.3",
|
|
21479
21486
|
typescript: "^7.0.2"
|
|
21480
21487
|
}
|
|
21481
21488
|
}),
|
|
@@ -22135,7 +22142,11 @@ export async function createReviewAttachmentAdapters(options: ReviewAttachmentOp
|
|
|
22135
22142
|
|
|
22136
22143
|
// src/shared/tools/app-starter-attachment-worker.ts
|
|
22137
22144
|
var STARTER_ATTACHMENT_WORKER = `import { createWorker, type CompiledModule } from "@supacloud/elysia";
|
|
22138
|
-
import
|
|
22145
|
+
import {
|
|
22146
|
+
createSupaCloudWorkflowTransport,
|
|
22147
|
+
type SupaCloudWorkflowClaim,
|
|
22148
|
+
type SupaCloudWorkflowsClient,
|
|
22149
|
+
} from "@supacloud/js";
|
|
22139
22150
|
import { createReviewAttachmentAdapters, type ReviewAttachmentOptions } from "./review-attachments";
|
|
22140
22151
|
|
|
22141
22152
|
export interface ReviewAttachmentWorkerOptions extends ReviewAttachmentOptions {
|
|
@@ -22160,9 +22171,6 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22160
22171
|
halted = true;
|
|
22161
22172
|
failure.reject(new Error("Attachment worker requires recovery"));
|
|
22162
22173
|
};
|
|
22163
|
-
const attempt = (claim: SupaCloudWorkflowClaim) => ({
|
|
22164
|
-
stepId: claim.stepId, messageId: claim.messageId, attempt: claim.attempt, workerId: claim.workerId,
|
|
22165
|
-
});
|
|
22166
22174
|
async function ownedClaim(claim: SupaCloudWorkflowClaim) {
|
|
22167
22175
|
if (claim.workflowName !== "review.verify-attachment" || claim.workflowVersion !== "1"
|
|
22168
22176
|
|| claim.stepKey !== "verify" || claim.maxAttempts !== 3 || claim.workerId !== workerId) {
|
|
@@ -22178,6 +22186,9 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22178
22186
|
));
|
|
22179
22187
|
if (!Array.isArray(bound) || bound.length !== 1) throw new Error("Attachment workflow ownership mismatch");
|
|
22180
22188
|
}
|
|
22189
|
+
const workflowTransport = createSupaCloudWorkflowTransport({
|
|
22190
|
+
workflows, workerId, visibilityTimeoutSeconds: 120, retryDelaySeconds: 5, acceptClaim: ownedClaim,
|
|
22191
|
+
});
|
|
22181
22192
|
const worker = createWorker({
|
|
22182
22193
|
modules: options.modules, deps: { dbClient: store }, workerId, concurrency: 1,
|
|
22183
22194
|
pollIntervalMs: options.pollIntervalMs ?? 1000,
|
|
@@ -22186,11 +22197,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22186
22197
|
async claim(signal): Promise<SupaCloudWorkflowClaim | null> {
|
|
22187
22198
|
if (halted || signal.aborted) return null;
|
|
22188
22199
|
try {
|
|
22189
|
-
|
|
22190
|
-
if (signal.aborted || halted) return null;
|
|
22191
|
-
if (!claim || claim.status !== "claimed") return null;
|
|
22192
|
-
await ownedClaim(claim);
|
|
22193
|
-
return claim;
|
|
22200
|
+
return await workflowTransport.claim(signal);
|
|
22194
22201
|
} catch {
|
|
22195
22202
|
if (!signal.aborted) fatal();
|
|
22196
22203
|
throw new Error("Attachment workflow claim failed");
|
|
@@ -22198,9 +22205,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22198
22205
|
},
|
|
22199
22206
|
async ack(claim, output) {
|
|
22200
22207
|
try {
|
|
22201
|
-
|
|
22202
|
-
// The compiled Job validates output; Workflow SDK validates the receipt.
|
|
22203
|
-
await workflows.complete({ ...attempt(claim), runOutput: { ...output } });
|
|
22208
|
+
await workflowTransport.ack(claim, output);
|
|
22204
22209
|
} catch {
|
|
22205
22210
|
fatal();
|
|
22206
22211
|
throw new Error("Attachment workflow completion is unconfirmed");
|
|
@@ -22208,9 +22213,7 @@ export async function createReviewAttachmentWorker(options: ReviewAttachmentWork
|
|
|
22208
22213
|
},
|
|
22209
22214
|
async fail(claim) {
|
|
22210
22215
|
try {
|
|
22211
|
-
|
|
22212
|
-
if (claim.attempt < claim.maxAttempts) await workflows.retry({ ...request, delaySeconds: 5 });
|
|
22213
|
-
else await workflows.fail(request);
|
|
22216
|
+
await workflowTransport.fail(claim, "Attachment verification failed");
|
|
22214
22217
|
} catch {
|
|
22215
22218
|
fatal();
|
|
22216
22219
|
throw new Error("Attachment workflow failure receipt is unconfirmed");
|
|
@@ -22394,9 +22397,11 @@ export class ReviewUploadsController {
|
|
|
22394
22397
|
var STARTER_UPLOAD_SCHEMA = `-- Apply explicitly with the migration owner.
|
|
22395
22398
|
ALTER TABLE public.starter_members ADD COLUMN storage_subject uuid UNIQUE;
|
|
22396
22399
|
GRANT SELECT (subject,enabled,can_approve,storage_subject) ON public.starter_members TO authenticated;
|
|
22400
|
+
DROP POLICY IF EXISTS starter_member_read ON public.starter_members;
|
|
22397
22401
|
CREATE POLICY starter_member_read ON public.starter_members FOR SELECT TO authenticated
|
|
22398
22402
|
USING (subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND storage_subject=auth.uid());
|
|
22399
22403
|
GRANT SELECT ON public.starter_reviews TO authenticated;
|
|
22404
|
+
DROP POLICY IF EXISTS starter_review_read ON public.starter_reviews;
|
|
22400
22405
|
CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authenticated
|
|
22401
22406
|
USING (owner_id=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub') AND EXISTS (
|
|
22402
22407
|
SELECT 1 FROM public.starter_members m WHERE m.subject=owner_id
|
|
@@ -22404,11 +22409,14 @@ CREATE POLICY starter_review_read ON public.starter_reviews FOR SELECT TO authen
|
|
|
22404
22409
|
));
|
|
22405
22410
|
INSERT INTO storage.buckets(id,name,public,file_size_limit,allowed_mime_types)
|
|
22406
22411
|
VALUES ('review-attachments','review-attachments',false,1048576,ARRAY['text/plain']);
|
|
22412
|
+
DROP POLICY IF EXISTS starter_attachment_read ON storage.objects;
|
|
22407
22413
|
CREATE POLICY starter_attachment_read ON storage.objects FOR SELECT TO authenticated
|
|
22408
22414
|
USING (bucket_id='review-attachments');
|
|
22415
|
+
DROP POLICY IF EXISTS starter_attachment_insert ON storage.objects;
|
|
22409
22416
|
CREATE POLICY starter_attachment_insert ON storage.objects FOR INSERT TO authenticated
|
|
22410
22417
|
WITH CHECK (bucket_id='review-attachments');
|
|
22411
22418
|
-- Restrictive fences also apply when the host already has permissive Storage policies.
|
|
22419
|
+
DROP POLICY IF EXISTS starter_attachment_owner_fence ON storage.objects;
|
|
22412
22420
|
CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE FOR ALL TO authenticated
|
|
22413
22421
|
USING (bucket_id<>'review-attachments' OR (split_part(name,'/',1)=auth.uid()::text AND EXISTS (
|
|
22414
22422
|
SELECT 1 FROM public.starter_reviews r JOIN public.starter_members m ON m.subject=r.owner_id
|
|
@@ -22421,8 +22429,10 @@ CREATE POLICY starter_attachment_owner_fence ON storage.objects AS RESTRICTIVE F
|
|
|
22421
22429
|
WHERE r.id=split_part(name,'/',2) AND r.state='draft' AND m.subject=COALESCE(NULLIF(auth.jwt()->>'external_sub',''),auth.jwt()->>'sub')
|
|
22422
22430
|
AND m.storage_subject=auth.uid() AND m.enabled AND m.can_approve
|
|
22423
22431
|
)));
|
|
22432
|
+
DROP POLICY IF EXISTS starter_attachment_update_fence ON storage.objects;
|
|
22424
22433
|
CREATE POLICY starter_attachment_update_fence ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated
|
|
22425
22434
|
USING (bucket_id<>'review-attachments') WITH CHECK (bucket_id<>'review-attachments');
|
|
22435
|
+
DROP POLICY IF EXISTS starter_attachment_delete_fence ON storage.objects;
|
|
22426
22436
|
CREATE POLICY starter_attachment_delete_fence ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated
|
|
22427
22437
|
USING (bucket_id<>'review-attachments');
|
|
22428
22438
|
`;
|
|
@@ -22570,6 +22580,11 @@ function renderStarterRuntimeRolesSchema(httpRole, workerRole) {
|
|
|
22570
22580
|
CREATE ROLE ${httpRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
|
22571
22581
|
CREATE ROLE ${workerRole} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
|
22572
22582
|
GRANT USAGE ON SCHEMA public TO ${httpRole}, ${workerRole};
|
|
22583
|
+
ALTER TABLE public.starter_application ENABLE ROW LEVEL SECURITY;
|
|
22584
|
+
ALTER TABLE public.starter_members ENABLE ROW LEVEL SECURITY;
|
|
22585
|
+
ALTER TABLE public.starter_reviews ENABLE ROW LEVEL SECURITY;
|
|
22586
|
+
ALTER TABLE public.starter_attachments ENABLE ROW LEVEL SECURITY;
|
|
22587
|
+
ALTER TABLE public.starter_attachment_results ENABLE ROW LEVEL SECURITY;
|
|
22573
22588
|
GRANT SELECT ON public.starter_application, public.starter_members,
|
|
22574
22589
|
public.starter_reviews, public.starter_attachments TO ${httpRole}, ${workerRole};
|
|
22575
22590
|
|
|
@@ -22583,34 +22598,49 @@ GRANT UPDATE(id) ON public.starter_reviews TO ${workerRole};
|
|
|
22583
22598
|
GRANT INSERT ON public.starter_attachments TO ${httpRole};
|
|
22584
22599
|
GRANT SELECT,INSERT ON public.starter_attachment_results TO ${workerRole};
|
|
22585
22600
|
|
|
22601
|
+
DROP POLICY IF EXISTS starter_backend_binding_read ON public.starter_application;
|
|
22586
22602
|
CREATE POLICY starter_backend_binding_read ON public.starter_application FOR SELECT
|
|
22587
22603
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22604
|
+
DROP POLICY IF EXISTS starter_backend_binding_lock ON public.starter_application;
|
|
22588
22605
|
CREATE POLICY starter_backend_binding_lock ON public.starter_application FOR UPDATE
|
|
22589
22606
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22607
|
+
DROP POLICY IF EXISTS starter_backend_binding_immutable ON public.starter_application;
|
|
22590
22608
|
CREATE POLICY starter_backend_binding_immutable ON public.starter_application AS RESTRICTIVE FOR UPDATE
|
|
22591
22609
|
TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
|
|
22610
|
+
DROP POLICY IF EXISTS starter_backend_member_read ON public.starter_members;
|
|
22592
22611
|
CREATE POLICY starter_backend_member_read ON public.starter_members FOR SELECT
|
|
22593
22612
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22613
|
+
DROP POLICY IF EXISTS starter_backend_member_lock ON public.starter_members;
|
|
22594
22614
|
CREATE POLICY starter_backend_member_lock ON public.starter_members FOR UPDATE
|
|
22595
22615
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22616
|
+
DROP POLICY IF EXISTS starter_backend_member_immutable ON public.starter_members;
|
|
22596
22617
|
CREATE POLICY starter_backend_member_immutable ON public.starter_members AS RESTRICTIVE FOR UPDATE
|
|
22597
22618
|
TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
|
|
22619
|
+
DROP POLICY IF EXISTS starter_backend_review_read ON public.starter_reviews;
|
|
22598
22620
|
CREATE POLICY starter_backend_review_read ON public.starter_reviews FOR SELECT
|
|
22599
22621
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22622
|
+
DROP POLICY IF EXISTS starter_backend_review_update ON public.starter_reviews;
|
|
22600
22623
|
CREATE POLICY starter_backend_review_update ON public.starter_reviews FOR UPDATE
|
|
22601
22624
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22625
|
+
DROP POLICY IF EXISTS starter_worker_review_immutable ON public.starter_reviews;
|
|
22602
22626
|
CREATE POLICY starter_worker_review_immutable ON public.starter_reviews AS RESTRICTIVE FOR UPDATE
|
|
22603
22627
|
TO ${workerRole} USING (true) WITH CHECK (false);
|
|
22628
|
+
DROP POLICY IF EXISTS starter_backend_attachment_read ON public.starter_attachments;
|
|
22604
22629
|
CREATE POLICY starter_backend_attachment_read ON public.starter_attachments FOR SELECT
|
|
22605
22630
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22631
|
+
DROP POLICY IF EXISTS starter_backend_attachment_lock ON public.starter_attachments;
|
|
22606
22632
|
CREATE POLICY starter_backend_attachment_lock ON public.starter_attachments FOR UPDATE
|
|
22607
22633
|
TO ${httpRole}, ${workerRole} USING (true);
|
|
22634
|
+
DROP POLICY IF EXISTS starter_backend_attachment_immutable ON public.starter_attachments;
|
|
22608
22635
|
CREATE POLICY starter_backend_attachment_immutable ON public.starter_attachments AS RESTRICTIVE FOR UPDATE
|
|
22609
22636
|
TO ${httpRole}, ${workerRole} USING (true) WITH CHECK (false);
|
|
22637
|
+
DROP POLICY IF EXISTS starter_http_attachment_insert ON public.starter_attachments;
|
|
22610
22638
|
CREATE POLICY starter_http_attachment_insert ON public.starter_attachments FOR INSERT
|
|
22611
22639
|
TO ${httpRole} WITH CHECK (true);
|
|
22640
|
+
DROP POLICY IF EXISTS starter_worker_result_read ON public.starter_attachment_results;
|
|
22612
22641
|
CREATE POLICY starter_worker_result_read ON public.starter_attachment_results FOR SELECT
|
|
22613
22642
|
TO ${workerRole} USING (true);
|
|
22643
|
+
DROP POLICY IF EXISTS starter_worker_result_insert ON public.starter_attachment_results;
|
|
22614
22644
|
CREATE POLICY starter_worker_result_insert ON public.starter_attachment_results FOR INSERT
|
|
22615
22645
|
TO ${workerRole} WITH CHECK (true);
|
|
22616
22646
|
|
|
@@ -23001,6 +23031,80 @@ export const attachmentResults = pgTable.withRLS("starter_attachment_results", {
|
|
|
23001
23031
|
export type ReviewRow = typeof reviews.$inferSelect;
|
|
23002
23032
|
export type NewReview = typeof reviews.$inferInsert;
|
|
23003
23033
|
`;
|
|
23034
|
+
var STARTER_DATABASE_MODULES = `import { defineDatabaseModule } from "@supacloud/db";
|
|
23035
|
+
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
23036
|
+
import { application, reviews, members, attachments, attachmentResults } from "./schema";
|
|
23037
|
+
|
|
23038
|
+
const backendPolicies = [
|
|
23039
|
+
["starter_backend_binding_read", "public.starter_application", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23040
|
+
["starter_backend_binding_lock", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23041
|
+
["starter_backend_binding_immutable", "public.starter_application", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23042
|
+
["starter_backend_member_read", "public.starter_members", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23043
|
+
["starter_backend_member_lock", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23044
|
+
["starter_backend_member_immutable", "public.starter_members", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23045
|
+
["starter_backend_review_read", "public.starter_reviews", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23046
|
+
["starter_backend_review_update", "public.starter_reviews", "update", ["starter_review_http"]],
|
|
23047
|
+
["starter_worker_review_immutable", "public.starter_reviews", "update", ["starter_review_worker"]],
|
|
23048
|
+
["starter_backend_attachment_read", "public.starter_attachments", "select", ["starter_review_http", "starter_review_worker"]],
|
|
23049
|
+
["starter_backend_attachment_lock", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23050
|
+
["starter_backend_attachment_immutable", "public.starter_attachments", "update", ["starter_review_http", "starter_review_worker"]],
|
|
23051
|
+
["starter_http_attachment_insert", "public.starter_attachments", "insert", ["starter_review_http"]],
|
|
23052
|
+
["starter_worker_result_read", "public.starter_attachment_results", "select", ["starter_review_worker"]],
|
|
23053
|
+
["starter_worker_result_insert", "public.starter_attachment_results", "insert", ["starter_review_worker"]],
|
|
23054
|
+
] as const;
|
|
23055
|
+
|
|
23056
|
+
// The module is the single live-Catalog ownership contract for starter tables.
|
|
23057
|
+
export default defineDatabaseModule({
|
|
23058
|
+
name: "starter",
|
|
23059
|
+
tables: [application, reviews, members, attachments, attachmentResults].map(table => {
|
|
23060
|
+
const config = getTableConfig(table);
|
|
23061
|
+
return (config.schema ?? "public") + "." + config.name;
|
|
23062
|
+
}),
|
|
23063
|
+
policies: [
|
|
23064
|
+
...backendPolicies.map(([name, table, operation, roles]) => ({
|
|
23065
|
+
name, table, operation, roles: [...roles], source: "migrations/004-review-runtime-roles.sql",
|
|
23066
|
+
tests: ["tests/database-governance.sql"],
|
|
23067
|
+
})),
|
|
23068
|
+
{
|
|
23069
|
+
name: "starter_member_read", table: "public.starter_members", operation: "select",
|
|
23070
|
+
roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
|
|
23071
|
+
tests: ["tests/database-governance.sql"],
|
|
23072
|
+
},
|
|
23073
|
+
{
|
|
23074
|
+
name: "starter_review_read", table: "public.starter_reviews", operation: "select",
|
|
23075
|
+
roles: ["authenticated"], source: "migrations/003-review-uploads.sql",
|
|
23076
|
+
tests: ["tests/database-governance.sql"],
|
|
23077
|
+
},
|
|
23078
|
+
],
|
|
23079
|
+
});
|
|
23080
|
+
`;
|
|
23081
|
+
var STARTER_DATABASE_TEST = `-- Read-only catalog assertions; behavioral authorization tests remain separate.
|
|
23082
|
+
BEGIN READ ONLY;
|
|
23083
|
+
DO $test$
|
|
23084
|
+
DECLARE
|
|
23085
|
+
owned text[] := ARRAY['starter_application','starter_reviews','starter_members',
|
|
23086
|
+
'starter_attachments','starter_attachment_results'];
|
|
23087
|
+
BEGIN
|
|
23088
|
+
IF (SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
23089
|
+
WHERE n.nspname='public' AND c.relname=ANY(owned) AND c.relrowsecurity) <> 5 THEN
|
|
23090
|
+
RAISE EXCEPTION 'All starter tables must exist with RLS enabled';
|
|
23091
|
+
END IF;
|
|
23092
|
+
IF (SELECT count(*) FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid
|
|
23093
|
+
JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
23094
|
+
WHERE n.nspname='public' AND c.relname=ANY(owned)) <> 17 THEN
|
|
23095
|
+
RAISE EXCEPTION 'Starter policy inventory changed; reconcile db/modules.ts';
|
|
23096
|
+
END IF;
|
|
23097
|
+
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')
|
|
23098
|
+
AND (rolsuper OR rolbypassrls)) THEN
|
|
23099
|
+
RAISE EXCEPTION 'Starter roles must not bypass row security';
|
|
23100
|
+
END IF;
|
|
23101
|
+
IF (SELECT count(*) FROM pg_roles WHERE rolname IN ('starter_review_http','starter_review_worker')) <> 2 THEN
|
|
23102
|
+
RAISE EXCEPTION 'Starter runtime roles are missing';
|
|
23103
|
+
END IF;
|
|
23104
|
+
END
|
|
23105
|
+
$test$;
|
|
23106
|
+
ROLLBACK;
|
|
23107
|
+
`;
|
|
23004
23108
|
var STARTER_DATABASE_GUIDE = `
|
|
23005
23109
|
## Database Source Workflow
|
|
23006
23110
|
|
|
@@ -23012,13 +23116,14 @@ Opaque JSON still needs runtime decoding.
|
|
|
23012
23116
|
After installing dependencies, run \`bun run db:generate\` once and commit db/contracts/.
|
|
23013
23117
|
The default \`bun run check\` and build fail on stale database contracts before any
|
|
23014
23118
|
compiler regeneration. After an intentional source change, run db:generate again,
|
|
23015
|
-
review the diff, then run check.
|
|
23016
|
-
The
|
|
23017
|
-
databaseGovernance.mode = "check"
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23119
|
+
review the diff, then run check.
|
|
23120
|
+
The command starter registers databaseGovernance.moduleFile = "db/modules.ts" and
|
|
23121
|
+
keeps the default \`databaseGovernance.mode = "check"\`: compile/check/build run
|
|
23122
|
+
the offline source-contract gate without a database. Use \`bun run dev:integration\`
|
|
23123
|
+
with the CLI's explicit loopback database URL, or set databaseGovernance.mode to
|
|
23124
|
+
"enforce" with an explicitly supplied \`DATABASE_URL\`, to run SQL lint and live
|
|
23125
|
+
Catalog reconciliation. Missing connections, stale source contracts and Catalog
|
|
23126
|
+
drift fail closed; no contracts or migrations are silently regenerated.
|
|
23022
23127
|
|
|
23023
23128
|
- Maintain one schema-qualified function with explicit grants per db/functions/*.sql.
|
|
23024
23129
|
- Preserve migrations/ and its existing executor/ledger. Add forward migrations only.
|
|
@@ -23042,6 +23147,8 @@ function starterDatabaseFiles() {
|
|
|
23042
23147
|
"database.sources.json": JSON.stringify(STARTER_DATABASE_CONFIG, null, 2) + `
|
|
23043
23148
|
`,
|
|
23044
23149
|
"db/schema.ts": STARTER_DATABASE_SCHEMA,
|
|
23150
|
+
"db/modules.ts": STARTER_DATABASE_MODULES,
|
|
23151
|
+
"tests/database-governance.sql": STARTER_DATABASE_TEST,
|
|
23045
23152
|
"db/functions/README.md": `Maintain one schema-qualified function and its explicit EXECUTE ACL per SQL file. No RPC functions are needed by the initial transactional starter.
|
|
23046
23153
|
`,
|
|
23047
23154
|
"drizzle.config.ts": `import { defineConfig } from "drizzle-kit";
|
|
@@ -23076,7 +23183,7 @@ function appStarterFiles(name) {
|
|
|
23076
23183
|
version: "0.0.0",
|
|
23077
23184
|
private: true,
|
|
23078
23185
|
type: "module",
|
|
23079
|
-
engines: { bun: ">=1.4.
|
|
23186
|
+
engines: { bun: ">=1.4.3" },
|
|
23080
23187
|
scripts: {
|
|
23081
23188
|
compile: "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js compile",
|
|
23082
23189
|
"check:generated": "bun --no-env-file node_modules/@supacloud/compiler/dist/cli.js check",
|
|
@@ -23112,7 +23219,7 @@ function appStarterFiles(name) {
|
|
|
23112
23219
|
devDependencies: {
|
|
23113
23220
|
"@supacloud/compiler": `^${compilerMetadata2.version}`,
|
|
23114
23221
|
"drizzle-kit": dbMetadata.devDependencies["drizzle-kit"],
|
|
23115
|
-
"@types/bun": "^1.4.
|
|
23222
|
+
"@types/bun": "^1.4.3",
|
|
23116
23223
|
typescript: "^7.0.2"
|
|
23117
23224
|
}
|
|
23118
23225
|
}),
|
|
@@ -23134,7 +23241,7 @@ function appStarterFiles(name) {
|
|
|
23134
23241
|
noEmit: true,
|
|
23135
23242
|
types: ["bun"]
|
|
23136
23243
|
},
|
|
23137
|
-
include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
|
|
23244
|
+
include: ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts", "generated/**/*.ts", "db/schema.ts", "db/modules.ts", "db/contracts/**/*.ts", "supacloud.config.ts", "drizzle*.config.ts"]
|
|
23138
23245
|
}),
|
|
23139
23246
|
"bunfig.toml": `env = false
|
|
23140
23247
|
`,
|
|
@@ -23168,7 +23275,7 @@ export default defineSupacloudConfig({
|
|
|
23168
23275
|
outDir: "generated",
|
|
23169
23276
|
strict: true,
|
|
23170
23277
|
moduleBoundaryPreset: "modular-monolith",
|
|
23171
|
-
databaseGovernance: { mode: "check" },
|
|
23278
|
+
databaseGovernance: { mode: "check", moduleFile: "db/modules.ts", databaseUrlEnv: "DATABASE_URL" },
|
|
23172
23279
|
effect: {
|
|
23173
23280
|
requireRouteEffects: true,
|
|
23174
23281
|
requireErrorMappings: true,
|
|
@@ -23182,7 +23289,6 @@ export default defineSupacloudConfig({
|
|
|
23182
23289
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
23183
23290
|
disallowControllerDirectDb: true,
|
|
23184
23291
|
detectOrphanModules: true,
|
|
23185
|
-
graphql: { schema: "graphql/schema.graphql" },
|
|
23186
23292
|
commandCapabilities: { permission: true, transaction: true, idempotency: true, audit: true },
|
|
23187
23293
|
delivery: {
|
|
23188
23294
|
version: 1,
|
|
@@ -23196,50 +23302,6 @@ export default defineSupacloudConfig({
|
|
|
23196
23302
|
},
|
|
23197
23303
|
},
|
|
23198
23304
|
});
|
|
23199
|
-
`,
|
|
23200
|
-
"graphql/schema.graphql": `# Offline query-contract example, not a deployed database schema.
|
|
23201
|
-
# SYNTHETIC TEST FIXTURE ONLY. Database First is the sole server-schema model.
|
|
23202
|
-
# Do not extend this fixture as a server schema; change database declarations and export.
|
|
23203
|
-
# Replace using a snapshot exported with the intended caller role before integration.
|
|
23204
|
-
type Query {
|
|
23205
|
-
reviewCollection(first: Int): ReviewConnection!
|
|
23206
|
-
}
|
|
23207
|
-
type ReviewConnection {
|
|
23208
|
-
edges: [ReviewEdge!]!
|
|
23209
|
-
}
|
|
23210
|
-
type ReviewEdge {
|
|
23211
|
-
node: Review!
|
|
23212
|
-
}
|
|
23213
|
-
type Review {
|
|
23214
|
-
id: ID!
|
|
23215
|
-
state: String!
|
|
23216
|
-
version: Int!
|
|
23217
|
-
}
|
|
23218
|
-
`,
|
|
23219
|
-
"src/review/reviews.graphql": `query ReviewList($first: Int = 20) {
|
|
23220
|
-
reviewCollection(first: $first) {
|
|
23221
|
-
edges { node { id state version } }
|
|
23222
|
-
}
|
|
23223
|
-
}
|
|
23224
|
-
`,
|
|
23225
|
-
"tests/graphql.test.ts": `import { expect, test } from "bun:test";
|
|
23226
|
-
import { createGraphqlClient, type ReviewListQuery } from "../generated/graphql";
|
|
23227
|
-
|
|
23228
|
-
test("generated query client preserves its read contract without a live database", async () => {
|
|
23229
|
-
const data: ReviewListQuery = {
|
|
23230
|
-
reviewCollection: { edges: [{ node: { id: "demo", state: "draft", version: 1 } }] },
|
|
23231
|
-
};
|
|
23232
|
-
const queries = createGraphqlClient({
|
|
23233
|
-
url: "https://project.example.test",
|
|
23234
|
-
getAccessToken: async () => "synthetic-user-token",
|
|
23235
|
-
fetch: async (_url, request) => {
|
|
23236
|
-
expect(new Headers(request?.headers).get("Authorization")).toBe("Bearer synthetic-user-token");
|
|
23237
|
-
expect(JSON.parse(String(request?.body)).variables).toEqual({ first: 10 });
|
|
23238
|
-
return Response.json({ data });
|
|
23239
|
-
},
|
|
23240
|
-
});
|
|
23241
|
-
expect(await queries.ReviewList({ first: 10 })).toEqual(data);
|
|
23242
|
-
});
|
|
23243
23305
|
`,
|
|
23244
23306
|
"REACTIVE.md": STARTER_REACTIVE_GUIDE,
|
|
23245
23307
|
"AGENTS.md": STARTER_REACTIVE_AGENTS + STARTER_DATABASE_GUIDE,
|
|
@@ -23249,11 +23311,30 @@ test("generated query client preserves its read contract without a live database
|
|
|
23249
23311
|
"scripts/dev.ts": `import { watchProject, compileOptionsFromConfig, loadSupacloudConfig } from "@supacloud/compiler";
|
|
23250
23312
|
|
|
23251
23313
|
if (process.env["APP_ENV"] !== "development") throw new Error("The demo server is development-only");
|
|
23314
|
+
const profile = process.env["SUPACLOUD_DEV_PROFILE"] === "integration" ? "integration" : "fast";
|
|
23315
|
+
if (profile === "integration") {
|
|
23316
|
+
const databaseUrl = process.env["SUPACLOUD_DEV_DATABASE_URL"];
|
|
23317
|
+
if (!databaseUrl) throw new Error("Integration requires an explicit loopback PostgreSQL database");
|
|
23318
|
+
const url = new URL(databaseUrl);
|
|
23319
|
+
if (!["postgres:", "postgresql:"].includes(url.protocol)
|
|
23320
|
+
|| !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
23321
|
+
|| url.pathname.length <= 1 || url.search || url.hash
|
|
23322
|
+
|| process.env["DATABASE_SOCKET_PATH"]
|
|
23323
|
+
|| (process.env["DATABASE_URL"] && process.env["DATABASE_URL"] !== databaseUrl)) {
|
|
23324
|
+
throw new Error("Integration requires an explicit loopback PostgreSQL database without conflicting settings");
|
|
23325
|
+
}
|
|
23326
|
+
process.env["DATABASE_URL"] = databaseUrl;
|
|
23327
|
+
}
|
|
23328
|
+
const configured = compileOptionsFromConfig(await loadSupacloudConfig());
|
|
23329
|
+
const governanceMode = profile === "integration" ? "enforce" as const : "check" as const;
|
|
23330
|
+
const options = configured.databaseGovernance
|
|
23331
|
+
? { ...configured, databaseGovernance: { ...configured.databaseGovernance, mode: governanceMode } }
|
|
23332
|
+
: configured;
|
|
23252
23333
|
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
23253
23334
|
let restarts = Promise.resolve();
|
|
23254
23335
|
let closing = false;
|
|
23255
23336
|
const watcher = watchProject({
|
|
23256
|
-
...
|
|
23337
|
+
...options,
|
|
23257
23338
|
writeOnError: false,
|
|
23258
23339
|
onEvent: (event) => {
|
|
23259
23340
|
if (event.type === "compile-error") console.error(event.diagnostics);
|
|
@@ -23603,7 +23684,6 @@ test("the production composition root rejects missing governance adapters", () =
|
|
|
23603
23684
|
\`\`\`sh
|
|
23604
23685
|
bun install
|
|
23605
23686
|
bun run db:generate
|
|
23606
|
-
bun run check
|
|
23607
23687
|
bun run dev
|
|
23608
23688
|
\`\`\`
|
|
23609
23689
|
|
|
@@ -23639,55 +23719,23 @@ origin. The authenticated transport rejects missing credentials and redirects
|
|
|
23639
23719
|
and does not replay a write after 401. Its underlying fetch must also be
|
|
23640
23720
|
single-attempt.
|
|
23641
23721
|
|
|
23642
|
-
Reuse the same session resolver for generated/graphql.ts when adopting queries.
|
|
23643
23722
|
The local memory demo is not this authenticated production integration.
|
|
23644
23723
|
Never use supacloud.commands or supacloud.workflows from a browser action:
|
|
23645
23724
|
those RPCs require service-role credentials. Browser business commands use
|
|
23646
23725
|
application endpoints, optionally composed through @supacloud/js/contracts.
|
|
23647
23726
|
Management API tokens and service-role keys remain on trusted servers.
|
|
23648
23727
|
|
|
23649
|
-
##
|
|
23650
|
-
|
|
23651
|
-
This starter uses Database First as its only GraphQL server-schema model.
|
|
23652
|
-
Drizzle and reviewed SQL define the database; pg_graphql reflects the migrated
|
|
23653
|
-
database under the caller's role. Do not add GraphQL resolver/decorator classes
|
|
23654
|
-
or independently authored server SDL. Business writes continue through Commands.
|
|
23728
|
+
## Reads
|
|
23655
23729
|
|
|
23656
|
-
|
|
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.
|
|
23730
|
+
Ordinary reads use PostgREST through the existing user-scoped
|
|
23731
|
+
@supabase/supabase-js client: client.from("starter_reviews").select("id,state,version").
|
|
23732
|
+
Use the caller's session, RLS and grants; never introduce a second session store.
|
|
23733
|
+
Business writes continue through the governed Command/RPC boundary.
|
|
23662
23734
|
|
|
23663
|
-
|
|
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.
|
|
23735
|
+
GraphQL is not generated or enabled by this starter. For a specific Query Contract,
|
|
23736
|
+
explicitly configure graphql.schema with a role-scoped database export and add
|
|
23737
|
+
checked query documents. Drizzle and append-only migrations remain the database
|
|
23738
|
+
authority; the export is a protocol snapshot, not a second schema source.
|
|
23691
23739
|
|
|
23692
23740
|
## Environments
|
|
23693
23741
|
|
|
@@ -23903,8 +23951,8 @@ bunx supacloud-compiler fix ./fix.json --dry-run
|
|
|
23903
23951
|
\`\`\`
|
|
23904
23952
|
|
|
23905
23953
|
Context packs include directional dependencies, relevant aspect files, diagnostics
|
|
23906
|
-
and static execution plans
|
|
23907
|
-
|
|
23954
|
+
and static execution plans, without provider values or source expressions.
|
|
23955
|
+
The optional --events example expects a trusted host
|
|
23908
23956
|
to capture approved onExecution metadata in { "version": 1, "events": [...] }.
|
|
23909
23957
|
It selects one opaque request ID, omits payloads and source expressions, and applies
|
|
23910
23958
|
input/output size limits. It does not read arbitrary logs, prove deployment versions,
|
|
@@ -24861,6 +24909,13 @@ async function runContext(args) {
|
|
|
24861
24909
|
fix: "supacloud app fix --fix <fix.json>",
|
|
24862
24910
|
graph: "supacloud app graph --format json",
|
|
24863
24911
|
explain: "supacloud app explain --target <name>"
|
|
24912
|
+
},
|
|
24913
|
+
defaults: {
|
|
24914
|
+
readTransport: "postgrest-supabase-js",
|
|
24915
|
+
writeTransport: "command-rpc",
|
|
24916
|
+
writeRetry: "disabled",
|
|
24917
|
+
externalOutcome: "receipt-reconcile",
|
|
24918
|
+
databaseSource: "drizzle-append-only"
|
|
24864
24919
|
}
|
|
24865
24920
|
};
|
|
24866
24921
|
if (args.format === "json")
|
|
@@ -29210,7 +29265,7 @@ function registerDeveloperExperienceTools(server, options = {}) {
|
|
|
29210
29265
|
// package.json
|
|
29211
29266
|
var package_default = {
|
|
29212
29267
|
name: "@supacloud/cli",
|
|
29213
|
-
version: "0.
|
|
29268
|
+
version: "0.96.0",
|
|
29214
29269
|
description: "Project-scoped CLI for SupaCloud users",
|
|
29215
29270
|
type: "module",
|
|
29216
29271
|
main: "./dist/index.js",
|
|
@@ -29262,19 +29317,19 @@ var package_default = {
|
|
|
29262
29317
|
directory: "packages/cli"
|
|
29263
29318
|
},
|
|
29264
29319
|
dependencies: {
|
|
29265
|
-
"@supacloud/compiler": "^0.
|
|
29266
|
-
"@supacloud/delivery": "0.
|
|
29267
|
-
"@supacloud/db": "0.
|
|
29320
|
+
"@supacloud/compiler": "^0.46.0",
|
|
29321
|
+
"@supacloud/delivery": "0.17.0",
|
|
29322
|
+
"@supacloud/db": "0.17.0",
|
|
29268
29323
|
typebox: "1.3.36"
|
|
29269
29324
|
},
|
|
29270
29325
|
overrides: {
|
|
29271
29326
|
"@supacloud/contracts": "0.8.1",
|
|
29272
|
-
"@supacloud/delivery": "0.
|
|
29327
|
+
"@supacloud/delivery": "0.17.0",
|
|
29273
29328
|
"@graphql-tools/utils": "^12.0.3"
|
|
29274
29329
|
},
|
|
29275
29330
|
devDependencies: {
|
|
29276
29331
|
"@supacloud/elysia": "^0.24.1",
|
|
29277
|
-
"@types/bun": "^1.4.
|
|
29332
|
+
"@types/bun": "^1.4.3",
|
|
29278
29333
|
effect: "4.0.2",
|
|
29279
29334
|
elysia: "2.0.0-beta.21",
|
|
29280
29335
|
typescript: "^7.0.2"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const STARTER_ATTACHMENT_WORKER = "import { createWorker, type CompiledModule } from \"@supacloud/elysia\";\nimport type
|
|
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.0",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -52,19 +52,19 @@
|
|
|
52
52
|
"directory": "packages/cli"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@supacloud/compiler": "^0.
|
|
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": {
|
|
66
66
|
"@supacloud/elysia": "^0.24.1",
|
|
67
|
-
"@types/bun": "^1.4.
|
|
67
|
+
"@types/bun": "^1.4.3",
|
|
68
68
|
"effect": "4.0.2",
|
|
69
69
|
"elysia": "2.0.0-beta.21",
|
|
70
70
|
"typescript": "^7.0.2"
|