okengine 0.14.1 → 0.15.2
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/package.json +3 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +3 -0
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +86 -27
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +120 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/http.ts +20 -3
- package/src/index.ts +10 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +131 -14
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx.ts +23 -0
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +13 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
package/src/drivers/pg-rls.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Postgres row-level security — identifiers
|
|
2
|
+
* Postgres row-level security — identifiers, `CREATE POLICY` SQL,
|
|
3
|
+
* Drizzle emit, and Gate identity helpers (`oke.gate` / `oke.user` / `oke.has_scope`).
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
/** Policy command (`FOR`). */
|
|
@@ -31,6 +32,110 @@ export type SqlPolicyAlterSpec = {
|
|
|
31
32
|
const POLICY_COMMANDS = new Set<string>(["ALL", "SELECT", "INSERT", "UPDATE", "DELETE"]);
|
|
32
33
|
const POLICY_BEHAVIORS = new Set<string>(["PERMISSIVE", "RESTRICTIVE"]);
|
|
33
34
|
|
|
35
|
+
/** Drizzle / Postgres `TO` targets that must not be quoted. */
|
|
36
|
+
const SPECIAL_POLICY_ROLES = new Set(["public", "current_role", "current_user", "session_user"]);
|
|
37
|
+
|
|
38
|
+
/** Drivers that honor `SET LOCAL row_security` + `oke.*` GUCs. */
|
|
39
|
+
export const RLS_CONTEXT_DRIVERS = new Set<string>(["postgres", "pglite"]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Non-superuser role the stamp `SET LOCAL ROLE`s into so RLS applies.
|
|
43
|
+
* Table owners and PGlite's `postgres` superuser otherwise bypass policies.
|
|
44
|
+
*/
|
|
45
|
+
export const OKE_RLS_ROLE = "oke_app";
|
|
46
|
+
|
|
47
|
+
/** Live Gate principal stamped onto a SQL statement. */
|
|
48
|
+
export type RlsIdentity = {
|
|
49
|
+
readonly gate: string;
|
|
50
|
+
readonly userId: string;
|
|
51
|
+
readonly scopes: readonly string[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One statement at a time — Bun.SQL `unsafe` / PGlite `query` reject
|
|
56
|
+
* multi-command batches. Safe to run repeatedly.
|
|
57
|
+
*/
|
|
58
|
+
export const OKE_RLS_HELPER_STATEMENTS: readonly string[] = [
|
|
59
|
+
"CREATE SCHEMA IF NOT EXISTS oke",
|
|
60
|
+
// DB user is often `oke` — default search_path `"$user", public` then
|
|
61
|
+
// creates unqualified tables in schema `oke`. Domain tables belong in public.
|
|
62
|
+
"SET search_path TO public, oke",
|
|
63
|
+
`DO $oke_search_path$
|
|
64
|
+
BEGIN
|
|
65
|
+
EXECUTE 'ALTER ROLE CURRENT_USER SET search_path TO public, oke';
|
|
66
|
+
EXCEPTION
|
|
67
|
+
WHEN others THEN NULL;
|
|
68
|
+
END
|
|
69
|
+
$oke_search_path$`,
|
|
70
|
+
`DO $oke_rehome$
|
|
71
|
+
DECLARE r record;
|
|
72
|
+
BEGIN
|
|
73
|
+
FOR r IN
|
|
74
|
+
SELECT c.relname AS name
|
|
75
|
+
FROM pg_class c
|
|
76
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
77
|
+
WHERE n.nspname = 'oke' AND c.relkind = 'r'
|
|
78
|
+
LOOP
|
|
79
|
+
IF NOT EXISTS (
|
|
80
|
+
SELECT 1
|
|
81
|
+
FROM pg_class c2
|
|
82
|
+
JOIN pg_namespace n2 ON n2.oid = c2.relnamespace
|
|
83
|
+
WHERE n2.nspname = 'public' AND c2.relname = r.name AND c2.relkind = 'r'
|
|
84
|
+
) THEN
|
|
85
|
+
EXECUTE format('ALTER TABLE oke.%I SET SCHEMA public', r.name);
|
|
86
|
+
END IF;
|
|
87
|
+
END LOOP;
|
|
88
|
+
END
|
|
89
|
+
$oke_rehome$`,
|
|
90
|
+
`CREATE OR REPLACE FUNCTION oke.gate() RETURNS text
|
|
91
|
+
LANGUAGE sql STABLE AS $$ SELECT current_setting('oke.gate', true) $$`,
|
|
92
|
+
`CREATE OR REPLACE FUNCTION oke.user() RETURNS text
|
|
93
|
+
LANGUAGE sql STABLE AS $$ SELECT current_setting('oke.user', true) $$`,
|
|
94
|
+
`CREATE OR REPLACE FUNCTION oke.has_scope(p_scope text) RETURNS boolean
|
|
95
|
+
LANGUAGE sql STABLE AS $$
|
|
96
|
+
SELECT CASE
|
|
97
|
+
WHEN current_setting('oke.scopes', true) IN ('') THEN false
|
|
98
|
+
ELSE jsonb_exists(current_setting('oke.scopes', true)::jsonb, p_scope)
|
|
99
|
+
END
|
|
100
|
+
$$`,
|
|
101
|
+
`DO $oke_rls_role$
|
|
102
|
+
BEGIN
|
|
103
|
+
CREATE ROLE ${OKE_RLS_ROLE} NOSUPERUSER NOBYPASSRLS NOLOGIN;
|
|
104
|
+
EXCEPTION
|
|
105
|
+
WHEN duplicate_object THEN NULL;
|
|
106
|
+
END
|
|
107
|
+
$oke_rls_role$`,
|
|
108
|
+
`GRANT USAGE ON SCHEMA public TO ${OKE_RLS_ROLE}`,
|
|
109
|
+
`GRANT USAGE ON SCHEMA oke TO ${OKE_RLS_ROLE}`,
|
|
110
|
+
`GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA oke TO ${OKE_RLS_ROLE}`,
|
|
111
|
+
`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${OKE_RLS_ROLE}`,
|
|
112
|
+
`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${OKE_RLS_ROLE}`,
|
|
113
|
+
`GRANT ${OKE_RLS_ROLE} TO CURRENT_USER`,
|
|
114
|
+
`DO $oke_app_search_path$
|
|
115
|
+
BEGIN
|
|
116
|
+
EXECUTE 'ALTER ROLE ${OKE_RLS_ROLE} SET search_path TO public, oke';
|
|
117
|
+
EXCEPTION
|
|
118
|
+
WHEN others THEN NULL;
|
|
119
|
+
END
|
|
120
|
+
$oke_app_search_path$`,
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* All {@link OKE_RLS_HELPER_STATEMENTS} as one script (PGlite `exec` / `oke db push`).
|
|
125
|
+
*/
|
|
126
|
+
export const OKE_RLS_HELPER_SQL = `${OKE_RLS_HELPER_STATEMENTS.join(";\n")};`;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Install `oke.*` helpers one statement at a time.
|
|
130
|
+
*
|
|
131
|
+
* @param exec - Statement runner
|
|
132
|
+
*/
|
|
133
|
+
export async function installOkeRlsHelpers(exec: (sql: string) => Promise<unknown>): Promise<void> {
|
|
134
|
+
for (const stmt of OKE_RLS_HELPER_STATEMENTS) {
|
|
135
|
+
await exec(stmt);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
34
139
|
/**
|
|
35
140
|
* Quote a Postgres identifier (`"`), stripping embedded quotes.
|
|
36
141
|
*
|
|
@@ -102,11 +207,38 @@ export function parseSqlPolicyRoles(raw: string): readonly string[] | null {
|
|
|
102
207
|
.filter((part) => part.length > 0);
|
|
103
208
|
if (parts.length === 0) return ["public"];
|
|
104
209
|
for (const part of parts) {
|
|
105
|
-
if (
|
|
210
|
+
if (!isPolicyRoleName(part)) return null;
|
|
106
211
|
}
|
|
107
212
|
return parts;
|
|
108
213
|
}
|
|
109
214
|
|
|
215
|
+
/**
|
|
216
|
+
* True when `name` is `public`, a Drizzle special role, or a simple identifier.
|
|
217
|
+
*
|
|
218
|
+
* @param name - Role token
|
|
219
|
+
*/
|
|
220
|
+
export function isPolicyRoleName(name: string): boolean {
|
|
221
|
+
return SPECIAL_POLICY_ROLES.has(name.toLowerCase()) || isPgIdent(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Format a `TO` role — special names stay unquoted.
|
|
226
|
+
*
|
|
227
|
+
* @param role - Role token
|
|
228
|
+
*/
|
|
229
|
+
export function formatPolicyRole(role: string): string {
|
|
230
|
+
return SPECIAL_POLICY_ROLES.has(role.toLowerCase()) ? role.toLowerCase() : quotePgIdent(role);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* JSON text stored in `oke.scopes` (sorted unique).
|
|
235
|
+
*
|
|
236
|
+
* @param scopes - Scope strings
|
|
237
|
+
*/
|
|
238
|
+
export function rlsScopesJson(scopes: readonly string[]): string {
|
|
239
|
+
return JSON.stringify([...new Set(scopes)].sort());
|
|
240
|
+
}
|
|
241
|
+
|
|
110
242
|
/**
|
|
111
243
|
* `CREATE POLICY` from a validated spec.
|
|
112
244
|
*
|
|
@@ -116,7 +248,7 @@ export function buildCreatePolicySql(spec: SqlPolicySpec): string {
|
|
|
116
248
|
const roles =
|
|
117
249
|
spec.roles.length === 0
|
|
118
250
|
? "public"
|
|
119
|
-
: spec.roles.map((role) => (role
|
|
251
|
+
: spec.roles.map((role) => formatPolicyRole(role)).join(", ");
|
|
120
252
|
let text = `CREATE POLICY ${quotePgIdent(spec.name)} ON ${quotePgIdent(spec.table)} AS ${spec.behavior} FOR ${spec.command} TO ${roles}`;
|
|
121
253
|
if (spec.using !== undefined && spec.using.trim() !== "") {
|
|
122
254
|
text += ` USING (${spec.using.trim()})`;
|
|
@@ -153,7 +285,7 @@ export function buildAlterPolicySql(spec: SqlPolicyAlterSpec): string {
|
|
|
153
285
|
const roles =
|
|
154
286
|
spec.roles.length === 0
|
|
155
287
|
? "public"
|
|
156
|
-
: spec.roles.map((role) => (role
|
|
288
|
+
: spec.roles.map((role) => formatPolicyRole(role)).join(", ");
|
|
157
289
|
parts.push(`TO ${roles}`);
|
|
158
290
|
}
|
|
159
291
|
if (spec.using !== undefined && spec.using.trim() !== "") {
|
|
@@ -189,9 +321,9 @@ export function parseSqlPolicySpec(raw: Readonly<Record<string, unknown>>): SqlP
|
|
|
189
321
|
const table = typeof raw.table === "string" ? raw.table.trim() : "";
|
|
190
322
|
if (!isPgPolicyName(name)) throw new Error(`invalid policy name "${name}"`);
|
|
191
323
|
if (!isPgIdent(table)) throw new Error(`invalid table name "${table}"`);
|
|
192
|
-
const command = parseSqlPolicyCommand(raw.command) ?? "SELECT";
|
|
193
|
-
const behavior = parseSqlPolicyBehavior(raw.behavior ?? raw.permissive) ?? "PERMISSIVE";
|
|
194
|
-
const rolesRaw =
|
|
324
|
+
const command = parseSqlPolicyCommand(raw.command) ?? parseSqlPolicyCommand(raw.for) ?? "SELECT";
|
|
325
|
+
const behavior = parseSqlPolicyBehavior(raw.behavior ?? raw.permissive ?? raw.as) ?? "PERMISSIVE";
|
|
326
|
+
const rolesRaw = rolesField(raw.roles ?? raw.to);
|
|
195
327
|
const roles = parseSqlPolicyRoles(rolesRaw);
|
|
196
328
|
if (!roles) throw new Error("invalid policy roles");
|
|
197
329
|
const using = typeof raw.using === "string" ? raw.using.trim() : "";
|
|
@@ -232,3 +364,97 @@ export function parseSqlPolicyRowId(id: string): { readonly table: string; reado
|
|
|
232
364
|
}
|
|
233
365
|
return { table: id.slice(0, cut), name: id.slice(cut + 1) };
|
|
234
366
|
}
|
|
367
|
+
|
|
368
|
+
function rolesField(raw: unknown): string {
|
|
369
|
+
if (typeof raw === "string") return raw;
|
|
370
|
+
if (Array.isArray(raw)) {
|
|
371
|
+
return raw.filter((part): part is string => typeof part === "string").join(", ");
|
|
372
|
+
}
|
|
373
|
+
return "public";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Drizzle `pgPolicy(...)` source for emit / Console copy-as-code.
|
|
378
|
+
*
|
|
379
|
+
* @param spec - Validated policy
|
|
380
|
+
*/
|
|
381
|
+
export function emitPgPolicySource(spec: SqlPolicySpec): string {
|
|
382
|
+
const to =
|
|
383
|
+
spec.roles.length === 1
|
|
384
|
+
? JSON.stringify(formatEmitRole(spec.roles[0]!))
|
|
385
|
+
: `[${spec.roles.map((role) => JSON.stringify(formatEmitRole(role))).join(", ")}]`;
|
|
386
|
+
const lines = [
|
|
387
|
+
`pgPolicy(${JSON.stringify(spec.name)}, {`,
|
|
388
|
+
` as: ${JSON.stringify(spec.behavior.toLowerCase())},`,
|
|
389
|
+
` to: ${to},`,
|
|
390
|
+
` for: ${JSON.stringify(spec.command.toLowerCase())},`,
|
|
391
|
+
];
|
|
392
|
+
if (spec.using !== undefined && spec.using.trim() !== "") {
|
|
393
|
+
lines.push(` using: sql\`${escapeSqlTemplate(spec.using.trim())}\`,`);
|
|
394
|
+
}
|
|
395
|
+
if (spec.withCheck !== undefined && spec.withCheck.trim() !== "") {
|
|
396
|
+
lines.push(` withCheck: sql\`${escapeSqlTemplate(spec.withCheck.trim())}\`,`);
|
|
397
|
+
}
|
|
398
|
+
lines.push(`})`);
|
|
399
|
+
return lines.join("\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* `store.schema.policy(...)` source for Console copy-as-code.
|
|
404
|
+
*
|
|
405
|
+
* @param spec - Validated policy
|
|
406
|
+
*/
|
|
407
|
+
export function emitStoreSchemaPolicySource(spec: SqlPolicySpec): string {
|
|
408
|
+
const to =
|
|
409
|
+
spec.roles.length === 1
|
|
410
|
+
? JSON.stringify(formatEmitRole(spec.roles[0]!))
|
|
411
|
+
: `[${spec.roles.map((role) => JSON.stringify(formatEmitRole(role))).join(", ")}]`;
|
|
412
|
+
const lines = [
|
|
413
|
+
`store.schema.policy(${JSON.stringify(spec.name)}, {`,
|
|
414
|
+
` as: ${JSON.stringify(spec.behavior.toLowerCase())},`,
|
|
415
|
+
` to: ${to},`,
|
|
416
|
+
` for: ${JSON.stringify(spec.command.toLowerCase())},`,
|
|
417
|
+
];
|
|
418
|
+
if (spec.using !== undefined && spec.using.trim() !== "") {
|
|
419
|
+
lines.push(` using: ${JSON.stringify(spec.using.trim())},`);
|
|
420
|
+
}
|
|
421
|
+
if (spec.withCheck !== undefined && spec.withCheck.trim() !== "") {
|
|
422
|
+
lines.push(` withCheck: ${JSON.stringify(spec.withCheck.trim())},`);
|
|
423
|
+
}
|
|
424
|
+
lines.push(`})`);
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function formatEmitRole(role: string): string {
|
|
429
|
+
return SPECIAL_POLICY_ROLES.has(role.toLowerCase()) ? role.toLowerCase() : role;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function escapeSqlTemplate(expr: string): string {
|
|
433
|
+
return expr.replaceAll("\\", "\\\\").replaceAll("`", "\\`");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** One statement in the RLS identity prelude (never a multi-command batch). */
|
|
437
|
+
export type RlsPreludeStatement = {
|
|
438
|
+
readonly sql: string;
|
|
439
|
+
readonly params?: readonly string[];
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Per-statement identity frame (no user SQL, no `BEGIN`).
|
|
444
|
+
*
|
|
445
|
+
* The caller pins these to one backend connection via `SqlConnection.transaction`.
|
|
446
|
+
* PGlite's `query` path is prepared statements and rejects `BEGIN; SET; SELECT`
|
|
447
|
+
* as one script.
|
|
448
|
+
*
|
|
449
|
+
* @param identity - Gate principal
|
|
450
|
+
*/
|
|
451
|
+
export function buildRlsIdentityPreludeSql(identity: RlsIdentity): readonly RlsPreludeStatement[] {
|
|
452
|
+
return [
|
|
453
|
+
{ sql: `SET LOCAL ROLE ${OKE_RLS_ROLE}` },
|
|
454
|
+
{ sql: "SET LOCAL row_security = on" },
|
|
455
|
+
{
|
|
456
|
+
sql: `SELECT set_config('oke.gate', ?, true), set_config('oke.user', ?, true), set_config('oke.scopes', ?, true)`,
|
|
457
|
+
params: [identity.gate, identity.userId, rlsScopesJson(identity.scopes)],
|
|
458
|
+
},
|
|
459
|
+
];
|
|
460
|
+
}
|
package/src/drivers/pglite.ts
CHANGED
|
@@ -77,21 +77,44 @@ export async function connectPglite(options: SqlConnectOptions = {}): Promise<Sq
|
|
|
77
77
|
resolved === "memory"
|
|
78
78
|
? await PGlite.create({ extensions: { vector } })
|
|
79
79
|
: await PGlite.create(resolved, { extensions: { vector } });
|
|
80
|
-
|
|
80
|
+
const connection: SqlConnection = {
|
|
81
81
|
driverId: "pglite",
|
|
82
82
|
role,
|
|
83
83
|
async query(sql, params = []) {
|
|
84
|
-
const rs = await db.query<SqlRow>(toPostgresParams(sql), [...params]);
|
|
84
|
+
const rs = await db.query<SqlRow>(toPostgresParams(sql, params), [...params]);
|
|
85
85
|
return rs.rows;
|
|
86
86
|
},
|
|
87
87
|
async exec(sql, params = []) {
|
|
88
|
-
|
|
88
|
+
// Prepared `query` rejects multi-command scripts (`BEGIN; SET; …`).
|
|
89
|
+
// Param-less SQL uses simple-protocol `exec` (helpers, BEGIN, SET).
|
|
90
|
+
if (params.length === 0) {
|
|
91
|
+
const results = await db.exec(sql);
|
|
92
|
+
const last = results[results.length - 1];
|
|
93
|
+
return { changes: last?.affectedRows ?? 0 };
|
|
94
|
+
}
|
|
95
|
+
const rs = await db.query<SqlRow>(toPostgresParams(sql, params), [...params]);
|
|
89
96
|
return { changes: rs.affectedRows ?? 0 };
|
|
90
97
|
},
|
|
98
|
+
async transaction(fn) {
|
|
99
|
+
await db.exec("BEGIN");
|
|
100
|
+
try {
|
|
101
|
+
const result = await fn(connection);
|
|
102
|
+
await db.exec("COMMIT");
|
|
103
|
+
return result;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
try {
|
|
106
|
+
await db.exec("ROLLBACK");
|
|
107
|
+
} catch {
|
|
108
|
+
// Already aborted.
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
91
113
|
async close() {
|
|
92
114
|
await db.close();
|
|
93
115
|
},
|
|
94
116
|
};
|
|
117
|
+
return connection;
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
/** Protocol-named pglite driver. */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres placeholder conversion.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { toPostgresParams } from "./postgres.ts";
|
|
7
|
+
|
|
8
|
+
describe("toPostgresParams", () => {
|
|
9
|
+
test("rewrites ? only when values are bound", () => {
|
|
10
|
+
expect(toPostgresParams("SELECT * FROM t WHERE id = ?", ["a"])).toBe(
|
|
11
|
+
"SELECT * FROM t WHERE id = $1",
|
|
12
|
+
);
|
|
13
|
+
expect(toPostgresParams("INSERT INTO t (a, b) VALUES (?, ?)", [1, 2])).toBe(
|
|
14
|
+
"INSERT INTO t (a, b) VALUES ($1, $2)",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("leaves jsonb ? operators unchanged when there are no values", () => {
|
|
19
|
+
const sql = "SELECT current_setting('oke.scopes', true)::jsonb ? p_scope";
|
|
20
|
+
expect(toPostgresParams(sql)).toBe(sql);
|
|
21
|
+
expect(toPostgresParams(sql, [])).toBe(sql);
|
|
22
|
+
});
|
|
23
|
+
});
|
package/src/drivers/postgres.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Protocol-named: Neon, Supabase, RDS, Timescale all speak postgres.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { SqlConnectOptions, SqlConnection, SqlDriver, SqlRow } from "./types.ts";
|
|
7
|
+
import type { SqlConnectOptions, SqlConnection, SqlDriver, SqlRole, SqlRow } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
/** Bun.SQL client checked out via {@link PostgresClientLike.reserve}. */
|
|
10
|
+
export interface PostgresReservedClient extends PostgresClientLike {
|
|
11
|
+
/** Return the connection to the pool. */
|
|
12
|
+
release(): void;
|
|
13
|
+
}
|
|
8
14
|
|
|
9
15
|
/** Minimal surface we use from Bun.SQL (and test fakes). */
|
|
10
16
|
export interface PostgresClientLike {
|
|
@@ -12,15 +18,98 @@ export interface PostgresClientLike {
|
|
|
12
18
|
sql: string,
|
|
13
19
|
values?: unknown[],
|
|
14
20
|
): PromiseLike<SqlRow[] | { length: number; changes?: number } | SqlRow[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Pin one pooled TCP connection. Required in front of PgDog — `begin()`
|
|
23
|
+
* then `unsafe()` on the parent client can checkout a second slot and
|
|
24
|
+
* deadlock the pool (`checkout timeout`).
|
|
25
|
+
*/
|
|
26
|
+
reserve?(): Promise<PostgresReservedClient>;
|
|
27
|
+
/**
|
|
28
|
+
* Reserve one pooled connection for a transaction (Bun.SQL).
|
|
29
|
+
*
|
|
30
|
+
* @param fn - Work on the reserved client
|
|
31
|
+
*/
|
|
32
|
+
begin?<T>(fn: (tx: PostgresClientLike) => Promise<T> | T): Promise<T>;
|
|
15
33
|
close?(options?: { timeout?: number }): Promise<void>;
|
|
16
34
|
}
|
|
17
35
|
|
|
36
|
+
/** Cap shared Bun.SQL pools so one process cannot outrun PgDog (default 10). */
|
|
37
|
+
export const POSTGRES_POOL_MAX = 8;
|
|
38
|
+
|
|
39
|
+
const sharedClients = new Map<string, PostgresClientLike>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the Postgres URL (injected, `DATABASE_URL`, then localhost).
|
|
43
|
+
*
|
|
44
|
+
* @param url - Explicit URL
|
|
45
|
+
*/
|
|
46
|
+
export function resolvePostgresUrl(url?: string): string {
|
|
47
|
+
return url ?? process.env.DATABASE_URL ?? "postgres://localhost:5432/oke";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One Bun.SQL pool per URL for store / journal / clock / vault / console.
|
|
52
|
+
*
|
|
53
|
+
* @param url - Connection URL
|
|
54
|
+
*/
|
|
55
|
+
export function sharedPostgresClient(url?: string): PostgresClientLike {
|
|
56
|
+
const key = resolvePostgresUrl(url);
|
|
57
|
+
const existing = sharedClients.get(key);
|
|
58
|
+
if (existing) return existing;
|
|
59
|
+
const created = new Bun.SQL(key, { max: POSTGRES_POOL_MAX }) as unknown as PostgresClientLike;
|
|
60
|
+
sharedClients.set(key, created);
|
|
61
|
+
return created;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Pin one pooled connection, `BEGIN`, run `fn`, `COMMIT`/`ROLLBACK`, release.
|
|
66
|
+
*
|
|
67
|
+
* Prefer `reserve()` — Bun.SQL `begin()` then `unsafe()` on the parent
|
|
68
|
+
* client can checkout a second slot and deadlock PgDog.
|
|
69
|
+
*
|
|
70
|
+
* @param client - Pool or test fake
|
|
71
|
+
* @param fn - Work on the pinned client
|
|
72
|
+
*/
|
|
73
|
+
export async function withPinnedPostgres<T>(
|
|
74
|
+
client: PostgresClientLike,
|
|
75
|
+
fn: (tx: PostgresClientLike) => Promise<T>,
|
|
76
|
+
): Promise<T> {
|
|
77
|
+
if (typeof client.reserve === "function") {
|
|
78
|
+
const reserved = await client.reserve();
|
|
79
|
+
try {
|
|
80
|
+
await reserved.unsafe("BEGIN");
|
|
81
|
+
try {
|
|
82
|
+
const result = await fn(reserved);
|
|
83
|
+
await reserved.unsafe("COMMIT");
|
|
84
|
+
return result;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
try {
|
|
87
|
+
await reserved.unsafe("ROLLBACK");
|
|
88
|
+
} catch {
|
|
89
|
+
// Already aborted.
|
|
90
|
+
}
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
reserved.release();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (typeof client.begin === "function") {
|
|
98
|
+
return client.begin(fn);
|
|
99
|
+
}
|
|
100
|
+
throw new Error("postgres client needs reserve() or begin() to pin a transaction");
|
|
101
|
+
}
|
|
102
|
+
|
|
18
103
|
/**
|
|
19
104
|
* Convert `?` placeholders to Postgres `$1…$n`.
|
|
105
|
+
* Leaves SQL unchanged when there are no bound values so operators
|
|
106
|
+
* like `jsonb ? key` are not rewritten.
|
|
20
107
|
*
|
|
21
108
|
* @param sql - SQL with `?` placeholders
|
|
109
|
+
* @param values - Bound values (empty skips conversion)
|
|
22
110
|
*/
|
|
23
|
-
export function toPostgresParams(sql: string): string {
|
|
111
|
+
export function toPostgresParams(sql: string, values: readonly unknown[] = []): string {
|
|
112
|
+
if (values.length === 0) return sql;
|
|
24
113
|
let i = 0;
|
|
25
114
|
return sql.replace(/\?/g, () => `$${++i}`);
|
|
26
115
|
}
|
|
@@ -32,21 +121,34 @@ export function toPostgresParams(sql: string): string {
|
|
|
32
121
|
*/
|
|
33
122
|
export async function connectPostgres(options: SqlConnectOptions = {}): Promise<SqlConnection> {
|
|
34
123
|
const role = options.role ?? "primary";
|
|
35
|
-
const client
|
|
36
|
-
|
|
37
|
-
|
|
124
|
+
const injected = options.client as PostgresClientLike | undefined;
|
|
125
|
+
const client = injected ?? sharedPostgresClient(options.url);
|
|
126
|
+
return wrapPostgresClient(client, role, { shared: injected === undefined });
|
|
127
|
+
}
|
|
38
128
|
|
|
39
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Bind a Bun.SQL-compatible client as {@link SqlConnection}.
|
|
131
|
+
*
|
|
132
|
+
* @param client - Pool or reserved transaction client
|
|
133
|
+
* @param role - Primary vs replica
|
|
134
|
+
* @param opts - Shared-pool / already-pinned flags
|
|
135
|
+
*/
|
|
136
|
+
function wrapPostgresClient(
|
|
137
|
+
client: PostgresClientLike,
|
|
138
|
+
role: SqlRole,
|
|
139
|
+
opts: { readonly shared?: boolean; readonly pinned?: boolean } = {},
|
|
140
|
+
): SqlConnection {
|
|
141
|
+
const connection: SqlConnection = {
|
|
40
142
|
driverId: "postgres",
|
|
41
143
|
role,
|
|
42
144
|
async query(sql, params = []) {
|
|
43
|
-
const pg = toPostgresParams(sql);
|
|
145
|
+
const pg = toPostgresParams(sql, params);
|
|
44
146
|
const result = await client.unsafe(pg, [...params]);
|
|
45
147
|
if (Array.isArray(result)) return result as SqlRow[];
|
|
46
148
|
return Array.from(result as ArrayLike<SqlRow>);
|
|
47
149
|
},
|
|
48
150
|
async exec(sql, params = []) {
|
|
49
|
-
const pg = toPostgresParams(sql);
|
|
151
|
+
const pg = toPostgresParams(sql, params);
|
|
50
152
|
const result = await client.unsafe(pg, [...params]);
|
|
51
153
|
if (
|
|
52
154
|
result &&
|
|
@@ -61,10 +163,15 @@ export async function connectPostgres(options: SqlConnectOptions = {}): Promise<
|
|
|
61
163
|
}
|
|
62
164
|
return { changes: 0 };
|
|
63
165
|
},
|
|
166
|
+
async transaction(fn) {
|
|
167
|
+
if (opts.pinned) return fn(connection);
|
|
168
|
+
return withPinnedPostgres(client, (tx) => fn(wrapPostgresClient(tx, role, { pinned: true })));
|
|
169
|
+
},
|
|
64
170
|
async close() {
|
|
65
|
-
await client.close?.();
|
|
171
|
+
if (!opts.shared) await client.close?.();
|
|
66
172
|
},
|
|
67
173
|
};
|
|
174
|
+
return connection;
|
|
68
175
|
}
|
|
69
176
|
|
|
70
177
|
/**
|
|
@@ -82,10 +189,27 @@ export function createPostgresFakeClient(): PostgresClientLike & {
|
|
|
82
189
|
|
|
83
190
|
return {
|
|
84
191
|
tables,
|
|
192
|
+
async begin(fn) {
|
|
193
|
+
return fn(this);
|
|
194
|
+
},
|
|
195
|
+
async reserve() {
|
|
196
|
+
return Object.assign(this, {
|
|
197
|
+
release() {
|
|
198
|
+
/* fake has no pool */
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
},
|
|
85
202
|
async unsafe(sql, values = []) {
|
|
86
203
|
const text = sql.trim();
|
|
87
204
|
// Accept both ? (pre-conversion) and $n forms.
|
|
88
205
|
const normalised = text.replace(/\$\d+/g, "?");
|
|
206
|
+
if (
|
|
207
|
+
/^(begin|commit|rollback|set\b|select set_config|create\b|grant\b|do\b|alter\b)\b/i.test(
|
|
208
|
+
normalised,
|
|
209
|
+
)
|
|
210
|
+
) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
89
213
|
|
|
90
214
|
const create =
|
|
91
215
|
/^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+("?[a-zA-Z_][a-zA-Z0-9_]*"?)\s*\((.+)\)\s*$/i.exec(
|
package/src/drivers/types.ts
CHANGED
|
@@ -62,6 +62,16 @@ export interface SqlConnection {
|
|
|
62
62
|
* @param params - Bound parameters
|
|
63
63
|
*/
|
|
64
64
|
exec(sql: string, params?: readonly unknown[]): Promise<{ changes: number }>;
|
|
65
|
+
/**
|
|
66
|
+
* Pin `fn` to one backend connection for a single transaction.
|
|
67
|
+
*
|
|
68
|
+
* Required for `SET LOCAL` RLS stamps on pooled `postgres` (Bun.SQL /
|
|
69
|
+
* PgDog transaction mode). A standalone `BEGIN` via {@link exec} returns
|
|
70
|
+
* the connection to the pool still in a transaction and exhausts checkout.
|
|
71
|
+
*
|
|
72
|
+
* @param fn - Work that must share the reserved connection
|
|
73
|
+
*/
|
|
74
|
+
transaction?<T>(fn: (tx: SqlConnection) => Promise<T>): Promise<T>;
|
|
65
75
|
/** Close the connection. */
|
|
66
76
|
close(): Promise<void>;
|
|
67
77
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* the effective state from the Store (console §5), never the code directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { clockRegistry } from "../../kernel/element-registries.ts";
|
|
9
|
+
|
|
8
10
|
/** Options for {@link clock}. */
|
|
9
11
|
export interface ClockOptions {
|
|
10
12
|
/** Cron expression (`m h dom mon dow`). */
|
|
@@ -40,6 +42,27 @@ export interface ClockDecl {
|
|
|
40
42
|
readonly description?: string;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* `clock()` pushes into the shared {@link clockRegistry}
|
|
47
|
+
* (`src/kernel/element-registries.ts`) so {@link oke} can auto-populate
|
|
48
|
+
* `clocks` with zero explicit array — mirrors the {@link on} trigger-drain
|
|
49
|
+
* registry (`src/kernel/on.ts`).
|
|
50
|
+
*
|
|
51
|
+
* Snapshot of every clock declared since the last reset.
|
|
52
|
+
*/
|
|
53
|
+
export function listClocks(): readonly ClockDecl[] {
|
|
54
|
+
return clockRegistry.slice();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clear the clock registry (tests / fresh app adopt).
|
|
59
|
+
*
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
export function resetClocks(): void {
|
|
63
|
+
clockRegistry.length = 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
/**
|
|
44
67
|
* Declare a named clock / cron schedule.
|
|
45
68
|
*
|
|
@@ -50,7 +73,7 @@ export function clock(name: string, options: ClockOptions = {}): ClockDecl {
|
|
|
50
73
|
if (!options.cron && !options.every) {
|
|
51
74
|
throw new TypeError(`clock("${name}"): require cron or every`);
|
|
52
75
|
}
|
|
53
|
-
|
|
76
|
+
const decl: ClockDecl = {
|
|
54
77
|
name,
|
|
55
78
|
cron: options.cron,
|
|
56
79
|
every: options.every,
|
|
@@ -58,4 +81,6 @@ export function clock(name: string, options: ClockOptions = {}): ClockDecl {
|
|
|
58
81
|
overridable: options.overridable ?? false,
|
|
59
82
|
...(options.description !== undefined ? { description: options.description } : {}),
|
|
60
83
|
};
|
|
84
|
+
clockRegistry.push(decl);
|
|
85
|
+
return decl;
|
|
61
86
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Gate boot audit — every HTTP trigger must declare auth posture.
|
|
3
3
|
*
|
|
4
|
-
* Fail loud: missing gate and missing
|
|
4
|
+
* Fail loud: missing gate and missing `.public()` → {@link GateBootError}.
|
|
5
5
|
* `unguardedHttp: "allow"` is honoured **only** when `env === "test"` — never a
|
|
6
|
-
* production-wide bypass. Migrate real apps with per-trigger
|
|
6
|
+
* production-wide bypass. Migrate real apps with per-trigger `.public()`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { GATE_PUBLIC_NAME } from "./declare.ts";
|
|
@@ -19,7 +19,7 @@ export interface GatePostureGap {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Thrown when HTTP triggers omit both a gate chain and
|
|
22
|
+
* Thrown when HTTP triggers omit both a gate chain and `.public()`.
|
|
23
23
|
* Lists every gap (Vault-style — fail once with the full set).
|
|
24
24
|
*/
|
|
25
25
|
export class GateBootError extends Error {
|
|
@@ -28,7 +28,7 @@ export class GateBootError extends Error {
|
|
|
28
28
|
constructor(gaps: readonly GatePostureGap[]) {
|
|
29
29
|
const lines = gaps.map((g) => ` - ${g.flowId} ${g.method} ${g.path}`);
|
|
30
30
|
super(
|
|
31
|
-
`gate boot failed — ${gaps.length} HTTP trigger(s) missing auth posture (attach a gate or
|
|
31
|
+
`gate boot failed — ${gaps.length} HTTP trigger(s) missing auth posture (attach a gate or .public()):\n${lines.join("\n")}`,
|
|
32
32
|
);
|
|
33
33
|
this.name = "GateBootError";
|
|
34
34
|
this.gaps = gaps;
|
|
@@ -84,7 +84,7 @@ export function hasHttpGatePosture(
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Collect HTTP triggers with neither a gate nor
|
|
87
|
+
* Collect HTTP triggers with neither a gate nor `.public()`.
|
|
88
88
|
*
|
|
89
89
|
* @param bindings - Adopted / registered bindings
|
|
90
90
|
*/
|