@fadhilp/stateql 0.6.0 → 0.8.0
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 +54 -16
- package/dist/src/cli.js +8 -3
- package/dist/src/connection.d.ts +3 -2
- package/dist/src/connection.js +14 -3
- package/dist/src/index.d.ts +1 -1
- package/dist/src/migrations.js +87 -1
- package/dist/src/response-data.js +1 -0
- package/dist/src/stateql.d.ts +4 -3
- package/dist/src/stateql.js +276 -189
- package/dist/src/store.d.ts +8 -2
- package/dist/src/store.js +15 -13
- package/dist/src/types.d.ts +26 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,12 +76,13 @@ its cache entry is valid. Use `--cache bypass` when a fresh read is required.
|
|
|
76
76
|
|
|
77
77
|
## Connections and profiles
|
|
78
78
|
|
|
79
|
-
A connection accepts exactly one source: a direct target, `--env`,
|
|
80
|
-
`--profile`.
|
|
79
|
+
A connection accepts exactly one source: a direct target, `--env`,
|
|
80
|
+
`--credential-ref`, or `--profile`.
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
83
|
stql connect <sqlite-path|postgres-url|mysql-url|mongodb-url> [--name NAME] [--read-write]
|
|
84
84
|
stql connect --env ENV [--name NAME] [--read-write]
|
|
85
|
+
stql connect --credential-ref REF [--name NAME] [--read-write]
|
|
85
86
|
stql connect --profile NAME
|
|
86
87
|
stql disconnect
|
|
87
88
|
stql status
|
|
@@ -113,19 +114,26 @@ connection metadata.
|
|
|
113
114
|
|
|
114
115
|
### Local profiles
|
|
115
116
|
|
|
116
|
-
Profiles store connection
|
|
117
|
-
|
|
118
|
-
with other StateQL
|
|
117
|
+
Profiles store exactly one connection target, environment-variable name, or
|
|
118
|
+
opaque credential reference together with read-only policy. Credential values
|
|
119
|
+
are never stored. Profiles persist under `STQL_HOME` with other StateQL
|
|
120
|
+
metadata, and list/show responses include `credential_ref` when configured.
|
|
119
121
|
|
|
120
122
|
```bash
|
|
121
123
|
stql profile add local ./app.sqlite --read-write
|
|
122
124
|
stql profile add production --env PROD_DATABASE_URL --read-only
|
|
125
|
+
stql profile add hosted --credential-ref 'vault://team/app' --read-only
|
|
123
126
|
stql profile list
|
|
124
127
|
stql profile show production
|
|
125
128
|
stql connect local
|
|
126
129
|
stql connect --profile production
|
|
127
130
|
```
|
|
128
131
|
|
|
132
|
+
Credential references are bounded nonempty opaque strings; StateQL does not
|
|
133
|
+
apply environment-variable syntax or normalization to them. They can only be
|
|
134
|
+
resolved by a trusted host `CredentialResolver`, so the standalone CLI may
|
|
135
|
+
store them in profiles but cannot connect with them.
|
|
136
|
+
|
|
129
137
|
A bare connection target matching a profile name resolves to that profile;
|
|
130
138
|
otherwise it remains a path or database URL.
|
|
131
139
|
|
|
@@ -253,8 +261,9 @@ cancels active work.
|
|
|
253
261
|
- MySQL deadlines destroy the active connection.
|
|
254
262
|
- MongoDB uses driver deadlines and closes stopped operations.
|
|
255
263
|
|
|
256
|
-
A timed-out write may return `OUTCOME_UNKNOWN` when its commit
|
|
257
|
-
proven.
|
|
264
|
+
A timed-out or cancelled write may return `OUTCOME_UNKNOWN` when its commit
|
|
265
|
+
status cannot be proven. Cancellation stops that command's driver work; it does
|
|
266
|
+
not close the `StateQL` actor, and later commands remain usable.
|
|
258
267
|
|
|
259
268
|
## Durable state and result reuse
|
|
260
269
|
|
|
@@ -424,6 +433,30 @@ if (response.ok) {
|
|
|
424
433
|
}
|
|
425
434
|
```
|
|
426
435
|
|
|
436
|
+
Hosts that dispatch batch-shaped commands can attach trusted metadata out of
|
|
437
|
+
band. `origin` is audit/source metadata only; it never changes actor membership,
|
|
438
|
+
workspace access, or write authorization.
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
const controller = new AbortController();
|
|
442
|
+
await stateql.executeCommand(
|
|
443
|
+
{ command: "query", sql: "SELECT * FROM users", cache: "bypass" },
|
|
444
|
+
{ origin: "user", signal: controller.signal },
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const userHistory = await stateql.history(50, { origin: "user" });
|
|
448
|
+
await stateql.executeCommand(
|
|
449
|
+
{ command: "history", limit: 50, history_origin: "user" },
|
|
450
|
+
{ origin: "model" },
|
|
451
|
+
);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Supported origins are `legacy`, `user`, `model`, `system`, and `api`. Existing
|
|
455
|
+
direct calls and `executeCommand(command)` calls are recorded as `legacy`.
|
|
456
|
+
`history_origin` is only a retrieval filter; putting an `origin` field in a
|
|
457
|
+
`BatchCommand` cannot attribute the command. `batch` accepts the same trusted
|
|
458
|
+
context as `options.executionContext` for all commands in that batch.
|
|
459
|
+
|
|
427
460
|
### Actor workspaces
|
|
428
461
|
|
|
429
462
|
`StateQL.forActor(...)` resolves the actor's attached session directly from
|
|
@@ -439,8 +472,9 @@ for user confirmation before changing membership or the shared connection.
|
|
|
439
472
|
|
|
440
473
|
### Harness credential resolution
|
|
441
474
|
|
|
442
|
-
Library integrations can resolve
|
|
443
|
-
trusted approval or secret-storage layer
|
|
475
|
+
Library integrations can resolve environment-variable names or opaque
|
|
476
|
+
credential references through a trusted approval or secret-storage layer
|
|
477
|
+
instead of mutating `process.env`:
|
|
444
478
|
|
|
445
479
|
```ts
|
|
446
480
|
import {
|
|
@@ -454,6 +488,7 @@ async function resolveCredential(
|
|
|
454
488
|
): Promise<string | undefined> {
|
|
455
489
|
const approved = await credentialBroker.request({
|
|
456
490
|
reference: request.reference,
|
|
491
|
+
source: request.source ?? "secret_env",
|
|
457
492
|
actor: request.actorId,
|
|
458
493
|
session: request.session.id,
|
|
459
494
|
operation: request.operation,
|
|
@@ -471,13 +506,16 @@ const stateql = StateQL.forActor({
|
|
|
471
506
|
});
|
|
472
507
|
```
|
|
473
508
|
|
|
474
|
-
When no custom resolver is configured, StateQL reads
|
|
475
|
-
`process.env
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
509
|
+
When no custom resolver is configured, StateQL reads only `secret_env`
|
|
510
|
+
references from `process.env`; `credential_ref` never falls back to the
|
|
511
|
+
environment. A configured resolver is authoritative for both sources: returning
|
|
512
|
+
`undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls back to the
|
|
513
|
+
process environment. Resolver requests include `source` (`secret_env` or
|
|
514
|
+
`credential_ref`) while retaining `reference`; source may be omitted only on
|
|
515
|
+
legacy secret-environment request objects. Resolvers may throw
|
|
516
|
+
`CredentialResolutionError` with `denied`, `cancelled`, `timeout`, or
|
|
517
|
+
`unavailable` to produce controlled, secret-free failures. Unknown resolver
|
|
518
|
+
errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED` response.
|
|
481
519
|
|
|
482
520
|
StateQL calls the resolver only immediately before database access, after SQL
|
|
483
521
|
safety and duplicate checks. Requests contain actor and session identity, the
|
package/dist/src/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const parsed = parseArgs({
|
|
|
13
13
|
name: { type: "string" },
|
|
14
14
|
profile: { type: "string" },
|
|
15
15
|
env: { type: "string" },
|
|
16
|
+
"credential-ref": { type: "string" },
|
|
16
17
|
"read-only": { type: "boolean" },
|
|
17
18
|
"read-write": { type: "boolean" },
|
|
18
19
|
params: { type: "string" },
|
|
@@ -143,6 +144,7 @@ async function dispatch() {
|
|
|
143
144
|
return stateql.connect(subcommand, {
|
|
144
145
|
...(values.name ? { name: values.name } : {}),
|
|
145
146
|
...(values.env ? { secretEnv: values.env } : {}),
|
|
147
|
+
...(values["credential-ref"] ? { credentialRef: values["credential-ref"] } : {}),
|
|
146
148
|
...(values.profile ? { profile: values.profile } : {}),
|
|
147
149
|
...(values["read-only"]
|
|
148
150
|
? { readOnly: true }
|
|
@@ -282,6 +284,7 @@ async function dispatchProfile(action, args) {
|
|
|
282
284
|
}
|
|
283
285
|
return stateql.addProfile(requireValue(args[0], "profile name"), args[1], {
|
|
284
286
|
...(values.env ? { secretEnv: values.env } : {}),
|
|
287
|
+
...(values["credential-ref"] ? { credentialRef: values["credential-ref"] } : {}),
|
|
285
288
|
readOnly: !values["read-write"],
|
|
286
289
|
});
|
|
287
290
|
case "list":
|
|
@@ -576,9 +579,10 @@ function helpText() {
|
|
|
576
579
|
Usage: stql <command> [arguments] [options]
|
|
577
580
|
|
|
578
581
|
Commands:
|
|
579
|
-
connect
|
|
580
|
-
|
|
581
|
-
|
|
582
|
+
connect TARGET | --env ENV | --credential-ref REF | --profile NAME
|
|
583
|
+
disconnect, status
|
|
584
|
+
profile add NAME [TARGET | --env ENV | --credential-ref REF]
|
|
585
|
+
profile list|show|remove
|
|
582
586
|
query, filter, exec, show, rows, count, columns, export
|
|
583
587
|
mongo query|exec|plan '<EJSON command>'
|
|
584
588
|
alias set
|
|
@@ -590,6 +594,7 @@ Commands:
|
|
|
590
594
|
|
|
591
595
|
SQL parameters: --params JSON, repeated --param VALUE, or --params-file FILE.
|
|
592
596
|
Deadline: --timeout-ms N (default: 30000). Ctrl+C cancels database work.
|
|
597
|
+
Credential refs require a trusted host CredentialResolver; the standalone CLI cannot resolve them.
|
|
593
598
|
State: --max-state-bytes N, --cache-ttl-seconds N, --result-ttl-seconds N.
|
|
594
599
|
Output: --output agent|json|jsonl|text|silent (default: agent).
|
|
595
600
|
Batch/pipe accept JSON array files or JSONL streams. Stop on first error.`;
|
package/dist/src/connection.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ConnectionRecord } from "./store.js";
|
|
2
|
-
import type { Driver, StateConfidence } from "./types.js";
|
|
2
|
+
import type { CredentialSource, Driver, StateConfidence } from "./types.js";
|
|
3
3
|
export declare function databaseIdentity(connection: ConnectionRecord): unknown;
|
|
4
4
|
export declare function detectDriver(target: string): Driver;
|
|
5
5
|
export declare function mongoDatabaseName(target: string): string;
|
|
6
|
-
export declare function credentialSource(value: string, expectedDriver?: Driver): {
|
|
6
|
+
export declare function credentialSource(value: string, expectedDriver?: Driver, referenceSource?: CredentialSource): {
|
|
7
7
|
driver: Driver;
|
|
8
8
|
source: string;
|
|
9
9
|
};
|
|
@@ -13,3 +13,4 @@ export declare function version(connection: ConnectionRecord): string;
|
|
|
13
13
|
export declare function confidence(connection: ConnectionRecord): StateConfidence;
|
|
14
14
|
export declare function validateProfileName(name: string): void;
|
|
15
15
|
export declare function isEnvironmentName(name: string): boolean;
|
|
16
|
+
export declare function validateCredentialRef(reference: unknown): asserts reference is string;
|
package/dist/src/connection.js
CHANGED
|
@@ -6,6 +6,7 @@ export function databaseIdentity(connection) {
|
|
|
6
6
|
database: connection.database_name,
|
|
7
7
|
source: connection.source,
|
|
8
8
|
secretEnvironment: connection.secret_env,
|
|
9
|
+
credentialReference: connection.credential_ref,
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
export function detectDriver(target) {
|
|
@@ -43,11 +44,14 @@ export function mongoDatabaseName(target) {
|
|
|
43
44
|
}
|
|
44
45
|
throw new StateQLError("INVALID_COMMAND", "MongoDB URL must include an explicit database name.");
|
|
45
46
|
}
|
|
46
|
-
export function credentialSource(value, expectedDriver) {
|
|
47
|
+
export function credentialSource(value, expectedDriver, referenceSource = "secret_env") {
|
|
48
|
+
const sourceLabel = referenceSource === "credential_ref"
|
|
49
|
+
? "Credential reference"
|
|
50
|
+
: "Secret environment variable";
|
|
47
51
|
const explicitSqlite = /^sqlite:(?!\/\/)/i.test(value);
|
|
48
52
|
const driver = explicitSqlite ? "sqlite" : detectDriver(value);
|
|
49
53
|
if (driver === "sqlite" && (!explicitSqlite || value.length === 7)) {
|
|
50
|
-
throw new StateQLError("INVALID_COMMAND",
|
|
54
|
+
throw new StateQLError("INVALID_COMMAND", `${sourceLabel} must contain a complete PostgreSQL/MySQL URL or an explicit sqlite: source; MongoDB URLs are also supported.`, {
|
|
51
55
|
suggestedAction: "Store the full database URL, or prefix an SQLite path with sqlite:.",
|
|
52
56
|
});
|
|
53
57
|
}
|
|
@@ -58,7 +62,7 @@ export function credentialSource(value, expectedDriver) {
|
|
|
58
62
|
throw new Error();
|
|
59
63
|
}
|
|
60
64
|
catch {
|
|
61
|
-
throw new StateQLError("INVALID_COMMAND",
|
|
65
|
+
throw new StateQLError("INVALID_COMMAND", `${sourceLabel} must contain a valid database URL.`);
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
68
|
if (expectedDriver && driver !== expectedDriver) {
|
|
@@ -101,3 +105,10 @@ export function validateProfileName(name) {
|
|
|
101
105
|
export function isEnvironmentName(name) {
|
|
102
106
|
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
103
107
|
}
|
|
108
|
+
export function validateCredentialRef(reference) {
|
|
109
|
+
if (typeof reference === "string" &&
|
|
110
|
+
reference.trim().length > 0 &&
|
|
111
|
+
reference.length <= 1_024)
|
|
112
|
+
return;
|
|
113
|
+
throw new StateQLError("INVALID_COMMAND", "Credential reference must be a nonempty string of at most 1024 characters.");
|
|
114
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { StateQL } from "./stateql.js";
|
|
2
2
|
export { CredentialResolutionError, StateQLError, exitCodeFor, } from "./errors.js";
|
|
3
3
|
export type { CredentialResolutionFailure } from "./errors.js";
|
|
4
|
-
export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CapabilitiesData, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfilesData, PurgeData, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
|
|
4
|
+
export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CapabilitiesData, CommandExecutionContext, CommandOrigin, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialSource, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, HistoryOptions, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfilesData, PurgeData, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
|
package/dist/src/migrations.js
CHANGED
|
@@ -64,6 +64,22 @@ const MIGRATIONS = [
|
|
|
64
64
|
requireColumns(db, "operations", ["outcome_json"]);
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: "history_origin_v1",
|
|
69
|
+
apply(db) {
|
|
70
|
+
addColumn(db, "history", "origin", "TEXT NOT NULL DEFAULT 'legacy'");
|
|
71
|
+
db.exec("CREATE INDEX IF NOT EXISTS history_session_origin ON history(session_id, origin)");
|
|
72
|
+
},
|
|
73
|
+
validate(db) {
|
|
74
|
+
requireColumns(db, "history", ["origin"]);
|
|
75
|
+
requireIndexes(db, ["history_session_origin"]);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "credential_refs_v1",
|
|
80
|
+
apply: migrateCredentialRefs,
|
|
81
|
+
validate: validateCredentialRefs,
|
|
82
|
+
},
|
|
67
83
|
];
|
|
68
84
|
export function runMigrations(db, now) {
|
|
69
85
|
db.exec(`
|
|
@@ -119,10 +135,15 @@ function createInitialSchema(db) {
|
|
|
119
135
|
name TEXT PRIMARY KEY,
|
|
120
136
|
target TEXT,
|
|
121
137
|
secret_env TEXT,
|
|
138
|
+
credential_ref TEXT,
|
|
122
139
|
read_only INTEGER NOT NULL,
|
|
123
140
|
created_at TEXT NOT NULL,
|
|
124
141
|
updated_at TEXT NOT NULL,
|
|
125
|
-
CHECK(
|
|
142
|
+
CHECK (
|
|
143
|
+
(target IS NOT NULL) +
|
|
144
|
+
(secret_env IS NOT NULL) +
|
|
145
|
+
(credential_ref IS NOT NULL) = 1
|
|
146
|
+
)
|
|
126
147
|
);
|
|
127
148
|
CREATE TABLE IF NOT EXISTS connections (
|
|
128
149
|
id TEXT PRIMARY KEY,
|
|
@@ -132,6 +153,7 @@ function createInitialSchema(db) {
|
|
|
132
153
|
database_name TEXT NOT NULL,
|
|
133
154
|
source TEXT NOT NULL,
|
|
134
155
|
secret_env TEXT,
|
|
156
|
+
credential_ref TEXT,
|
|
135
157
|
read_only INTEGER NOT NULL,
|
|
136
158
|
version INTEGER NOT NULL,
|
|
137
159
|
created_at TEXT NOT NULL,
|
|
@@ -221,6 +243,7 @@ function createInitialSchema(db) {
|
|
|
221
243
|
session_id TEXT NOT NULL,
|
|
222
244
|
actor_id TEXT NOT NULL,
|
|
223
245
|
command TEXT NOT NULL,
|
|
246
|
+
origin TEXT NOT NULL DEFAULT 'legacy',
|
|
224
247
|
sql TEXT,
|
|
225
248
|
handle TEXT,
|
|
226
249
|
executed INTEGER NOT NULL,
|
|
@@ -289,6 +312,69 @@ function validateSharedSessionActors(db) {
|
|
|
289
312
|
throw new Error(`State migration left ${table}.${column} empty.`);
|
|
290
313
|
}
|
|
291
314
|
}
|
|
315
|
+
function migrateCredentialRefs(db) {
|
|
316
|
+
const profileColumns = new Set(db.prepare("PRAGMA table_info(profiles)").all().map((column) => column.name));
|
|
317
|
+
const hasCredentialRef = profileColumns.has("credential_ref");
|
|
318
|
+
if (!profileCredentialRefSchemaCurrent(db)) {
|
|
319
|
+
const missingSources = db.prepare(`SELECT COUNT(*) AS count FROM profiles
|
|
320
|
+
WHERE target IS NULL AND secret_env IS NULL${hasCredentialRef ? " AND credential_ref IS NULL" : ""}`).get();
|
|
321
|
+
if (missingSources.count) {
|
|
322
|
+
throw new Error("State migration found a profile without a connection source.");
|
|
323
|
+
}
|
|
324
|
+
const legacyCredentialRef = hasCredentialRef ? "credential_ref" : "NULL";
|
|
325
|
+
db.exec(`
|
|
326
|
+
ALTER TABLE profiles RENAME TO profiles_legacy_credential_refs_v1;
|
|
327
|
+
CREATE TABLE profiles (
|
|
328
|
+
name TEXT PRIMARY KEY,
|
|
329
|
+
target TEXT,
|
|
330
|
+
secret_env TEXT,
|
|
331
|
+
credential_ref TEXT,
|
|
332
|
+
read_only INTEGER NOT NULL,
|
|
333
|
+
created_at TEXT NOT NULL,
|
|
334
|
+
updated_at TEXT NOT NULL,
|
|
335
|
+
CHECK (
|
|
336
|
+
(target IS NOT NULL) +
|
|
337
|
+
(secret_env IS NOT NULL) +
|
|
338
|
+
(credential_ref IS NOT NULL) = 1
|
|
339
|
+
)
|
|
340
|
+
);
|
|
341
|
+
INSERT INTO profiles
|
|
342
|
+
(name, target, secret_env, credential_ref, read_only, created_at, updated_at)
|
|
343
|
+
SELECT
|
|
344
|
+
name,
|
|
345
|
+
CASE
|
|
346
|
+
WHEN ${legacyCredentialRef} IS NULL AND secret_env IS NULL THEN target
|
|
347
|
+
ELSE NULL
|
|
348
|
+
END,
|
|
349
|
+
CASE WHEN ${legacyCredentialRef} IS NULL THEN secret_env ELSE NULL END,
|
|
350
|
+
${legacyCredentialRef},
|
|
351
|
+
read_only,
|
|
352
|
+
created_at,
|
|
353
|
+
updated_at
|
|
354
|
+
FROM profiles_legacy_credential_refs_v1;
|
|
355
|
+
DROP TABLE profiles_legacy_credential_refs_v1;
|
|
356
|
+
`);
|
|
357
|
+
}
|
|
358
|
+
addColumn(db, "connections", "credential_ref", "TEXT");
|
|
359
|
+
}
|
|
360
|
+
function validateCredentialRefs(db) {
|
|
361
|
+
requireColumns(db, "profiles", ["credential_ref"]);
|
|
362
|
+
requireColumns(db, "connections", ["credential_ref"]);
|
|
363
|
+
if (!profileCredentialRefSchemaCurrent(db)) {
|
|
364
|
+
throw new Error("State migration did not enforce exactly one profile source.");
|
|
365
|
+
}
|
|
366
|
+
const invalid = db.prepare(`SELECT COUNT(*) AS count FROM profiles
|
|
367
|
+
WHERE (target IS NOT NULL) +
|
|
368
|
+
(secret_env IS NOT NULL) +
|
|
369
|
+
(credential_ref IS NOT NULL) != 1`).get();
|
|
370
|
+
if (invalid.count) {
|
|
371
|
+
throw new Error("State migration left a profile with ambiguous connection sources.");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function profileCredentialRefSchemaCurrent(db) {
|
|
375
|
+
const row = db.prepare("SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = 'profiles'").get();
|
|
376
|
+
return Boolean(row?.sql && /CHECK\s*\(\s*\(target IS NOT NULL\)\s*\+\s*\(secret_env IS NOT NULL\)\s*\+\s*\(credential_ref IS NOT NULL\)\s*=\s*1\s*\)/i.test(row.sql));
|
|
377
|
+
}
|
|
292
378
|
function addColumn(db, table, column, definition) {
|
|
293
379
|
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
294
380
|
if (!columns.some((candidate) => candidate.name === column)) {
|
package/dist/src/stateql.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchOptions, CapabilitiesData, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, StateQLActorOptions, StateQLOptions, StateQLSnapshot, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
|
|
1
|
+
import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, CommandExecutionContext, BatchOptions, CapabilitiesData, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, HistoryOptions, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, StateQLActorOptions, StateQLOptions, StateQLSnapshot, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
|
|
2
2
|
export declare class StateQL {
|
|
3
3
|
static forActor(options: StateQLActorOptions): StateQL;
|
|
4
4
|
private readonly store;
|
|
@@ -12,6 +12,7 @@ export declare class StateQL {
|
|
|
12
12
|
private readonly maxResultBytes;
|
|
13
13
|
private readonly timeoutMs;
|
|
14
14
|
private readonly signal?;
|
|
15
|
+
private readonly commandContexts;
|
|
15
16
|
private readonly credentialResolver?;
|
|
16
17
|
private readonly now;
|
|
17
18
|
private closed;
|
|
@@ -57,11 +58,11 @@ export declare class StateQL {
|
|
|
57
58
|
plan(sql: string, options?: PlanOptions): Promise<Response<PlanData>>;
|
|
58
59
|
mongoPlan(command: MongoWriteCommand, options?: MongoPlanOptions): Promise<Response<PlanData>>;
|
|
59
60
|
apply(planId: string, options?: ExecutionOptions): Promise<Response<ApplyData>>;
|
|
60
|
-
history(limit?: number): Promise<Response<HistoryData>>;
|
|
61
|
+
history(limit?: number, options?: HistoryOptions): Promise<Response<HistoryData>>;
|
|
61
62
|
doctor(): Promise<Response<DoctorData>>;
|
|
62
63
|
purge(scope?: "expired" | "results" | "history" | "all"): Promise<Response<PurgeData>>;
|
|
63
64
|
capabilities(): Promise<Response<CapabilitiesData>>;
|
|
64
|
-
executeCommand(command: BatchCommand): Promise<Response<unknown>>;
|
|
65
|
+
executeCommand(command: BatchCommand, context?: CommandExecutionContext): Promise<Response<unknown>>;
|
|
65
66
|
batch(commands: Iterable<BatchCommand> | AsyncIterable<BatchCommand>, options?: BatchOptions): AsyncGenerator<Response<unknown>>;
|
|
66
67
|
private performExec;
|
|
67
68
|
private performMongoExec;
|
package/dist/src/stateql.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { writeFileSync } from "node:fs";
|
|
2
3
|
import { basename, resolve } from "node:path";
|
|
3
4
|
import { env } from "node:process";
|
|
4
5
|
import { AdapterExecutionError, AdapterWriteError, BatchWriteError, createAdapter, createAdapterContext, } from "./adapters.js";
|
|
5
|
-
import { confidence, credentialSource, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, mongoDatabaseName, normalizeSqliteSource, validateProfileName, version, } from "./connection.js";
|
|
6
|
+
import { confidence, credentialSource, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, mongoDatabaseName, normalizeSqliteSource, validateCredentialRef, validateProfileName, version, } from "./connection.js";
|
|
6
7
|
import { asStateQLError, CredentialResolutionError, StateQLError, } from "./errors.js";
|
|
7
8
|
import { analyzeMongoWriteSafety, deserializeMongoWriteCommand, serializeMongoCommand, validateMongoReadCommand, validateMongoWriteCommand, MongoAdapter, } from "./mongodb.js";
|
|
8
9
|
import { filterMaterializedRows, prepareFilterStatement, validateFilterParameters, } from "./filter.js";
|
|
@@ -41,6 +42,7 @@ export class StateQL {
|
|
|
41
42
|
maxResultBytes;
|
|
42
43
|
timeoutMs;
|
|
43
44
|
signal;
|
|
45
|
+
commandContexts = new AsyncLocalStorage();
|
|
44
46
|
credentialResolver;
|
|
45
47
|
now;
|
|
46
48
|
closed = false;
|
|
@@ -88,12 +90,16 @@ export class StateQL {
|
|
|
88
90
|
if (session.active_transaction_id) {
|
|
89
91
|
throw new StateQLError("TRANSACTION_FAILED", "Commit or roll back the active transaction before connecting again.");
|
|
90
92
|
}
|
|
91
|
-
const sourceCount = [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
const sourceCount = [
|
|
94
|
+
target,
|
|
95
|
+
options.profile,
|
|
96
|
+
options.secretEnv,
|
|
97
|
+
options.credentialRef,
|
|
98
|
+
].filter((value) => value !== undefined).length;
|
|
99
|
+
if (sourceCount !== 1) {
|
|
100
|
+
throw new StateQLError("INVALID_COMMAND", "Use exactly one connection target, profile, secret environment variable, or credential reference.");
|
|
95
101
|
}
|
|
96
|
-
const implicitProfile = !options.profile && !options.secretEnv && target
|
|
102
|
+
const implicitProfile = !options.profile && !options.secretEnv && !options.credentialRef && target
|
|
97
103
|
? this.store.getProfile(target)
|
|
98
104
|
: undefined;
|
|
99
105
|
const profile = options.profile
|
|
@@ -102,16 +108,28 @@ export class StateQL {
|
|
|
102
108
|
if (options.profile && !profile) {
|
|
103
109
|
throw new StateQLError("CONNECTION_NOT_FOUND", `Profile "${options.profile}" was not found.`, { suggestedAction: "Run stql profile list." });
|
|
104
110
|
}
|
|
111
|
+
if (profile &&
|
|
112
|
+
[profile.target, profile.secret_env, profile.credential_ref]
|
|
113
|
+
.filter((value) => value !== null).length !== 1) {
|
|
114
|
+
throw new StateQLError("STATE_CORRUPTED", `Profile "${profile.name}" does not have exactly one connection source.`);
|
|
115
|
+
}
|
|
105
116
|
const resolvedTarget = profile?.target ?? target;
|
|
106
117
|
const secretEnv = options.secretEnv ?? profile?.secret_env ?? undefined;
|
|
107
|
-
|
|
118
|
+
const credentialRef = options.credentialRef ?? profile?.credential_ref ?? undefined;
|
|
119
|
+
if (secretEnv !== undefined && !isEnvironmentName(secretEnv)) {
|
|
108
120
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
109
121
|
}
|
|
122
|
+
if (credentialRef !== undefined)
|
|
123
|
+
validateCredentialRef(credentialRef);
|
|
124
|
+
const credentialReference = secretEnv ?? credentialRef;
|
|
125
|
+
const credentialReferenceSource = secretEnv !== undefined
|
|
126
|
+
? "secret_env"
|
|
127
|
+
: credentialRef !== undefined ? "credential_ref" : undefined;
|
|
110
128
|
const readOnly = options.readOnly ??
|
|
111
129
|
(profile ? Boolean(profile.read_only) : true);
|
|
112
130
|
const context = this.executionContext(options);
|
|
113
|
-
const secret =
|
|
114
|
-
? await this.resolveCredential(
|
|
131
|
+
const secret = credentialReference && credentialReferenceSource
|
|
132
|
+
? await this.resolveCredential(credentialReference, credentialReferenceSource, session, "connect", readOnly ? "read" : "write", context, {
|
|
115
133
|
...(profile ? { profile: { name: profile.name } } : {}),
|
|
116
134
|
requestedReadOnly: readOnly,
|
|
117
135
|
})
|
|
@@ -119,23 +137,23 @@ export class StateQL {
|
|
|
119
137
|
if (!secret) {
|
|
120
138
|
throw new StateQLError("INVALID_COMMAND", "Connection target is required.");
|
|
121
139
|
}
|
|
122
|
-
const resolvedSource =
|
|
123
|
-
? credentialSource(secret)
|
|
140
|
+
const resolvedSource = credentialReferenceSource
|
|
141
|
+
? credentialSource(secret, undefined, credentialReferenceSource)
|
|
124
142
|
: { driver: detectDriver(secret), source: secret };
|
|
125
143
|
const { driver } = resolvedSource;
|
|
126
144
|
if (driver !== "sqlite" &&
|
|
127
|
-
!
|
|
145
|
+
!credentialReference &&
|
|
128
146
|
databaseUrlHasSecret(secret)) {
|
|
129
|
-
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env.`, {
|
|
130
|
-
suggestedAction: "
|
|
147
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`, {
|
|
148
|
+
suggestedAction: "Use an environment variable or trusted host credential reference.",
|
|
131
149
|
});
|
|
132
150
|
}
|
|
133
|
-
const adapterSource =
|
|
151
|
+
const adapterSource = credentialReferenceSource
|
|
134
152
|
? resolvedSource.source
|
|
135
153
|
: driver === "sqlite" ? normalizeSqliteSource(secret) : secret;
|
|
136
154
|
const source = driver === "sqlite"
|
|
137
155
|
? adapterSource
|
|
138
|
-
:
|
|
156
|
+
: credentialReferenceSource
|
|
139
157
|
? redact(secret)
|
|
140
158
|
: adapterSource;
|
|
141
159
|
const databaseName = driver === "sqlite"
|
|
@@ -151,6 +169,7 @@ export class StateQL {
|
|
|
151
169
|
database_name: databaseName,
|
|
152
170
|
source,
|
|
153
171
|
secret_env: secretEnv ?? null,
|
|
172
|
+
credential_ref: credentialRef ?? null,
|
|
154
173
|
read_only: readOnly ? 1 : 0,
|
|
155
174
|
version: 0,
|
|
156
175
|
created_at: this.now().toISOString(),
|
|
@@ -178,6 +197,7 @@ export class StateQL {
|
|
|
178
197
|
databaseName,
|
|
179
198
|
source,
|
|
180
199
|
...(secretEnv ? { secretEnv } : {}),
|
|
200
|
+
...(credentialRef ? { credentialRef } : {}),
|
|
181
201
|
readOnly,
|
|
182
202
|
});
|
|
183
203
|
if (!connection) {
|
|
@@ -204,20 +224,25 @@ export class StateQL {
|
|
|
204
224
|
async addProfile(name, target, options = {}) {
|
|
205
225
|
return this.run("profile.add", async () => {
|
|
206
226
|
validateProfileName(name);
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
const sourceCount = [target, options.secretEnv, options.credentialRef]
|
|
228
|
+
.filter((value) => value !== undefined).length;
|
|
229
|
+
if (sourceCount !== 1 || target === "") {
|
|
230
|
+
throw new StateQLError("INVALID_COMMAND", "Profile requires exactly one target, secret environment variable, or credential reference.");
|
|
209
231
|
}
|
|
210
232
|
if (this.store.getProfile(name)) {
|
|
211
233
|
throw new StateQLError("INVALID_COMMAND", `Profile "${name}" already exists.`);
|
|
212
234
|
}
|
|
213
|
-
if (options.secretEnv && !isEnvironmentName(options.secretEnv)) {
|
|
235
|
+
if (options.secretEnv !== undefined && !isEnvironmentName(options.secretEnv)) {
|
|
214
236
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
215
237
|
}
|
|
238
|
+
if (options.credentialRef !== undefined) {
|
|
239
|
+
validateCredentialRef(options.credentialRef);
|
|
240
|
+
}
|
|
216
241
|
let storedTarget = target;
|
|
217
242
|
if (target) {
|
|
218
243
|
const driver = detectDriver(target);
|
|
219
244
|
if (driver !== "sqlite" && databaseUrlHasSecret(target)) {
|
|
220
|
-
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env.`);
|
|
245
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`);
|
|
221
246
|
}
|
|
222
247
|
if (driver === "sqlite")
|
|
223
248
|
storedTarget = normalizeSqliteSource(target);
|
|
@@ -226,6 +251,7 @@ export class StateQL {
|
|
|
226
251
|
name,
|
|
227
252
|
target: storedTarget,
|
|
228
253
|
secretEnv: options.secretEnv,
|
|
254
|
+
credentialRef: options.credentialRef,
|
|
229
255
|
readOnly: options.readOnly ?? true,
|
|
230
256
|
});
|
|
231
257
|
return {
|
|
@@ -1303,11 +1329,13 @@ export class StateQL {
|
|
|
1303
1329
|
}
|
|
1304
1330
|
}, () => historySql);
|
|
1305
1331
|
}
|
|
1306
|
-
async history(limit = 20) {
|
|
1332
|
+
async history(limit = 20, options = {}) {
|
|
1307
1333
|
return this.run("history", async (session) => ({
|
|
1308
1334
|
data: {
|
|
1309
1335
|
history: this.store
|
|
1310
|
-
.history(session.id, positiveInteger(limit, "limit")
|
|
1336
|
+
.history(session.id, positiveInteger(limit, "limit"), options.origin === undefined
|
|
1337
|
+
? undefined
|
|
1338
|
+
: parseCommandOrigin(options.origin))
|
|
1311
1339
|
.map(historyEntry),
|
|
1312
1340
|
},
|
|
1313
1341
|
}));
|
|
@@ -1364,169 +1392,182 @@ export class StateQL {
|
|
|
1364
1392
|
},
|
|
1365
1393
|
}));
|
|
1366
1394
|
}
|
|
1367
|
-
async executeCommand(command) {
|
|
1368
|
-
|
|
1369
|
-
return this.batchFailure("Batch command must be an object.");
|
|
1370
|
-
}
|
|
1395
|
+
async executeCommand(command, context = {}) {
|
|
1396
|
+
let activeContext;
|
|
1371
1397
|
try {
|
|
1372
|
-
|
|
1373
|
-
case "connect":
|
|
1374
|
-
return this.connect(command.target, {
|
|
1375
|
-
name: command.name,
|
|
1376
|
-
readOnly: command.read_only,
|
|
1377
|
-
secretEnv: command.secret_env,
|
|
1378
|
-
profile: command.profile,
|
|
1379
|
-
timeoutMs: command.timeout_ms,
|
|
1380
|
-
});
|
|
1381
|
-
case "disconnect":
|
|
1382
|
-
return this.disconnect();
|
|
1383
|
-
case "status":
|
|
1384
|
-
return this.status();
|
|
1385
|
-
case "profile.add":
|
|
1386
|
-
return this.addProfile(batchString(command.name, "name"), command.target, {
|
|
1387
|
-
readOnly: command.read_only ?? true,
|
|
1388
|
-
secretEnv: command.secret_env,
|
|
1389
|
-
});
|
|
1390
|
-
case "profile.list":
|
|
1391
|
-
return this.listProfiles();
|
|
1392
|
-
case "profile.show":
|
|
1393
|
-
return this.showProfile(batchString(command.name, "name"));
|
|
1394
|
-
case "profile.remove":
|
|
1395
|
-
return this.removeProfile(batchString(command.name, "name"));
|
|
1396
|
-
case "session.start":
|
|
1397
|
-
return this.startSession(batchString(command.name, "name"));
|
|
1398
|
-
case "session.list":
|
|
1399
|
-
return this.listSessions();
|
|
1400
|
-
case "session.show":
|
|
1401
|
-
return this.showSession(command.name);
|
|
1402
|
-
case "session.summary":
|
|
1403
|
-
return this.sessionSummary();
|
|
1404
|
-
case "session.close":
|
|
1405
|
-
return this.closeSession();
|
|
1406
|
-
case "query": {
|
|
1407
|
-
const response = await this.query(batchString(command.sql, "sql"), {
|
|
1408
|
-
params: command.params ?? [],
|
|
1409
|
-
cache: command.cache ?? "auto",
|
|
1410
|
-
timeoutMs: command.timeout_ms,
|
|
1411
|
-
});
|
|
1412
|
-
if (!response.ok || !command.as)
|
|
1413
|
-
return response;
|
|
1414
|
-
const resultId = response.data.result_id;
|
|
1415
|
-
if (typeof resultId !== "string")
|
|
1416
|
-
return response;
|
|
1417
|
-
this.store.setAlias(response.session_id, command.as, resultId);
|
|
1418
|
-
return {
|
|
1419
|
-
...response,
|
|
1420
|
-
data: { ...response.data, alias: command.as },
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
case "mongo.query": {
|
|
1424
|
-
const response = await this.mongoQuery(command.mongo, {
|
|
1425
|
-
cache: command.cache ?? "auto",
|
|
1426
|
-
timeoutMs: command.timeout_ms,
|
|
1427
|
-
});
|
|
1428
|
-
if (!response.ok || !command.as)
|
|
1429
|
-
return response;
|
|
1430
|
-
const resultId = response.data.result_id;
|
|
1431
|
-
if (typeof resultId !== "string")
|
|
1432
|
-
return response;
|
|
1433
|
-
this.store.setAlias(response.session_id, command.as, resultId);
|
|
1434
|
-
return {
|
|
1435
|
-
...response,
|
|
1436
|
-
data: { ...response.data, alias: command.as },
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
case "filter": {
|
|
1440
|
-
const response = await this.filter(batchString(command.handle, "handle"), batchString(command.where, "where"), { params: command.params ?? [] });
|
|
1441
|
-
if (!response.ok || !command.as)
|
|
1442
|
-
return response;
|
|
1443
|
-
const resultId = response.data.result_id;
|
|
1444
|
-
if (typeof resultId !== "string")
|
|
1445
|
-
return response;
|
|
1446
|
-
this.store.setAlias(response.session_id, command.as, resultId);
|
|
1447
|
-
return {
|
|
1448
|
-
...response,
|
|
1449
|
-
data: { ...response.data, alias: command.as },
|
|
1450
|
-
};
|
|
1451
|
-
}
|
|
1452
|
-
case "exec":
|
|
1453
|
-
return this.exec(batchString(command.sql, "sql"), {
|
|
1454
|
-
params: command.params ?? [],
|
|
1455
|
-
replay: command.replay ?? false,
|
|
1456
|
-
idempotencyKey: command.idempotency_key,
|
|
1457
|
-
allowUnbounded: command.allow_unbounded ?? false,
|
|
1458
|
-
allowDestructive: command.allow_destructive ?? false,
|
|
1459
|
-
timeoutMs: command.timeout_ms,
|
|
1460
|
-
});
|
|
1461
|
-
case "mongo.exec":
|
|
1462
|
-
return this.mongoExec(command.mongo, {
|
|
1463
|
-
replay: command.replay ?? false,
|
|
1464
|
-
idempotencyKey: command.idempotency_key,
|
|
1465
|
-
allowUnbounded: command.allow_unbounded ?? false,
|
|
1466
|
-
allowDestructive: command.allow_destructive ?? false,
|
|
1467
|
-
timeoutMs: command.timeout_ms,
|
|
1468
|
-
});
|
|
1469
|
-
case "show":
|
|
1470
|
-
return this.show(batchString(command.handle, "handle"));
|
|
1471
|
-
case "rows":
|
|
1472
|
-
return this.rows(batchString(command.handle, "handle"), {
|
|
1473
|
-
offset: command.offset ?? 0,
|
|
1474
|
-
limit: command.limit ?? 20,
|
|
1475
|
-
});
|
|
1476
|
-
case "count":
|
|
1477
|
-
return this.count(batchString(command.handle, "handle"));
|
|
1478
|
-
case "columns":
|
|
1479
|
-
return this.columns(batchString(command.handle, "handle"));
|
|
1480
|
-
case "alias.set":
|
|
1481
|
-
return this.setAlias(batchString(command.name, "name"), batchString(command.handle, "handle"));
|
|
1482
|
-
case "inspect":
|
|
1483
|
-
return this.inspect(batchString(command.kind, "kind"), command.table, {
|
|
1484
|
-
timeoutMs: command.timeout_ms,
|
|
1485
|
-
});
|
|
1486
|
-
case "transaction.begin":
|
|
1487
|
-
return this.beginTransaction(command.isolation);
|
|
1488
|
-
case "transaction.status":
|
|
1489
|
-
return this.transactionStatus(command.handle);
|
|
1490
|
-
case "transaction.commit":
|
|
1491
|
-
return this.commitTransaction(command.handle, {
|
|
1492
|
-
timeoutMs: command.timeout_ms,
|
|
1493
|
-
});
|
|
1494
|
-
case "transaction.rollback":
|
|
1495
|
-
return this.rollbackTransaction(command.handle);
|
|
1496
|
-
case "plan":
|
|
1497
|
-
return this.plan(batchString(command.sql, "sql"), {
|
|
1498
|
-
params: command.params ?? [],
|
|
1499
|
-
allowUnbounded: command.allow_unbounded ?? false,
|
|
1500
|
-
allowDestructive: command.allow_destructive,
|
|
1501
|
-
timeoutMs: command.timeout_ms,
|
|
1502
|
-
});
|
|
1503
|
-
case "mongo.plan":
|
|
1504
|
-
return this.mongoPlan(command.mongo, {
|
|
1505
|
-
allowUnbounded: command.allow_unbounded ?? false,
|
|
1506
|
-
allowDestructive: command.allow_destructive,
|
|
1507
|
-
timeoutMs: command.timeout_ms,
|
|
1508
|
-
});
|
|
1509
|
-
case "apply":
|
|
1510
|
-
return this.apply(batchString(command.handle, "handle"), {
|
|
1511
|
-
timeoutMs: command.timeout_ms,
|
|
1512
|
-
});
|
|
1513
|
-
case "history":
|
|
1514
|
-
return this.history(command.limit ?? 20);
|
|
1515
|
-
case "receipt":
|
|
1516
|
-
return this.receipt(batchString(command.handle, "handle"));
|
|
1517
|
-
case "doctor":
|
|
1518
|
-
return this.doctor();
|
|
1519
|
-
case "purge":
|
|
1520
|
-
return this.purge(command.scope ?? "expired");
|
|
1521
|
-
case "capabilities":
|
|
1522
|
-
return this.capabilities();
|
|
1523
|
-
default:
|
|
1524
|
-
return this.batchFailure(`Unknown batch command "${String(command.command)}".`);
|
|
1525
|
-
}
|
|
1398
|
+
activeContext = mergeCommandExecutionContext(this.commandContexts.getStore(), context);
|
|
1526
1399
|
}
|
|
1527
1400
|
catch (error) {
|
|
1528
1401
|
return this.batchFailure(errorMessage(error));
|
|
1529
1402
|
}
|
|
1403
|
+
return this.commandContexts.run(activeContext, async () => {
|
|
1404
|
+
if (!command || typeof command !== "object") {
|
|
1405
|
+
return this.batchFailure("Batch command must be an object.");
|
|
1406
|
+
}
|
|
1407
|
+
try {
|
|
1408
|
+
switch (command.command) {
|
|
1409
|
+
case "connect":
|
|
1410
|
+
return this.connect(command.target, {
|
|
1411
|
+
name: command.name,
|
|
1412
|
+
readOnly: command.read_only,
|
|
1413
|
+
secretEnv: command.secret_env,
|
|
1414
|
+
credentialRef: command.credential_ref,
|
|
1415
|
+
profile: command.profile,
|
|
1416
|
+
timeoutMs: command.timeout_ms,
|
|
1417
|
+
});
|
|
1418
|
+
case "disconnect":
|
|
1419
|
+
return this.disconnect();
|
|
1420
|
+
case "status":
|
|
1421
|
+
return this.status();
|
|
1422
|
+
case "profile.add":
|
|
1423
|
+
return this.addProfile(batchString(command.name, "name"), command.target, {
|
|
1424
|
+
readOnly: command.read_only ?? true,
|
|
1425
|
+
secretEnv: command.secret_env,
|
|
1426
|
+
credentialRef: command.credential_ref,
|
|
1427
|
+
});
|
|
1428
|
+
case "profile.list":
|
|
1429
|
+
return this.listProfiles();
|
|
1430
|
+
case "profile.show":
|
|
1431
|
+
return this.showProfile(batchString(command.name, "name"));
|
|
1432
|
+
case "profile.remove":
|
|
1433
|
+
return this.removeProfile(batchString(command.name, "name"));
|
|
1434
|
+
case "session.start":
|
|
1435
|
+
return this.startSession(batchString(command.name, "name"));
|
|
1436
|
+
case "session.list":
|
|
1437
|
+
return this.listSessions();
|
|
1438
|
+
case "session.show":
|
|
1439
|
+
return this.showSession(command.name);
|
|
1440
|
+
case "session.summary":
|
|
1441
|
+
return this.sessionSummary();
|
|
1442
|
+
case "session.close":
|
|
1443
|
+
return this.closeSession();
|
|
1444
|
+
case "query": {
|
|
1445
|
+
const response = await this.query(batchString(command.sql, "sql"), {
|
|
1446
|
+
params: command.params ?? [],
|
|
1447
|
+
cache: command.cache ?? "auto",
|
|
1448
|
+
timeoutMs: command.timeout_ms,
|
|
1449
|
+
});
|
|
1450
|
+
if (!response.ok || !command.as)
|
|
1451
|
+
return response;
|
|
1452
|
+
const resultId = response.data.result_id;
|
|
1453
|
+
if (typeof resultId !== "string")
|
|
1454
|
+
return response;
|
|
1455
|
+
this.store.setAlias(response.session_id, command.as, resultId);
|
|
1456
|
+
return {
|
|
1457
|
+
...response,
|
|
1458
|
+
data: { ...response.data, alias: command.as },
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
case "mongo.query": {
|
|
1462
|
+
const response = await this.mongoQuery(command.mongo, {
|
|
1463
|
+
cache: command.cache ?? "auto",
|
|
1464
|
+
timeoutMs: command.timeout_ms,
|
|
1465
|
+
});
|
|
1466
|
+
if (!response.ok || !command.as)
|
|
1467
|
+
return response;
|
|
1468
|
+
const resultId = response.data.result_id;
|
|
1469
|
+
if (typeof resultId !== "string")
|
|
1470
|
+
return response;
|
|
1471
|
+
this.store.setAlias(response.session_id, command.as, resultId);
|
|
1472
|
+
return {
|
|
1473
|
+
...response,
|
|
1474
|
+
data: { ...response.data, alias: command.as },
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
case "filter": {
|
|
1478
|
+
const response = await this.filter(batchString(command.handle, "handle"), batchString(command.where, "where"), { params: command.params ?? [] });
|
|
1479
|
+
if (!response.ok || !command.as)
|
|
1480
|
+
return response;
|
|
1481
|
+
const resultId = response.data.result_id;
|
|
1482
|
+
if (typeof resultId !== "string")
|
|
1483
|
+
return response;
|
|
1484
|
+
this.store.setAlias(response.session_id, command.as, resultId);
|
|
1485
|
+
return {
|
|
1486
|
+
...response,
|
|
1487
|
+
data: { ...response.data, alias: command.as },
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
case "exec":
|
|
1491
|
+
return this.exec(batchString(command.sql, "sql"), {
|
|
1492
|
+
params: command.params ?? [],
|
|
1493
|
+
replay: command.replay ?? false,
|
|
1494
|
+
idempotencyKey: command.idempotency_key,
|
|
1495
|
+
allowUnbounded: command.allow_unbounded ?? false,
|
|
1496
|
+
allowDestructive: command.allow_destructive ?? false,
|
|
1497
|
+
timeoutMs: command.timeout_ms,
|
|
1498
|
+
});
|
|
1499
|
+
case "mongo.exec":
|
|
1500
|
+
return this.mongoExec(command.mongo, {
|
|
1501
|
+
replay: command.replay ?? false,
|
|
1502
|
+
idempotencyKey: command.idempotency_key,
|
|
1503
|
+
allowUnbounded: command.allow_unbounded ?? false,
|
|
1504
|
+
allowDestructive: command.allow_destructive ?? false,
|
|
1505
|
+
timeoutMs: command.timeout_ms,
|
|
1506
|
+
});
|
|
1507
|
+
case "show":
|
|
1508
|
+
return this.show(batchString(command.handle, "handle"));
|
|
1509
|
+
case "rows":
|
|
1510
|
+
return this.rows(batchString(command.handle, "handle"), {
|
|
1511
|
+
offset: command.offset ?? 0,
|
|
1512
|
+
limit: command.limit ?? 20,
|
|
1513
|
+
});
|
|
1514
|
+
case "count":
|
|
1515
|
+
return this.count(batchString(command.handle, "handle"));
|
|
1516
|
+
case "columns":
|
|
1517
|
+
return this.columns(batchString(command.handle, "handle"));
|
|
1518
|
+
case "alias.set":
|
|
1519
|
+
return this.setAlias(batchString(command.name, "name"), batchString(command.handle, "handle"));
|
|
1520
|
+
case "inspect":
|
|
1521
|
+
return this.inspect(batchString(command.kind, "kind"), command.table, {
|
|
1522
|
+
timeoutMs: command.timeout_ms,
|
|
1523
|
+
});
|
|
1524
|
+
case "transaction.begin":
|
|
1525
|
+
return this.beginTransaction(command.isolation);
|
|
1526
|
+
case "transaction.status":
|
|
1527
|
+
return this.transactionStatus(command.handle);
|
|
1528
|
+
case "transaction.commit":
|
|
1529
|
+
return this.commitTransaction(command.handle, {
|
|
1530
|
+
timeoutMs: command.timeout_ms,
|
|
1531
|
+
});
|
|
1532
|
+
case "transaction.rollback":
|
|
1533
|
+
return this.rollbackTransaction(command.handle);
|
|
1534
|
+
case "plan":
|
|
1535
|
+
return this.plan(batchString(command.sql, "sql"), {
|
|
1536
|
+
params: command.params ?? [],
|
|
1537
|
+
allowUnbounded: command.allow_unbounded ?? false,
|
|
1538
|
+
allowDestructive: command.allow_destructive,
|
|
1539
|
+
timeoutMs: command.timeout_ms,
|
|
1540
|
+
});
|
|
1541
|
+
case "mongo.plan":
|
|
1542
|
+
return this.mongoPlan(command.mongo, {
|
|
1543
|
+
allowUnbounded: command.allow_unbounded ?? false,
|
|
1544
|
+
allowDestructive: command.allow_destructive,
|
|
1545
|
+
timeoutMs: command.timeout_ms,
|
|
1546
|
+
});
|
|
1547
|
+
case "apply":
|
|
1548
|
+
return this.apply(batchString(command.handle, "handle"), {
|
|
1549
|
+
timeoutMs: command.timeout_ms,
|
|
1550
|
+
});
|
|
1551
|
+
case "history":
|
|
1552
|
+
return this.history(command.limit ?? 20, {
|
|
1553
|
+
origin: command.history_origin,
|
|
1554
|
+
});
|
|
1555
|
+
case "receipt":
|
|
1556
|
+
return this.receipt(batchString(command.handle, "handle"));
|
|
1557
|
+
case "doctor":
|
|
1558
|
+
return this.doctor();
|
|
1559
|
+
case "purge":
|
|
1560
|
+
return this.purge(command.scope ?? "expired");
|
|
1561
|
+
case "capabilities":
|
|
1562
|
+
return this.capabilities();
|
|
1563
|
+
default:
|
|
1564
|
+
return this.batchFailure(`Unknown batch command "${String(command.command)}".`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
catch (error) {
|
|
1568
|
+
return this.batchFailure(errorMessage(error));
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1530
1571
|
}
|
|
1531
1572
|
async *batch(commands, options = {}) {
|
|
1532
1573
|
const maxCommands = options.maxCommands ?? 1_000;
|
|
@@ -1541,7 +1582,7 @@ export class StateQL {
|
|
|
1541
1582
|
yield await this.batchFailure(`Batch cannot exceed ${maxCommands} commands.`);
|
|
1542
1583
|
return;
|
|
1543
1584
|
}
|
|
1544
|
-
const response = await this.executeCommand(command);
|
|
1585
|
+
const response = await this.executeCommand(command, options.executionContext);
|
|
1545
1586
|
yield response;
|
|
1546
1587
|
if (!response.ok && !options.continueOnError)
|
|
1547
1588
|
return;
|
|
@@ -2007,9 +2048,13 @@ export class StateQL {
|
|
|
2007
2048
|
throw new StateQLError("PERMISSION_DENIED", `Actor "${this.actorId}" is not attached to session "${session.name}".`);
|
|
2008
2049
|
}
|
|
2009
2050
|
async resolveConnectionSource(connection, session, operation, access, context) {
|
|
2010
|
-
|
|
2051
|
+
const reference = connection.secret_env ?? connection.credential_ref;
|
|
2052
|
+
if (!reference)
|
|
2011
2053
|
return connection.source;
|
|
2012
|
-
const
|
|
2054
|
+
const source = connection.secret_env
|
|
2055
|
+
? "secret_env"
|
|
2056
|
+
: "credential_ref";
|
|
2057
|
+
const value = await this.resolveCredential(reference, source, session, operation, access, context, {
|
|
2013
2058
|
connection: {
|
|
2014
2059
|
id: connection.id,
|
|
2015
2060
|
name: connection.name,
|
|
@@ -2018,9 +2063,9 @@ export class StateQL {
|
|
|
2018
2063
|
readOnly: Boolean(connection.read_only),
|
|
2019
2064
|
},
|
|
2020
2065
|
});
|
|
2021
|
-
return credentialSource(value, connection.driver).source;
|
|
2066
|
+
return credentialSource(value, connection.driver, source).source;
|
|
2022
2067
|
}
|
|
2023
|
-
async resolveCredential(reference, session, operation, access, context, details = {}) {
|
|
2068
|
+
async resolveCredential(reference, source, session, operation, access, context, details = {}) {
|
|
2024
2069
|
const resolver = this.credentialResolver;
|
|
2025
2070
|
if (!resolver) {
|
|
2026
2071
|
if (context.signal?.aborted) {
|
|
@@ -2029,13 +2074,16 @@ export class StateQL {
|
|
|
2029
2074
|
if (context.deadline <= Date.now()) {
|
|
2030
2075
|
throw credentialStateQLError(reference, new CredentialResolutionError("timeout"));
|
|
2031
2076
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2077
|
+
if (source === "secret_env") {
|
|
2078
|
+
const value = env[reference];
|
|
2079
|
+
if (value)
|
|
2080
|
+
return value;
|
|
2081
|
+
}
|
|
2035
2082
|
throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
|
|
2036
2083
|
}
|
|
2037
2084
|
const request = {
|
|
2038
2085
|
reference,
|
|
2086
|
+
source,
|
|
2039
2087
|
actorId: this.actorId,
|
|
2040
2088
|
session: { id: session.id, name: session.name },
|
|
2041
2089
|
operation,
|
|
@@ -2074,7 +2122,7 @@ export class StateQL {
|
|
|
2074
2122
|
}
|
|
2075
2123
|
}
|
|
2076
2124
|
executionContext(options) {
|
|
2077
|
-
return createAdapterContext(executionTimeout(options.timeoutMs ?? this.timeoutMs), options.signal
|
|
2125
|
+
return createAdapterContext(executionTimeout(options.timeoutMs ?? this.timeoutMs), combineAbortSignals(options.signal, this.commandContexts.getStore()?.signal, this.signal));
|
|
2078
2126
|
}
|
|
2079
2127
|
resultData(result, cached) {
|
|
2080
2128
|
const rows = this.store.resultRows(result);
|
|
@@ -2104,6 +2152,7 @@ export class StateQL {
|
|
|
2104
2152
|
}
|
|
2105
2153
|
async run(command, action, historySql) {
|
|
2106
2154
|
const started = performance.now();
|
|
2155
|
+
const origin = this.commandContexts.getStore()?.origin ?? "legacy";
|
|
2107
2156
|
let session = this.store.ensureSession(this.sessionName);
|
|
2108
2157
|
const commandId = this.store.nextId("cmd");
|
|
2109
2158
|
if (!this.store.isSessionMember(session.id, this.actorId)) {
|
|
@@ -2126,6 +2175,7 @@ export class StateQL {
|
|
|
2126
2175
|
id: commandId,
|
|
2127
2176
|
sessionId: session.id,
|
|
2128
2177
|
actorId: this.actorId,
|
|
2178
|
+
origin,
|
|
2129
2179
|
command,
|
|
2130
2180
|
...(result.handle ? { handle: result.handle } : {}),
|
|
2131
2181
|
...(sqlText !== undefined ? { sql: sqlText } : {}),
|
|
@@ -2157,6 +2207,7 @@ export class StateQL {
|
|
|
2157
2207
|
id: commandId,
|
|
2158
2208
|
sessionId: session.id,
|
|
2159
2209
|
actorId: this.actorId,
|
|
2210
|
+
origin,
|
|
2160
2211
|
command,
|
|
2161
2212
|
...(sqlText !== undefined ? { sql: sqlText } : {}),
|
|
2162
2213
|
executed: stateqlError.details.executed,
|
|
@@ -2185,6 +2236,7 @@ function historyEntry(item) {
|
|
|
2185
2236
|
timestamp: item.timestamp,
|
|
2186
2237
|
session_id: item.session_id,
|
|
2187
2238
|
actor_id: item.actor_id,
|
|
2239
|
+
origin: item.origin,
|
|
2188
2240
|
command: item.command,
|
|
2189
2241
|
sql: item.sql,
|
|
2190
2242
|
handle: item.handle,
|
|
@@ -2194,6 +2246,41 @@ function historyEntry(item) {
|
|
|
2194
2246
|
error_code: item.error_code,
|
|
2195
2247
|
};
|
|
2196
2248
|
}
|
|
2249
|
+
const COMMAND_ORIGINS = new Set([
|
|
2250
|
+
"legacy",
|
|
2251
|
+
"user",
|
|
2252
|
+
"model",
|
|
2253
|
+
"system",
|
|
2254
|
+
"api",
|
|
2255
|
+
]);
|
|
2256
|
+
function parseCommandOrigin(value) {
|
|
2257
|
+
if (typeof value === "string" && COMMAND_ORIGINS.has(value)) {
|
|
2258
|
+
return value;
|
|
2259
|
+
}
|
|
2260
|
+
throw new StateQLError("INVALID_COMMAND", `Unknown command origin "${String(value)}".`);
|
|
2261
|
+
}
|
|
2262
|
+
function mergeCommandExecutionContext(inherited, supplied) {
|
|
2263
|
+
if (!supplied || typeof supplied !== "object") {
|
|
2264
|
+
throw new StateQLError("INVALID_COMMAND", "Command execution context must be an object.");
|
|
2265
|
+
}
|
|
2266
|
+
if (supplied.signal !== undefined && !(supplied.signal instanceof AbortSignal)) {
|
|
2267
|
+
throw new StateQLError("INVALID_COMMAND", "Command execution context signal must be an AbortSignal.");
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
signal: combineAbortSignals(inherited?.signal, supplied.signal),
|
|
2271
|
+
origin: supplied.origin === undefined
|
|
2272
|
+
? inherited?.origin
|
|
2273
|
+
: parseCommandOrigin(supplied.origin),
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
function combineAbortSignals(...signals) {
|
|
2277
|
+
const present = signals.filter((signal) => signal !== undefined);
|
|
2278
|
+
if (present.length === 0)
|
|
2279
|
+
return undefined;
|
|
2280
|
+
if (present.length === 1)
|
|
2281
|
+
return present[0];
|
|
2282
|
+
return AbortSignal.any(present);
|
|
2283
|
+
}
|
|
2197
2284
|
function markTransactionOutcomeUnknown(store, transactionId, sessionId, actorId) {
|
|
2198
2285
|
try {
|
|
2199
2286
|
store.markTransactionOutcomeUnknown(transactionId, sessionId, actorId);
|
package/dist/src/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import type { Column, Driver, MongoWriteOutcome, Row, SqlParameters, StateConfidence } from "./types.js";
|
|
2
|
+
import type { Column, CommandOrigin, Driver, MongoWriteOutcome, Row, SqlParameters, StateConfidence } from "./types.js";
|
|
3
3
|
export interface SessionRecord {
|
|
4
4
|
id: string;
|
|
5
5
|
name: string;
|
|
@@ -17,6 +17,7 @@ export interface ConnectionRecord {
|
|
|
17
17
|
database_name: string;
|
|
18
18
|
source: string;
|
|
19
19
|
secret_env: string | null;
|
|
20
|
+
credential_ref: string | null;
|
|
20
21
|
read_only: number;
|
|
21
22
|
version: number;
|
|
22
23
|
created_at: string;
|
|
@@ -25,6 +26,7 @@ export interface ProfileRecord {
|
|
|
25
26
|
name: string;
|
|
26
27
|
target: string | null;
|
|
27
28
|
secret_env: string | null;
|
|
29
|
+
credential_ref: string | null;
|
|
28
30
|
read_only: number;
|
|
29
31
|
created_at: string;
|
|
30
32
|
updated_at: string;
|
|
@@ -98,6 +100,7 @@ export interface HistoryRecord {
|
|
|
98
100
|
timestamp: string;
|
|
99
101
|
session_id: string;
|
|
100
102
|
actor_id: string;
|
|
103
|
+
origin: CommandOrigin;
|
|
101
104
|
command: string;
|
|
102
105
|
sql: string | null;
|
|
103
106
|
handle: string | null;
|
|
@@ -135,6 +138,7 @@ export declare class StateStore {
|
|
|
135
138
|
name: string;
|
|
136
139
|
target?: string;
|
|
137
140
|
secretEnv?: string;
|
|
141
|
+
credentialRef?: string;
|
|
138
142
|
readOnly: boolean;
|
|
139
143
|
}): ProfileRecord;
|
|
140
144
|
getProfile(name: string): ProfileRecord | undefined;
|
|
@@ -148,6 +152,7 @@ export declare class StateStore {
|
|
|
148
152
|
databaseName: string;
|
|
149
153
|
source: string;
|
|
150
154
|
secretEnv?: string;
|
|
155
|
+
credentialRef?: string;
|
|
151
156
|
readOnly: boolean;
|
|
152
157
|
}): ConnectionRecord | undefined;
|
|
153
158
|
getConnection(id: string): ConnectionRecord | undefined;
|
|
@@ -270,6 +275,7 @@ export declare class StateStore {
|
|
|
270
275
|
addHistory(input: {
|
|
271
276
|
sessionId: string;
|
|
272
277
|
actorId: string;
|
|
278
|
+
origin?: CommandOrigin;
|
|
273
279
|
command: string;
|
|
274
280
|
sql?: string;
|
|
275
281
|
handle?: string;
|
|
@@ -279,7 +285,7 @@ export declare class StateStore {
|
|
|
279
285
|
errorCode?: string;
|
|
280
286
|
id?: string;
|
|
281
287
|
}): HistoryRecord;
|
|
282
|
-
history(sessionId: string, limit: number): HistoryRecord[];
|
|
288
|
+
history(sessionId: string, limit: number, origin?: CommandOrigin): HistoryRecord[];
|
|
283
289
|
recentOperations(sessionId: string, limit: number): OperationRecord[];
|
|
284
290
|
knownResults(sessionId: string, limit: number): Array<ResultRecord & {
|
|
285
291
|
alias: string | null;
|
package/dist/src/store.js
CHANGED
|
@@ -235,9 +235,9 @@ export class StateStore {
|
|
|
235
235
|
const timestamp = this.now().toISOString();
|
|
236
236
|
this.db
|
|
237
237
|
.prepare(`INSERT INTO profiles
|
|
238
|
-
(name, target, secret_env, read_only, created_at, updated_at)
|
|
239
|
-
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
240
|
-
.run(input.name, input.target ?? null, input.secretEnv ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
|
|
238
|
+
(name, target, secret_env, credential_ref, read_only, created_at, updated_at)
|
|
239
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
240
|
+
.run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
|
|
241
241
|
return this.getProfile(input.name);
|
|
242
242
|
}
|
|
243
243
|
getProfile(name) {
|
|
@@ -274,9 +274,9 @@ export class StateStore {
|
|
|
274
274
|
this.db
|
|
275
275
|
.prepare(`INSERT INTO connections
|
|
276
276
|
(id, session_id, name, driver, database_name, source, secret_env,
|
|
277
|
-
read_only, version, created_at)
|
|
278
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
|
|
279
|
-
.run(id, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.readOnly ? 1 : 0, timestamp);
|
|
277
|
+
credential_ref, read_only, version, created_at)
|
|
278
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
|
|
279
|
+
.run(id, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp);
|
|
280
280
|
this.db
|
|
281
281
|
.prepare(`UPDATE sessions
|
|
282
282
|
SET active_connection_id = ?, updated_at = ?
|
|
@@ -415,6 +415,8 @@ export class StateStore {
|
|
|
415
415
|
AND previous_connection.source = current_connection.source
|
|
416
416
|
AND COALESCE(previous_connection.secret_env, '') =
|
|
417
417
|
COALESCE(current_connection.secret_env, '')
|
|
418
|
+
AND COALESCE(previous_connection.credential_ref, '') =
|
|
419
|
+
COALESCE(current_connection.credential_ref, '')
|
|
418
420
|
AND previous_connection.database_name = current_connection.database_name
|
|
419
421
|
ORDER BY operations.created_at DESC LIMIT 1`)
|
|
420
422
|
.get(input.connectionId, input.idempotencyKey);
|
|
@@ -795,10 +797,10 @@ export class StateStore {
|
|
|
795
797
|
const id = input.id ?? this.nextId("cmd");
|
|
796
798
|
this.db
|
|
797
799
|
.prepare(`INSERT INTO history
|
|
798
|
-
(id, timestamp, session_id, actor_id, command, sql, handle,
|
|
799
|
-
cached, success, error_code)
|
|
800
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
801
|
-
.run(id, this.now().toISOString(), input.sessionId, input.actorId, input.command, boundedHistorySql(input.sql), input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
|
|
800
|
+
(id, timestamp, session_id, actor_id, origin, command, sql, handle,
|
|
801
|
+
executed, cached, success, error_code)
|
|
802
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
803
|
+
.run(id, this.now().toISOString(), input.sessionId, input.actorId, input.origin ?? "legacy", input.command, boundedHistorySql(input.sql), input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
|
|
802
804
|
this.db
|
|
803
805
|
.prepare(`DELETE FROM history
|
|
804
806
|
WHERE rowid IN (
|
|
@@ -812,13 +814,13 @@ export class StateStore {
|
|
|
812
814
|
.prepare("SELECT * FROM history WHERE id = ?")
|
|
813
815
|
.get(id);
|
|
814
816
|
}
|
|
815
|
-
history(sessionId, limit) {
|
|
817
|
+
history(sessionId, limit, origin) {
|
|
816
818
|
return this.db
|
|
817
819
|
.prepare(`SELECT * FROM history
|
|
818
|
-
WHERE session_id = ?
|
|
820
|
+
WHERE session_id = ?${origin === undefined ? "" : " AND origin = ?"}
|
|
819
821
|
ORDER BY rowid DESC
|
|
820
822
|
LIMIT ?`)
|
|
821
|
-
.all(sessionId, limit);
|
|
823
|
+
.all(...(origin === undefined ? [sessionId, limit] : [sessionId, origin, limit]));
|
|
822
824
|
}
|
|
823
825
|
recentOperations(sessionId, limit) {
|
|
824
826
|
return this.db
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
export type SqlDriver = "sqlite" | "postgres" | "mysql";
|
|
2
2
|
export type Driver = SqlDriver | "mongodb";
|
|
3
|
+
export type CommandOrigin = "legacy" | "user" | "model" | "system" | "api";
|
|
4
|
+
/** Trusted host metadata for one executeCommand call; never part of BatchCommand input. */
|
|
5
|
+
export interface CommandExecutionContext {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
origin?: CommandOrigin;
|
|
8
|
+
}
|
|
3
9
|
export type CredentialAccess = "read" | "write";
|
|
4
10
|
export type CredentialOperation = "connect" | "query" | "inspect" | "plan" | "exec" | "apply" | "transaction.commit";
|
|
5
|
-
export
|
|
11
|
+
export type CredentialSource = "secret_env" | "credential_ref";
|
|
12
|
+
interface CredentialRequestBase {
|
|
6
13
|
reference: string;
|
|
7
14
|
actorId: string;
|
|
8
15
|
session: {
|
|
@@ -24,6 +31,12 @@ export interface CredentialRequest {
|
|
|
24
31
|
readOnly: boolean;
|
|
25
32
|
};
|
|
26
33
|
}
|
|
34
|
+
/** Omitted source remains backward-compatible with legacy secret_env requests. */
|
|
35
|
+
export type CredentialRequest = CredentialRequestBase & ({
|
|
36
|
+
source?: "secret_env";
|
|
37
|
+
} | {
|
|
38
|
+
source: "credential_ref";
|
|
39
|
+
});
|
|
27
40
|
export type CredentialResolver = (request: CredentialRequest) => string | undefined | Promise<string | undefined>;
|
|
28
41
|
export type StateConfidence = "authoritative" | "transaction_snapshot" | "database_reported" | "local" | "ttl_based" | "unknown";
|
|
29
42
|
export interface Warning {
|
|
@@ -64,6 +77,7 @@ export interface HistoryEntry {
|
|
|
64
77
|
timestamp: string;
|
|
65
78
|
session_id: string;
|
|
66
79
|
actor_id: string;
|
|
80
|
+
origin: CommandOrigin;
|
|
67
81
|
command: string;
|
|
68
82
|
sql: string | null;
|
|
69
83
|
handle: string | null;
|
|
@@ -184,6 +198,9 @@ export interface ExecutionOptions {
|
|
|
184
198
|
timeoutMs?: number;
|
|
185
199
|
signal?: AbortSignal;
|
|
186
200
|
}
|
|
201
|
+
export interface HistoryOptions {
|
|
202
|
+
origin?: CommandOrigin;
|
|
203
|
+
}
|
|
187
204
|
export interface StateQLOptions extends ExecutionOptions {
|
|
188
205
|
home?: string;
|
|
189
206
|
session?: string;
|
|
@@ -229,10 +246,12 @@ export interface ConnectOptions extends ExecutionOptions {
|
|
|
229
246
|
readOnly?: boolean;
|
|
230
247
|
secretEnv?: string;
|
|
231
248
|
profile?: string;
|
|
249
|
+
credentialRef?: string;
|
|
232
250
|
}
|
|
233
251
|
export interface ProfileOptions {
|
|
234
252
|
readOnly?: boolean;
|
|
235
253
|
secretEnv?: string;
|
|
254
|
+
credentialRef?: string;
|
|
236
255
|
}
|
|
237
256
|
export interface RowsOptions {
|
|
238
257
|
offset?: number;
|
|
@@ -263,6 +282,7 @@ export interface BatchCommand {
|
|
|
263
282
|
cache?: "auto" | "bypass" | "require";
|
|
264
283
|
read_only?: boolean;
|
|
265
284
|
secret_env?: string;
|
|
285
|
+
credential_ref?: string;
|
|
266
286
|
profile?: string;
|
|
267
287
|
replay?: boolean;
|
|
268
288
|
idempotency_key?: string;
|
|
@@ -272,11 +292,14 @@ export interface BatchCommand {
|
|
|
272
292
|
limit?: number;
|
|
273
293
|
isolation?: string;
|
|
274
294
|
timeout_ms?: number;
|
|
295
|
+
/** Retrieval filter for the history command; does not attribute this command. */
|
|
296
|
+
history_origin?: CommandOrigin;
|
|
275
297
|
scope?: "expired" | "results" | "history" | "all";
|
|
276
298
|
}
|
|
277
299
|
export interface BatchOptions {
|
|
278
300
|
continueOnError?: boolean;
|
|
279
301
|
maxCommands?: number;
|
|
302
|
+
executionContext?: CommandExecutionContext;
|
|
280
303
|
}
|
|
281
304
|
export interface Column {
|
|
282
305
|
name: string;
|
|
@@ -298,6 +321,7 @@ export interface ProfileData {
|
|
|
298
321
|
profile: string;
|
|
299
322
|
target: string | null;
|
|
300
323
|
secret_env: string | null;
|
|
324
|
+
credential_ref: string | null;
|
|
301
325
|
read_only: boolean;
|
|
302
326
|
}
|
|
303
327
|
export interface ProfilesData {
|
|
@@ -521,3 +545,4 @@ export interface CloseSessionData {
|
|
|
521
545
|
session_id: string;
|
|
522
546
|
state: string;
|
|
523
547
|
}
|
|
548
|
+
export {};
|