@fadhilp/stateql 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -415,6 +415,7 @@ const stateql = StateQL.forActor({
415
415
  home: "./.stql",
416
416
  actor: "pi-session-id",
417
417
  timeoutMs: 30_000,
418
+ credentialTimeoutMs: 120_000,
418
419
  maxResultBytes: 16 * 1024 * 1024,
419
420
  maxStateBytes: 256 * 1024 * 1024,
420
421
  });
@@ -506,6 +507,10 @@ const stateql = StateQL.forActor({
506
507
  });
507
508
  ```
508
509
 
510
+ Credential resolution has its own two-minute default deadline
511
+ (`credentialTimeoutMs`) and remains cancellable through `request.signal`.
512
+ The database-operation timeout begins after a credential is resolved.
513
+
509
514
  When no custom resolver is configured, StateQL reads only `secret_env`
510
515
  references from `process.env`; `credential_ref` never falls back to the
511
516
  environment. A configured resolver is authoritative for both sources: returning
@@ -9,6 +9,7 @@ export interface WriteResult {
9
9
  }
10
10
  export interface AdapterContext {
11
11
  deadline: number;
12
+ timeoutMs?: number;
12
13
  signal?: AbortSignal;
13
14
  }
14
15
  export declare class AdapterExecutionError extends Error {
@@ -32,6 +32,7 @@ export class AdapterWriteError extends Error {
32
32
  export function createAdapterContext(timeoutMs, signal) {
33
33
  return {
34
34
  deadline: Date.now() + timeoutMs,
35
+ timeoutMs,
35
36
  ...(signal ? { signal } : {}),
36
37
  };
37
38
  }
@@ -11,6 +11,7 @@ export declare class StateQL {
11
11
  private readonly maxResultRows;
12
12
  private readonly maxResultBytes;
13
13
  private readonly timeoutMs;
14
+ private readonly credentialTimeoutMs;
14
15
  private readonly signal?;
15
16
  private readonly commandContexts;
16
17
  private readonly credentialResolver?;
@@ -13,6 +13,7 @@ import { StateStore, } from "./store.js";
13
13
  import { compactRows, defaultHome, hash, isSqlParameters, parseJson, redact, } from "./util.js";
14
14
  const DEFAULT_SNAPSHOT_HISTORY_LIMIT = 50;
15
15
  const MAX_SNAPSHOT_HISTORY_LIMIT = 100;
16
+ const DEFAULT_CREDENTIAL_RESOLUTION_TIMEOUT_MS = 120_000;
16
17
  export class StateQL {
17
18
  static forActor(options) {
18
19
  if (!options.actor.trim()) {
@@ -41,6 +42,7 @@ export class StateQL {
41
42
  maxResultRows;
42
43
  maxResultBytes;
43
44
  timeoutMs;
45
+ credentialTimeoutMs;
44
46
  signal;
45
47
  commandContexts = new AsyncLocalStorage();
46
48
  credentialResolver;
@@ -60,6 +62,7 @@ export class StateQL {
60
62
  this.maxResultRows = positiveInteger(options.maxResultRows ?? 10_000, "maxResultRows");
61
63
  this.maxResultBytes = positiveInteger(options.maxResultBytes ?? 16 * 1024 * 1024, "maxResultBytes");
62
64
  this.timeoutMs = executionTimeout(options.timeoutMs ?? 30_000);
65
+ this.credentialTimeoutMs = executionTimeout(options.credentialTimeoutMs ?? DEFAULT_CREDENTIAL_RESOLUTION_TIMEOUT_MS, "credentialTimeoutMs");
63
66
  const maxStateBytes = positiveInteger(options.maxStateBytes ?? 256 * 1024 * 1024, "maxStateBytes");
64
67
  this.signal = options.signal;
65
68
  this.credentialResolver = options.credentialResolver;
@@ -2067,39 +2070,41 @@ export class StateQL {
2067
2070
  }
2068
2071
  async resolveCredential(reference, source, session, operation, access, context, details = {}) {
2069
2072
  const resolver = this.credentialResolver;
2073
+ const credentialContext = createAdapterContext(this.credentialTimeoutMs, context.signal);
2074
+ let value;
2070
2075
  if (!resolver) {
2071
- if (context.signal?.aborted) {
2076
+ if (credentialContext.signal?.aborted) {
2072
2077
  throw credentialStateQLError(reference, new CredentialResolutionError("cancelled"));
2073
2078
  }
2074
- if (context.deadline <= Date.now()) {
2079
+ if (credentialContext.deadline <= Date.now()) {
2075
2080
  throw credentialStateQLError(reference, new CredentialResolutionError("timeout"));
2076
2081
  }
2077
- if (source === "secret_env") {
2078
- const value = env[reference];
2079
- if (value)
2080
- return value;
2081
- }
2082
- throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
2082
+ if (source === "secret_env")
2083
+ value = env[reference];
2083
2084
  }
2084
- const request = {
2085
- reference,
2086
- source,
2087
- actorId: this.actorId,
2088
- session: { id: session.id, name: session.name },
2089
- operation,
2090
- access,
2091
- ...(context.signal ? { signal: context.signal } : {}),
2092
- ...details,
2093
- };
2094
- try {
2095
- const value = await resolveCredentialBeforeDeadline(resolver, request, context);
2096
- if (!value)
2097
- throw new CredentialResolutionError("unavailable");
2098
- return value;
2085
+ else {
2086
+ const request = {
2087
+ reference,
2088
+ source,
2089
+ actorId: this.actorId,
2090
+ session: { id: session.id, name: session.name },
2091
+ operation,
2092
+ access,
2093
+ ...(context.signal ? { signal: context.signal } : {}),
2094
+ ...details,
2095
+ };
2096
+ try {
2097
+ value = await resolveCredentialBeforeDeadline(resolver, request, credentialContext);
2098
+ }
2099
+ catch (error) {
2100
+ throw credentialStateQLError(reference, error);
2101
+ }
2099
2102
  }
2100
- catch (error) {
2101
- throw credentialStateQLError(reference, error);
2103
+ if (!value) {
2104
+ throw credentialStateQLError(reference, new CredentialResolutionError("unavailable"));
2102
2105
  }
2106
+ context.deadline = Date.now() + (context.timeoutMs ?? this.timeoutMs);
2107
+ return value;
2103
2108
  }
2104
2109
  async openAdapter(connection, context, source) {
2105
2110
  try {
@@ -2435,19 +2440,19 @@ function credentialStateQLError(reference, error) {
2435
2440
  case "cancelled":
2436
2441
  return new StateQLError("OPERATION_CANCELLED", "Credential resolution was cancelled.", { retryable: true });
2437
2442
  case "timeout":
2438
- return new StateQLError("DEADLINE_EXCEEDED", "Credential resolution exceeded the operation deadline.", { retryable: true });
2443
+ return new StateQLError("DEADLINE_EXCEEDED", "Credential resolution exceeded the credential deadline.", { retryable: true });
2439
2444
  }
2440
2445
  }
2441
2446
  return new StateQLError("CREDENTIAL_RESOLUTION_FAILED", `Credential reference "${reference}" could not be resolved.`, { retryable: true });
2442
2447
  }
2443
2448
  function safeCredentialErrorMessage(error, source) {
2444
2449
  return redact(errorMessage(error).split(source).join("[credential redacted]"))
2445
- .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+(?::[^\s/@]*)?@/giu, "$1***@");
2450
+ .replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s\/@]+(?::[^\s\/@]*)?@/giu, "$1***@");
2446
2451
  }
2447
- function executionTimeout(value) {
2448
- const timeout = positiveInteger(value, "timeoutMs");
2452
+ function executionTimeout(value, name = "timeoutMs") {
2453
+ const timeout = positiveInteger(value, name);
2449
2454
  if (timeout > 2_147_483_647) {
2450
- throw new StateQLError("INVALID_COMMAND", "timeoutMs cannot exceed 2147483647 milliseconds.");
2455
+ throw new StateQLError("INVALID_COMMAND", `${name} cannot exceed 2147483647 milliseconds.`);
2451
2456
  }
2452
2457
  return timeout;
2453
2458
  }
@@ -213,6 +213,8 @@ export interface StateQLOptions extends ExecutionOptions {
213
213
  maxResultBytes?: number;
214
214
  maxStateBytes?: number;
215
215
  credentialResolver?: CredentialResolver;
216
+ /** Maximum time allowed for one credential resolution; defaults to two minutes. */
217
+ credentialTimeoutMs?: number;
216
218
  now?: () => Date;
217
219
  }
218
220
  export type StateQLActorOptions = Omit<StateQLOptions, "session" | "actor"> & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",