@fadhilp/stateql 0.7.0 → 0.8.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 +32 -14
- package/dist/src/adapters.d.ts +1 -0
- package/dist/src/adapters.js +1 -0
- 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 +75 -1
- package/dist/src/response-data.js +1 -0
- package/dist/src/stateql.d.ts +1 -0
- package/dist/src/stateql.js +89 -51
- package/dist/src/store.d.ts +4 -0
- package/dist/src/store.js +8 -6
- package/dist/src/types.d.ts +15 -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
|
|
|
@@ -407,6 +415,7 @@ const stateql = StateQL.forActor({
|
|
|
407
415
|
home: "./.stql",
|
|
408
416
|
actor: "pi-session-id",
|
|
409
417
|
timeoutMs: 30_000,
|
|
418
|
+
credentialTimeoutMs: 120_000,
|
|
410
419
|
maxResultBytes: 16 * 1024 * 1024,
|
|
411
420
|
maxStateBytes: 256 * 1024 * 1024,
|
|
412
421
|
});
|
|
@@ -464,8 +473,9 @@ for user confirmation before changing membership or the shared connection.
|
|
|
464
473
|
|
|
465
474
|
### Harness credential resolution
|
|
466
475
|
|
|
467
|
-
Library integrations can resolve
|
|
468
|
-
trusted approval or secret-storage layer
|
|
476
|
+
Library integrations can resolve environment-variable names or opaque
|
|
477
|
+
credential references through a trusted approval or secret-storage layer
|
|
478
|
+
instead of mutating `process.env`:
|
|
469
479
|
|
|
470
480
|
```ts
|
|
471
481
|
import {
|
|
@@ -479,6 +489,7 @@ async function resolveCredential(
|
|
|
479
489
|
): Promise<string | undefined> {
|
|
480
490
|
const approved = await credentialBroker.request({
|
|
481
491
|
reference: request.reference,
|
|
492
|
+
source: request.source ?? "secret_env",
|
|
482
493
|
actor: request.actorId,
|
|
483
494
|
session: request.session.id,
|
|
484
495
|
operation: request.operation,
|
|
@@ -496,13 +507,20 @@ const stateql = StateQL.forActor({
|
|
|
496
507
|
});
|
|
497
508
|
```
|
|
498
509
|
|
|
499
|
-
|
|
500
|
-
`
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
510
|
+
Credential resolution has its own two-minute default deadline
|
|
511
|
+
(`credentialTimeoutMs`) and remains cancellable through `request.signal`.
|
|
512
|
+
The database-operation timeout begins after a credential is resolved.
|
|
513
|
+
|
|
514
|
+
When no custom resolver is configured, StateQL reads only `secret_env`
|
|
515
|
+
references from `process.env`; `credential_ref` never falls back to the
|
|
516
|
+
environment. A configured resolver is authoritative for both sources: returning
|
|
517
|
+
`undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls back to the
|
|
518
|
+
process environment. Resolver requests include `source` (`secret_env` or
|
|
519
|
+
`credential_ref`) while retaining `reference`; source may be omitted only on
|
|
520
|
+
legacy secret-environment request objects. Resolvers may throw
|
|
521
|
+
`CredentialResolutionError` with `denied`, `cancelled`, `timeout`, or
|
|
522
|
+
`unavailable` to produce controlled, secret-free failures. Unknown resolver
|
|
523
|
+
errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED` response.
|
|
506
524
|
|
|
507
525
|
StateQL calls the resolver only immediately before database access, after SQL
|
|
508
526
|
safety and duplicate checks. Requests contain actor and session identity, the
|
package/dist/src/adapters.d.ts
CHANGED
package/dist/src/adapters.js
CHANGED
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, CommandExecutionContext, CommandOrigin, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, 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";
|
|
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
|
@@ -75,6 +75,11 @@ const MIGRATIONS = [
|
|
|
75
75
|
requireIndexes(db, ["history_session_origin"]);
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
|
+
{
|
|
79
|
+
name: "credential_refs_v1",
|
|
80
|
+
apply: migrateCredentialRefs,
|
|
81
|
+
validate: validateCredentialRefs,
|
|
82
|
+
},
|
|
78
83
|
];
|
|
79
84
|
export function runMigrations(db, now) {
|
|
80
85
|
db.exec(`
|
|
@@ -130,10 +135,15 @@ function createInitialSchema(db) {
|
|
|
130
135
|
name TEXT PRIMARY KEY,
|
|
131
136
|
target TEXT,
|
|
132
137
|
secret_env TEXT,
|
|
138
|
+
credential_ref TEXT,
|
|
133
139
|
read_only INTEGER NOT NULL,
|
|
134
140
|
created_at TEXT NOT NULL,
|
|
135
141
|
updated_at TEXT NOT NULL,
|
|
136
|
-
CHECK(
|
|
142
|
+
CHECK (
|
|
143
|
+
(target IS NOT NULL) +
|
|
144
|
+
(secret_env IS NOT NULL) +
|
|
145
|
+
(credential_ref IS NOT NULL) = 1
|
|
146
|
+
)
|
|
137
147
|
);
|
|
138
148
|
CREATE TABLE IF NOT EXISTS connections (
|
|
139
149
|
id TEXT PRIMARY KEY,
|
|
@@ -143,6 +153,7 @@ function createInitialSchema(db) {
|
|
|
143
153
|
database_name TEXT NOT NULL,
|
|
144
154
|
source TEXT NOT NULL,
|
|
145
155
|
secret_env TEXT,
|
|
156
|
+
credential_ref TEXT,
|
|
146
157
|
read_only INTEGER NOT NULL,
|
|
147
158
|
version INTEGER NOT NULL,
|
|
148
159
|
created_at TEXT NOT NULL,
|
|
@@ -301,6 +312,69 @@ function validateSharedSessionActors(db) {
|
|
|
301
312
|
throw new Error(`State migration left ${table}.${column} empty.`);
|
|
302
313
|
}
|
|
303
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
|
+
}
|
|
304
378
|
function addColumn(db, table, column, definition) {
|
|
305
379
|
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
306
380
|
if (!columns.some((candidate) => candidate.name === column)) {
|
package/dist/src/stateql.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare class StateQL {
|
|
|
11
11
|
private readonly maxResultRows;
|
|
12
12
|
private readonly maxResultBytes;
|
|
13
13
|
private readonly timeoutMs;
|
|
14
|
+
private readonly credentialTimeoutMs;
|
|
14
15
|
private readonly signal?;
|
|
15
16
|
private readonly commandContexts;
|
|
16
17
|
private readonly credentialResolver?;
|
package/dist/src/stateql.js
CHANGED
|
@@ -3,7 +3,7 @@ import { writeFileSync } from "node:fs";
|
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
4
|
import { env } from "node:process";
|
|
5
5
|
import { AdapterExecutionError, AdapterWriteError, BatchWriteError, createAdapter, createAdapterContext, } from "./adapters.js";
|
|
6
|
-
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";
|
|
7
7
|
import { asStateQLError, CredentialResolutionError, StateQLError, } from "./errors.js";
|
|
8
8
|
import { analyzeMongoWriteSafety, deserializeMongoWriteCommand, serializeMongoCommand, validateMongoReadCommand, validateMongoWriteCommand, MongoAdapter, } from "./mongodb.js";
|
|
9
9
|
import { filterMaterializedRows, prepareFilterStatement, validateFilterParameters, } from "./filter.js";
|
|
@@ -13,6 +13,7 @@ import { StateStore, } from "./store.js";
|
|
|
13
13
|
import { compactRows, defaultHome, hash, isSqlParameters, parseJson, redact, } from "./util.js";
|
|
14
14
|
const DEFAULT_SNAPSHOT_HISTORY_LIMIT = 50;
|
|
15
15
|
const MAX_SNAPSHOT_HISTORY_LIMIT = 100;
|
|
16
|
+
const DEFAULT_CREDENTIAL_RESOLUTION_TIMEOUT_MS = 120_000;
|
|
16
17
|
export class StateQL {
|
|
17
18
|
static forActor(options) {
|
|
18
19
|
if (!options.actor.trim()) {
|
|
@@ -41,6 +42,7 @@ export class StateQL {
|
|
|
41
42
|
maxResultRows;
|
|
42
43
|
maxResultBytes;
|
|
43
44
|
timeoutMs;
|
|
45
|
+
credentialTimeoutMs;
|
|
44
46
|
signal;
|
|
45
47
|
commandContexts = new AsyncLocalStorage();
|
|
46
48
|
credentialResolver;
|
|
@@ -60,6 +62,7 @@ export class StateQL {
|
|
|
60
62
|
this.maxResultRows = positiveInteger(options.maxResultRows ?? 10_000, "maxResultRows");
|
|
61
63
|
this.maxResultBytes = positiveInteger(options.maxResultBytes ?? 16 * 1024 * 1024, "maxResultBytes");
|
|
62
64
|
this.timeoutMs = executionTimeout(options.timeoutMs ?? 30_000);
|
|
65
|
+
this.credentialTimeoutMs = executionTimeout(options.credentialTimeoutMs ?? DEFAULT_CREDENTIAL_RESOLUTION_TIMEOUT_MS, "credentialTimeoutMs");
|
|
63
66
|
const maxStateBytes = positiveInteger(options.maxStateBytes ?? 256 * 1024 * 1024, "maxStateBytes");
|
|
64
67
|
this.signal = options.signal;
|
|
65
68
|
this.credentialResolver = options.credentialResolver;
|
|
@@ -90,12 +93,16 @@ export class StateQL {
|
|
|
90
93
|
if (session.active_transaction_id) {
|
|
91
94
|
throw new StateQLError("TRANSACTION_FAILED", "Commit or roll back the active transaction before connecting again.");
|
|
92
95
|
}
|
|
93
|
-
const sourceCount = [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const sourceCount = [
|
|
97
|
+
target,
|
|
98
|
+
options.profile,
|
|
99
|
+
options.secretEnv,
|
|
100
|
+
options.credentialRef,
|
|
101
|
+
].filter((value) => value !== undefined).length;
|
|
102
|
+
if (sourceCount !== 1) {
|
|
103
|
+
throw new StateQLError("INVALID_COMMAND", "Use exactly one connection target, profile, secret environment variable, or credential reference.");
|
|
97
104
|
}
|
|
98
|
-
const implicitProfile = !options.profile && !options.secretEnv && target
|
|
105
|
+
const implicitProfile = !options.profile && !options.secretEnv && !options.credentialRef && target
|
|
99
106
|
? this.store.getProfile(target)
|
|
100
107
|
: undefined;
|
|
101
108
|
const profile = options.profile
|
|
@@ -104,16 +111,28 @@ export class StateQL {
|
|
|
104
111
|
if (options.profile && !profile) {
|
|
105
112
|
throw new StateQLError("CONNECTION_NOT_FOUND", `Profile "${options.profile}" was not found.`, { suggestedAction: "Run stql profile list." });
|
|
106
113
|
}
|
|
114
|
+
if (profile &&
|
|
115
|
+
[profile.target, profile.secret_env, profile.credential_ref]
|
|
116
|
+
.filter((value) => value !== null).length !== 1) {
|
|
117
|
+
throw new StateQLError("STATE_CORRUPTED", `Profile "${profile.name}" does not have exactly one connection source.`);
|
|
118
|
+
}
|
|
107
119
|
const resolvedTarget = profile?.target ?? target;
|
|
108
120
|
const secretEnv = options.secretEnv ?? profile?.secret_env ?? undefined;
|
|
109
|
-
|
|
121
|
+
const credentialRef = options.credentialRef ?? profile?.credential_ref ?? undefined;
|
|
122
|
+
if (secretEnv !== undefined && !isEnvironmentName(secretEnv)) {
|
|
110
123
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
111
124
|
}
|
|
125
|
+
if (credentialRef !== undefined)
|
|
126
|
+
validateCredentialRef(credentialRef);
|
|
127
|
+
const credentialReference = secretEnv ?? credentialRef;
|
|
128
|
+
const credentialReferenceSource = secretEnv !== undefined
|
|
129
|
+
? "secret_env"
|
|
130
|
+
: credentialRef !== undefined ? "credential_ref" : undefined;
|
|
112
131
|
const readOnly = options.readOnly ??
|
|
113
132
|
(profile ? Boolean(profile.read_only) : true);
|
|
114
133
|
const context = this.executionContext(options);
|
|
115
|
-
const secret =
|
|
116
|
-
? await this.resolveCredential(
|
|
134
|
+
const secret = credentialReference && credentialReferenceSource
|
|
135
|
+
? await this.resolveCredential(credentialReference, credentialReferenceSource, session, "connect", readOnly ? "read" : "write", context, {
|
|
117
136
|
...(profile ? { profile: { name: profile.name } } : {}),
|
|
118
137
|
requestedReadOnly: readOnly,
|
|
119
138
|
})
|
|
@@ -121,23 +140,23 @@ export class StateQL {
|
|
|
121
140
|
if (!secret) {
|
|
122
141
|
throw new StateQLError("INVALID_COMMAND", "Connection target is required.");
|
|
123
142
|
}
|
|
124
|
-
const resolvedSource =
|
|
125
|
-
? credentialSource(secret)
|
|
143
|
+
const resolvedSource = credentialReferenceSource
|
|
144
|
+
? credentialSource(secret, undefined, credentialReferenceSource)
|
|
126
145
|
: { driver: detectDriver(secret), source: secret };
|
|
127
146
|
const { driver } = resolvedSource;
|
|
128
147
|
if (driver !== "sqlite" &&
|
|
129
|
-
!
|
|
148
|
+
!credentialReference &&
|
|
130
149
|
databaseUrlHasSecret(secret)) {
|
|
131
|
-
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env.`, {
|
|
132
|
-
suggestedAction: "
|
|
150
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`, {
|
|
151
|
+
suggestedAction: "Use an environment variable or trusted host credential reference.",
|
|
133
152
|
});
|
|
134
153
|
}
|
|
135
|
-
const adapterSource =
|
|
154
|
+
const adapterSource = credentialReferenceSource
|
|
136
155
|
? resolvedSource.source
|
|
137
156
|
: driver === "sqlite" ? normalizeSqliteSource(secret) : secret;
|
|
138
157
|
const source = driver === "sqlite"
|
|
139
158
|
? adapterSource
|
|
140
|
-
:
|
|
159
|
+
: credentialReferenceSource
|
|
141
160
|
? redact(secret)
|
|
142
161
|
: adapterSource;
|
|
143
162
|
const databaseName = driver === "sqlite"
|
|
@@ -153,6 +172,7 @@ export class StateQL {
|
|
|
153
172
|
database_name: databaseName,
|
|
154
173
|
source,
|
|
155
174
|
secret_env: secretEnv ?? null,
|
|
175
|
+
credential_ref: credentialRef ?? null,
|
|
156
176
|
read_only: readOnly ? 1 : 0,
|
|
157
177
|
version: 0,
|
|
158
178
|
created_at: this.now().toISOString(),
|
|
@@ -180,6 +200,7 @@ export class StateQL {
|
|
|
180
200
|
databaseName,
|
|
181
201
|
source,
|
|
182
202
|
...(secretEnv ? { secretEnv } : {}),
|
|
203
|
+
...(credentialRef ? { credentialRef } : {}),
|
|
183
204
|
readOnly,
|
|
184
205
|
});
|
|
185
206
|
if (!connection) {
|
|
@@ -206,20 +227,25 @@ export class StateQL {
|
|
|
206
227
|
async addProfile(name, target, options = {}) {
|
|
207
228
|
return this.run("profile.add", async () => {
|
|
208
229
|
validateProfileName(name);
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
const sourceCount = [target, options.secretEnv, options.credentialRef]
|
|
231
|
+
.filter((value) => value !== undefined).length;
|
|
232
|
+
if (sourceCount !== 1 || target === "") {
|
|
233
|
+
throw new StateQLError("INVALID_COMMAND", "Profile requires exactly one target, secret environment variable, or credential reference.");
|
|
211
234
|
}
|
|
212
235
|
if (this.store.getProfile(name)) {
|
|
213
236
|
throw new StateQLError("INVALID_COMMAND", `Profile "${name}" already exists.`);
|
|
214
237
|
}
|
|
215
|
-
if (options.secretEnv && !isEnvironmentName(options.secretEnv)) {
|
|
238
|
+
if (options.secretEnv !== undefined && !isEnvironmentName(options.secretEnv)) {
|
|
216
239
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
217
240
|
}
|
|
241
|
+
if (options.credentialRef !== undefined) {
|
|
242
|
+
validateCredentialRef(options.credentialRef);
|
|
243
|
+
}
|
|
218
244
|
let storedTarget = target;
|
|
219
245
|
if (target) {
|
|
220
246
|
const driver = detectDriver(target);
|
|
221
247
|
if (driver !== "sqlite" && databaseUrlHasSecret(target)) {
|
|
222
|
-
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env.`);
|
|
248
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`);
|
|
223
249
|
}
|
|
224
250
|
if (driver === "sqlite")
|
|
225
251
|
storedTarget = normalizeSqliteSource(target);
|
|
@@ -228,6 +254,7 @@ export class StateQL {
|
|
|
228
254
|
name,
|
|
229
255
|
target: storedTarget,
|
|
230
256
|
secretEnv: options.secretEnv,
|
|
257
|
+
credentialRef: options.credentialRef,
|
|
231
258
|
readOnly: options.readOnly ?? true,
|
|
232
259
|
});
|
|
233
260
|
return {
|
|
@@ -1387,6 +1414,7 @@ export class StateQL {
|
|
|
1387
1414
|
name: command.name,
|
|
1388
1415
|
readOnly: command.read_only,
|
|
1389
1416
|
secretEnv: command.secret_env,
|
|
1417
|
+
credentialRef: command.credential_ref,
|
|
1390
1418
|
profile: command.profile,
|
|
1391
1419
|
timeoutMs: command.timeout_ms,
|
|
1392
1420
|
});
|
|
@@ -1398,6 +1426,7 @@ export class StateQL {
|
|
|
1398
1426
|
return this.addProfile(batchString(command.name, "name"), command.target, {
|
|
1399
1427
|
readOnly: command.read_only ?? true,
|
|
1400
1428
|
secretEnv: command.secret_env,
|
|
1429
|
+
credentialRef: command.credential_ref,
|
|
1401
1430
|
});
|
|
1402
1431
|
case "profile.list":
|
|
1403
1432
|
return this.listProfiles();
|
|
@@ -2022,9 +2051,13 @@ export class StateQL {
|
|
|
2022
2051
|
throw new StateQLError("PERMISSION_DENIED", `Actor "${this.actorId}" is not attached to session "${session.name}".`);
|
|
2023
2052
|
}
|
|
2024
2053
|
async resolveConnectionSource(connection, session, operation, access, context) {
|
|
2025
|
-
|
|
2054
|
+
const reference = connection.secret_env ?? connection.credential_ref;
|
|
2055
|
+
if (!reference)
|
|
2026
2056
|
return connection.source;
|
|
2027
|
-
const
|
|
2057
|
+
const source = connection.secret_env
|
|
2058
|
+
? "secret_env"
|
|
2059
|
+
: "credential_ref";
|
|
2060
|
+
const value = await this.resolveCredential(reference, source, session, operation, access, context, {
|
|
2028
2061
|
connection: {
|
|
2029
2062
|
id: connection.id,
|
|
2030
2063
|
name: connection.name,
|
|
@@ -2033,40 +2066,45 @@ export class StateQL {
|
|
|
2033
2066
|
readOnly: Boolean(connection.read_only),
|
|
2034
2067
|
},
|
|
2035
2068
|
});
|
|
2036
|
-
return credentialSource(value, connection.driver).source;
|
|
2069
|
+
return credentialSource(value, connection.driver, source).source;
|
|
2037
2070
|
}
|
|
2038
|
-
async resolveCredential(reference, session, operation, access, context, details = {}) {
|
|
2071
|
+
async resolveCredential(reference, source, session, operation, access, context, details = {}) {
|
|
2039
2072
|
const resolver = this.credentialResolver;
|
|
2073
|
+
const credentialContext = createAdapterContext(this.credentialTimeoutMs, context.signal);
|
|
2074
|
+
let value;
|
|
2040
2075
|
if (!resolver) {
|
|
2041
|
-
if (
|
|
2076
|
+
if (credentialContext.signal?.aborted) {
|
|
2042
2077
|
throw credentialStateQLError(reference, new CredentialResolutionError("cancelled"));
|
|
2043
2078
|
}
|
|
2044
|
-
if (
|
|
2079
|
+
if (credentialContext.deadline <= Date.now()) {
|
|
2045
2080
|
throw credentialStateQLError(reference, new CredentialResolutionError("timeout"));
|
|
2046
2081
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
return value;
|
|
2050
|
-
throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
|
|
2082
|
+
if (source === "secret_env")
|
|
2083
|
+
value = env[reference];
|
|
2051
2084
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2085
|
+
else {
|
|
2086
|
+
const request = {
|
|
2087
|
+
reference,
|
|
2088
|
+
source,
|
|
2089
|
+
actorId: this.actorId,
|
|
2090
|
+
session: { id: session.id, name: session.name },
|
|
2091
|
+
operation,
|
|
2092
|
+
access,
|
|
2093
|
+
...(context.signal ? { signal: context.signal } : {}),
|
|
2094
|
+
...details,
|
|
2095
|
+
};
|
|
2096
|
+
try {
|
|
2097
|
+
value = await resolveCredentialBeforeDeadline(resolver, request, credentialContext);
|
|
2098
|
+
}
|
|
2099
|
+
catch (error) {
|
|
2100
|
+
throw credentialStateQLError(reference, error);
|
|
2101
|
+
}
|
|
2066
2102
|
}
|
|
2067
|
-
|
|
2068
|
-
throw credentialStateQLError(reference,
|
|
2103
|
+
if (!value) {
|
|
2104
|
+
throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
|
|
2069
2105
|
}
|
|
2106
|
+
context.deadline = Date.now() + (context.timeoutMs ?? this.timeoutMs);
|
|
2107
|
+
return value;
|
|
2070
2108
|
}
|
|
2071
2109
|
async openAdapter(connection, context, source) {
|
|
2072
2110
|
try {
|
|
@@ -2402,19 +2440,19 @@ function credentialStateQLError(reference, error) {
|
|
|
2402
2440
|
case "cancelled":
|
|
2403
2441
|
return new StateQLError("OPERATION_CANCELLED", "Credential resolution was cancelled.", { retryable: true });
|
|
2404
2442
|
case "timeout":
|
|
2405
|
-
return new StateQLError("DEADLINE_EXCEEDED", "Credential resolution exceeded the
|
|
2443
|
+
return new StateQLError("DEADLINE_EXCEEDED", "Credential resolution exceeded the credential deadline.", { retryable: true });
|
|
2406
2444
|
}
|
|
2407
2445
|
}
|
|
2408
2446
|
return new StateQLError("CREDENTIAL_RESOLUTION_FAILED", `Credential reference "${reference}" could not be resolved.`, { retryable: true });
|
|
2409
2447
|
}
|
|
2410
2448
|
function safeCredentialErrorMessage(error, source) {
|
|
2411
2449
|
return redact(errorMessage(error).split(source).join("[credential redacted]"))
|
|
2412
|
-
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s
|
|
2450
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s\/@]+(?::[^\s\/@]*)?@/giu, "$1***@");
|
|
2413
2451
|
}
|
|
2414
|
-
function executionTimeout(value) {
|
|
2415
|
-
const timeout = positiveInteger(value,
|
|
2452
|
+
function executionTimeout(value, name = "timeoutMs") {
|
|
2453
|
+
const timeout = positiveInteger(value, name);
|
|
2416
2454
|
if (timeout > 2_147_483_647) {
|
|
2417
|
-
throw new StateQLError("INVALID_COMMAND",
|
|
2455
|
+
throw new StateQLError("INVALID_COMMAND", `${name} cannot exceed 2147483647 milliseconds.`);
|
|
2418
2456
|
}
|
|
2419
2457
|
return timeout;
|
|
2420
2458
|
}
|
package/dist/src/store.d.ts
CHANGED
|
@@ -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;
|
|
@@ -136,6 +138,7 @@ export declare class StateStore {
|
|
|
136
138
|
name: string;
|
|
137
139
|
target?: string;
|
|
138
140
|
secretEnv?: string;
|
|
141
|
+
credentialRef?: string;
|
|
139
142
|
readOnly: boolean;
|
|
140
143
|
}): ProfileRecord;
|
|
141
144
|
getProfile(name: string): ProfileRecord | undefined;
|
|
@@ -149,6 +152,7 @@ export declare class StateStore {
|
|
|
149
152
|
databaseName: string;
|
|
150
153
|
source: string;
|
|
151
154
|
secretEnv?: string;
|
|
155
|
+
credentialRef?: string;
|
|
152
156
|
readOnly: boolean;
|
|
153
157
|
}): ConnectionRecord | undefined;
|
|
154
158
|
getConnection(id: string): ConnectionRecord | undefined;
|
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);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -8,7 +8,8 @@ export interface CommandExecutionContext {
|
|
|
8
8
|
}
|
|
9
9
|
export type CredentialAccess = "read" | "write";
|
|
10
10
|
export type CredentialOperation = "connect" | "query" | "inspect" | "plan" | "exec" | "apply" | "transaction.commit";
|
|
11
|
-
export
|
|
11
|
+
export type CredentialSource = "secret_env" | "credential_ref";
|
|
12
|
+
interface CredentialRequestBase {
|
|
12
13
|
reference: string;
|
|
13
14
|
actorId: string;
|
|
14
15
|
session: {
|
|
@@ -30,6 +31,12 @@ export interface CredentialRequest {
|
|
|
30
31
|
readOnly: boolean;
|
|
31
32
|
};
|
|
32
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
|
+
});
|
|
33
40
|
export type CredentialResolver = (request: CredentialRequest) => string | undefined | Promise<string | undefined>;
|
|
34
41
|
export type StateConfidence = "authoritative" | "transaction_snapshot" | "database_reported" | "local" | "ttl_based" | "unknown";
|
|
35
42
|
export interface Warning {
|
|
@@ -206,6 +213,8 @@ export interface StateQLOptions extends ExecutionOptions {
|
|
|
206
213
|
maxResultBytes?: number;
|
|
207
214
|
maxStateBytes?: number;
|
|
208
215
|
credentialResolver?: CredentialResolver;
|
|
216
|
+
/** Maximum time allowed for one credential resolution; defaults to two minutes. */
|
|
217
|
+
credentialTimeoutMs?: number;
|
|
209
218
|
now?: () => Date;
|
|
210
219
|
}
|
|
211
220
|
export type StateQLActorOptions = Omit<StateQLOptions, "session" | "actor"> & {
|
|
@@ -239,10 +248,12 @@ export interface ConnectOptions extends ExecutionOptions {
|
|
|
239
248
|
readOnly?: boolean;
|
|
240
249
|
secretEnv?: string;
|
|
241
250
|
profile?: string;
|
|
251
|
+
credentialRef?: string;
|
|
242
252
|
}
|
|
243
253
|
export interface ProfileOptions {
|
|
244
254
|
readOnly?: boolean;
|
|
245
255
|
secretEnv?: string;
|
|
256
|
+
credentialRef?: string;
|
|
246
257
|
}
|
|
247
258
|
export interface RowsOptions {
|
|
248
259
|
offset?: number;
|
|
@@ -273,6 +284,7 @@ export interface BatchCommand {
|
|
|
273
284
|
cache?: "auto" | "bypass" | "require";
|
|
274
285
|
read_only?: boolean;
|
|
275
286
|
secret_env?: string;
|
|
287
|
+
credential_ref?: string;
|
|
276
288
|
profile?: string;
|
|
277
289
|
replay?: boolean;
|
|
278
290
|
idempotency_key?: string;
|
|
@@ -311,6 +323,7 @@ export interface ProfileData {
|
|
|
311
323
|
profile: string;
|
|
312
324
|
target: string | null;
|
|
313
325
|
secret_env: string | null;
|
|
326
|
+
credential_ref: string | null;
|
|
314
327
|
read_only: boolean;
|
|
315
328
|
}
|
|
316
329
|
export interface ProfilesData {
|
|
@@ -534,3 +547,4 @@ export interface CloseSessionData {
|
|
|
534
547
|
session_id: string;
|
|
535
548
|
state: string;
|
|
536
549
|
}
|
|
550
|
+
export {};
|