@fjall/schema-gate 10.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,50 @@
1
+ Fjall Proprietary Software Licence
2
+
3
+ Copyright (c) 2026 Fjall. All rights reserved.
4
+
5
+ This software, including all source, object, bundled, and minified forms
6
+ ("the Software"), is the proprietary and confidential property of Fjall.
7
+
8
+ 1. Permitted Use. Subject to the terms of this Licence, Fjall grants you
9
+ a non-exclusive, non-transferable, revocable licence to install the
10
+ Software via the npm registry and to execute it solely for the purpose
11
+ of deploying, operating, and managing your own applications and
12
+ infrastructure on cloud providers.
13
+
14
+ 2. Restrictions. You may NOT, and may not permit any third party to:
15
+ (a) copy, redistribute, sublicense, sell, rent, lease, or otherwise
16
+ transfer the Software;
17
+ (b) modify, adapt, translate, or create derivative works of the Software;
18
+ (c) reverse engineer, decompile, disassemble, deminify, or otherwise
19
+ attempt to derive the source code, structure, or organisation of
20
+ the Software, except to the minimum extent expressly permitted by
21
+ applicable mandatory law;
22
+ (d) use the Software, or any portion of it, to develop, train, or
23
+ improve any product or service that competes with Fjall;
24
+ (e) remove, alter, or obscure any proprietary notices contained in
25
+ the Software;
26
+ (f) publish, share, or otherwise disclose the Software or its contents
27
+ to any third party.
28
+
29
+ 3. Ownership. All right, title, and interest in and to the Software,
30
+ including all intellectual property rights, remain with Fjall. No
31
+ rights are granted except as expressly set out in this Licence.
32
+
33
+ 4. Termination. This Licence terminates automatically if you breach any
34
+ of its terms. Upon termination you must cease all use of the Software
35
+ and destroy all copies in your possession.
36
+
37
+ 5. Disclaimer of Warranty. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT
38
+ WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
39
+ THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
40
+ AND NON-INFRINGEMENT.
41
+
42
+ 6. Limitation of Liability. IN NO EVENT SHALL FJALL BE LIABLE FOR ANY
43
+ INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES
44
+ ARISING OUT OF OR RELATED TO THE SOFTWARE, EVEN IF ADVISED OF THE
45
+ POSSIBILITY OF SUCH DAMAGES.
46
+
47
+ 7. Governing Law. This Licence is governed by the laws of England and
48
+ Wales, without regard to conflict of laws principles.
49
+
50
+ For commercial licensing enquiries, contact: contact@fjall.io
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": 2,
3
+ "algorithm": "sha256",
4
+ "sources": [
5
+ "src"
6
+ ],
7
+ "extraFiles": [
8
+ "../scripts/minify-dist.mjs",
9
+ "package.json",
10
+ "tsconfig.json"
11
+ ],
12
+ "hash": "505139b3e780af0997b693326624892b3c01dd78547001cfcfb3901dbc749aa4",
13
+ "files": {
14
+ "src/bin.ts": "3acde81b3b914cc4",
15
+ "src/index.ts": "ab1151c1b7f1bc38",
16
+ "src/runSchemaGate.ts": "f0a4b0fec6a095de",
17
+ "src/types.ts": "67a71f4a53cc1e83",
18
+ "../scripts/minify-dist.mjs": "6b2e4b0df8aec601",
19
+ "package.json": "abbd127fa295a73e",
20
+ "tsconfig.json": "8a11f0e3fce6a36a"
21
+ },
22
+ "outputHash": "283ec27ac60724f090ed3d911a0bd9f4e15b8be9e195d25d490a75b9b225c9ad",
23
+ "outputFileCount": 9
24
+ }
package/dist/.minified ADDED
@@ -0,0 +1 @@
1
+ 4 files minified at 2026-08-11T22:22:01.205Z
package/dist/bin.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Container entrypoint for the synthetic `fjall-schema-gate` container.
4
+ * Wires the real drivers into `runSchemaGate` and exits with the taxonomy
5
+ * code — the dependent app containers' `dependsOn: SUCCESS` turns any
6
+ * non-zero exit into a pre-RUNNING task stop the deployment circuit breaker
7
+ * can count.
8
+ */
9
+ export {};
package/dist/bin.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ var m=Object.defineProperty;var o=(e,t)=>m(e,"name",{value:t,configurable:!0});import{createFjallClickHouseClient as p}from"@fjall/clickhouse";import{getErrorMessage as s,maskSensitiveOutput as a}from"@fjall/util";import d from"pg";import{runSchemaGate as f}from"./runSchemaGate.js";import{SCHEMA_GATE_EXIT as S}from"./types.js";const{Client:g}=d,w="FjallSchemaGate",i=1e4;function r(e,t,n={}){process.stdout.write(`${JSON.stringify({timestamp:new Date().toISOString(),level:e,namespace:w,message:t,...n})}
3
+ `)}o(r,"log");function u(e,t){return new g({connectionString:e,connectionTimeoutMillis:i,query_timeout:i,statement_timeout:i,...t?{ssl:{rejectUnauthorized:!1}}:{}})}o(u,"buildPgClient");const C={async connectPg(e){const t=u(e,!0);try{return await t.connect(),t}catch(n){if(!/does not support SSL/i.test(s(n)))throw n;await t.end().catch(l=>{r("warn","Postgres TLS probe cleanup failed",{error:a(s(l))})}),r("info","Server does not support TLS \u2014 connecting in plaintext");const c=u(e,!1);return await c.connect(),c}},createChClient(e){return p({url:e.url,username:e.username,password:e.password,...e.caCert!==void 0?{caCert:e.caCert}:{},request_timeout:i})},sleep:o(e=>new Promise(t=>setTimeout(t,e)),"sleep"),log:r};for(const[e,t]of[["SIGTERM",143],["SIGINT",130]])process.on(e,()=>{r("warn",`Received ${e} \u2014 exiting without a verdict`),process.exit(t)});f(process.env,C).then(e=>{process.exit(e)}).catch(e=>{r("error","Gate crashed",{error:a(s(e))}),process.exit(S.configError)});
@@ -0,0 +1,2 @@
1
+ export { CONNECTION_ATTEMPTS, RETRY_DELAY_MS, runSchemaGate, } from "./runSchemaGate.js";
2
+ export { SCHEMA_GATE_EXIT, type SchemaGateChClientOpts, type SchemaGateChHandle, type SchemaGateDeps, type SchemaGateExitCode, type SchemaGateLogLevel, type SchemaGateLogger, type SchemaGatePgHandle, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{CONNECTION_ATTEMPTS as r,RETRY_DELAY_MS as _,runSchemaGate as e}from"./runSchemaGate.js";import{SCHEMA_GATE_EXIT as A}from"./types.js";export{r as CONNECTION_ATTEMPTS,_ as RETRY_DELAY_MS,A as SCHEMA_GATE_EXIT,e as runSchemaGate};
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Core gate logic for the synthetic `fjall-schema-gate` container.
3
+ *
4
+ * Reads the env contract injected by `EcsCompute` (names shared via
5
+ * `@fjall/util/migration` — coupled values across the construct↔runner
6
+ * boundary), verifies each configured target against the live database with
7
+ * the tolerant comparator (`actual >= expected`; rollbacks after expand-only
8
+ * migrations must boot), and returns an exit code from the taxonomy in
9
+ * `types.ts`.
10
+ *
11
+ * Connection attempts are bounded: `CONNECTION_ATTEMPTS` tries,
12
+ * `RETRY_DELAY_MS` apart, per target. Targets verify in parallel so the
13
+ * worst-case wall clock stays inside the dependent containers'
14
+ * `startTimeout` (120s) with headroom.
15
+ */
16
+ import { type SchemaGateDeps, type SchemaGateExitCode } from "./types.js";
17
+ export declare const CONNECTION_ATTEMPTS = 3;
18
+ export declare const RETRY_DELAY_MS = 5000;
19
+ /**
20
+ * Bound on how long a post-verdict client close may hold up the exit path.
21
+ * The verdict is already in hand — a close that outlives this is abandoned
22
+ * (the process exits immediately after, dropping the socket).
23
+ */
24
+ export declare const CLIENT_CLOSE_TIMEOUT_MS = 2000;
25
+ /**
26
+ * Run every configured verification target and reduce to one exit code.
27
+ * See `SCHEMA_GATE_EXIT` for the taxonomy and its precedence rules.
28
+ */
29
+ export declare function runSchemaGate(env: Record<string, string | undefined>, deps: SchemaGateDeps): Promise<SchemaGateExitCode>;
@@ -0,0 +1 @@
1
+ var R=Object.defineProperty;var i=(e,t)=>R(e,"name",{value:t,configurable:!0});import{verifyExpectedClickHouseSchemaVersion as k}from"@fjall/clickhouse";import{maskSensitiveOutput as H}from"@fjall/util";import{EXPECTED_CH_SCHEMA_VERSION_ENV as m,EXPECTED_SCHEMA_VERSION_ENV as p,EXPECTED_SCHEMA_VERSION_TOOL_ENV as C,SCHEMA_ADMIN_PASSWORD_ENV as M,SCHEMA_ADMIN_USER_ENV as b,SCHEMA_GATE_CH_CA_CERT_ENV as P,SCHEMA_GATE_CH_DATABASE_ENV as V,SCHEMA_GATE_CH_URL_ENV as D,SCHEMA_GATE_DB_PASSWORD_ENV as G,SCHEMA_GATE_DB_URL_BASE_ENV as S,SCHEMA_GATE_DB_USER_ENV as O,verifyExpectedSchemaVersion as U}from"@fjall/util/migration";import{SCHEMA_GATE_EXIT as f}from"./types.js";const g=3,x=5e3,w=500,v="42P01",I=new Set(["60","81"]),$=2e3,L=new Set(["postgresql:","postgres:"]);class _ extends Error{static{i(this,"GateConfigError")}}function d(e){const t=e instanceof Error?e.message:String(e),a=H(t);return a.length>w?`${a.slice(0,w)}...`:a}i(d,"maskedError");function E(e,t,a){const r=e[t];if(r===void 0||r==="")throw new _(`${a}, but ${t} is missing or empty. The construct injects both together \u2014 an incomplete set points at an author override or a framework bug.`);return r}i(E,"requireEnv");function B(e,t,a){let r;try{r=new URL(e)}catch{throw new _(`${S} is not a parseable URL`)}if(!L.has(r.protocol))throw new _(`${S} has scheme '${r.protocol}' \u2014 the materialised gate supports the postgres engine family only. Set schemaGate: { materialise: false } for this service.`);return r.username=encodeURIComponent(t),r.password=encodeURIComponent(a),r.searchParams.delete("sslmode"),r.toString()}i(B,"composePgUrl");function y(e){return typeof e=="object"&&e!==null&&"code"in e&&e.code===v}i(y,"isUndefinedTableError");function A(e){if(typeof e!="object"||e===null)return!1;const t=e.code;return typeof t=="string"&&I.has(t)}i(A,"isChNeverMigratedError");async function T(e,t,a){await Promise.race([a().catch(r=>{e.log("warn",`${t} client close failed`,{error:d(r)})}),e.sleep($)])}i(T,"closeWithTimeout");async function N(e,t,a){let r;for(let n=1;n<=g;n++)try{return await a()}catch(s){if(r=s,y(s)||A(s))throw s;e.log("warn","Verification attempt failed",{target:t,attempt:n,of:g,error:d(s)}),n<g&&await e.sleep(x)}throw r}i(N,"withRetry");async function X(e,t,a){const r=`${p} is set`,n=E(e,C,r);if(n!=="prisma")throw new _(`${C}='${n}' \u2014 the materialised gate supports tool "prisma" only (a custom tool's metadata query cannot be expressed through the env contract). Set schemaGate: { materialise: false } for this service.`);const s=E(e,S,r),u=E(e,O,r),l=E(e,G,r),o=B(s,u,l);try{const c=await N(t,"postgres",async()=>{const h=await t.connectPg(o);try{return await U({tool:"prisma",expected:a,client:h})}finally{await T(t,"Postgres",()=>h.end())}});return{target:"postgres",kind:c.matches?"pass":"refused",expected:a,actual:c.actual}}catch(c){return y(c)?{target:"postgres",kind:"refused",expected:a,actual:null,detail:"migrations table absent \u2014 the database has never been migrated while the deployed image expects a schema version"}:{target:"postgres",kind:"connection-error",expected:a,detail:d(c)}}}i(X,"verifyPostgres");async function j(e,t,a){const r=`${m} is set`,n=E(e,D,r),s=E(e,b,r),u=E(e,M,r),l=e[P],o=e[V];try{const c=await N(t,"clickhouse",async()=>{const h=t.createChClient({url:n,username:s,password:u,...l!==void 0&&l!==""?{caCert:l}:{}});try{return await k({client:h,expected:a,...o!==void 0&&o!==""?{database:o}:{}})}finally{await T(t,"ClickHouse",()=>h.close())}});return{target:"clickhouse",kind:c.matches?"pass":"refused",expected:a,actual:c.actual}}catch(c){return A(c)?{target:"clickhouse",kind:"refused",expected:a,actual:null,detail:"migrations audit table absent \u2014 the ClickHouse database has never been migrated while the deployed image expects a schema version"}:{target:"clickhouse",kind:"connection-error",expected:a,detail:d(c)}}}i(j,"verifyClickHouse");async function J(e,t){const a=e[p],r=e[m],n=a!==void 0&&a!=="",s=r!==void 0&&r!=="";if(!n&&!s)return t.log("error","Nothing to verify \u2014 no expected-version env present",{hint:`neither ${p} nor ${m} is set; the construct should never materialise a gate container without one`}),f.configError;const u=[];n&&u.push(X(e,t,a)),s&&u.push(j(e,t,r));let l;try{l=await Promise.all(u)}catch(o){return o instanceof _?(t.log("error","Gate configuration invalid",{error:d(o)}),f.configError):(t.log("error","Gate failed unexpectedly",{error:d(o)}),f.configError)}for(const o of l)o.kind==="pass"?t.log("info","Schema version satisfied",{target:o.target,expected:o.expected,actual:o.actual??null}):o.kind==="refused"?t.log("error","REFUSED \u2014 schema version not satisfied",{target:o.target,expected:o.expected,actual:o.actual??null,...o.detail!==void 0?{detail:o.detail}:{}}):t.log("error","Verification target unreachable after retries",{target:o.target,expected:o.expected,attempts:g,...o.detail!==void 0?{error:o.detail}:{}});return l.some(o=>o.kind==="refused")?f.refused:l.some(o=>o.kind==="connection-error")?f.connectionError:f.pass}i(J,"runSchemaGate");export{$ as CLIENT_CLOSE_TIMEOUT_MS,g as CONNECTION_ATTEMPTS,x as RETRY_DELAY_MS,J as runSchemaGate};
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Exit taxonomy for the materialised gate container. The dependent app
3
+ * containers declare `dependsOn: [{ container: "fjall-schema-gate",
4
+ * condition: "SUCCESS" }]`, so ANY non-zero exit stops the task pre-RUNNING
5
+ * and counts toward the deployment circuit breaker's stage-1 arithmetic —
6
+ * the codes exist for the forensic log and the deploy-time ECS tail, not for
7
+ * ECS itself.
8
+ *
9
+ * - `pass` (0) — every configured target verified; app containers start.
10
+ * - `refused` (1) — a target's live schema does not satisfy the expected
11
+ * version (forward skew, or a never-migrated database). The gate did its
12
+ * job; the deploy must roll back or the migration must land first.
13
+ * - `configError` (2) — the gate could not even attempt verification:
14
+ * missing/empty env, unsupported tool or engine family, or an internal
15
+ * error. Points at a construct bug or an author override.
16
+ * - `connectionError` (3) — a target was unreachable after the bounded
17
+ * retry budget. Indeterminate: the schema may be fine.
18
+ *
19
+ * Precedence when targets disagree: `configError` is detected before any
20
+ * connection is attempted; a verified `refused` outranks `connectionError`
21
+ * (a refusal is definitive evidence, an unreachable sibling is not).
22
+ */
23
+ export declare const SCHEMA_GATE_EXIT: {
24
+ readonly pass: 0;
25
+ readonly refused: 1;
26
+ readonly configError: 2;
27
+ readonly connectionError: 3;
28
+ };
29
+ export type SchemaGateExitCode = (typeof SCHEMA_GATE_EXIT)[keyof typeof SCHEMA_GATE_EXIT];
30
+ export type SchemaGateLogLevel = "info" | "warn" | "error";
31
+ /**
32
+ * Structured one-line JSON log sink. The container's stdout lands in the
33
+ * service's CloudWatch log group (and, on abnormal stop, in the task-stop
34
+ * watchdog's forensic log via `stoppedReason`), so every line must be
35
+ * self-contained and pre-masked.
36
+ */
37
+ export type SchemaGateLogger = (level: SchemaGateLogLevel, message: string, context?: Record<string, unknown>) => void;
38
+ /** Minimal Postgres handle the runner needs — `pg.Client` satisfies it. */
39
+ export interface SchemaGatePgHandle {
40
+ query(sql: string): Promise<{
41
+ rows: Array<Record<string, unknown>>;
42
+ }>;
43
+ end(): Promise<void>;
44
+ }
45
+ /** Minimal ClickHouse handle — `@clickhouse/client` satisfies it. */
46
+ export interface SchemaGateChHandle {
47
+ query(params: {
48
+ query: string;
49
+ format: "JSONEachRow";
50
+ }): Promise<{
51
+ json(): Promise<unknown>;
52
+ }>;
53
+ close(): Promise<void>;
54
+ }
55
+ export interface SchemaGateChClientOpts {
56
+ url: string;
57
+ username: string;
58
+ password: string;
59
+ caCert?: string;
60
+ }
61
+ /**
62
+ * Injectable seams. `bin.ts` supplies the real drivers; tests supply fakes
63
+ * that can express every failure shape (refusal, missing table, connect
64
+ * timeout) — a fake that cannot express the bug shape cannot catch it.
65
+ */
66
+ export interface SchemaGateDeps {
67
+ connectPg(connectionUrl: string): Promise<SchemaGatePgHandle>;
68
+ createChClient(opts: SchemaGateChClientOpts): SchemaGateChHandle;
69
+ sleep(ms: number): Promise<void>;
70
+ log: SchemaGateLogger;
71
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ const r={pass:0,refused:1,configError:2,connectionError:3};export{r as SCHEMA_GATE_EXIT};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@fjall/schema-gate",
3
+ "version": "10.1.1",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/fjall-tech/fjall.git",
7
+ "directory": "schema-gate"
8
+ },
9
+ "description": "Schema-version gate runner for the synthetic fjall-schema-gate ECS container. Verifies the deployed image's expected schema version against the live database (Postgres + ClickHouse) and exits with a structured taxonomy so a refusing task stops pre-RUNNING and trips the deployment circuit breaker.",
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "bin": {
14
+ "fjall-schema-gate": "dist/bin.js"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist/"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "clean": "rm -rf ./dist ./sourcemaps",
30
+ "clean:node": "rm -rf ./node_modules",
31
+ "build": "npm run clean && npx tsc && node ../scripts/minify-dist.mjs dist && node ../scripts/stamp-build.mjs",
32
+ "prepack": "node ../scripts/check-dist-freshness.mjs",
33
+ "watch": "npm run build && npx tsc-watch",
34
+ "watch:only": "npx tsc-watch",
35
+ "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
36
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
37
+ "lint": "eslint src/",
38
+ "lint:fix": "eslint src/ --fix",
39
+ "test": "vitest run",
40
+ "typecheck": "npx tsc --noEmit"
41
+ },
42
+ "author": "",
43
+ "license": "SEE LICENSE IN LICENSE",
44
+ "devDependencies": {
45
+ "@types/node": "^26.1.2",
46
+ "@types/pg": "^8.16.0",
47
+ "prettier": "^3.9.6",
48
+ "tsc-watch": "^7.2.1",
49
+ "typescript": "^6.0.3",
50
+ "vitest": "^4.1.10"
51
+ },
52
+ "dependencies": {
53
+ "@clickhouse/client": "^1.23.1",
54
+ "@fjall/clickhouse": "^10.1.1",
55
+ "@fjall/util": "^10.1.1",
56
+ "pg": "^8.16.3"
57
+ },
58
+ "engines": {
59
+ "node": ">=22.0.0"
60
+ }
61
+ }