@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 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 `q_1`.
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":"q_1","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"},{"id":18,"name":"Linus","email":"linus@kernel.org"}],"truncated":false,"cached":false,"total":3,"next_offset":null}
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
- `q_1` is a durable snapshot. Filter it locally without accessing the original
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 q_1 "email LIKE ?" --param "%@example.com"
50
+ stql filter q_k7m2v5x9c3d6f8h4j2n7p5r9tw "email LIKE ?" --param "%@example.com"
51
51
  ```
52
52
 
53
53
  ```json
54
- {"ok":true,"handle":"q_2","rows":[{"id":7,"name":"Ada","email":"ada@example.com"},{"id":12,"name":"Grace","email":"grace@example.com"}],"truncated":false,"cached":false,"total":2,"next_offset":null}
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 q_2
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":"q_2","rows":[{"id":7,"name":"Ada","email":"ada@example.com"}],"total":2,"truncated":true,"next_offset":1}
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 `q_1` while
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 MongoDB password. Credential-bearing
112
- URLs must be supplied through `--env`. SQLite paths remain persisted as
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. 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.
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.forActor(...)` resolves the actor's attached session directly from
464
- StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
465
- On first use, it creates a legacy-compatible session named after the actor. Use
466
- `new StateQL({ session, actor })` when the session is already known.
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
- Membership is managed only through the library API, not batch commands:
469
- `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
470
- `listActors(session)`, and `resolveActor(actorId)`. An existing member must link
471
- an actor before that actor opens an existing workspace. Integrations should ask
472
- for user confirmation before changing membership or the shared connection.
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 or opaque
477
- credential references through a trusted approval or secret-storage layer
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` 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.
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
- Returned values must be complete PostgreSQL, MySQL, MongoDB, or Redis URLs, or
531
- explicit `sqlite:` sources. StateQL validates the source and its stored driver before
532
- adapter construction and normalizes SQLite paths. Credential-bearing database
533
- URLs are redacted before connection metadata is persisted and never enter
534
- history, snapshots, cache keys, or responses. SQLite paths remain persisted
535
- connection metadata, as they are for direct SQLite connections.
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` and receives a cryptographically random 10-character
551
- lowercase base32 `display_alias`. `ResultData.alias` normally equals that alias.
552
- When a batch command supplies `as`, `alias` remains the caller alias for backward
553
- compatibility while `display_alias` remains canonical. Generated aliases are
554
- session-scoped, allocated atomically with the result, stable on cache reuse, and
555
- cannot be reassigned by `setAlias`; explicit aliases and all old handles continue
556
- to resolve.
557
-
558
- Connections likewise retain canonical `conn_*` IDs and receive persistent random
559
- 10-character lowercase base32 aliases, exposed as `alias` and `display_alias` by
560
- `connect()` and `snapshot().connection` (optional in snapshot types for older
561
- producers). Connection aliases are unique within the state store, allocated
562
- atomically with the connection, and backfilled for existing records on startup.
563
- They survive reopening; reconnecting creates a new ID and alias. They are display
564
- identities only, separate from result aliases; internal references and lookups
565
- continue to use canonical connection IDs.
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. Supplying any source field
579
- replaces the source atomically: exactly one non-null source is required and the
580
- other source columns are cleared. Direct non-SQLite URLs containing credentials
581
- or secret-like query parameters are rejected. `profile.list/show/update` return
582
- only `{profile,target,secret_env,credential_ref,read_only}`. `target` is therefore
583
- a normalized SQLite path or a secret-free URL; reference-backed profiles expose
584
- only the environment-variable name or opaque credential reference, never a
585
- resolved value. Profile changes affect subsequent `connect` calls and do not
586
- silently mutate an already-open connection.
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
 
@@ -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;
@@ -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 url;
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
- throw new StateQLError("INVALID_COMMAND", "Invalid MongoDB URL.");
33
- }
34
- if (!["mongodb:", "mongodb+srv:"].includes(url.protocol.toLowerCase()) ||
35
- !url.hostname) {
36
- throw new StateQLError("INVALID_COMMAND", "Invalid MongoDB URL.");
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(url.pathname.replace(/^\//, ""));
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 url = new URL(target);
102
- return (Boolean(url.username) ||
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
  }
@@ -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";