@fadhilp/stateql 0.10.1 → 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 -58
- package/dist/src/connection.d.ts +7 -1
- package/dist/src/connection.js +117 -12
- package/dist/src/migrations.js +120 -0
- package/dist/src/response-data.js +1 -0
- package/dist/src/stateql.d.ts +2 -0
- package/dist/src/stateql.js +154 -56
- package/dist/src/store.d.ts +7 -1
- package/dist/src/store.js +94 -66
- package/dist/src/types.d.ts +9 -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) {
|
|
@@ -250,12 +283,14 @@ export class StateQL {
|
|
|
250
283
|
target,
|
|
251
284
|
secretEnv: options.secretEnv,
|
|
252
285
|
credentialRef: options.credentialRef,
|
|
286
|
+
passwordRef: options.passwordRef,
|
|
253
287
|
});
|
|
254
288
|
const profile = this.store.addProfile({
|
|
255
289
|
name,
|
|
256
290
|
target: source.target ?? undefined,
|
|
257
291
|
secretEnv: source.secretEnv ?? undefined,
|
|
258
292
|
credentialRef: source.credentialRef ?? undefined,
|
|
293
|
+
passwordRef: source.passwordRef ?? undefined,
|
|
259
294
|
readOnly: options.readOnly ?? true,
|
|
260
295
|
});
|
|
261
296
|
return {
|
|
@@ -272,22 +307,32 @@ export class StateQL {
|
|
|
272
307
|
if (!existing)
|
|
273
308
|
throw new StateQLError("CONNECTION_NOT_FOUND", `Profile "${name}" was not found.`);
|
|
274
309
|
if (!changes || typeof changes !== "object" || Array.isArray(changes) ||
|
|
275
|
-
Object.keys(changes).some((key) => !["target", "secretEnv", "credentialRef", "readOnly"].includes(key))) {
|
|
310
|
+
Object.keys(changes).some((key) => !["target", "secretEnv", "credentialRef", "passwordRef", "readOnly"].includes(key))) {
|
|
276
311
|
throw new StateQLError("INVALID_COMMAND", "Profile update contains unknown fields.");
|
|
277
312
|
}
|
|
278
313
|
if (changes.readOnly !== undefined && typeof changes.readOnly !== "boolean")
|
|
279
314
|
throw new StateQLError("INVALID_COMMAND", "Profile readOnly must be boolean.");
|
|
280
315
|
const changesSource = Object.hasOwn(changes, "target") || Object.hasOwn(changes, "secretEnv") || Object.hasOwn(changes, "credentialRef");
|
|
281
|
-
|
|
316
|
+
const changesPassword = Object.hasOwn(changes, "passwordRef");
|
|
317
|
+
if (!changesSource && !changesPassword && changes.readOnly === undefined)
|
|
282
318
|
throw new StateQLError("INVALID_COMMAND", "Profile update has no changes.");
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
});
|
|
286
330
|
const profile = this.store.updateProfile({
|
|
287
331
|
name,
|
|
288
332
|
target: source.target,
|
|
289
333
|
secretEnv: source.secretEnv,
|
|
290
334
|
credentialRef: source.credentialRef,
|
|
335
|
+
passwordRef: source.passwordRef,
|
|
291
336
|
readOnly: changes.readOnly ?? Boolean(existing.read_only),
|
|
292
337
|
});
|
|
293
338
|
if (!profile)
|
|
@@ -1711,7 +1756,7 @@ export class StateQL {
|
|
|
1711
1756
|
const planParameters = compiled?.params ?? (nativePlan || tableUpdates
|
|
1712
1757
|
? undefined
|
|
1713
1758
|
: parseJson(plan.parameters, `plan "${plan.id}" parameters`, isSqlParameters));
|
|
1714
|
-
const claimToken = this.store.
|
|
1759
|
+
const claimToken = this.store.randomId("claim");
|
|
1715
1760
|
const claimed = this.store.claimPlan(plan.id, session.id, this.actorId, claimToken);
|
|
1716
1761
|
if (!claimed) {
|
|
1717
1762
|
throw new StateQLError("STALE_PLAN", "Plan is already being applied.");
|
|
@@ -1900,6 +1945,7 @@ export class StateQL {
|
|
|
1900
1945
|
readOnly: command.read_only,
|
|
1901
1946
|
secretEnv: command.secret_env,
|
|
1902
1947
|
credentialRef: command.credential_ref,
|
|
1948
|
+
passwordRef: command.password_ref ?? undefined,
|
|
1903
1949
|
profile: command.profile,
|
|
1904
1950
|
timeoutMs: command.timeout_ms,
|
|
1905
1951
|
});
|
|
@@ -1912,12 +1958,14 @@ export class StateQL {
|
|
|
1912
1958
|
readOnly: command.read_only ?? true,
|
|
1913
1959
|
secretEnv: command.secret_env,
|
|
1914
1960
|
credentialRef: command.credential_ref,
|
|
1961
|
+
passwordRef: command.password_ref ?? undefined,
|
|
1915
1962
|
});
|
|
1916
1963
|
case "profile.update":
|
|
1917
1964
|
return this.updateProfile(batchString(command.name, "name"), {
|
|
1918
1965
|
...(command.target !== undefined ? { target: command.target } : {}),
|
|
1919
1966
|
...(command.secret_env !== undefined ? { secretEnv: command.secret_env } : {}),
|
|
1920
1967
|
...(command.credential_ref !== undefined ? { credentialRef: command.credential_ref } : {}),
|
|
1968
|
+
...(command.password_ref !== undefined ? { passwordRef: command.password_ref } : {}),
|
|
1921
1969
|
...(command.read_only !== undefined ? { readOnly: command.read_only } : {}),
|
|
1922
1970
|
});
|
|
1923
1971
|
case "profile.list":
|
|
@@ -2718,6 +2766,28 @@ export class StateQL {
|
|
|
2718
2766
|
throw new StateQLError("PERMISSION_DENIED", `Actor "${this.actorId}" is not attached to session "${session.name}".`);
|
|
2719
2767
|
}
|
|
2720
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
|
+
}
|
|
2721
2791
|
const reference = connection.secret_env ?? connection.credential_ref;
|
|
2722
2792
|
if (!reference)
|
|
2723
2793
|
return connection.source;
|
|
@@ -2735,7 +2805,7 @@ export class StateQL {
|
|
|
2735
2805
|
});
|
|
2736
2806
|
return credentialSource(value, connection.driver, source).source;
|
|
2737
2807
|
}
|
|
2738
|
-
async resolveCredential(reference, source, session, operation, access, context, details = {}) {
|
|
2808
|
+
async resolveCredential(reference, source, session, operation, access, context, details = {}, passwordTarget) {
|
|
2739
2809
|
const resolver = this.credentialResolver;
|
|
2740
2810
|
const credentialContext = createAdapterContext(this.credentialTimeoutMs, context.signal);
|
|
2741
2811
|
let value;
|
|
@@ -2750,9 +2820,8 @@ export class StateQL {
|
|
|
2750
2820
|
value = env[reference];
|
|
2751
2821
|
}
|
|
2752
2822
|
else {
|
|
2753
|
-
const
|
|
2823
|
+
const baseRequest = {
|
|
2754
2824
|
reference,
|
|
2755
|
-
source,
|
|
2756
2825
|
actorId: this.actorId,
|
|
2757
2826
|
session: { id: session.id, name: session.name },
|
|
2758
2827
|
operation,
|
|
@@ -2760,6 +2829,9 @@ export class StateQL {
|
|
|
2760
2829
|
...(context.signal ? { signal: context.signal } : {}),
|
|
2761
2830
|
...details,
|
|
2762
2831
|
};
|
|
2832
|
+
const request = source === "password_ref"
|
|
2833
|
+
? { ...baseRequest, source, target: passwordTarget }
|
|
2834
|
+
: { ...baseRequest, source };
|
|
2763
2835
|
try {
|
|
2764
2836
|
value = await resolveCredentialBeforeDeadline(resolver, request, credentialContext);
|
|
2765
2837
|
}
|
|
@@ -2767,7 +2839,7 @@ export class StateQL {
|
|
|
2767
2839
|
throw credentialStateQLError(reference, error);
|
|
2768
2840
|
}
|
|
2769
2841
|
}
|
|
2770
|
-
if (
|
|
2842
|
+
if (value === undefined || (source !== "password_ref" && value === "")) {
|
|
2771
2843
|
throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
|
|
2772
2844
|
}
|
|
2773
2845
|
context.deadline = Date.now() + (context.timeoutMs ?? this.timeoutMs);
|
|
@@ -2844,8 +2916,8 @@ export class StateQL {
|
|
|
2844
2916
|
const category = historyCategory(command);
|
|
2845
2917
|
const internal = commandContext?.internal ?? false;
|
|
2846
2918
|
let session = this.store.ensureSession(this.sessionName);
|
|
2847
|
-
const commandId = this.store.nextId("cmd");
|
|
2848
2919
|
if (!this.store.isSessionMember(session.id, this.actorId)) {
|
|
2920
|
+
const commandId = this.store.randomId("cmd");
|
|
2849
2921
|
const error = new StateQLError("PERMISSION_DENIED", `Actor "${this.actorId}" is not attached to session "${session.name}".`);
|
|
2850
2922
|
return {
|
|
2851
2923
|
ok: false,
|
|
@@ -2861,8 +2933,7 @@ export class StateQL {
|
|
|
2861
2933
|
const result = await action(session);
|
|
2862
2934
|
const responseSession = result.session ?? session;
|
|
2863
2935
|
const sqlText = resolveHistorySql(historySql);
|
|
2864
|
-
this.store.addHistory({
|
|
2865
|
-
id: commandId,
|
|
2936
|
+
const history = this.store.addHistory({
|
|
2866
2937
|
sessionId: session.id,
|
|
2867
2938
|
actorId: this.actorId,
|
|
2868
2939
|
origin,
|
|
@@ -2878,7 +2949,7 @@ export class StateQL {
|
|
|
2878
2949
|
});
|
|
2879
2950
|
return {
|
|
2880
2951
|
ok: true,
|
|
2881
|
-
command_id:
|
|
2952
|
+
command_id: history.id,
|
|
2882
2953
|
session_id: responseSession.id,
|
|
2883
2954
|
data: result.data,
|
|
2884
2955
|
warnings: result.warnings ?? [],
|
|
@@ -2896,8 +2967,7 @@ export class StateQL {
|
|
|
2896
2967
|
catch (error) {
|
|
2897
2968
|
const stateqlError = asStateQLError(error);
|
|
2898
2969
|
const sqlText = resolveHistorySql(historySql);
|
|
2899
|
-
this.store.addHistory({
|
|
2900
|
-
id: commandId,
|
|
2970
|
+
const history = this.store.addHistory({
|
|
2901
2971
|
sessionId: session.id,
|
|
2902
2972
|
actorId: this.actorId,
|
|
2903
2973
|
origin,
|
|
@@ -2913,7 +2983,7 @@ export class StateQL {
|
|
|
2913
2983
|
});
|
|
2914
2984
|
return {
|
|
2915
2985
|
ok: false,
|
|
2916
|
-
command_id:
|
|
2986
|
+
command_id: history.id,
|
|
2917
2987
|
session_id: session.id,
|
|
2918
2988
|
error: stateqlError.details,
|
|
2919
2989
|
meta: {
|
|
@@ -3214,7 +3284,23 @@ function credentialStateQLError(reference, error) {
|
|
|
3214
3284
|
return new StateQLError("CREDENTIAL_RESOLUTION_FAILED", `Credential reference "${reference}" could not be resolved.`, { retryable: true });
|
|
3215
3285
|
}
|
|
3216
3286
|
function safeCredentialErrorMessage(error, source) {
|
|
3217
|
-
|
|
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)
|
|
3218
3304
|
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s\/@]+(?::[^\s\/@]*)?@/giu, "$1***@");
|
|
3219
3305
|
}
|
|
3220
3306
|
function executionTimeout(value, name = "timeoutMs") {
|
|
@@ -3236,15 +3322,27 @@ function validatedProfileSource(input) {
|
|
|
3236
3322
|
throw new StateQLError("INVALID_COMMAND", "Secret environment variable name is invalid.");
|
|
3237
3323
|
if (input.credentialRef !== undefined)
|
|
3238
3324
|
validateCredentialRef(input.credentialRef);
|
|
3325
|
+
if (input.passwordRef !== undefined)
|
|
3326
|
+
validateCredentialRef(input.passwordRef);
|
|
3239
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
|
+
}
|
|
3240
3331
|
if (target) {
|
|
3241
|
-
const driver =
|
|
3242
|
-
|
|
3332
|
+
const driver = input.passwordRef !== undefined
|
|
3333
|
+
? validatePasswordReferenceTarget(target)
|
|
3334
|
+
: detectDriver(target);
|
|
3335
|
+
if (input.passwordRef === undefined && driver !== "sqlite" && databaseUrlHasSecret(target))
|
|
3243
3336
|
throw new StateQLError("PERMISSION_DENIED", `Credential-bearing ${databaseDisplayName(driver)} URLs must use --env or --credential-ref.`);
|
|
3244
3337
|
if (driver === "sqlite")
|
|
3245
3338
|
target = normalizeSqliteSource(target);
|
|
3246
3339
|
}
|
|
3247
|
-
return {
|
|
3340
|
+
return {
|
|
3341
|
+
target,
|
|
3342
|
+
secretEnv: input.secretEnv ?? null,
|
|
3343
|
+
credentialRef: input.credentialRef ?? null,
|
|
3344
|
+
passwordRef: input.passwordRef ?? null,
|
|
3345
|
+
};
|
|
3248
3346
|
}
|
|
3249
3347
|
function historyCategory(command) {
|
|
3250
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
|
@@ -19,6 +19,7 @@ export interface ConnectionRecord {
|
|
|
19
19
|
source: string;
|
|
20
20
|
secret_env: string | null;
|
|
21
21
|
credential_ref: string | null;
|
|
22
|
+
password_ref: string | null;
|
|
22
23
|
read_only: number;
|
|
23
24
|
version: number;
|
|
24
25
|
created_at: string;
|
|
@@ -28,6 +29,7 @@ export interface ProfileRecord {
|
|
|
28
29
|
target: string | null;
|
|
29
30
|
secret_env: string | null;
|
|
30
31
|
credential_ref: string | null;
|
|
32
|
+
password_ref: string | null;
|
|
31
33
|
read_only: number;
|
|
32
34
|
created_at: string;
|
|
33
35
|
updated_at: string;
|
|
@@ -126,7 +128,8 @@ export declare class StateStore {
|
|
|
126
128
|
private closed;
|
|
127
129
|
constructor(home: string, now: () => Date, maxStateBytes?: number);
|
|
128
130
|
close(): void;
|
|
129
|
-
|
|
131
|
+
randomId(prefix: string): string;
|
|
132
|
+
private insertWithRandomId;
|
|
130
133
|
ensureSession(name?: string): SessionRecord;
|
|
131
134
|
bootstrapSession(name: string, actorId: string, ensureLegacyMembership: boolean): SessionRecord;
|
|
132
135
|
createSession(name: string): SessionRecord;
|
|
@@ -144,6 +147,7 @@ export declare class StateStore {
|
|
|
144
147
|
target?: string;
|
|
145
148
|
secretEnv?: string;
|
|
146
149
|
credentialRef?: string;
|
|
150
|
+
passwordRef?: string;
|
|
147
151
|
readOnly: boolean;
|
|
148
152
|
}): ProfileRecord;
|
|
149
153
|
updateProfile(input: {
|
|
@@ -151,6 +155,7 @@ export declare class StateStore {
|
|
|
151
155
|
target: string | null;
|
|
152
156
|
secretEnv: string | null;
|
|
153
157
|
credentialRef: string | null;
|
|
158
|
+
passwordRef: string | null;
|
|
154
159
|
readOnly: boolean;
|
|
155
160
|
}): ProfileRecord | undefined;
|
|
156
161
|
getProfile(name: string): ProfileRecord | undefined;
|
|
@@ -165,6 +170,7 @@ export declare class StateStore {
|
|
|
165
170
|
source: string;
|
|
166
171
|
secretEnv?: string;
|
|
167
172
|
credentialRef?: string;
|
|
173
|
+
passwordRef?: string;
|
|
168
174
|
readOnly: boolean;
|
|
169
175
|
}): ConnectionRecord | undefined;
|
|
170
176
|
private allocateConnectionAlias;
|