@fadhilp/stateql 0.4.0 → 0.4.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/README.md +18 -4
- package/dist/src/adapters.d.ts +1 -0
- package/dist/src/adapters.js +18 -1
- package/dist/src/connection.d.ts +4 -0
- package/dist/src/connection.js +27 -1
- package/dist/src/stateql.js +14 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ Example first page:
|
|
|
64
64
|
Running the same normalized query with the same parameters reuses `q_1` while
|
|
65
65
|
its cache is valid. Use `--cache bypass` when a fresh read is required.
|
|
66
66
|
|
|
67
|
-
PostgreSQL and MySQL credentials should come from environment variables
|
|
67
|
+
PostgreSQL and MySQL credentials should come from environment variables. The
|
|
68
|
+
variable must contain the complete connection URL, not only its password.
|
|
69
|
+
Environment-backed SQLite paths require an explicit `sqlite:` prefix.
|
|
68
70
|
|
|
69
71
|
```bash
|
|
70
72
|
export APP_DATABASE_URL='postgres://user:password@host/app'
|
|
@@ -72,15 +74,25 @@ stql connect --env APP_DATABASE_URL --name app --read-only
|
|
|
72
74
|
|
|
73
75
|
export MYSQL_DATABASE_URL='mysql://user:password@host/app'
|
|
74
76
|
stql connect --env MYSQL_DATABASE_URL --name mysql-app --read-only
|
|
77
|
+
|
|
78
|
+
export SQLITE_DATABASE='sqlite:./app.sqlite'
|
|
79
|
+
stql connect --env SQLITE_DATABASE --name local --read-only
|
|
75
80
|
```
|
|
76
81
|
|
|
82
|
+
A connection accepts exactly one direct target, `--env`, or `--profile` source.
|
|
83
|
+
For PostgreSQL, StateQL preserves strict TLS verification by normalizing
|
|
84
|
+
`sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening the
|
|
85
|
+
adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
|
|
86
|
+
`uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
|
|
87
|
+
|
|
77
88
|
MySQL uses positional `?` parameters. MariaDB compatibility is not currently
|
|
78
89
|
claimed.
|
|
79
90
|
|
|
80
91
|
## Commands
|
|
81
92
|
|
|
82
93
|
```text
|
|
83
|
-
stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--
|
|
94
|
+
stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
|
|
95
|
+
stql connect --env ENV [--name NAME] [--read-write]
|
|
84
96
|
stql connect --profile NAME
|
|
85
97
|
stql status
|
|
86
98
|
stql profile add|list|show|remove
|
|
@@ -301,8 +313,10 @@ secret-free failures. Unknown resolver errors are replaced with a generic
|
|
|
301
313
|
StateQL calls the resolver only immediately before database access, after SQL
|
|
302
314
|
safety and duplicate checks. Requests contain actor/session identity, the
|
|
303
315
|
operation's effective read/write access, an abort signal, and sanitized
|
|
304
|
-
connection metadata. Returned values
|
|
305
|
-
|
|
316
|
+
connection metadata. Returned values must be complete PostgreSQL/MySQL URLs or
|
|
317
|
+
explicit `sqlite:` sources. StateQL validates the source and its stored driver
|
|
318
|
+
before adapter construction, and normalizes SQLite paths. Credential-bearing
|
|
319
|
+
PostgreSQL and MySQL URLs are redacted before connection
|
|
306
320
|
metadata is persisted and never enter history, snapshots, cache keys, or
|
|
307
321
|
responses. SQLite paths remain persisted connection metadata, as they are for
|
|
308
322
|
direct SQLite connections. Harnesses remain responsible for approval policy,
|
package/dist/src/adapters.d.ts
CHANGED
|
@@ -37,3 +37,4 @@ export declare function createAdapterContext(timeoutMs: number, signal?: AbortSi
|
|
|
37
37
|
export declare function createAdapter(connection: ConnectionRecord, context: AdapterContext, input: {
|
|
38
38
|
source: string;
|
|
39
39
|
}): Promise<Adapter>;
|
|
40
|
+
export declare function normalizePostgresConnectionString(source: string): string;
|
package/dist/src/adapters.js
CHANGED
|
@@ -182,6 +182,23 @@ class SQLiteAdapter {
|
|
|
182
182
|
this.pending.clear();
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
const STRICT_POSTGRES_SSL_MODE_ALIASES = new Set(["prefer", "require", "verify-ca"]);
|
|
186
|
+
export function normalizePostgresConnectionString(source) {
|
|
187
|
+
try {
|
|
188
|
+
const url = new URL(source);
|
|
189
|
+
const parameters = [...url.searchParams.entries()];
|
|
190
|
+
const libpqCompat = parameters.filter(([key]) => key === "uselibpqcompat").at(-1)?.[1];
|
|
191
|
+
const sslMode = parameters.filter(([key]) => key === "sslmode").at(-1)?.[1];
|
|
192
|
+
if (libpqCompat === "true" || !sslMode || !STRICT_POSTGRES_SSL_MODE_ALIASES.has(sslMode))
|
|
193
|
+
return source;
|
|
194
|
+
url.searchParams.delete("sslmode");
|
|
195
|
+
url.searchParams.append("sslmode", "verify-full");
|
|
196
|
+
return url.toString();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return source;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
185
202
|
class PostgresAdapter {
|
|
186
203
|
readOnly;
|
|
187
204
|
context;
|
|
@@ -194,7 +211,7 @@ class PostgresAdapter {
|
|
|
194
211
|
this.context = context;
|
|
195
212
|
const timeout = Math.min(2_147_483_647, remainingMilliseconds(context));
|
|
196
213
|
this.client = new Client({
|
|
197
|
-
connectionString: source,
|
|
214
|
+
connectionString: normalizePostgresConnectionString(source),
|
|
198
215
|
connectionTimeoutMillis: timeout,
|
|
199
216
|
statement_timeout: timeout,
|
|
200
217
|
});
|
package/dist/src/connection.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { ConnectionRecord } from "./store.js";
|
|
|
2
2
|
import type { Driver, StateConfidence } from "./types.js";
|
|
3
3
|
export declare function databaseIdentity(connection: ConnectionRecord): unknown;
|
|
4
4
|
export declare function detectDriver(target: string): Driver;
|
|
5
|
+
export declare function credentialSource(value: string, expectedDriver?: Driver): {
|
|
6
|
+
driver: Driver;
|
|
7
|
+
source: string;
|
|
8
|
+
};
|
|
5
9
|
export declare function normalizeSqliteSource(target: string): string;
|
|
6
10
|
export declare function databaseUrlHasSecret(target: string): boolean;
|
|
7
11
|
export declare function version(connection: ConnectionRecord): string;
|
package/dist/src/connection.js
CHANGED
|
@@ -18,8 +18,34 @@ export function detectDriver(target) {
|
|
|
18
18
|
}
|
|
19
19
|
return "sqlite";
|
|
20
20
|
}
|
|
21
|
+
export function credentialSource(value, expectedDriver) {
|
|
22
|
+
const explicitSqlite = /^sqlite:(?!\/\/)/i.test(value);
|
|
23
|
+
const driver = explicitSqlite ? "sqlite" : detectDriver(value);
|
|
24
|
+
if (driver === "sqlite" && (!explicitSqlite || value.length === 7)) {
|
|
25
|
+
throw new StateQLError("INVALID_COMMAND", "Secret environment variable must contain a complete PostgreSQL/MySQL URL or an explicit sqlite: source.", {
|
|
26
|
+
suggestedAction: "Store the full database URL, or prefix an SQLite path with sqlite:.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (driver !== "sqlite") {
|
|
30
|
+
try {
|
|
31
|
+
const url = new URL(value);
|
|
32
|
+
if (!url.hostname && !url.pathname.replaceAll("/", ""))
|
|
33
|
+
throw new Error();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new StateQLError("INVALID_COMMAND", "Secret environment variable must contain a valid database URL.");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (expectedDriver && driver !== expectedDriver) {
|
|
40
|
+
throw new StateQLError("INVALID_COMMAND", "Resolved credential driver does not match the selected database connection.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
driver,
|
|
44
|
+
source: driver === "sqlite" ? normalizeSqliteSource(value) : value,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
21
47
|
export function normalizeSqliteSource(target) {
|
|
22
|
-
const source = target.
|
|
48
|
+
const source = target.replace(/^sqlite:/i, "");
|
|
23
49
|
if (source === ":memory:") {
|
|
24
50
|
throw new StateQLError("INVALID_COMMAND", "SQLite :memory: databases cannot persist across StateQL commands.");
|
|
25
51
|
}
|
package/dist/src/stateql.js
CHANGED
|
@@ -2,7 +2,7 @@ import { writeFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { env } from "node:process";
|
|
4
4
|
import { AdapterExecutionError, AdapterWriteError, BatchWriteError, createAdapter, createAdapterContext, } from "./adapters.js";
|
|
5
|
-
import { confidence, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, normalizeSqliteSource, validateProfileName, version, } from "./connection.js";
|
|
5
|
+
import { confidence, credentialSource, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, normalizeSqliteSource, validateProfileName, version, } from "./connection.js";
|
|
6
6
|
import { asStateQLError, CredentialResolutionError, StateQLError, } from "./errors.js";
|
|
7
7
|
import { filterMaterializedRows, prepareFilterStatement, validateFilterParameters, } from "./filter.js";
|
|
8
8
|
import { operationData, paginationWarnings, profileData, rowsToCsv, sessionData, transactionData, } from "./response-data.js";
|
|
@@ -72,8 +72,10 @@ export class StateQL {
|
|
|
72
72
|
if (session.active_transaction_id) {
|
|
73
73
|
throw new StateQLError("TRANSACTION_FAILED", "Commit or roll back the active transaction before connecting again.");
|
|
74
74
|
}
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const sourceCount = [target, options.profile, options.secretEnv]
|
|
76
|
+
.filter((value) => value !== undefined).length;
|
|
77
|
+
if (sourceCount > 1) {
|
|
78
|
+
throw new StateQLError("INVALID_COMMAND", "Use exactly one connection target, profile, or secret environment variable.");
|
|
77
79
|
}
|
|
78
80
|
const implicitProfile = !options.profile && !options.secretEnv && target
|
|
79
81
|
? this.store.getProfile(target)
|
|
@@ -101,7 +103,10 @@ export class StateQL {
|
|
|
101
103
|
if (!secret) {
|
|
102
104
|
throw new StateQLError("INVALID_COMMAND", "Connection target is required.");
|
|
103
105
|
}
|
|
104
|
-
const
|
|
106
|
+
const resolvedSource = secretEnv
|
|
107
|
+
? credentialSource(secret)
|
|
108
|
+
: { driver: detectDriver(secret), source: secret };
|
|
109
|
+
const { driver } = resolvedSource;
|
|
105
110
|
if (driver !== "sqlite" &&
|
|
106
111
|
!secretEnv &&
|
|
107
112
|
databaseUrlHasSecret(secret)) {
|
|
@@ -109,7 +114,9 @@ export class StateQL {
|
|
|
109
114
|
suggestedAction: "Set the URL in an environment variable and reconnect with --env NAME.",
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
|
-
const adapterSource =
|
|
117
|
+
const adapterSource = secretEnv
|
|
118
|
+
? resolvedSource.source
|
|
119
|
+
: driver === "sqlite" ? normalizeSqliteSource(secret) : secret;
|
|
113
120
|
const source = driver === "sqlite"
|
|
114
121
|
? adapterSource
|
|
115
122
|
: secretEnv
|
|
@@ -1508,7 +1515,7 @@ export class StateQL {
|
|
|
1508
1515
|
async resolveConnectionSource(connection, session, operation, access, context) {
|
|
1509
1516
|
if (!connection.secret_env)
|
|
1510
1517
|
return connection.source;
|
|
1511
|
-
|
|
1518
|
+
const value = await this.resolveCredential(connection.secret_env, session, operation, access, context, {
|
|
1512
1519
|
connection: {
|
|
1513
1520
|
id: connection.id,
|
|
1514
1521
|
name: connection.name,
|
|
@@ -1517,6 +1524,7 @@ export class StateQL {
|
|
|
1517
1524
|
readOnly: Boolean(connection.read_only),
|
|
1518
1525
|
},
|
|
1519
1526
|
});
|
|
1527
|
+
return credentialSource(value, connection.driver).source;
|
|
1520
1528
|
}
|
|
1521
1529
|
async resolveCredential(reference, session, operation, access, context, details = {}) {
|
|
1522
1530
|
const resolver = this.credentialResolver;
|