@fadhilp/stateql 0.13.0 → 0.13.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 +53 -828
- package/docs/databases.md +225 -0
- package/docs/library.md +268 -0
- package/docs/usage.md +352 -0
- package/package.json +2 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Database support
|
|
2
|
+
|
|
3
|
+
[Home](../README.md) · [Usage](usage.md) · [Database support](databases.md) · [TypeScript library](library.md)
|
|
4
|
+
|
|
5
|
+
- [Driver notes](#driver-notes)
|
|
6
|
+
- [SQL statement support](#sql-statement-support)
|
|
7
|
+
- [Dialect upserts](#dialect-upserts)
|
|
8
|
+
- [PostgreSQL diagnostics and maintenance](#postgresql-diagnostics-and-maintenance)
|
|
9
|
+
- [SQLite and MySQL diagnostics and maintenance](#sqlite-and-mysql-diagnostics-and-maintenance)
|
|
10
|
+
- [Native MongoDB](#native-mongodb)
|
|
11
|
+
- [Native Redis](#native-redis)
|
|
12
|
+
|
|
13
|
+
Connect using the [usage guide](usage.md#connections-and-profiles). All writes
|
|
14
|
+
require a read-write connection; SQL/MongoDB approvals and retry rules are
|
|
15
|
+
covered under [write safety](usage.md#write-safety).
|
|
16
|
+
|
|
17
|
+
## Driver notes
|
|
18
|
+
|
|
19
|
+
- **SQLite:** use a filesystem path for direct connections or `sqlite:` for an
|
|
20
|
+
environment-backed path.
|
|
21
|
+
- **PostgreSQL:** StateQL preserves strict TLS verification by normalizing
|
|
22
|
+
`sslmode=prefer`, `require`, and `verify-ca` to `verify-full` before opening
|
|
23
|
+
the adapter. Use `sslmode=verify-full` explicitly for clarity. Setting
|
|
24
|
+
`uselibpqcompat=true` opts out and keeps libpq-compatible SSL semantics.
|
|
25
|
+
- **MySQL:** uses positional `?` parameters. MariaDB compatibility is not
|
|
26
|
+
currently claimed.
|
|
27
|
+
- **MongoDB:** supports `mongodb://` and `mongodb+srv://` URLs with an explicit
|
|
28
|
+
database path. SQL methods are rejected; use the native MongoDB methods below.
|
|
29
|
+
- **Redis:** supports `redis://` and TLS-backed `rediss://` URLs. SQL methods and
|
|
30
|
+
legacy `inspect` commands are unavailable; use native commands and catalog
|
|
31
|
+
inspection instead.
|
|
32
|
+
|
|
33
|
+
## SQL statement support
|
|
34
|
+
|
|
35
|
+
StateQL parses and classifies one `SELECT`, `INSERT`, `REPLACE`, `UPDATE`,
|
|
36
|
+
`DELETE`, `CREATE`, `ALTER`, `DROP`, or `TRUNCATE` statement through
|
|
37
|
+
`node-sql-parser`, subject to dialect support and safety validation. Reads use
|
|
38
|
+
`query`; mutations and DDL use durable `exec`/`plan`/`apply` operations. Raw
|
|
39
|
+
transaction-control SQL is unsupported: use
|
|
40
|
+
[staged transactions](usage.md#transactions), not `BEGIN` or `COMMIT` statements.
|
|
41
|
+
MongoDB and Redis use separate native command APIs.
|
|
42
|
+
|
|
43
|
+
Unrecognized or parser-unsupported forms remain blocked. The
|
|
44
|
+
[SQL command roadmap](../SQL_COMMAND_ROADMAP.md) describes future categories,
|
|
45
|
+
not permission to execute them.
|
|
46
|
+
|
|
47
|
+
## Dialect upserts
|
|
48
|
+
|
|
49
|
+
PostgreSQL `INSERT ... ON CONFLICT DO NOTHING|UPDATE` and MySQL `INSERT ... ON
|
|
50
|
+
DUPLICATE KEY UPDATE` are structurally validated and recorded with statement
|
|
51
|
+
type `upsert`. Finite `VALUES` and MySQL `INSERT ... SET` sources use normal
|
|
52
|
+
write policy. An update-upsert fed by `SELECT` requires `--allow-unbounded`
|
|
53
|
+
because its candidate row count is not statically bounded. Upserts support direct
|
|
54
|
+
`exec`, `plan`/`apply`, and staged transactions; hidden additional writes are rejected.
|
|
55
|
+
|
|
56
|
+
MySQL `INSERT IGNORE` and SQLite `INSERT OR IGNORE` remain non-overwriting
|
|
57
|
+
inserts. SQLite `INSERT OR REPLACE` retains destructive-operation approval,
|
|
58
|
+
while SQLite modern `ON CONFLICT ... DO UPDATE` and every `MERGE` form remain
|
|
59
|
+
blocked until the parser can expose their complete mutation structure.
|
|
60
|
+
|
|
61
|
+
## PostgreSQL diagnostics and maintenance
|
|
62
|
+
|
|
63
|
+
Run PostgreSQL plans through `query`:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
stql query "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM jobs WHERE id = 42"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Plain `EXPLAIN` may plan a structurally validated `SELECT`, `INSERT`, `UPDATE`,
|
|
70
|
+
or `DELETE`. Because `EXPLAIN ANALYZE` executes its inner statement, StateQL
|
|
71
|
+
accepts only a validated read-only `SELECT`; `SELECT INTO`, writing CTEs, and
|
|
72
|
+
mutations are rejected. Diagnostics execute inside PostgreSQL `BEGIN READ ONLY`
|
|
73
|
+
and are never reused from cache. `--cache require` therefore returns
|
|
74
|
+
`CACHE_MISS` without executing the diagnostic.
|
|
75
|
+
|
|
76
|
+
Legacy `EXPLAIN [ANALYZE] [VERBOSE] statement` is also supported. Option lists
|
|
77
|
+
allow PostgreSQL boolean diagnostic options plus `FORMAT` and `SERIALIZE`;
|
|
78
|
+
unknown, duplicate, or malformed options fail closed. PostgreSQL's read-only
|
|
79
|
+
transaction protects database writes, but cannot contain external effects from
|
|
80
|
+
user-defined or incorrectly labelled functions.
|
|
81
|
+
|
|
82
|
+
StateQL supports PostgreSQL 14–18. Top-level `VALUES` is a bounded read and
|
|
83
|
+
accepts normal PostgreSQL positional parameters. It is conservatively
|
|
84
|
+
non-cacheable because expressions may be volatile. The following narrow `SHOW`
|
|
85
|
+
allowlist is also available as non-cacheable diagnostics:
|
|
86
|
+
`server_version`, `server_version_num`, `transaction_read_only`,
|
|
87
|
+
`transaction_isolation`, and `default_transaction_isolation`. `SHOW ALL` and
|
|
88
|
+
other settings remain blocked. Syntax accepted by StateQL but introduced by a
|
|
89
|
+
newer PostgreSQL release may be rejected safely by an older server.
|
|
90
|
+
|
|
91
|
+
`VACUUM`, `ANALYZE`, `REINDEX`, and `CLUSTER` are PostgreSQL maintenance writes:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
stql exec "VACUUM (ANALYZE) public.jobs" --allow-destructive
|
|
95
|
+
stql plan "REINDEX TABLE public.jobs" --allow-destructive
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The PostgreSQL 14–18 grammar includes parenthesized `REINDEX CONCURRENTLY`,
|
|
99
|
+
PostgreSQL 16 `BUFFER_USAGE_LIMIT` for `VACUUM`/`ANALYZE`, optional
|
|
100
|
+
`DATABASE`/`SYSTEM` reindex names, and PostgreSQL 18 `ONLY table *` maintenance
|
|
101
|
+
targets. Memory sizes accept an integer number of kilobytes or a quoted
|
|
102
|
+
`B|kB|MB|GB|TB` value. Older servers may reject newer forms after dispatch, so
|
|
103
|
+
StateQL retains conservative unknown-outcome handling.
|
|
104
|
+
|
|
105
|
+
They require a read-write connection and `--allow-destructive`, reject StateQL
|
|
106
|
+
parameters and optimistic row-count preconditions, and run as individually
|
|
107
|
+
tracked autocommit operations. They cannot be staged in a StateQL transaction,
|
|
108
|
+
even where a PostgreSQL variant could run inside a database transaction. A
|
|
109
|
+
timeout, cancellation, or error after dispatch is reported as `OUTCOME_UNKNOWN`;
|
|
110
|
+
inspect database state before replaying it.
|
|
111
|
+
|
|
112
|
+
## SQLite and MySQL diagnostics and maintenance
|
|
113
|
+
|
|
114
|
+
SQLite supports `EXPLAIN QUERY PLAN` for structurally read-only `SELECT`
|
|
115
|
+
statements. MySQL supports `EXPLAIN SELECT` plus bare `SHOW TABLES`,
|
|
116
|
+
`SHOW COLUMNS FROM table`, and `SHOW INDEX|INDEXES FROM table`. Broader
|
|
117
|
+
`EXPLAIN`, `SHOW`, and write-bearing forms remain blocked.
|
|
118
|
+
|
|
119
|
+
MySQL executable comments (`/*! ... */`) are rejected throughout SQL. Because
|
|
120
|
+
StateQL does not assume a server `sql_mode`, quoting that could expose these
|
|
121
|
+
comments under `ANSI_QUOTES` or `NO_BACKSLASH_ESCAPES` is also rejected.
|
|
122
|
+
|
|
123
|
+
These diagnostics use `query`, work with read-only connections, preserve the
|
|
124
|
+
original statement instead of applying StateQL's limiting SQL wrapper, and are
|
|
125
|
+
never reused from cache. Materialized results still receive StateQL's row and
|
|
126
|
+
byte checks.
|
|
127
|
+
|
|
128
|
+
SQLite also supports bare `VACUUM`, plus `ANALYZE [target]` and
|
|
129
|
+
`REINDEX [target]` with at most one unqualified or double-quoted target:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
stql exec "ANALYZE jobs" --allow-destructive
|
|
133
|
+
stql exec "REINDEX jobs_created_at_idx" --allow-destructive
|
|
134
|
+
stql exec "VACUUM" --allow-destructive
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
These commands require a read-write connection, reject parameters, run as
|
|
138
|
+
individually tracked autocommit operations, and cannot be staged. A timeout,
|
|
139
|
+
cancellation, or error after dispatch is reported as `OUTCOME_UNKNOWN`.
|
|
140
|
+
`VACUUM INTO`, schema-qualified targets, paths, `ATTACH`, and arbitrary `PRAGMA`
|
|
141
|
+
remain blocked.
|
|
142
|
+
|
|
143
|
+
MySQL supports one optionally qualified bare or backtick-quoted target for
|
|
144
|
+
`ANALYZE TABLE`, `OPTIMIZE TABLE`, and `CHECK TABLE`:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
stql exec "ANALYZE TABLE jobs" --allow-destructive
|
|
148
|
+
stql plan "OPTIMIZE TABLE jobs" --allow-destructive
|
|
149
|
+
stql query "CHECK TABLE jobs"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`ANALYZE` and `OPTIMIZE` are durable autocommit writes requiring a read-write
|
|
153
|
+
connection and destructive approval; server-reported error rows become known
|
|
154
|
+
failed operations, while timeout or cancellation after dispatch remains
|
|
155
|
+
`OUTCOME_UNKNOWN`. `CHECK TABLE` is an unwrapped, non-cacheable autocommit read
|
|
156
|
+
that works on read-only connections and retains normal result limits. All three
|
|
157
|
+
reject StateQL parameters, options, multiple targets, and additional
|
|
158
|
+
statements, and none can run during a staged transaction.
|
|
159
|
+
## Native MongoDB
|
|
160
|
+
|
|
161
|
+
MongoDB commands use official Extended JSON (EJSON), so BSON values survive the
|
|
162
|
+
CLI boundary:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
stql mongo query '{"operation":"find","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}}}'
|
|
166
|
+
stql mongo exec '{"operation":"updateOne","collection":"users","filter":{"_id":{"$oid":"507f1f77bcf86cd799439011"}},"update":{"$set":{"seen_at":{"$date":"2026-01-01T00:00:00Z"}}}}'
|
|
167
|
+
stql mongo plan '{"operation":"deleteMany","collection":"users","filter":{"disabled":true}}' --allow-destructive
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The TypeScript equivalents are `mongoQuery(command)`, `mongoExec(command)`, and
|
|
171
|
+
`mongoPlan(command)`. Supported reads are `find` and `aggregate`; writes are
|
|
172
|
+
`insertOne`, `insertMany`, `updateOne`, `updateMany`, `replaceOne`, `deleteOne`,
|
|
173
|
+
and `deleteMany`. Result documents are JSON-safe, order-preserving EJSON: for example,
|
|
174
|
+
ObjectIds and dates appear as `{ "$oid": "..." }` and
|
|
175
|
+
`{ "$date": { "$numberLong": "..." } }`.
|
|
176
|
+
|
|
177
|
+
Empty update, replacement, or delete filters require `--allow-unbounded`;
|
|
178
|
+
deletes and replacements also require `--allow-destructive`. Mongo inspection
|
|
179
|
+
accepts `collections`, `collection`, `columns`, `indexes`, and `constraints`
|
|
180
|
+
(`schema` and `table` remain aliases shared with SQL drivers). MongoDB cache
|
|
181
|
+
confidence is TTL-based: external writes are not detected, so use
|
|
182
|
+
`--cache bypass` for a fresh read.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const result = await stateql.mongoQuery({
|
|
186
|
+
operation: "find",
|
|
187
|
+
collection: "users",
|
|
188
|
+
filter: { active: true },
|
|
189
|
+
options: { sort: { _id: 1 }, limit: 50 },
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Native Redis
|
|
194
|
+
|
|
195
|
+
Redis/Rediss URLs support URL database selection, password or ACL username,
|
|
196
|
+
and TLS (`rediss`). Supply credential-bearing URLs through `--env` or a trusted
|
|
197
|
+
full-URL credential reference. Library/batch callers may instead use a
|
|
198
|
+
[password reference](library.md#password-references) with a password-free target.
|
|
199
|
+
Native commands accept `{command: string, args?: string[]}`:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
stql redis query '{"command":"GET","args":["app:status"]}'
|
|
203
|
+
stql redis exec '{"command":"SET","args":["app:status","ready"]}' --idempotency-key status-ready
|
|
204
|
+
stql redis plan '{"command":"SET","args":["app:status","paused"]}'
|
|
205
|
+
stql objects key --search app --limit 50
|
|
206
|
+
stql object key app:status
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Writes and plans require a read-write connection. The TypeScript methods are:
|
|
210
|
+
|
|
211
|
+
- `redisQuery`: `GET`, `MGET`, `TYPE`, `EXISTS`, `TTL`, `PTTL`, `HGET`, `HMGET`,
|
|
212
|
+
bounded `LRANGE`, and bounded `SCAN`/`HSCAN`/`SSCAN`/`ZSCAN`.
|
|
213
|
+
- `redisExec` and `redisPlan`: one-key `SET`, `DEL`, `HSET`, `HDEL`, `LPUSH`,
|
|
214
|
+
`RPUSH`, `SADD`, `SREM`, `ZADD`, or `ZREM` mutation.
|
|
215
|
+
- `describeObject({kind:"key",name})`: bounded string/hash/list/set/zset value
|
|
216
|
+
inspection with TTL and continuation metadata where applicable.
|
|
217
|
+
|
|
218
|
+
Arguments are UTF-8 strings, at most 100 values/256 KiB; materialized replies are
|
|
219
|
+
at most 1 MiB. `KEYS`, scripts, modules, pub/sub, blocking commands, admin/flush,
|
|
220
|
+
and arbitrary commands are rejected. Key discovery always uses SCAN. A Redis
|
|
221
|
+
plan snapshots one bounded key and `apply` uses an isolated `WATCH` + one-command
|
|
222
|
+
`MULTI/EXEC`; a pre-apply content or expiry change returns `ROW_CONFLICT` and is
|
|
223
|
+
never retried automatically. Direct `redisExec` has Redis single-command
|
|
224
|
+
atomicity only. Redis has no SQL rollback or StateQL staged transaction support;
|
|
225
|
+
a lost write/EXEC reply is reported as `OUTCOME_UNKNOWN` and remains blocked.
|
package/docs/library.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# TypeScript library
|
|
2
|
+
|
|
3
|
+
[Home](../README.md) · [Usage](usage.md) · [Database support](databases.md) · [TypeScript library](library.md)
|
|
4
|
+
|
|
5
|
+
- [Getting started](#getting-started)
|
|
6
|
+
- [Responses and command context](#responses-and-command-context)
|
|
7
|
+
- [Actor workspaces](#actor-workspaces)
|
|
8
|
+
- [Credential resolution](#credential-resolution)
|
|
9
|
+
- [Safe profile updates](#safe-profile-updates)
|
|
10
|
+
|
|
11
|
+
## Getting started
|
|
12
|
+
|
|
13
|
+
Install with `npm install @fadhilp/stateql`. This example connects to an existing
|
|
14
|
+
SQLite database containing a `users` table, queries it, and filters the stored
|
|
15
|
+
result. `forActor()` resolves or creates a session; it does not establish a
|
|
16
|
+
database connection for a new session.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { StateQL } from "@fadhilp/stateql";
|
|
20
|
+
|
|
21
|
+
const stateql = StateQL.forActor({
|
|
22
|
+
home: "./.stql",
|
|
23
|
+
actor: "audit",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const connected = await stateql.connect("./app.sqlite", { readOnly: true });
|
|
28
|
+
if (!connected.ok) throw new Error(connected.error.message);
|
|
29
|
+
|
|
30
|
+
const result = await stateql.query(
|
|
31
|
+
"SELECT id, email FROM users WHERE status = ? ORDER BY id LIMIT 50",
|
|
32
|
+
{ params: ["active"], timeoutMs: 5_000 },
|
|
33
|
+
);
|
|
34
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
35
|
+
|
|
36
|
+
const filtered = await stateql.filter(result.data.result_id, "email LIKE ?", {
|
|
37
|
+
params: ["%@example.com"],
|
|
38
|
+
});
|
|
39
|
+
if (!filtered.ok) throw new Error(filtered.error.message);
|
|
40
|
+
|
|
41
|
+
console.log(filtered.data.preview);
|
|
42
|
+
} finally {
|
|
43
|
+
stateql.close();
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`close()` releases the client's local store; it does not disconnect or delete
|
|
48
|
+
the durable session. Await active commands before closing. Use `disconnect()`
|
|
49
|
+
only when you intend to remove the session's shared active connection.
|
|
50
|
+
|
|
51
|
+
Options include `timeoutMs`, `credentialTimeoutMs`, `maxResultBytes`,
|
|
52
|
+
`maxStateBytes`, `cacheTtlSeconds`, and `resultTtlSeconds`. Database calls also
|
|
53
|
+
accept `signal` for cancellation. See [limits](usage.md#result-lifetime-and-limits)
|
|
54
|
+
and [deadlines](usage.md#deadlines-and-cancellation) for defaults and behavior.
|
|
55
|
+
The same [write safety](usage.md#write-safety) and
|
|
56
|
+
[database restrictions](databases.md) apply to library calls.
|
|
57
|
+
|
|
58
|
+
## Responses and command context
|
|
59
|
+
|
|
60
|
+
Library responses retain the full response envelope regardless of the CLI
|
|
61
|
+
output mode. Check `response.ok` before reading `response.data`; failures expose
|
|
62
|
+
`response.error`. Queries return typed `ResultData` with `result_id`, `preview`,
|
|
63
|
+
and result metadata. Reuse `result_id` with methods such as `filter`, `rows`,
|
|
64
|
+
`count`, and `exportResult`; the CLI instead presents its primary ID as `handle`.
|
|
65
|
+
|
|
66
|
+
The following snippets assume an open client and an appropriate connection.
|
|
67
|
+
|
|
68
|
+
Hosts that dispatch batch-shaped commands can attach trusted metadata out of
|
|
69
|
+
band. `origin` is audit/source metadata only; it never changes actor membership,
|
|
70
|
+
workspace access, or write authorization.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
await stateql.executeCommand(
|
|
75
|
+
{ command: "query", sql: "SELECT id, email FROM users ORDER BY id LIMIT 50", cache: "bypass" },
|
|
76
|
+
{ origin: "user", signal: controller.signal },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const userHistory = await stateql.history(50, { origin: "user" });
|
|
80
|
+
await stateql.executeCommand(
|
|
81
|
+
{ command: "history", limit: 50, history_origin: "user" },
|
|
82
|
+
{ origin: "model" },
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Supported origins are `legacy`, `user`, `model`, `system`, and `api`. Existing
|
|
87
|
+
direct calls and `executeCommand(command)` calls are recorded as `legacy`.
|
|
88
|
+
`history_origin` is only a retrieval filter; putting an `origin` field in a
|
|
89
|
+
`BatchCommand` cannot attribute the command. `batch` accepts the same trusted
|
|
90
|
+
context as `options.executionContext` for all commands in that batch.
|
|
91
|
+
|
|
92
|
+
## Actor workspaces
|
|
93
|
+
|
|
94
|
+
`StateQL.forWorkspace(...)` is a trusted-host primitive that atomically creates
|
|
95
|
+
or reopens a durable workspace, attaches the requested actor, and returns a
|
|
96
|
+
client bound to that actor:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const stateql = StateQL.forWorkspace({
|
|
100
|
+
home: "./.stql",
|
|
101
|
+
workspace: "team-audit",
|
|
102
|
+
actor: "audit-worker-1",
|
|
103
|
+
credentialResolver,
|
|
104
|
+
signal,
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Repeated opens of the same actor and workspace are idempotent. An actor already
|
|
109
|
+
attached elsewhere fails with a `StateQLError` whose code is
|
|
110
|
+
`PERMISSION_DENIED`; StateQL never moves or merges it. All actor options,
|
|
111
|
+
including limits, credential resolution, cancellation, `home`, and `now`, are
|
|
112
|
+
preserved. The workspace name also reserves a same-named actor identity for
|
|
113
|
+
legacy compatibility, so workspace and actor identifiers must be globally
|
|
114
|
+
collision-free. The returned client is still bound only to `actor`, preserving
|
|
115
|
+
plan, transaction, operation, and history ownership.
|
|
116
|
+
|
|
117
|
+
`StateQL.forActor(...)` retains its existing behavior: it resolves the actor's
|
|
118
|
+
attached session directly from StateQL storage and creates a legacy-compatible
|
|
119
|
+
session named after the actor on first use. Use `new StateQL({ session, actor })`
|
|
120
|
+
when the session and membership are already known.
|
|
121
|
+
|
|
122
|
+
Membership management and `forWorkspace` are library-only host capabilities,
|
|
123
|
+
not batch or CLI commands. Existing member-authorized management remains
|
|
124
|
+
available through `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
|
|
125
|
+
`listActors(session)`, and `resolveActor(actorId)`. Integrations should ask for
|
|
126
|
+
user confirmation before changing membership or the shared connection; a host
|
|
127
|
+
calling `forWorkspace` is responsible for authorizing that workspace access.
|
|
128
|
+
|
|
129
|
+
## Credential resolution
|
|
130
|
+
|
|
131
|
+
Library integrations can resolve environment-variable names, opaque full-URL
|
|
132
|
+
credential references, or password-only references through a trusted approval
|
|
133
|
+
or secret-storage layer instead of mutating `process.env`.
|
|
134
|
+
|
|
135
|
+
Integrations pinned to an older published package should gate setup before
|
|
136
|
+
sending `password_ref`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
if ((StateQL.passwordReferenceVersion ?? 0) < 1) {
|
|
140
|
+
throw new Error("Installed StateQL does not support password references.");
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`passwordReferenceVersion = 1` guarantees the password-only resolver request,
|
|
145
|
+
validation, persistence, reconnect, and redaction contract documented below.
|
|
146
|
+
|
|
147
|
+
In this adapter example, `credentialBroker` is supplied by the host application;
|
|
148
|
+
it is not a StateQL export.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import {
|
|
152
|
+
CredentialResolutionError,
|
|
153
|
+
StateQL,
|
|
154
|
+
type CredentialRequest,
|
|
155
|
+
} from "@fadhilp/stateql";
|
|
156
|
+
|
|
157
|
+
async function resolveCredential(
|
|
158
|
+
request: CredentialRequest,
|
|
159
|
+
): Promise<string | undefined> {
|
|
160
|
+
const approved = await credentialBroker.request({
|
|
161
|
+
reference: request.reference,
|
|
162
|
+
source: request.source ?? "secret_env",
|
|
163
|
+
actor: request.actorId,
|
|
164
|
+
session: request.session.id,
|
|
165
|
+
operation: request.operation,
|
|
166
|
+
access: request.access,
|
|
167
|
+
signal: request.signal,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (approved.denied) throw new CredentialResolutionError("denied");
|
|
171
|
+
return approved.value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const stateql = StateQL.forActor({
|
|
175
|
+
actor: "agent-session-id",
|
|
176
|
+
credentialResolver: resolveCredential,
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Credential resolution has its own two-minute default deadline
|
|
181
|
+
(`credentialTimeoutMs`) and remains cancellable through `request.signal`.
|
|
182
|
+
The database-operation timeout begins after a credential is resolved.
|
|
183
|
+
|
|
184
|
+
When no custom resolver is configured, StateQL reads only `secret_env`
|
|
185
|
+
references from `process.env`; `credential_ref` and `password_ref` never fall
|
|
186
|
+
back to the environment. A configured resolver is authoritative for all
|
|
187
|
+
sources: returning `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls
|
|
188
|
+
back to the process environment. Resolver requests retain `reference` and
|
|
189
|
+
include `source` (`secret_env`, `credential_ref`, or `password_ref`); source may
|
|
190
|
+
be omitted only on legacy secret-environment request objects. A `password_ref`
|
|
191
|
+
request additionally includes the exact password-free effective `target`.
|
|
192
|
+
Resolvers may throw `CredentialResolutionError` with `denied`, `cancelled`,
|
|
193
|
+
`timeout`, or `unavailable` to produce controlled, secret-free failures. Unknown
|
|
194
|
+
resolver errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED`
|
|
195
|
+
response.
|
|
196
|
+
|
|
197
|
+
StateQL calls the resolver only immediately before database access, after SQL
|
|
198
|
+
safety and duplicate checks. Requests contain actor and session identity, the
|
|
199
|
+
operation's effective read/write access, an abort signal, and sanitized
|
|
200
|
+
connection metadata.
|
|
201
|
+
|
|
202
|
+
For `secret_env` and `credential_ref`, returned values must be complete
|
|
203
|
+
PostgreSQL, MySQL, MongoDB, or Redis URLs, or explicit `sqlite:` sources. For
|
|
204
|
+
`password_ref`, the resolver returns only the password; an explicit empty string
|
|
205
|
+
is a resolved password, while `undefined` fails closed. StateQL percent-encodes
|
|
206
|
+
and injects only that password into the original target for adapter use, leaving
|
|
207
|
+
all nonsecret URL/TLS/CA bytes unchanged. It persists only the original target
|
|
208
|
+
and reference. Resolved credentials never enter connection metadata, history,
|
|
209
|
+
snapshots, cache keys, responses, or stored errors.
|
|
210
|
+
|
|
211
|
+
Hosts remain responsible for approval policy, binding lifetime, revocation,
|
|
212
|
+
and keeping values out of their own logs and model-visible data.
|
|
213
|
+
|
|
214
|
+
For writes, credential resolution happens after StateQL atomically reserves the
|
|
215
|
+
operation for duplicate protection. A resolution failure keeps a non-executed
|
|
216
|
+
`failed` audit record, does not consume the idempotency key, and permits a safe
|
|
217
|
+
retry.
|
|
218
|
+
|
|
219
|
+
### Password references
|
|
220
|
+
|
|
221
|
+
Library callers can keep nonsecret endpoint, username, database, TLS, and CA
|
|
222
|
+
options in the literal URL while resolving only its password:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
await stateql.connect(
|
|
226
|
+
"postgres://app@db.example/app?sslmode=verify-full&sslrootcert=/etc/app-ca.pem",
|
|
227
|
+
{ passwordRef: "vault://database/app/password", readOnly: true },
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The same field is accepted by `addProfile`, `updateProfile`, and batch
|
|
232
|
+
`connect`/`profile.add`/`profile.update` commands (snake case in batch input).
|
|
233
|
+
Targets with an embedded password or query parameters that override endpoint or
|
|
234
|
+
credential fields are rejected before credential resolution or driver access.
|
|
235
|
+
|
|
236
|
+
## Safe profile updates
|
|
237
|
+
|
|
238
|
+
`updateProfile(name, options)` accepts optional `target`, `secretEnv`,
|
|
239
|
+
`credentialRef`, `passwordRef`, and `readOnly` fields. The four string fields
|
|
240
|
+
also accept `null` for explicit clearing.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const updated = await stateql.updateProfile("production", { readOnly: true });
|
|
244
|
+
if (!updated.ok) throw new Error(updated.error.message);
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Omitting all source and password-reference fields keeps the existing source and
|
|
248
|
+
adjunct reference. Supplying any source field replaces the source atomically:
|
|
249
|
+
exactly one non-null source is required and the other source columns are
|
|
250
|
+
cleared. An omitted `passwordRef` is preserved when the existing literal target
|
|
251
|
+
is unchanged; changing the target clears it unless the update explicitly supplies
|
|
252
|
+
a replacement. Replacing the source with `secretEnv` or `credentialRef` clears
|
|
253
|
+
it. An explicit non-null password reference combined with either
|
|
254
|
+
reference-backed source is rejected. Direct URLs
|
|
255
|
+
with embedded passwords or secret-like query parameters are rejected.
|
|
256
|
+
`profile.list/show/update` return only
|
|
257
|
+
`{profile,target,secret_env,credential_ref,password_ref,read_only}`. Profile
|
|
258
|
+
changes affect subsequent `connect` calls and do not silently mutate an
|
|
259
|
+
already-open connection.
|
|
260
|
+
|
|
261
|
+
The `password_refs_v1` migration adds nullable `password_ref` columns to both
|
|
262
|
+
profiles and connections and enforces that they accompany only literal target
|
|
263
|
+
configuration. It rejects incompatible profile schemas/rows instead of dropping
|
|
264
|
+
references. Downgrading a state home containing password references is
|
|
265
|
+
unsupported: older binaries do not resolve this source and may attempt the
|
|
266
|
+
password-free target using ambient/trust authentication; constraint-protected
|
|
267
|
+
source replacements may also fail. Use the same or newer StateQL binary, or
|
|
268
|
+
explicitly clear all password references before downgrade.
|