@fadhilp/stateql 0.4.1 → 0.4.4
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 +228 -123
- package/dist/src/adapters.d.ts +1 -0
- package/dist/src/adapters.js +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# StateQL
|
|
2
2
|
|
|
3
|
-
StateQL is a stateful database CLI for AI agents and
|
|
4
|
-
safe interface for querying, changing, and inspecting
|
|
5
|
-
|
|
3
|
+
StateQL is a stateful database CLI and TypeScript library for AI agents and
|
|
4
|
+
automation. It provides a safe interface for querying, changing, and inspecting
|
|
5
|
+
SQLite, PostgreSQL, and MySQL databases while keeping results reusable and
|
|
6
|
+
operations traceable across commands.
|
|
7
|
+
|
|
8
|
+
StateQL is built around durable handles:
|
|
9
|
+
|
|
10
|
+
1. Run a query and receive a result handle such as `q_1`.
|
|
11
|
+
2. Reuse, filter, page, count, alias, or export that stored result without
|
|
12
|
+
rerunning the original SQL.
|
|
13
|
+
3. Use operation, plan, and transaction handles to inspect and control writes.
|
|
6
14
|
|
|
7
15
|
Requires Node.js 22.5 or newer.
|
|
8
16
|
|
|
9
17
|
## Quick start
|
|
10
18
|
|
|
19
|
+
Install the CLI:
|
|
20
|
+
|
|
11
21
|
```bash
|
|
12
22
|
npm install -g @fadhilp/stateql
|
|
13
23
|
```
|
|
14
24
|
|
|
15
|
-
Connect to an existing SQLite database
|
|
16
|
-
query. Parameters keep values separate from SQL; `ORDER BY` makes paging
|
|
17
|
-
stable, while `LIMIT` bounds work at the database.
|
|
25
|
+
Connect to an existing SQLite database and run a bounded, parameterized query:
|
|
18
26
|
|
|
19
27
|
```bash
|
|
20
28
|
export STQL_SESSION=audit
|
|
@@ -27,14 +35,16 @@ stql query \
|
|
|
27
35
|
--param 2026-01-01
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
Parameters keep values separate from SQL. `ORDER BY` makes paging stable, and
|
|
39
|
+
`LIMIT` bounds work at the database. The default `agent` output is compact,
|
|
40
|
+
one-line JSON:
|
|
31
41
|
|
|
32
42
|
```json
|
|
33
43
|
{"ok":true,"handle":"q_1","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"},{"id":18,"name":"Linus","email":"linus@kernel.org"}],"truncated":false,"cached":false,"total":3,"next_offset":null}
|
|
34
44
|
```
|
|
35
45
|
|
|
36
|
-
`q_1` is a durable
|
|
37
|
-
|
|
46
|
+
`q_1` is a durable snapshot. Filter it locally without accessing the original
|
|
47
|
+
database:
|
|
38
48
|
|
|
39
49
|
```bash
|
|
40
50
|
stql filter q_1 "email LIKE ?" --param "%@example.com"
|
|
@@ -44,8 +54,8 @@ stql filter q_1 "email LIKE ?" --param "%@example.com"
|
|
|
44
54
|
{"ok":true,"handle":"q_2","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"}],"truncated":false,"cached":false,"total":2,"next_offset":null}
|
|
45
55
|
```
|
|
46
56
|
|
|
47
|
-
|
|
48
|
-
or export it
|
|
57
|
+
The filtered snapshot receives its own handle. Give it a readable alias, page
|
|
58
|
+
through it, inspect its count, or export it without rerunning SQL:
|
|
49
59
|
|
|
50
60
|
```bash
|
|
51
61
|
stql alias set example-users q_2
|
|
@@ -62,7 +72,22 @@ Example first page:
|
|
|
62
72
|
```
|
|
63
73
|
|
|
64
74
|
Running the same normalized query with the same parameters reuses `q_1` while
|
|
65
|
-
its cache is valid. Use `--cache bypass` when a fresh read is required.
|
|
75
|
+
its cache entry is valid. Use `--cache bypass` when a fresh read is required.
|
|
76
|
+
|
|
77
|
+
## Connections and profiles
|
|
78
|
+
|
|
79
|
+
A connection accepts exactly one source: a direct target, `--env`, or
|
|
80
|
+
`--profile`.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
|
|
84
|
+
stql connect --env ENV [--name NAME] [--read-write]
|
|
85
|
+
stql connect --profile NAME
|
|
86
|
+
stql disconnect
|
|
87
|
+
stql status
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Environment-backed credentials
|
|
66
91
|
|
|
67
92
|
PostgreSQL and MySQL credentials should come from environment variables. The
|
|
68
93
|
variable must contain the complete connection URL, not only its password.
|
|
@@ -79,16 +104,45 @@ export SQLITE_DATABASE='sqlite:./app.sqlite'
|
|
|
79
104
|
stql connect --env SQLITE_DATABASE --name local --read-only
|
|
80
105
|
```
|
|
81
106
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
StateQL stores no PostgreSQL or MySQL password. Credential-bearing URLs must be
|
|
108
|
+
supplied through `--env`. SQLite paths remain persisted as connection metadata.
|
|
109
|
+
|
|
110
|
+
### Local profiles
|
|
111
|
+
|
|
112
|
+
Profiles store connection targets, read-only policy, and environment-variable
|
|
113
|
+
names. Credential values are never stored. Profiles persist under `STQL_HOME`
|
|
114
|
+
with other StateQL metadata.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
stql profile add local ./app.sqlite --read-write
|
|
118
|
+
stql profile add production --env PROD_DATABASE_URL --read-only
|
|
119
|
+
stql profile list
|
|
120
|
+
stql profile show production
|
|
121
|
+
stql connect local
|
|
122
|
+
stql connect --profile production
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A bare connection target matching a profile name resolves to that profile;
|
|
126
|
+
otherwise it remains a path or database URL.
|
|
127
|
+
|
|
128
|
+
### Driver notes
|
|
85
129
|
|
|
86
|
-
|
|
130
|
+
- **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
|
|
131
|
+
environment-backed path.
|
|
132
|
+
- **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
|
|
133
|
+
`sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
|
|
134
|
+
the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
|
|
135
|
+
`uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
|
|
136
|
+
- **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
|
|
137
|
+
currently claimed.
|
|
138
|
+
|
|
139
|
+
## CLI reference
|
|
87
140
|
|
|
88
141
|
```text
|
|
89
142
|
stql connect <sqlite-path|postgres-url|mysql-url> [--name NAME] [--read-write]
|
|
90
143
|
stql connect --env ENV [--name NAME] [--read-write]
|
|
91
144
|
stql connect --profile NAME
|
|
145
|
+
stql disconnect
|
|
92
146
|
stql status
|
|
93
147
|
stql profile add|list|show|remove
|
|
94
148
|
stql session start|list|show|summary|close
|
|
@@ -98,6 +152,7 @@ stql exec <sql> [--params JSON | --param VALUE...] [--idempotency-key KEY] [--re
|
|
|
98
152
|
[--allow-unbounded] [--allow-destructive]
|
|
99
153
|
stql show|count|columns <result-handle>
|
|
100
154
|
stql rows <result-handle> [--offset N] [--limit N]
|
|
155
|
+
stql alias set <name> <result-handle>
|
|
101
156
|
stql export <result-handle> --output FILE [--format json|jsonl|csv]
|
|
102
157
|
stql inspect schema|table|columns|indexes|constraints [table]
|
|
103
158
|
stql transaction begin|status|commit|rollback [--isolation LEVEL]
|
|
@@ -110,56 +165,128 @@ stql batch [commands.json|commands.jsonl|-] [--continue-on-error]
|
|
|
110
165
|
stql pipe [--continue-on-error]
|
|
111
166
|
```
|
|
112
167
|
|
|
113
|
-
|
|
114
|
-
cancels active work. SQLite runs in a killable child process so long synchronous
|
|
115
|
-
statements cannot block StateQL's event loop. PostgreSQL uses server-side
|
|
116
|
-
`statement_timeout` plus client deadlines. MySQL deadlines destroy the active
|
|
117
|
-
connection. A timed-out write may return `OUTCOME_UNKNOWN` when commit status
|
|
118
|
-
cannot be proven.
|
|
168
|
+
### SQL parameters
|
|
119
169
|
|
|
120
|
-
|
|
170
|
+
For shell-safe positional parameters, repeat `--param`. JSON scalars become
|
|
171
|
+
their native types; other values remain strings.
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
173
|
+
```powershell
|
|
174
|
+
stql exec "INSERT INTO users (name, status) VALUES (?, ?)" `
|
|
175
|
+
--param Ada --param trial
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Use `--params JSON` for a JSON array or named parameters. Use
|
|
179
|
+
`--params-file FILE` when JSON is awkward to quote; `--params-file -` reads
|
|
180
|
+
JSON from standard input.
|
|
181
|
+
|
|
182
|
+
### Output modes
|
|
183
|
+
|
|
184
|
+
CLI output defaults to compact, one-line `agent` JSON. Successful responses
|
|
185
|
+
flatten useful data and expose the primary durable ID as `handle`. Errors retain
|
|
186
|
+
their complete error object. Empty warnings and tracing metadata are omitted.
|
|
125
187
|
|
|
126
188
|
```json
|
|
127
189
|
{"ok":false,"error":{"code":"UNBOUNDED_MUTATION","message":"Mutation has no WHERE clause.","retryable":false,"executed":false,"override_flag":"--allow-unbounded"}}
|
|
128
190
|
```
|
|
129
191
|
|
|
130
|
-
|
|
131
|
-
`--output jsonl` for that envelope on one line. `--output text` prints a short
|
|
132
|
-
human status; `--output silent` prints only a successful handle. Set
|
|
133
|
-
`STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
|
|
134
|
-
file, so use `STQL_OUTPUT` to choose its response mode. Library responses keep
|
|
135
|
-
the full envelope regardless of CLI mode.
|
|
192
|
+
Other modes are:
|
|
136
193
|
|
|
137
|
-
|
|
138
|
-
|
|
194
|
+
- `--output json`: original pretty, verbose envelope.
|
|
195
|
+
- `--output jsonl`: verbose envelope on one line.
|
|
196
|
+
- `--output text`: short human-readable status.
|
|
197
|
+
- `--output silent`: only a successful handle.
|
|
139
198
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
199
|
+
Set `STQL_OUTPUT` to choose a mode globally. For `export`, `--output` names the
|
|
200
|
+
file, so use `STQL_OUTPUT` to choose the command's response mode. Library
|
|
201
|
+
responses always keep the full envelope.
|
|
202
|
+
|
|
203
|
+
### Deadlines and cancellation
|
|
204
|
+
|
|
205
|
+
Database commands accept `--timeout-ms N`; the default is 30,000 ms. `Ctrl+C`
|
|
206
|
+
cancels active work.
|
|
207
|
+
|
|
208
|
+
- SQLite runs in a killable child process so long synchronous statements cannot
|
|
209
|
+
block StateQL's event loop.
|
|
210
|
+
- PostgreSQL combines server-side `statement_timeout` with client deadlines.
|
|
211
|
+
- MySQL deadlines destroy the active connection.
|
|
212
|
+
|
|
213
|
+
A timed-out write may return `OUTCOME_UNKNOWN` when its commit status cannot be
|
|
214
|
+
proven.
|
|
215
|
+
|
|
216
|
+
## Durable state and result reuse
|
|
217
|
+
|
|
218
|
+
State metadata lives under `STQL_HOME`, or the platform data directory when
|
|
219
|
+
unset. StateQL keeps connections, sessions, handles, aliases, cache entries,
|
|
220
|
+
plans, transactions, history, and receipts available across CLI invocations.
|
|
221
|
+
|
|
222
|
+
### Sessions and actors
|
|
223
|
+
|
|
224
|
+
Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select an
|
|
225
|
+
attached actor. A session is a shared workspace: attached actors reuse its
|
|
226
|
+
connection, handles, aliases, cache, and state version. Plans and staged
|
|
227
|
+
transactions remain owned by the actor that created them.
|
|
228
|
+
|
|
229
|
+
Callers that omit `actor` keep the legacy behavior where the actor ID is the
|
|
230
|
+
session name.
|
|
144
231
|
|
|
145
|
-
|
|
146
|
-
quote. `--params-file -` reads JSON from standard input.
|
|
232
|
+
### Result lifetime and limits
|
|
147
233
|
|
|
148
|
-
|
|
234
|
+
SQLite result rows are materialized locally for durable access. Read cache
|
|
235
|
+
entries expire after five minutes, and materialized handles expire after 24
|
|
236
|
+
hours. Expired results and plans are deleted the next time StateQL opens.
|
|
149
237
|
|
|
150
|
-
|
|
238
|
+
Queries exceeding 10,000 rows or 16 MiB of serialized row data fail before
|
|
239
|
+
persistence. Narrow the `WHERE` clause, add `LIMIT`, or select fewer columns.
|
|
240
|
+
These caps bound persisted materialization; the independent deadline bounds
|
|
241
|
+
execution time.
|
|
242
|
+
|
|
243
|
+
Command history keeps the latest 10,000 entries per session. SQLite cache reuse
|
|
244
|
+
also checks the database file signature. PostgreSQL and MySQL cache reuse is
|
|
245
|
+
labeled `ttl_based` and is never authoritative.
|
|
246
|
+
|
|
247
|
+
### Local filtering
|
|
248
|
+
|
|
249
|
+
`filter` evaluates one scalar SQLite predicate against a stored result. It
|
|
250
|
+
preserves source order, state metadata, and expiry, and never accesses the
|
|
251
|
+
original database.
|
|
252
|
+
|
|
253
|
+
Use parameters for values. Subqueries, query-shaping clauses, and
|
|
254
|
+
non-allowlisted functions are rejected. Common deterministic functions such as
|
|
255
|
+
`lower`, `upper`, `length`, and `coalesce` are supported.
|
|
256
|
+
|
|
257
|
+
## Write safety
|
|
258
|
+
|
|
259
|
+
Destructive and unbounded operations require `--allow-destructive` and
|
|
260
|
+
`--allow-unbounded`, respectively. The flags are independent.
|
|
261
|
+
|
|
262
|
+
`plan` validates and stores a write for later application. A plan persists only
|
|
263
|
+
the flags explicitly supplied when it is created; `apply` never adds
|
|
264
|
+
authorization.
|
|
265
|
+
|
|
266
|
+
Use an idempotency key to protect retryable writes from duplicate execution:
|
|
151
267
|
|
|
152
268
|
```bash
|
|
153
|
-
stql
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
stql connect local
|
|
157
|
-
stql connect --profile production
|
|
269
|
+
stql exec "UPDATE jobs SET claimed = 1 WHERE id = ?" \
|
|
270
|
+
--param 42 \
|
|
271
|
+
--idempotency-key claim-job-42
|
|
158
272
|
```
|
|
159
273
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
274
|
+
If a write starts but StateQL cannot safely record its final outcome, it returns
|
|
275
|
+
`OUTCOME_UNKNOWN` and blocks automatic replay. Inspect database state before
|
|
276
|
+
using `--replay`. Interrupted commits remain fail-closed; stale `committing`
|
|
277
|
+
records become `outcome_unknown` after five minutes.
|
|
278
|
+
|
|
279
|
+
### Transactions
|
|
280
|
+
|
|
281
|
+
Transactions are staged in local state so they survive CLI invocations, then
|
|
282
|
+
executed atomically on commit. While a transaction is active, StateQL rejects
|
|
283
|
+
database reads, plans, connection changes, and disconnects. Commit or roll back
|
|
284
|
+
first.
|
|
285
|
+
|
|
286
|
+
SQLite supports `serializable`. PostgreSQL and MySQL also support
|
|
287
|
+
`repeatable read`, `read committed`, and `read uncommitted`. Server reads run
|
|
288
|
+
inside database-enforced read-only transactions. MySQL staged transactions
|
|
289
|
+
reject DDL because MySQL implicitly commits those statements.
|
|
163
290
|
|
|
164
291
|
## Batch and pipes
|
|
165
292
|
|
|
@@ -168,6 +295,8 @@ policy, and environment-variable names. Credential values are never stored.
|
|
|
168
295
|
the first error unless `--continue-on-error` is set. Output defaults to one
|
|
169
296
|
compact `agent` JSON object per line.
|
|
170
297
|
|
|
298
|
+
Pipe commands directly:
|
|
299
|
+
|
|
171
300
|
```bash
|
|
172
301
|
printf '%s\n' \
|
|
173
302
|
'{"command":"query","sql":"SELECT id, email FROM users ORDER BY id","as":"users"}' \
|
|
@@ -176,6 +305,8 @@ printf '%s\n' \
|
|
|
176
305
|
stql pipe
|
|
177
306
|
```
|
|
178
307
|
|
|
308
|
+
Or save a JSON array as `commands.json`:
|
|
309
|
+
|
|
179
310
|
```json
|
|
180
311
|
[
|
|
181
312
|
{
|
|
@@ -192,55 +323,21 @@ printf '%s\n' \
|
|
|
192
323
|
]
|
|
193
324
|
```
|
|
194
325
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Batch filters use `where` for the predicate and may assign the derived result
|
|
199
|
-
with `as`. Database commands may set `timeout_ms`; otherwise they use the
|
|
200
|
-
30-second default.
|
|
201
|
-
|
|
202
|
-
State metadata lives under `STQL_HOME`, or the platform data directory when
|
|
203
|
-
unset. Set `STQL_SESSION` to select a named session and `STQL_ACTOR` to select
|
|
204
|
-
an attached actor for CLI invocations. A session is a shared workspace:
|
|
205
|
-
attached actors reuse its connection, handles, aliases, cache, and
|
|
206
|
-
state version, while plans and staged transactions remain owned by their
|
|
207
|
-
creating actor. Callers that omit `actor` keep the legacy behavior where the
|
|
208
|
-
actor ID is the session name.
|
|
209
|
-
|
|
210
|
-
Read cache entries expire after five minutes; materialized handles expire after
|
|
211
|
-
24 hours. Expired results and plans are deleted when StateQL next opens. Queries
|
|
212
|
-
exceeding 10,000 rows or 16 MiB of serialized row data fail before persistence;
|
|
213
|
-
add a narrower `WHERE` clause, `LIMIT`, or smaller column selection. These caps
|
|
214
|
-
bound persisted materialization, while the independent deadline bounds execution
|
|
215
|
-
time. Command history keeps the latest 10,000 entries per session. SQLite cache reuse also checks
|
|
216
|
-
the database file signature; PostgreSQL and MySQL reuse is labeled `ttl_based`,
|
|
217
|
-
never authoritative. Transactions are staged in local state so they survive CLI
|
|
218
|
-
invocations, then executed atomically on commit. Database reads, plans,
|
|
219
|
-
connection changes, and disconnects are rejected while a transaction is active;
|
|
220
|
-
commit or roll back first. SQLite supports `serializable`;
|
|
221
|
-
PostgreSQL and MySQL also support `repeatable read`, `read committed`, and
|
|
222
|
-
`read uncommitted`. Server reads run inside database-enforced read-only
|
|
223
|
-
transactions. MySQL staged transactions reject DDL because MySQL implicitly
|
|
224
|
-
commits those statements.
|
|
326
|
+
```bash
|
|
327
|
+
stql batch commands.json
|
|
328
|
+
```
|
|
225
329
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
`
|
|
230
|
-
|
|
231
|
-
database. Use parameters for values. Subqueries, query-shaping clauses, and
|
|
232
|
-
non-allowlisted functions are rejected; common deterministic functions such as
|
|
233
|
-
`lower`, `upper`, `length`, and `coalesce` are supported.
|
|
330
|
+
Batch fields use snake case. Supported command names match CLI paths, such as
|
|
331
|
+
`filter`, `transaction.begin`, `session.summary`, `alias.set`, `plan`, and
|
|
332
|
+
`apply`. Batch filters use `where` for the predicate and may assign the derived
|
|
333
|
+
result with `as`. Database commands may set `timeout_ms`; otherwise they use the
|
|
334
|
+
30-second default.
|
|
234
335
|
|
|
235
|
-
|
|
236
|
-
independently. Plans persist only flags explicitly supplied when the plan is
|
|
237
|
-
created; `apply` never adds authorization. If a database write starts but its
|
|
238
|
-
final outcome cannot be recorded safely, StateQL returns `OUTCOME_UNKNOWN` and
|
|
239
|
-
blocks automatic replay. Inspect database state before using `--replay`.
|
|
240
|
-
Interrupted commits remain fail-closed; stale `committing` records become
|
|
241
|
-
`outcome_unknown` after five minutes.
|
|
336
|
+
## TypeScript library
|
|
242
337
|
|
|
243
|
-
|
|
338
|
+
The package exports the same stateful operations for programmatic use. Library
|
|
339
|
+
responses retain the full response envelope regardless of the configured CLI
|
|
340
|
+
output mode.
|
|
244
341
|
|
|
245
342
|
```ts
|
|
246
343
|
import { StateQL } from "@fadhilp/stateql";
|
|
@@ -251,11 +348,13 @@ const stateql = StateQL.forActor({
|
|
|
251
348
|
timeoutMs: 30_000,
|
|
252
349
|
maxResultBytes: 16 * 1024 * 1024,
|
|
253
350
|
});
|
|
351
|
+
|
|
254
352
|
const controller = new AbortController();
|
|
255
353
|
const response = await stateql.query("SELECT * FROM users", {
|
|
256
354
|
signal: controller.signal,
|
|
257
355
|
timeoutMs: 5_000,
|
|
258
356
|
});
|
|
357
|
+
|
|
259
358
|
if (response.ok) {
|
|
260
359
|
const handle = (response.data as { result_id: string }).result_id;
|
|
261
360
|
await stateql.filter(handle, "email LIKE ?", {
|
|
@@ -264,6 +363,19 @@ if (response.ok) {
|
|
|
264
363
|
}
|
|
265
364
|
```
|
|
266
365
|
|
|
366
|
+
### Actor workspaces
|
|
367
|
+
|
|
368
|
+
`StateQL.forActor(...)` resolves the actor's attached session directly from
|
|
369
|
+
StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
|
|
370
|
+
On first use, it creates a legacy-compatible session named after the actor. Use
|
|
371
|
+
`new StateQL({ session, actor })` when the session is already known.
|
|
372
|
+
|
|
373
|
+
Membership is managed only through the library API, not batch commands:
|
|
374
|
+
`linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
|
|
375
|
+
`listActors(session)`, and `resolveActor(actorId)`. An existing member must link
|
|
376
|
+
an actor before that actor opens an existing workspace. Integrations should ask
|
|
377
|
+
for user confirmation before changing membership or the shared connection.
|
|
378
|
+
|
|
267
379
|
### Harness credential resolution
|
|
268
380
|
|
|
269
381
|
Library integrations can resolve a profile's credential reference through a
|
|
@@ -287,6 +399,7 @@ async function resolveCredential(
|
|
|
287
399
|
access: request.access,
|
|
288
400
|
signal: request.signal,
|
|
289
401
|
});
|
|
402
|
+
|
|
290
403
|
if (approved.denied) throw new CredentialResolutionError("denied");
|
|
291
404
|
return approved.value;
|
|
292
405
|
}
|
|
@@ -297,39 +410,31 @@ const stateql = StateQL.forActor({
|
|
|
297
410
|
});
|
|
298
411
|
```
|
|
299
412
|
|
|
300
|
-
When no custom resolver is configured, StateQL
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
`
|
|
305
|
-
|
|
413
|
+
When no custom resolver is configured, StateQL reads references from
|
|
414
|
+
`process.env`. A configured resolver is authoritative: returning `undefined`
|
|
415
|
+
produces `CREDENTIAL_UNAVAILABLE` and never falls back to the process
|
|
416
|
+
environment. Resolvers may throw `CredentialResolutionError` with `denied`,
|
|
417
|
+
`cancelled`, `timeout`, or `unavailable` to produce controlled, secret-free
|
|
418
|
+
failures. Unknown resolver errors are replaced with a generic
|
|
306
419
|
`CREDENTIAL_RESOLUTION_FAILED` response.
|
|
307
420
|
|
|
308
421
|
StateQL calls the resolver only immediately before database access, after SQL
|
|
309
|
-
safety and duplicate checks. Requests contain actor
|
|
422
|
+
safety and duplicate checks. Requests contain actor and session identity, the
|
|
310
423
|
operation's effective read/write access, an abort signal, and sanitized
|
|
311
|
-
connection metadata.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
424
|
+
connection metadata.
|
|
425
|
+
|
|
426
|
+
Returned values must be complete PostgreSQL or MySQL URLs, or explicit
|
|
427
|
+
`sqlite:` sources. StateQL validates the source and its stored driver before
|
|
428
|
+
adapter construction and normalizes SQLite paths. Credential-bearing
|
|
429
|
+
PostgreSQL and MySQL URLs are redacted before connection metadata is persisted
|
|
430
|
+
and never enter history, snapshots, cache keys, or responses. SQLite paths
|
|
431
|
+
remain persisted connection metadata, as they are for direct SQLite
|
|
432
|
+
connections.
|
|
433
|
+
|
|
434
|
+
Harnesses remain responsible for approval policy, binding lifetime, revocation,
|
|
435
|
+
and keeping values out of their own logs and model-visible data.
|
|
320
436
|
|
|
321
437
|
For writes, credential resolution happens after StateQL atomically reserves the
|
|
322
438
|
operation for duplicate protection. A resolution failure keeps a non-executed
|
|
323
439
|
`failed` audit record, does not consume the idempotency key, and permits a safe
|
|
324
440
|
retry.
|
|
325
|
-
|
|
326
|
-
`StateQL.forActor(...)` resolves the actor's attached session directly from
|
|
327
|
-
StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
|
|
328
|
-
On first use, it creates a legacy-compatible session named after the actor.
|
|
329
|
-
Use `new StateQL({ session, actor })` when the session is already known.
|
|
330
|
-
|
|
331
|
-
Membership is managed only through the library API, not batch commands:
|
|
332
|
-
`linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
|
|
333
|
-
`listActors(session)`, and `resolveActor(actorId)`. An existing member must link
|
|
334
|
-
an actor before that actor opens an existing workspace. Integrations should ask
|
|
335
|
-
for user confirmation before changing membership or the shared connection.
|
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
|
});
|