@fadhilp/stateql 0.4.0 → 0.4.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/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,20 @@ 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.
77
83
  MySQL uses positional `?` parameters. MariaDB compatibility is not currently
78
84
  claimed.
79
85
 
80
86
  ## Commands
81
87
 
82
88
  ```text
83
- stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--env ENV] [--read-write]
89
+ stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
90
+ stql connect --env ENV [--name NAME] [--read-write]
84
91
  stql connect --profile NAME
85
92
  stql status
86
93
  stql profile add|list|show|remove
@@ -301,8 +308,10 @@ secret-free failures. Unknown resolver errors are replaced with a generic
301
308
  StateQL calls the resolver only immediately before database access, after SQL
302
309
  safety and duplicate checks. Requests contain actor/session identity, the
303
310
  operation's effective read/write access, an abort signal, and sanitized
304
- connection metadata. Returned values are passed directly to the adapter.
305
- Credential-bearing PostgreSQL and MySQL URLs are redacted before connection
311
+ connection metadata. Returned values must be complete PostgreSQL/MySQL URLs or
312
+ explicit `sqlite:` sources. StateQL validates the source and its stored driver
313
+ before adapter construction, and normalizes SQLite paths. Credential-bearing
314
+ PostgreSQL and MySQL URLs are redacted before connection
306
315
  metadata is persisted and never enter history, snapshots, cache keys, or
307
316
  responses. SQLite paths remain persisted connection metadata, as they are for
308
317
  direct SQLite connections. Harnesses remain responsible for approval policy,
@@ -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;
@@ -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.startsWith("sqlite:") ? target.slice(7) : 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
  }
@@ -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
- if (options.profile && target) {
76
- throw new StateQLError("INVALID_COMMAND", "Use either a profile or a connection target, not both.");
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 driver = detectDriver(secret);
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 = driver === "sqlite" ? normalizeSqliteSource(secret) : secret;
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
- return this.resolveCredential(connection.secret_env, session, operation, access, context, {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",