@fadhilp/stateql 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -49
- package/dist/src/connection.d.ts +7 -1
- package/dist/src/connection.js +117 -12
- package/dist/src/migrations.js +131 -0
- package/dist/src/response-data.js +1 -0
- package/dist/src/stateql.d.ts +2 -0
- package/dist/src/stateql.js +158 -56
- package/dist/src/store.d.ts +10 -1
- package/dist/src/store.js +118 -66
- package/dist/src/types.d.ts +15 -1
- package/package.json +1 -1
package/dist/src/stateql.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeFileSync } from "node:fs";
|
|
|
5
5
|
import { basename, resolve } from "node:path";
|
|
6
6
|
import { env } from "node:process";
|
|
7
7
|
import { AdapterExecutionError, AdapterWriteError, BatchWriteError, createAdapter, createAdapterContext, } from "./adapters.js";
|
|
8
|
-
import { confidence, credentialSource, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, mongoDatabaseName, redisDatabaseName, normalizeSqliteSource, validateCredentialRef, validateProfileName, version, } from "./connection.js";
|
|
8
|
+
import { confidence, credentialSource, databaseIdentity, databaseUrlHasSecret, detectDriver, isEnvironmentName, injectPassword, mongoDatabaseName, redisDatabaseName, normalizeSqliteSource, validateCredentialRef, validatePasswordReferenceTarget, validateProfileName, version, } from "./connection.js";
|
|
9
9
|
import { asStateQLError, CredentialResolutionError, StateQLError, } from "./errors.js";
|
|
10
10
|
import { analyzeMongoWriteSafety, deserializeMongoWriteCommand, serializeMongoCommand, validateMongoReadCommand, validateMongoWriteCommand, MongoAdapter, } from "./mongodb.js";
|
|
11
11
|
import { deserializeRedisCommand, RedisAdapter, serializeRedisCommand, validateRedisReadCommand, validateRedisWriteCommand, } from "./redis.js";
|
|
@@ -18,6 +18,8 @@ const DEFAULT_SNAPSHOT_HISTORY_LIMIT = 50;
|
|
|
18
18
|
const MAX_SNAPSHOT_HISTORY_LIMIT = 100;
|
|
19
19
|
const DEFAULT_CREDENTIAL_RESOLUTION_TIMEOUT_MS = 120_000;
|
|
20
20
|
export class StateQL {
|
|
21
|
+
/** Runtime contract marker for passwordRef/password_ref support. */
|
|
22
|
+
static passwordReferenceVersion = 1;
|
|
21
23
|
static forActor(options) {
|
|
22
24
|
if (!options.actor.trim()) {
|
|
23
25
|
throw new StateQLError("INVALID_COMMAND", "Actor ID is required.");
|
|
@@ -112,7 +114,13 @@ export class StateQL {
|
|
|
112
114
|
if (sourceCount !== 1) {
|
|
113
115
|
throw new StateQLError("INVALID_COMMAND", "Use exactly one connection target, profile, secret environment variable, or credential reference.");
|
|
114
116
|
}
|
|
115
|
-
|
|
117
|
+
if (options.passwordRef !== undefined &&
|
|
118
|
+
(target === undefined || options.profile !== undefined ||
|
|
119
|
+
options.secretEnv !== undefined || options.credentialRef !== undefined)) {
|
|
120
|
+
throw new StateQLError("INVALID_COMMAND", "A password reference requires a literal remote connection target.");
|
|
121
|
+
}
|
|
122
|
+
const implicitProfile = !options.profile && !options.secretEnv && !options.credentialRef &&
|
|
123
|
+
options.passwordRef === undefined && target
|
|
116
124
|
? this.store.getProfile(target)
|
|
117
125
|
: undefined;
|
|
118
126
|
const profile = options.profile
|
|
@@ -122,18 +130,27 @@ export class StateQL {
|
|
|
122
130
|
throw new StateQLError("CONNECTION_NOT_FOUND", `Profile "${options.profile}" was not found.`, { suggestedAction: "Run stql profile list." });
|
|
123
131
|
}
|
|
124
132
|
if (profile &&
|
|
125
|
-
[profile.target, profile.secret_env, profile.credential_ref]
|
|
126
|
-
.filter((value) => value !== null).length !== 1
|
|
127
|
-
|
|
133
|
+
([profile.target, profile.secret_env, profile.credential_ref]
|
|
134
|
+
.filter((value) => value !== null).length !== 1 ||
|
|
135
|
+
(profile.password_ref !== null && profile.target === null))) {
|
|
136
|
+
throw new StateQLError("STATE_CORRUPTED", `Profile "${profile.name}" has an invalid connection source.`);
|
|
128
137
|
}
|
|
129
138
|
const resolvedTarget = profile?.target ?? target;
|
|
130
139
|
const secretEnv = options.secretEnv ?? profile?.secret_env ?? undefined;
|
|
131
140
|
const credentialRef = options.credentialRef ?? profile?.credential_ref ?? undefined;
|
|
141
|
+
const passwordRef = options.passwordRef ?? profile?.password_ref ?? undefined;
|
|
132
142
|
if (secretEnv !== undefined && !isEnvironmentName(secretEnv)) {
|
|
133
143
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
134
144
|
}
|
|
135
145
|
if (credentialRef !== undefined)
|
|
136
146
|
validateCredentialRef(credentialRef);
|
|
147
|
+
if (passwordRef !== undefined) {
|
|
148
|
+
validateCredentialRef(passwordRef);
|
|
149
|
+
if (!resolvedTarget) {
|
|
150
|
+
throw new StateQLError("INVALID_COMMAND", "A password reference requires a literal remote connection target.");
|
|
151
|
+
}
|
|
152
|
+
validatePasswordReferenceTarget(resolvedTarget);
|
|
153
|
+
}
|
|
137
154
|
const credentialReference = secretEnv ?? credentialRef;
|
|
138
155
|
const credentialReferenceSource = secretEnv !== undefined
|
|
139
156
|
? "secret_env"
|
|
@@ -141,50 +158,65 @@ export class StateQL {
|
|
|
141
158
|
const readOnly = options.readOnly ??
|
|
142
159
|
(profile ? Boolean(profile.read_only) : true);
|
|
143
160
|
const context = this.executionContext(options);
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
let adapterSource;
|
|
162
|
+
let driver;
|
|
163
|
+
if (passwordRef !== undefined) {
|
|
164
|
+
const password = await this.resolveCredential(passwordRef, "password_ref", session, "connect", readOnly ? "read" : "write", context, {
|
|
146
165
|
...(profile ? { profile: { name: profile.name } } : {}),
|
|
147
166
|
requestedReadOnly: readOnly,
|
|
148
|
-
})
|
|
149
|
-
: resolvedTarget;
|
|
150
|
-
if (!secret) {
|
|
151
|
-
throw new StateQLError("INVALID_COMMAND", "Connection target is required.");
|
|
152
|
-
}
|
|
153
|
-
const resolvedSource = credentialReferenceSource
|
|
154
|
-
? credentialSource(secret, undefined, credentialReferenceSource)
|
|
155
|
-
: { driver: detectDriver(secret), source: secret };
|
|
156
|
-
const { driver } = resolvedSource;
|
|
157
|
-
if (driver !== "sqlite" &&
|
|
158
|
-
!credentialReference &&
|
|
159
|
-
databaseUrlHasSecret(secret)) {
|
|
160
|
-
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`, {
|
|
161
|
-
suggestedAction: "Use an environment variable or trusted host credential reference.",
|
|
162
|
-
});
|
|
167
|
+
}, resolvedTarget);
|
|
168
|
+
({ driver, source: adapterSource } = injectPassword(resolvedTarget, password));
|
|
163
169
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
else {
|
|
171
|
+
const secret = credentialReference && credentialReferenceSource
|
|
172
|
+
? await this.resolveCredential(credentialReference, credentialReferenceSource, session, "connect", readOnly ? "read" : "write", context, {
|
|
173
|
+
...(profile ? { profile: { name: profile.name } } : {}),
|
|
174
|
+
requestedReadOnly: readOnly,
|
|
175
|
+
})
|
|
176
|
+
: resolvedTarget;
|
|
177
|
+
if (!secret) {
|
|
178
|
+
throw new StateQLError("INVALID_COMMAND", "Connection target is required.");
|
|
179
|
+
}
|
|
180
|
+
const resolvedSource = credentialReferenceSource
|
|
181
|
+
? credentialSource(secret, undefined, credentialReferenceSource)
|
|
182
|
+
: { driver: detectDriver(secret), source: secret };
|
|
183
|
+
driver = resolvedSource.driver;
|
|
184
|
+
if (driver !== "sqlite" &&
|
|
185
|
+
!credentialReference &&
|
|
186
|
+
databaseUrlHasSecret(secret)) {
|
|
187
|
+
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`, {
|
|
188
|
+
suggestedAction: "Use an environment variable or trusted host credential reference.",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
adapterSource = credentialReferenceSource
|
|
192
|
+
? resolvedSource.source
|
|
193
|
+
: driver === "sqlite" ? normalizeSqliteSource(secret) : secret;
|
|
194
|
+
}
|
|
195
|
+
const persistedSource = passwordRef !== undefined
|
|
196
|
+
? resolvedTarget
|
|
197
|
+
: driver === "sqlite"
|
|
198
|
+
? adapterSource
|
|
199
|
+
: credentialReferenceSource
|
|
200
|
+
? redact(adapterSource)
|
|
201
|
+
: adapterSource;
|
|
202
|
+
const identitySource = passwordRef !== undefined ? resolvedTarget : adapterSource;
|
|
172
203
|
const databaseName = driver === "sqlite"
|
|
173
204
|
? basename(adapterSource)
|
|
174
205
|
: driver === "mongodb"
|
|
175
|
-
? mongoDatabaseName(
|
|
206
|
+
? mongoDatabaseName(identitySource)
|
|
176
207
|
: driver === "redis"
|
|
177
|
-
? redisDatabaseName(
|
|
178
|
-
: new URL(
|
|
208
|
+
? redisDatabaseName(identitySource)
|
|
209
|
+
: new URL(identitySource).pathname.replace(/^\//, "") || driver;
|
|
179
210
|
const draft = {
|
|
180
211
|
id: "pending",
|
|
181
212
|
session_id: session.id,
|
|
182
213
|
name: options.name ?? profile?.name ?? databaseName,
|
|
183
214
|
driver,
|
|
184
215
|
database_name: databaseName,
|
|
185
|
-
source,
|
|
216
|
+
source: persistedSource,
|
|
186
217
|
secret_env: secretEnv ?? null,
|
|
187
218
|
credential_ref: credentialRef ?? null,
|
|
219
|
+
password_ref: passwordRef ?? null,
|
|
188
220
|
read_only: readOnly ? 1 : 0,
|
|
189
221
|
version: 0,
|
|
190
222
|
created_at: this.now().toISOString(),
|
|
@@ -212,9 +244,10 @@ export class StateQL {
|
|
|
212
244
|
name: draft.name,
|
|
213
245
|
driver,
|
|
214
246
|
databaseName,
|
|
215
|
-
source,
|
|
247
|
+
source: persistedSource,
|
|
216
248
|
...(secretEnv ? { secretEnv } : {}),
|
|
217
249
|
...(credentialRef ? { credentialRef } : {}),
|
|
250
|
+
...(passwordRef !== undefined ? { passwordRef } : {}),
|
|
218
251
|
readOnly,
|
|
219
252
|
});
|
|
220
253
|
if (!connection) {
|
|
@@ -223,6 +256,8 @@ export class StateQL {
|
|
|
223
256
|
return {
|
|
224
257
|
data: {
|
|
225
258
|
connection_id: connection.id,
|
|
259
|
+
alias: connection.alias,
|
|
260
|
+
display_alias: connection.alias,
|
|
226
261
|
driver,
|
|
227
262
|
database: databaseName,
|
|
228
263
|
name: connection.name,
|
|
@@ -248,12 +283,14 @@ export class StateQL {
|
|
|
248
283
|
target,
|
|
249
284
|
secretEnv: options.secretEnv,
|
|
250
285
|
credentialRef: options.credentialRef,
|
|
286
|
+
passwordRef: options.passwordRef,
|
|
251
287
|
});
|
|
252
288
|
const profile = this.store.addProfile({
|
|
253
289
|
name,
|
|
254
290
|
target: source.target ?? undefined,
|
|
255
291
|
secretEnv: source.secretEnv ?? undefined,
|
|
256
292
|
credentialRef: source.credentialRef ?? undefined,
|
|
293
|
+
passwordRef: source.passwordRef ?? undefined,
|
|
257
294
|
readOnly: options.readOnly ?? true,
|
|
258
295
|
});
|
|
259
296
|
return {
|
|
@@ -270,22 +307,32 @@ export class StateQL {
|
|
|
270
307
|
if (!existing)
|
|
271
308
|
throw new StateQLError("CONNECTION_NOT_FOUND", `Profile "${name}" was not found.`);
|
|
272
309
|
if (!changes || typeof changes !== "object" || Array.isArray(changes) ||
|
|
273
|
-
Object.keys(changes).some((key) => !["target", "secretEnv", "credentialRef", "readOnly"].includes(key))) {
|
|
310
|
+
Object.keys(changes).some((key) => !["target", "secretEnv", "credentialRef", "passwordRef", "readOnly"].includes(key))) {
|
|
274
311
|
throw new StateQLError("INVALID_COMMAND", "Profile update contains unknown fields.");
|
|
275
312
|
}
|
|
276
313
|
if (changes.readOnly !== undefined && typeof changes.readOnly !== "boolean")
|
|
277
314
|
throw new StateQLError("INVALID_COMMAND", "Profile readOnly must be boolean.");
|
|
278
315
|
const changesSource = Object.hasOwn(changes, "target") || Object.hasOwn(changes, "secretEnv") || Object.hasOwn(changes, "credentialRef");
|
|
279
|
-
|
|
316
|
+
const changesPassword = Object.hasOwn(changes, "passwordRef");
|
|
317
|
+
if (!changesSource && !changesPassword && changes.readOnly === undefined)
|
|
280
318
|
throw new StateQLError("INVALID_COMMAND", "Profile update has no changes.");
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
319
|
+
const targetSource = changesSource ? changes.target ?? undefined : existing.target ?? undefined;
|
|
320
|
+
const source = validatedProfileSource({
|
|
321
|
+
target: targetSource,
|
|
322
|
+
secretEnv: changesSource ? changes.secretEnv ?? undefined : existing.secret_env ?? undefined,
|
|
323
|
+
credentialRef: changesSource ? changes.credentialRef ?? undefined : existing.credential_ref ?? undefined,
|
|
324
|
+
passwordRef: changesPassword
|
|
325
|
+
? changes.passwordRef ?? undefined
|
|
326
|
+
: !changesSource || targetSource === existing.target
|
|
327
|
+
? existing.password_ref ?? undefined
|
|
328
|
+
: undefined,
|
|
329
|
+
});
|
|
284
330
|
const profile = this.store.updateProfile({
|
|
285
331
|
name,
|
|
286
332
|
target: source.target,
|
|
287
333
|
secretEnv: source.secretEnv,
|
|
288
334
|
credentialRef: source.credentialRef,
|
|
335
|
+
passwordRef: source.passwordRef,
|
|
289
336
|
readOnly: changes.readOnly ?? Boolean(existing.read_only),
|
|
290
337
|
});
|
|
291
338
|
if (!profile)
|
|
@@ -368,6 +415,8 @@ export class StateQL {
|
|
|
368
415
|
connection: connection
|
|
369
416
|
? {
|
|
370
417
|
connection_id: connection.id,
|
|
418
|
+
alias: connection.alias,
|
|
419
|
+
display_alias: connection.alias,
|
|
371
420
|
name: connection.name,
|
|
372
421
|
status: "connected",
|
|
373
422
|
driver: connection.driver,
|
|
@@ -1707,7 +1756,7 @@ export class StateQL {
|
|
|
1707
1756
|
const planParameters = compiled?.params ?? (nativePlan || tableUpdates
|
|
1708
1757
|
? undefined
|
|
1709
1758
|
: parseJson(plan.parameters, `plan "${plan.id}" parameters`, isSqlParameters));
|
|
1710
|
-
const claimToken = this.store.
|
|
1759
|
+
const claimToken = this.store.randomId("claim");
|
|
1711
1760
|
const claimed = this.store.claimPlan(plan.id, session.id, this.actorId, claimToken);
|
|
1712
1761
|
if (!claimed) {
|
|
1713
1762
|
throw new StateQLError("STALE_PLAN", "Plan is already being applied.");
|
|
@@ -1896,6 +1945,7 @@ export class StateQL {
|
|
|
1896
1945
|
readOnly: command.read_only,
|
|
1897
1946
|
secretEnv: command.secret_env,
|
|
1898
1947
|
credentialRef: command.credential_ref,
|
|
1948
|
+
passwordRef: command.password_ref ?? undefined,
|
|
1899
1949
|
profile: command.profile,
|
|
1900
1950
|
timeoutMs: command.timeout_ms,
|
|
1901
1951
|
});
|
|
@@ -1908,12 +1958,14 @@ export class StateQL {
|
|
|
1908
1958
|
readOnly: command.read_only ?? true,
|
|
1909
1959
|
secretEnv: command.secret_env,
|
|
1910
1960
|
credentialRef: command.credential_ref,
|
|
1961
|
+
passwordRef: command.password_ref ?? undefined,
|
|
1911
1962
|
});
|
|
1912
1963
|
case "profile.update":
|
|
1913
1964
|
return this.updateProfile(batchString(command.name, "name"), {
|
|
1914
1965
|
...(command.target !== undefined ? { target: command.target } : {}),
|
|
1915
1966
|
...(command.secret_env !== undefined ? { secretEnv: command.secret_env } : {}),
|
|
1916
1967
|
...(command.credential_ref !== undefined ? { credentialRef: command.credential_ref } : {}),
|
|
1968
|
+
...(command.password_ref !== undefined ? { passwordRef: command.password_ref } : {}),
|
|
1917
1969
|
...(command.read_only !== undefined ? { readOnly: command.read_only } : {}),
|
|
1918
1970
|
});
|
|
1919
1971
|
case "profile.list":
|
|
@@ -2714,6 +2766,28 @@ export class StateQL {
|
|
|
2714
2766
|
throw new StateQLError("PERMISSION_DENIED", `Actor "${this.actorId}" is not attached to session "${session.name}".`);
|
|
2715
2767
|
}
|
|
2716
2768
|
async resolveConnectionSource(connection, session, operation, access, context) {
|
|
2769
|
+
const references = [connection.secret_env, connection.credential_ref, connection.password_ref]
|
|
2770
|
+
.filter((value) => value !== null);
|
|
2771
|
+
if (references.length > 1) {
|
|
2772
|
+
throw new StateQLError("STATE_CORRUPTED", "Connection has ambiguous credential references.");
|
|
2773
|
+
}
|
|
2774
|
+
if (connection.password_ref !== null) {
|
|
2775
|
+
validateCredentialRef(connection.password_ref);
|
|
2776
|
+
const driver = validatePasswordReferenceTarget(connection.source);
|
|
2777
|
+
if (driver !== connection.driver) {
|
|
2778
|
+
throw new StateQLError("STATE_CORRUPTED", "Password-reference target driver does not match the stored connection.");
|
|
2779
|
+
}
|
|
2780
|
+
const password = await this.resolveCredential(connection.password_ref, "password_ref", session, operation, access, context, {
|
|
2781
|
+
connection: {
|
|
2782
|
+
id: connection.id,
|
|
2783
|
+
name: connection.name,
|
|
2784
|
+
driver: connection.driver,
|
|
2785
|
+
database: connection.database_name,
|
|
2786
|
+
readOnly: Boolean(connection.read_only),
|
|
2787
|
+
},
|
|
2788
|
+
}, connection.source);
|
|
2789
|
+
return injectPassword(connection.source, password).source;
|
|
2790
|
+
}
|
|
2717
2791
|
const reference = connection.secret_env ?? connection.credential_ref;
|
|
2718
2792
|
if (!reference)
|
|
2719
2793
|
return connection.source;
|
|
@@ -2731,7 +2805,7 @@ export class StateQL {
|
|
|
2731
2805
|
});
|
|
2732
2806
|
return credentialSource(value, connection.driver, source).source;
|
|
2733
2807
|
}
|
|
2734
|
-
async resolveCredential(reference, source, session, operation, access, context, details = {}) {
|
|
2808
|
+
async resolveCredential(reference, source, session, operation, access, context, details = {}, passwordTarget) {
|
|
2735
2809
|
const resolver = this.credentialResolver;
|
|
2736
2810
|
const credentialContext = createAdapterContext(this.credentialTimeoutMs, context.signal);
|
|
2737
2811
|
let value;
|
|
@@ -2746,9 +2820,8 @@ export class StateQL {
|
|
|
2746
2820
|
value = env[reference];
|
|
2747
2821
|
}
|
|
2748
2822
|
else {
|
|
2749
|
-
const
|
|
2823
|
+
const baseRequest = {
|
|
2750
2824
|
reference,
|
|
2751
|
-
source,
|
|
2752
2825
|
actorId: this.actorId,
|
|
2753
2826
|
session: { id: session.id, name: session.name },
|
|
2754
2827
|
operation,
|
|
@@ -2756,6 +2829,9 @@ export class StateQL {
|
|
|
2756
2829
|
...(context.signal ? { signal: context.signal } : {}),
|
|
2757
2830
|
...details,
|
|
2758
2831
|
};
|
|
2832
|
+
const request = source === "password_ref"
|
|
2833
|
+
? { ...baseRequest, source, target: passwordTarget }
|
|
2834
|
+
: { ...baseRequest, source };
|
|
2759
2835
|
try {
|
|
2760
2836
|
value = await resolveCredentialBeforeDeadline(resolver, request, credentialContext);
|
|
2761
2837
|
}
|
|
@@ -2763,7 +2839,7 @@ export class StateQL {
|
|
|
2763
2839
|
throw credentialStateQLError(reference, error);
|
|
2764
2840
|
}
|
|
2765
2841
|
}
|
|
2766
|
-
if (
|
|
2842
|
+
if (value === undefined || (source !== "password_ref" && value === "")) {
|
|
2767
2843
|
throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
|
|
2768
2844
|
}
|
|
2769
2845
|
context.deadline = Date.now() + (context.timeoutMs ?? this.timeoutMs);
|
|
@@ -2840,8 +2916,8 @@ export class StateQL {
|
|
|
2840
2916
|
const category = historyCategory(command);
|
|
2841
2917
|
const internal = commandContext?.internal ?? false;
|
|
2842
2918
|
let session = this.store.ensureSession(this.sessionName);
|
|
2843
|
-
const commandId = this.store.nextId("cmd");
|
|
2844
2919
|
if (!this.store.isSessionMember(session.id, this.actorId)) {
|
|
2920
|
+
const commandId = this.store.randomId("cmd");
|
|
2845
2921
|
const error = new StateQLError("PERMISSION_DENIED", `Actor "${this.actorId}" is not attached to session "${session.name}".`);
|
|
2846
2922
|
return {
|
|
2847
2923
|
ok: false,
|
|
@@ -2857,8 +2933,7 @@ export class StateQL {
|
|
|
2857
2933
|
const result = await action(session);
|
|
2858
2934
|
const responseSession = result.session ?? session;
|
|
2859
2935
|
const sqlText = resolveHistorySql(historySql);
|
|
2860
|
-
this.store.addHistory({
|
|
2861
|
-
id: commandId,
|
|
2936
|
+
const history = this.store.addHistory({
|
|
2862
2937
|
sessionId: session.id,
|
|
2863
2938
|
actorId: this.actorId,
|
|
2864
2939
|
origin,
|
|
@@ -2874,7 +2949,7 @@ export class StateQL {
|
|
|
2874
2949
|
});
|
|
2875
2950
|
return {
|
|
2876
2951
|
ok: true,
|
|
2877
|
-
command_id:
|
|
2952
|
+
command_id: history.id,
|
|
2878
2953
|
session_id: responseSession.id,
|
|
2879
2954
|
data: result.data,
|
|
2880
2955
|
warnings: result.warnings ?? [],
|
|
@@ -2892,8 +2967,7 @@ export class StateQL {
|
|
|
2892
2967
|
catch (error) {
|
|
2893
2968
|
const stateqlError = asStateQLError(error);
|
|
2894
2969
|
const sqlText = resolveHistorySql(historySql);
|
|
2895
|
-
this.store.addHistory({
|
|
2896
|
-
id: commandId,
|
|
2970
|
+
const history = this.store.addHistory({
|
|
2897
2971
|
sessionId: session.id,
|
|
2898
2972
|
actorId: this.actorId,
|
|
2899
2973
|
origin,
|
|
@@ -2909,7 +2983,7 @@ export class StateQL {
|
|
|
2909
2983
|
});
|
|
2910
2984
|
return {
|
|
2911
2985
|
ok: false,
|
|
2912
|
-
command_id:
|
|
2986
|
+
command_id: history.id,
|
|
2913
2987
|
session_id: session.id,
|
|
2914
2988
|
error: stateqlError.details,
|
|
2915
2989
|
meta: {
|
|
@@ -3210,7 +3284,23 @@ function credentialStateQLError(reference, error) {
|
|
|
3210
3284
|
return new StateQLError("CREDENTIAL_RESOLUTION_FAILED", `Credential reference "${reference}" could not be resolved.`, { retryable: true });
|
|
3211
3285
|
}
|
|
3212
3286
|
function safeCredentialErrorMessage(error, source) {
|
|
3213
|
-
|
|
3287
|
+
let message = errorMessage(error).split(source).join("[credential redacted]");
|
|
3288
|
+
const authority = /^[a-z][a-z\d+.-]*:\/\/([^/?#]*)/i.exec(source)?.[1];
|
|
3289
|
+
if (authority) {
|
|
3290
|
+
const at = authority.lastIndexOf("@");
|
|
3291
|
+
const colon = at < 0 ? -1 : authority.slice(0, at).indexOf(":");
|
|
3292
|
+
const password = colon < 0 ? "" : authority.slice(colon + 1, at);
|
|
3293
|
+
const secrets = new Set([password]);
|
|
3294
|
+
try {
|
|
3295
|
+
secrets.add(decodeURIComponent(password));
|
|
3296
|
+
}
|
|
3297
|
+
catch { /* malformed values stay encoded */ }
|
|
3298
|
+
for (const secret of secrets) {
|
|
3299
|
+
if (secret)
|
|
3300
|
+
message = message.split(secret).join("[credential redacted]");
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
return redact(message)
|
|
3214
3304
|
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s\/@]+(?::[^\s\/@]*)?@/giu, "$1***@");
|
|
3215
3305
|
}
|
|
3216
3306
|
function executionTimeout(value, name = "timeoutMs") {
|
|
@@ -3232,15 +3322,27 @@ function validatedProfileSource(input) {
|
|
|
3232
3322
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
3233
3323
|
if (input.credentialRef !== undefined)
|
|
3234
3324
|
validateCredentialRef(input.credentialRef);
|
|
3325
|
+
if (input.passwordRef !== undefined)
|
|
3326
|
+
validateCredentialRef(input.passwordRef);
|
|
3235
3327
|
let target = input.target ?? null;
|
|
3328
|
+
if (input.passwordRef !== undefined && target === null) {
|
|
3329
|
+
throw new StateQLError("INVALID_COMMAND", "A password reference requires a literal remote profile target.");
|
|
3330
|
+
}
|
|
3236
3331
|
if (target) {
|
|
3237
|
-
const driver =
|
|
3238
|
-
|
|
3332
|
+
const driver = input.passwordRef !== undefined
|
|
3333
|
+
? validatePasswordReferenceTarget(target)
|
|
3334
|
+
: detectDriver(target);
|
|
3335
|
+
if (input.passwordRef === undefined && driver !== "sqlite" && databaseUrlHasSecret(target))
|
|
3239
3336
|
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`);
|
|
3240
3337
|
if (driver === "sqlite")
|
|
3241
3338
|
target = normalizeSqliteSource(target);
|
|
3242
3339
|
}
|
|
3243
|
-
return {
|
|
3340
|
+
return {
|
|
3341
|
+
target,
|
|
3342
|
+
secretEnv: input.secretEnv ?? null,
|
|
3343
|
+
credentialRef: input.credentialRef ?? null,
|
|
3344
|
+
passwordRef: input.passwordRef ?? null,
|
|
3345
|
+
};
|
|
3244
3346
|
}
|
|
3245
3347
|
function historyCategory(command) {
|
|
3246
3348
|
if (["query", "exec", "plan", "apply", "mongo.query", "mongo.exec", "mongo.plan", "redis.query", "redis.exec", "redis.plan", "filter"].includes(command))
|
package/dist/src/store.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface SessionRecord {
|
|
|
11
11
|
}
|
|
12
12
|
export interface ConnectionRecord {
|
|
13
13
|
id: string;
|
|
14
|
+
alias?: string;
|
|
14
15
|
session_id: string;
|
|
15
16
|
name: string;
|
|
16
17
|
driver: Driver;
|
|
@@ -18,6 +19,7 @@ export interface ConnectionRecord {
|
|
|
18
19
|
source: string;
|
|
19
20
|
secret_env: string | null;
|
|
20
21
|
credential_ref: string | null;
|
|
22
|
+
password_ref: string | null;
|
|
21
23
|
read_only: number;
|
|
22
24
|
version: number;
|
|
23
25
|
created_at: string;
|
|
@@ -27,6 +29,7 @@ export interface ProfileRecord {
|
|
|
27
29
|
target: string | null;
|
|
28
30
|
secret_env: string | null;
|
|
29
31
|
credential_ref: string | null;
|
|
32
|
+
password_ref: string | null;
|
|
30
33
|
read_only: number;
|
|
31
34
|
created_at: string;
|
|
32
35
|
updated_at: string;
|
|
@@ -125,7 +128,8 @@ export declare class StateStore {
|
|
|
125
128
|
private closed;
|
|
126
129
|
constructor(home: string, now: () => Date, maxStateBytes?: number);
|
|
127
130
|
close(): void;
|
|
128
|
-
|
|
131
|
+
randomId(prefix: string): string;
|
|
132
|
+
private insertWithRandomId;
|
|
129
133
|
ensureSession(name?: string): SessionRecord;
|
|
130
134
|
bootstrapSession(name: string, actorId: string, ensureLegacyMembership: boolean): SessionRecord;
|
|
131
135
|
createSession(name: string): SessionRecord;
|
|
@@ -143,6 +147,7 @@ export declare class StateStore {
|
|
|
143
147
|
target?: string;
|
|
144
148
|
secretEnv?: string;
|
|
145
149
|
credentialRef?: string;
|
|
150
|
+
passwordRef?: string;
|
|
146
151
|
readOnly: boolean;
|
|
147
152
|
}): ProfileRecord;
|
|
148
153
|
updateProfile(input: {
|
|
@@ -150,6 +155,7 @@ export declare class StateStore {
|
|
|
150
155
|
target: string | null;
|
|
151
156
|
secretEnv: string | null;
|
|
152
157
|
credentialRef: string | null;
|
|
158
|
+
passwordRef: string | null;
|
|
153
159
|
readOnly: boolean;
|
|
154
160
|
}): ProfileRecord | undefined;
|
|
155
161
|
getProfile(name: string): ProfileRecord | undefined;
|
|
@@ -164,8 +170,11 @@ export declare class StateStore {
|
|
|
164
170
|
source: string;
|
|
165
171
|
secretEnv?: string;
|
|
166
172
|
credentialRef?: string;
|
|
173
|
+
passwordRef?: string;
|
|
167
174
|
readOnly: boolean;
|
|
168
175
|
}): ConnectionRecord | undefined;
|
|
176
|
+
private allocateConnectionAlias;
|
|
177
|
+
private backfillConnectionAliases;
|
|
169
178
|
getConnection(id: string): ConnectionRecord | undefined;
|
|
170
179
|
activeConnection(session: SessionRecord): ConnectionRecord | undefined;
|
|
171
180
|
disconnect(sessionId: string, actorId: string): boolean;
|