@fadhilp/stateql 0.10.1 → 0.11.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 +144 -67
- package/dist/src/connection.d.ts +7 -1
- package/dist/src/connection.js +117 -12
- package/dist/src/index.d.ts +1 -1
- package/dist/src/migrations.js +120 -0
- package/dist/src/response-data.js +1 -0
- package/dist/src/stateql.d.ts +5 -1
- package/dist/src/stateql.js +176 -57
- package/dist/src/store.d.ts +8 -1
- package/dist/src/store.js +144 -66
- package/dist/src/types.d.ts +13 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ reusable and operations traceable across commands.
|
|
|
7
7
|
|
|
8
8
|
StateQL is built around durable handles:
|
|
9
9
|
|
|
10
|
-
1. Run a query and receive a result handle such as `
|
|
10
|
+
1. Run a query and receive a result handle such as `q_k7m2v5x9c3d6f8h4j2n7p5r9tw`.
|
|
11
11
|
2. Reuse, filter, page, count, alias, or export that stored result without
|
|
12
12
|
rerunning the original SQL.
|
|
13
13
|
3. Use operation, plan, and transaction handles to inspect and control writes.
|
|
@@ -40,25 +40,25 @@ Parameters keep values separate from SQL. `ORDER BY` makes paging stable, and
|
|
|
40
40
|
one-line JSON:
|
|
41
41
|
|
|
42
42
|
```json
|
|
43
|
-
{"ok":true,"handle":"
|
|
43
|
+
{"ok":true,"handle":"q_k7m2v5x9c3d6f8h4j2n7p5r9tw","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"},{"id":18,"name":"Linus","email":"linus@kernel.org"}],"truncated":false,"cached":false,"total":3,"next_offset":null}
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
`
|
|
46
|
+
`q_k7m2v5x9c3d6f8h4j2n7p5r9tw` is a durable snapshot. Filter it locally without accessing the original
|
|
47
47
|
database:
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
stql filter
|
|
50
|
+
stql filter q_k7m2v5x9c3d6f8h4j2n7p5r9tw "email LIKE ?" --param "%@example.com"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
```json
|
|
54
|
-
{"ok":true,"handle":"
|
|
54
|
+
{"ok":true,"handle":"q_z4n8c2v6b3m7k5j9h2g4f6d8sa","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"}],"truncated":false,"cached":false,"total":2,"next_offset":null}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
The filtered snapshot receives its own handle. Give it a readable alias, page
|
|
58
58
|
through it, inspect its count, or export it without rerunning SQL:
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
|
-
stql alias set example-users
|
|
61
|
+
stql alias set example-users q_z4n8c2v6b3m7k5j9h2g4f6d8sa
|
|
62
62
|
stql rows example-users --offset 0 --limit 1
|
|
63
63
|
stql rows example-users --offset 1 --limit 1
|
|
64
64
|
stql count example-users
|
|
@@ -68,16 +68,18 @@ stql export example-users --output example-users.csv --format csv
|
|
|
68
68
|
Example first page:
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
|
-
{"ok":true,"handle":"
|
|
71
|
+
{"ok":true,"handle":"q_z4n8c2v6b3m7k5j9h2g4f6d8sa","rows":[{"id":7,"name":"Ada","email":"ada@example.com"}],"total":2,"truncated":true,"next_offset":1}
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Running the same normalized query with the same parameters reuses `
|
|
74
|
+
Running the same normalized query with the same parameters reuses `q_k7m2v5x9c3d6f8h4j2n7p5r9tw` while
|
|
75
75
|
its cache entry is valid. Use `--cache bypass` when a fresh read is required.
|
|
76
76
|
|
|
77
77
|
## Connections and profiles
|
|
78
78
|
|
|
79
79
|
A connection accepts exactly one source: a direct target, `--env`,
|
|
80
|
-
`--credential-ref`, or `--profile`.
|
|
80
|
+
`--credential-ref`, or `--profile`. Library and batch callers may additionally
|
|
81
|
+
attach `passwordRef`/`password_ref` to a literal password-free remote target;
|
|
82
|
+
it is not a fourth source.
|
|
81
83
|
|
|
82
84
|
```bash
|
|
83
85
|
stql connect <sqlite-path|postgres-url|mysql-url|mongodb-url> [--name NAME] [--read-write]
|
|
@@ -108,16 +110,18 @@ export SQLITE_DATABASE='sqlite:./app.sqlite'
|
|
|
108
110
|
stql connect --env SQLITE_DATABASE --name local --read-only
|
|
109
111
|
```
|
|
110
112
|
|
|
111
|
-
StateQL stores no PostgreSQL, MySQL, or
|
|
112
|
-
URLs must be supplied through `--env
|
|
113
|
-
connection metadata.
|
|
113
|
+
StateQL stores no PostgreSQL, MySQL, MongoDB, or Redis password.
|
|
114
|
+
Credential-bearing URLs must be supplied through `--env` or an opaque full-URL
|
|
115
|
+
credential reference. SQLite paths remain persisted as connection metadata.
|
|
114
116
|
|
|
115
117
|
### Local profiles
|
|
116
118
|
|
|
117
119
|
Profiles store exactly one connection target, environment-variable name, or
|
|
118
|
-
opaque credential reference together with read-only policy.
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
opaque credential reference together with read-only policy. A remote literal
|
|
121
|
+
target may additionally store a `password_ref`; SQLite, environment-backed, and
|
|
122
|
+
full-URL `credential_ref` profiles cannot. Credential values are never stored.
|
|
123
|
+
Profiles persist under `STQL_HOME` with other StateQL metadata, and list/show
|
|
124
|
+
responses include nullable `credential_ref` and `password_ref` fields.
|
|
121
125
|
|
|
122
126
|
```bash
|
|
123
127
|
stql profile add local ./app.sqlite --read-write
|
|
@@ -134,6 +138,21 @@ apply environment-variable syntax or normalization to them. They can only be
|
|
|
134
138
|
resolved by a trusted host `CredentialResolver`, so the standalone CLI may
|
|
135
139
|
store them in profiles but cannot connect with them.
|
|
136
140
|
|
|
141
|
+
Library callers can keep nonsecret endpoint, username, database, TLS, and CA
|
|
142
|
+
options in the literal URL while resolving only its password:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
await stateql.connect(
|
|
146
|
+
"postgres://app@db.example/app?sslmode=verify-full&sslrootcert=/etc/app-ca.pem",
|
|
147
|
+
{ passwordRef: "vault://database/app/password", readOnly: true },
|
|
148
|
+
);
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The same field is accepted by `addProfile`, `updateProfile`, and batch
|
|
152
|
+
`connect`/`profile.add`/`profile.update` commands (snake case in batch input).
|
|
153
|
+
Targets with an embedded password or query parameters that override endpoint or
|
|
154
|
+
credential fields are rejected before credential resolution or driver access.
|
|
155
|
+
|
|
137
156
|
A bare connection target matching a profile name resolves to that profile;
|
|
138
157
|
otherwise it remains a path or database URL.
|
|
139
158
|
|
|
@@ -460,22 +479,58 @@ context as `options.executionContext` for all commands in that batch.
|
|
|
460
479
|
|
|
461
480
|
### Actor workspaces
|
|
462
481
|
|
|
463
|
-
`StateQL.
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
482
|
+
`StateQL.forWorkspace(...)` is a trusted-host primitive that atomically creates
|
|
483
|
+
or reopens a durable workspace, attaches the requested actor, and returns a
|
|
484
|
+
client bound to that actor:
|
|
485
|
+
|
|
486
|
+
```ts
|
|
487
|
+
const stateql = StateQL.forWorkspace({
|
|
488
|
+
home: "./.stql",
|
|
489
|
+
workspace: "pylon-global",
|
|
490
|
+
actor: "pylon-session:abc123",
|
|
491
|
+
credentialResolver,
|
|
492
|
+
signal,
|
|
493
|
+
});
|
|
494
|
+
```
|
|
467
495
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
`
|
|
471
|
-
|
|
472
|
-
|
|
496
|
+
Repeated opens of the same actor and workspace are idempotent. An actor already
|
|
497
|
+
attached elsewhere fails with a `StateQLError` whose code is
|
|
498
|
+
`PERMISSION_DENIED`; StateQL never moves or merges it. All actor options,
|
|
499
|
+
including limits, credential resolution, cancellation, `home`, and `now`, are
|
|
500
|
+
preserved. The workspace name also reserves a same-named actor identity for
|
|
501
|
+
legacy compatibility, so workspace and actor identifiers must be globally
|
|
502
|
+
collision-free. The returned client is still bound only to `actor`, preserving
|
|
503
|
+
plan, transaction, operation, and history ownership.
|
|
504
|
+
|
|
505
|
+
`StateQL.forActor(...)` retains its existing behavior: it resolves the actor's
|
|
506
|
+
attached session directly from StateQL storage and creates a legacy-compatible
|
|
507
|
+
session named after the actor on first use. Use `new StateQL({ session, actor })`
|
|
508
|
+
when the session and membership are already known.
|
|
509
|
+
|
|
510
|
+
Membership management and `forWorkspace` are library-only host capabilities,
|
|
511
|
+
not batch or CLI commands. Existing member-authorized management remains
|
|
512
|
+
available through `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
|
|
513
|
+
`listActors(session)`, and `resolveActor(actorId)`. Integrations should ask for
|
|
514
|
+
user confirmation before changing membership or the shared connection; a host
|
|
515
|
+
calling `forWorkspace` is responsible for authorizing that workspace access.
|
|
473
516
|
|
|
474
517
|
### Harness credential resolution
|
|
475
518
|
|
|
476
|
-
Library integrations can resolve environment-variable names
|
|
477
|
-
credential references through a trusted approval
|
|
478
|
-
instead of mutating `process.env`:
|
|
519
|
+
Library integrations can resolve environment-variable names, opaque full-URL
|
|
520
|
+
credential references, or password-only references through a trusted approval
|
|
521
|
+
or secret-storage layer instead of mutating `process.env`:
|
|
522
|
+
|
|
523
|
+
Integrations pinned to an older published package should gate setup before
|
|
524
|
+
sending `password_ref`:
|
|
525
|
+
|
|
526
|
+
```ts
|
|
527
|
+
if ((StateQL.passwordReferenceVersion ?? 0) < 1) {
|
|
528
|
+
throw new Error("Installed StateQL does not support password references.");
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
`passwordReferenceVersion = 1` guarantees the password-only resolver request,
|
|
533
|
+
validation, persistence, reconnect, and redaction contract documented below.
|
|
479
534
|
|
|
480
535
|
```ts
|
|
481
536
|
import {
|
|
@@ -512,27 +567,31 @@ Credential resolution has its own two-minute default deadline
|
|
|
512
567
|
The database-operation timeout begins after a credential is resolved.
|
|
513
568
|
|
|
514
569
|
When no custom resolver is configured, StateQL reads only `secret_env`
|
|
515
|
-
references from `process.env`; `credential_ref`
|
|
516
|
-
environment. A configured resolver is authoritative for
|
|
517
|
-
`undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls
|
|
518
|
-
process environment. Resolver requests
|
|
519
|
-
`
|
|
520
|
-
legacy secret-environment request objects.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
570
|
+
references from `process.env`; `credential_ref` and `password_ref` never fall
|
|
571
|
+
back to the environment. A configured resolver is authoritative for all
|
|
572
|
+
sources: returning `undefined` produces `CREDENTIAL_UNAVAILABLE` and never falls
|
|
573
|
+
back to the process environment. Resolver requests retain `reference` and
|
|
574
|
+
include `source` (`secret_env`, `credential_ref`, or `password_ref`); source may
|
|
575
|
+
be omitted only on legacy secret-environment request objects. A `password_ref`
|
|
576
|
+
request additionally includes the exact password-free effective `target`.
|
|
577
|
+
Resolvers may throw `CredentialResolutionError` with `denied`, `cancelled`,
|
|
578
|
+
`timeout`, or `unavailable` to produce controlled, secret-free failures. Unknown
|
|
579
|
+
resolver errors are replaced with a generic `CREDENTIAL_RESOLUTION_FAILED`
|
|
580
|
+
response.
|
|
524
581
|
|
|
525
582
|
StateQL calls the resolver only immediately before database access, after SQL
|
|
526
583
|
safety and duplicate checks. Requests contain actor and session identity, the
|
|
527
584
|
operation's effective read/write access, an abort signal, and sanitized
|
|
528
585
|
connection metadata.
|
|
529
586
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
587
|
+
For `secret_env` and `credential_ref`, returned values must be complete
|
|
588
|
+
PostgreSQL, MySQL, MongoDB, or Redis URLs, or explicit `sqlite:` sources. For
|
|
589
|
+
`password_ref`, the resolver returns only the password; an explicit empty string
|
|
590
|
+
is a resolved password, while `undefined` fails closed. StateQL percent-encodes
|
|
591
|
+
and injects only that password into the original target for adapter use, leaving
|
|
592
|
+
all nonsecret URL/TLS/CA bytes unchanged. It persists only the original target
|
|
593
|
+
and reference. Resolved credentials never enter connection metadata, history,
|
|
594
|
+
snapshots, cache keys, responses, or stored errors.
|
|
536
595
|
|
|
537
596
|
Harnesses remain responsible for approval policy, binding lifetime, revocation,
|
|
538
597
|
and keeping values out of their own logs and model-visible data.
|
|
@@ -547,22 +606,26 @@ retry.
|
|
|
547
606
|
### Result identities and aliases
|
|
548
607
|
|
|
549
608
|
Every materialized SQL, MongoDB, Redis, table, or derived result keeps its
|
|
550
|
-
immutable `q_*` `result_id
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
609
|
+
immutable canonical `q_*` `result_id`. New canonical resource IDs use a
|
|
610
|
+
cryptographically random 26-character lowercase base32 suffix; existing
|
|
611
|
+
incremental IDs such as `q_121` remain valid and are not rewritten. Results also
|
|
612
|
+
receive a random 10-character lowercase base32 `display_alias`.
|
|
613
|
+
`ResultData.alias` normally equals that alias. When a batch command supplies
|
|
614
|
+
`as`, `alias` remains the caller alias for backward compatibility while
|
|
615
|
+
`display_alias` remains canonical. Generated aliases are session-scoped,
|
|
616
|
+
allocated atomically with the result, stable on cache reuse, and cannot be
|
|
617
|
+
reassigned by `setAlias`; explicit aliases and all old handles continue to
|
|
618
|
+
resolve.
|
|
619
|
+
|
|
620
|
+
Connections likewise retain canonical `conn_*` IDs with random 26-character
|
|
621
|
+
suffixes and receive persistent random 10-character lowercase base32 aliases,
|
|
622
|
+
exposed as `alias` and `display_alias` by `connect()` and
|
|
623
|
+
`snapshot().connection` (optional in snapshot types for older producers).
|
|
624
|
+
Connection aliases are unique within the state store, allocated atomically with
|
|
625
|
+
the connection, and backfilled for existing records on startup. They survive
|
|
626
|
+
reopening; reconnecting creates a new ID and alias. They are display identities
|
|
627
|
+
only, separate from result aliases; internal references and lookups continue to
|
|
628
|
+
use canonical connection IDs.
|
|
566
629
|
|
|
567
630
|
### Safe profile updates
|
|
568
631
|
|
|
@@ -571,19 +634,33 @@ updateProfile(name, {
|
|
|
571
634
|
target?: string | null,
|
|
572
635
|
secretEnv?: string | null,
|
|
573
636
|
credentialRef?: string | null,
|
|
637
|
+
passwordRef?: string | null,
|
|
574
638
|
readOnly?: boolean,
|
|
575
639
|
})
|
|
576
640
|
```
|
|
577
641
|
|
|
578
|
-
Omitting all source fields keeps the existing source
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
a
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
642
|
+
Omitting all source and password-reference fields keeps the existing source and
|
|
643
|
+
adjunct reference. Supplying any source field replaces the source atomically:
|
|
644
|
+
exactly one non-null source is required and the other source columns are
|
|
645
|
+
cleared. An omitted `passwordRef` is preserved when the existing literal target
|
|
646
|
+
is unchanged; changing the target clears it unless the update explicitly supplies
|
|
647
|
+
a replacement. Replacing the source with `secretEnv` or `credentialRef` clears
|
|
648
|
+
it. An explicit non-null password reference combined with either
|
|
649
|
+
reference-backed source is rejected. Direct URLs
|
|
650
|
+
with embedded passwords or secret-like query parameters are rejected.
|
|
651
|
+
`profile.list/show/update` return only
|
|
652
|
+
`{profile,target,secret_env,credential_ref,password_ref,read_only}`. Profile
|
|
653
|
+
changes affect subsequent `connect` calls and do not silently mutate an
|
|
654
|
+
already-open connection.
|
|
655
|
+
|
|
656
|
+
The `password_refs_v1` migration adds nullable `password_ref` columns to both
|
|
657
|
+
profiles and connections and enforces that they accompany only literal target
|
|
658
|
+
configuration. It rejects incompatible profile schemas/rows instead of dropping
|
|
659
|
+
references. Downgrading a state home containing password references is
|
|
660
|
+
unsupported: older binaries do not resolve this source and may attempt the
|
|
661
|
+
password-free target using ambient/trust authentication; constraint-protected
|
|
662
|
+
source replacements may also fail. Use the same or newer StateQL binary, or
|
|
663
|
+
explicitly clear all password references before downgrade.
|
|
587
664
|
|
|
588
665
|
### Bounded catalog
|
|
589
666
|
|
package/dist/src/connection.d.ts
CHANGED
|
@@ -4,12 +4,18 @@ 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
6
|
export declare function redisDatabaseName(target: string): string;
|
|
7
|
-
export declare function credentialSource(value: string, expectedDriver?: Driver, referenceSource?: CredentialSource): {
|
|
7
|
+
export declare function credentialSource(value: string, expectedDriver?: Driver, referenceSource?: Exclude<CredentialSource, "password_ref">): {
|
|
8
8
|
driver: Driver;
|
|
9
9
|
source: string;
|
|
10
10
|
};
|
|
11
11
|
export declare function normalizeSqliteSource(target: string): string;
|
|
12
12
|
export declare function databaseUrlHasSecret(target: string): boolean;
|
|
13
|
+
export declare function validatePasswordReferenceTarget(target: string): Driver;
|
|
14
|
+
/** Injects only password userinfo while retaining every nonsecret target byte. */
|
|
15
|
+
export declare function injectPassword(target: string, password: string): {
|
|
16
|
+
driver: Driver;
|
|
17
|
+
source: string;
|
|
18
|
+
};
|
|
13
19
|
export declare function version(connection: ConnectionRecord): string;
|
|
14
20
|
export declare function confidence(connection: ConnectionRecord): StateConfidence;
|
|
15
21
|
export declare function validateProfileName(name: string): void;
|
package/dist/src/connection.js
CHANGED
|
@@ -7,6 +7,7 @@ export function databaseIdentity(connection) {
|
|
|
7
7
|
source: connection.source,
|
|
8
8
|
secretEnvironment: connection.secret_env,
|
|
9
9
|
credentialReference: connection.credential_ref,
|
|
10
|
+
passwordReference: connection.password_ref,
|
|
10
11
|
};
|
|
11
12
|
}
|
|
12
13
|
export function detectDriver(target) {
|
|
@@ -24,22 +25,26 @@ export function detectDriver(target) {
|
|
|
24
25
|
return "sqlite";
|
|
25
26
|
}
|
|
26
27
|
export function mongoDatabaseName(target) {
|
|
27
|
-
let
|
|
28
|
+
let pathname;
|
|
28
29
|
try {
|
|
29
|
-
url = new URL(target);
|
|
30
|
+
const url = new URL(target);
|
|
31
|
+
if (!["mongodb:", "mongodb+srv:"].includes(url.protocol.toLowerCase()) || !url.hostname)
|
|
32
|
+
throw new Error();
|
|
33
|
+
pathname = url.pathname;
|
|
30
34
|
}
|
|
31
35
|
catch {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const match = /^mongodb(?:\+srv)?:\/\/([^/?#]+)(\/[^?#]*)?(?:[?#]|$)/i.exec(target);
|
|
37
|
+
if (!match)
|
|
38
|
+
throw new StateQLError("INVALID_COMMAND", "Invalid MongoDB URL.");
|
|
39
|
+
const authority = match[1];
|
|
40
|
+
const at = authority.lastIndexOf("@");
|
|
41
|
+
validateMongoHosts(authority.slice(at + 1), /^mongodb\+srv:/i.test(target));
|
|
42
|
+
pathname = match[2] ?? "";
|
|
37
43
|
}
|
|
38
44
|
try {
|
|
39
|
-
const database = decodeURIComponent(
|
|
40
|
-
if (database && !database.includes("/") && !database.includes("\0"))
|
|
45
|
+
const database = decodeURIComponent(pathname.replace(/^\//, ""));
|
|
46
|
+
if (database && !database.includes("/") && !database.includes("\0"))
|
|
41
47
|
return database;
|
|
42
|
-
}
|
|
43
48
|
}
|
|
44
49
|
catch {
|
|
45
50
|
// Report malformed escaping as an invalid explicit database name.
|
|
@@ -98,8 +103,15 @@ export function normalizeSqliteSource(target) {
|
|
|
98
103
|
}
|
|
99
104
|
export function databaseUrlHasSecret(target) {
|
|
100
105
|
try {
|
|
101
|
-
const
|
|
102
|
-
|
|
106
|
+
const match = /^([a-z][a-z\d+.-]*:\/\/)([^/?#]*)([\s\S]*)$/i.exec(target);
|
|
107
|
+
if (!match)
|
|
108
|
+
throw new Error();
|
|
109
|
+
const authority = match[2];
|
|
110
|
+
const at = authority.lastIndexOf("@");
|
|
111
|
+
const url = /^mongodb(?:\+srv)?:\/\//i.test(target)
|
|
112
|
+
? new URL(`http://placeholder${match[3]}`)
|
|
113
|
+
: new URL(target);
|
|
114
|
+
return ((at >= 0 && authority.slice(0, at).includes(":")) ||
|
|
103
115
|
Boolean(url.password) ||
|
|
104
116
|
[...url.searchParams.keys()].some((key) => /pass|token|secret|private[_-]?key|api[_-]?key/i.test(key)));
|
|
105
117
|
}
|
|
@@ -107,6 +119,99 @@ export function databaseUrlHasSecret(target) {
|
|
|
107
119
|
throw new StateQLError("INVALID_COMMAND", "Invalid database URL.");
|
|
108
120
|
}
|
|
109
121
|
}
|
|
122
|
+
const AMBIGUOUS_PASSWORD_TARGET_PARAMETERS = new Set([
|
|
123
|
+
"host",
|
|
124
|
+
"hostaddr",
|
|
125
|
+
"hostname",
|
|
126
|
+
"port",
|
|
127
|
+
"socket",
|
|
128
|
+
"socketpath",
|
|
129
|
+
"user",
|
|
130
|
+
"username",
|
|
131
|
+
]);
|
|
132
|
+
export function validatePasswordReferenceTarget(target) {
|
|
133
|
+
if (/\s/.test(target)) {
|
|
134
|
+
throw new StateQLError("INVALID_COMMAND", "Password-reference target must not contain whitespace.");
|
|
135
|
+
}
|
|
136
|
+
const driver = detectDriver(target);
|
|
137
|
+
if (driver === "sqlite") {
|
|
138
|
+
throw new StateQLError("INVALID_COMMAND", "Password references require a remote PostgreSQL, MySQL, MongoDB, or Redis target.");
|
|
139
|
+
}
|
|
140
|
+
const authorityMatch = /^([a-z][a-z\d+.-]*:\/\/)([^/?#]*)([\s\S]*)$/i.exec(target);
|
|
141
|
+
if (!authorityMatch) {
|
|
142
|
+
throw new StateQLError("INVALID_COMMAND", "Password-reference target must be a valid remote database URL.");
|
|
143
|
+
}
|
|
144
|
+
const authority = authorityMatch[2];
|
|
145
|
+
const at = authority.lastIndexOf("@");
|
|
146
|
+
const userinfo = at >= 0 ? authority.slice(0, at) : "";
|
|
147
|
+
if (userinfo.includes(":") || userinfo.includes("@")) {
|
|
148
|
+
throw new StateQLError("PERMISSION_DENIED", "Password-reference target must not contain an embedded password.");
|
|
149
|
+
}
|
|
150
|
+
if (/%(?![0-9a-f]{2})/i.test(userinfo)) {
|
|
151
|
+
throw new StateQLError("INVALID_COMMAND", "Password-reference target contains malformed username escaping.");
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (userinfo)
|
|
155
|
+
decodeURIComponent(userinfo);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
throw new StateQLError("INVALID_COMMAND", "Password-reference target contains malformed username escaping.");
|
|
159
|
+
}
|
|
160
|
+
let url;
|
|
161
|
+
try {
|
|
162
|
+
if (driver === "mongodb") {
|
|
163
|
+
validateMongoHosts(authority.slice(at + 1), /^mongodb\+srv:/i.test(target));
|
|
164
|
+
url = new URL(`http://placeholder${authorityMatch[3]}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
url = new URL(target);
|
|
168
|
+
if (!url.hostname || url.password)
|
|
169
|
+
throw new Error();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw new StateQLError("INVALID_COMMAND", "Password-reference target must be a valid remote database URL.");
|
|
174
|
+
}
|
|
175
|
+
for (const key of url.searchParams.keys()) {
|
|
176
|
+
const normalized = key.toLowerCase().replaceAll("_", "").replaceAll("-", "");
|
|
177
|
+
if (AMBIGUOUS_PASSWORD_TARGET_PARAMETERS.has(normalized) ||
|
|
178
|
+
/pass|token|secret|privatekey|apikey/.test(normalized)) {
|
|
179
|
+
throw new StateQLError("PERMISSION_DENIED", "Password-reference target must not contain endpoint or credential query overrides.");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (driver === "mongodb" && !userinfo) {
|
|
183
|
+
throw new StateQLError("INVALID_COMMAND", "MongoDB password-reference targets require a username.");
|
|
184
|
+
}
|
|
185
|
+
return driver;
|
|
186
|
+
}
|
|
187
|
+
function validateMongoHosts(hosts, srv) {
|
|
188
|
+
const entries = hosts.split(",");
|
|
189
|
+
if (!hosts || (srv && entries.length !== 1) || entries.some((host) => {
|
|
190
|
+
if (srv)
|
|
191
|
+
return !/^[^:[\],%]+$/u.test(host);
|
|
192
|
+
return !(/^[^:[\],%]+(?::\d+)?$/u.test(host) || /^\[[0-9a-f:.]+\](?::\d+)?$/iu.test(host));
|
|
193
|
+
})) {
|
|
194
|
+
throw new StateQLError("INVALID_COMMAND", "Password-reference target must be a valid remote database URL.");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** Injects only password userinfo while retaining every nonsecret target byte. */
|
|
198
|
+
export function injectPassword(target, password) {
|
|
199
|
+
const driver = validatePasswordReferenceTarget(target);
|
|
200
|
+
let encoded;
|
|
201
|
+
try {
|
|
202
|
+
encoded = encodeURIComponent(password);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
throw new StateQLError("CREDENTIAL_RESOLUTION_FAILED", "Resolved password could not be encoded.");
|
|
206
|
+
}
|
|
207
|
+
const match = /^([a-z][a-z\d+.-]*:\/\/)([^/?#]*)([\s\S]*)$/i.exec(target);
|
|
208
|
+
const authority = match[2];
|
|
209
|
+
const at = authority.lastIndexOf("@");
|
|
210
|
+
const injectedAuthority = at >= 0
|
|
211
|
+
? `${authority.slice(0, at)}:${encoded}${authority.slice(at)}`
|
|
212
|
+
: `:${encoded}@${authority}`;
|
|
213
|
+
return { driver, source: `${match[1]}${injectedAuthority}${match[3]}` };
|
|
214
|
+
}
|
|
110
215
|
export function version(connection) {
|
|
111
216
|
return `sv_${connection.version}`;
|
|
112
217
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { StateQL } from "./stateql.js";
|
|
|
2
2
|
export { CredentialResolutionError, StateQLError, exitCodeFor, } from "./errors.js";
|
|
3
3
|
export type { CredentialResolutionFailure } from "./errors.js";
|
|
4
4
|
export type { TableChange, TableIdentity, TableUpdate } from "./table-editor.js";
|
|
5
|
-
export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CatalogObject, CatalogObjectKind, DescribeObjectData, ListObjectsData, ListObjectsFilter, 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, HistoryCategory, HistoryOptions, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfileUpdateOptions, ProfilesData, PurgeData, RedisCommand, RedisExecOptions, RedisPlanOptions, RedisQueryOptions, RedisWriteOutcome, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StateQLSnapshotOptions, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
|
|
5
|
+
export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CatalogObject, CatalogObjectKind, DescribeObjectData, ListObjectsData, ListObjectsFilter, 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, HistoryCategory, HistoryOptions, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfileUpdateOptions, ProfilesData, PurgeData, RedisCommand, RedisExecOptions, RedisPlanOptions, RedisQueryOptions, RedisWriteOutcome, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StateQLSnapshotOptions, StateQLWorkspaceOptions, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
|