@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.
@@ -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,9 +128,11 @@ export declare class StateStore {
126
128
  private closed;
127
129
  constructor(home: string, now: () => Date, maxStateBytes?: number);
128
130
  close(): void;
129
- nextId(prefix: string): string;
131
+ randomId(prefix: string): string;
132
+ private insertWithRandomId;
130
133
  ensureSession(name?: string): SessionRecord;
131
134
  bootstrapSession(name: string, actorId: string, ensureLegacyMembership: boolean): SessionRecord;
135
+ bootstrapWorkspace(name: string, actorId: string): SessionRecord;
132
136
  createSession(name: string): SessionRecord;
133
137
  isSessionMember(sessionId: string, actorId: string): boolean;
134
138
  linkActor(sessionId: string, requestingActorId: string, actorId: string): "linked" | "already_linked" | "actor_conflict" | "denied";
@@ -144,6 +148,7 @@ export declare class StateStore {
144
148
  target?: string;
145
149
  secretEnv?: string;
146
150
  credentialRef?: string;
151
+ passwordRef?: string;
147
152
  readOnly: boolean;
148
153
  }): ProfileRecord;
149
154
  updateProfile(input: {
@@ -151,6 +156,7 @@ export declare class StateStore {
151
156
  target: string | null;
152
157
  secretEnv: string | null;
153
158
  credentialRef: string | null;
159
+ passwordRef: string | null;
154
160
  readOnly: boolean;
155
161
  }): ProfileRecord | undefined;
156
162
  getProfile(name: string): ProfileRecord | undefined;
@@ -165,6 +171,7 @@ export declare class StateStore {
165
171
  source: string;
166
172
  secretEnv?: string;
167
173
  credentialRef?: string;
174
+ passwordRef?: string;
168
175
  readOnly: boolean;
169
176
  }): ConnectionRecord | undefined;
170
177
  private allocateConnectionAlias;
package/dist/src/store.js CHANGED
@@ -53,14 +53,22 @@ export class StateStore {
53
53
  this.closed = true;
54
54
  this.db.close();
55
55
  }
56
- nextId(prefix) {
57
- this.db
58
- .prepare("INSERT OR IGNORE INTO counters(prefix, value) VALUES (?, 0)")
59
- .run(prefix);
60
- const row = this.db
61
- .prepare("UPDATE counters SET value = value + 1 WHERE prefix = ? RETURNING value")
62
- .get(prefix);
63
- return `${prefix}_${row.value}`;
56
+ randomId(prefix) {
57
+ return `${prefix}_${randomBase32Id()}`;
58
+ }
59
+ insertWithRandomId(prefix, table, insert) {
60
+ for (let attempt = 0; attempt < 64; attempt++) {
61
+ const id = this.randomId(prefix);
62
+ try {
63
+ insert(id);
64
+ return id;
65
+ }
66
+ catch (error) {
67
+ if (!isIdCollision(error, table))
68
+ throw error;
69
+ }
70
+ }
71
+ throw new Error(`Could not allocate a unique ${prefix} ID.`);
64
72
  }
65
73
  ensureSession(name = "default") {
66
74
  const existing = this.getSessionByName(name);
@@ -87,12 +95,13 @@ export class StateStore {
87
95
  .get(name);
88
96
  const created = !row;
89
97
  if (!row) {
90
- const id = this.nextId("s");
91
- this.db
92
- .prepare(`INSERT INTO sessions
93
- (id, name, status, created_at, updated_at)
94
- VALUES (?, ?, 'active', ?, ?)`)
95
- .run(id, name, timestamp, timestamp);
98
+ const id = this.insertWithRandomId("s", "sessions", (candidate) => {
99
+ this.db
100
+ .prepare(`INSERT INTO sessions
101
+ (id, name, status, created_at, updated_at)
102
+ VALUES (?, ?, 'active', ?, ?)`)
103
+ .run(candidate, name, timestamp, timestamp);
104
+ });
96
105
  row = { id, status: "active" };
97
106
  }
98
107
  else if (row.status !== "active") {
@@ -123,6 +132,56 @@ export class StateStore {
123
132
  throw error;
124
133
  }
125
134
  }
135
+ bootstrapWorkspace(name, actorId) {
136
+ const timestamp = this.now().toISOString();
137
+ this.db.exec("BEGIN IMMEDIATE");
138
+ try {
139
+ let row = this.db
140
+ .prepare("SELECT id, status FROM sessions WHERE name = ? LIMIT 1")
141
+ .get(name);
142
+ const identities = actorId === name ? [actorId] : [name, actorId];
143
+ for (const identity of identities) {
144
+ const existing = this.resolveActor(identity);
145
+ if (existing && existing.id !== row?.id) {
146
+ throw new StateQLError("PERMISSION_DENIED", `Actor "${identity}" is already attached to workspace "${existing.name}" and cannot be attached to workspace "${name}".`);
147
+ }
148
+ }
149
+ if (!row) {
150
+ const id = this.insertWithRandomId("s", "sessions", (candidate) => {
151
+ this.db
152
+ .prepare(`INSERT INTO sessions
153
+ (id, name, status, created_at, updated_at)
154
+ VALUES (?, ?, 'active', ?, ?)`)
155
+ .run(candidate, name, timestamp, timestamp);
156
+ });
157
+ row = { id, status: "active" };
158
+ }
159
+ else if (row.status !== "active") {
160
+ this.db
161
+ .prepare(`UPDATE sessions
162
+ SET status = 'active', updated_at = ?
163
+ WHERE id = ?`)
164
+ .run(timestamp, row.id);
165
+ }
166
+ for (const identity of identities) {
167
+ if (this.isSessionMember(row.id, identity))
168
+ continue;
169
+ this.db
170
+ .prepare(`INSERT INTO session_members(session_id, actor_id, attached_at)
171
+ VALUES (?, ?, ?)`)
172
+ .run(row.id, identity, timestamp);
173
+ }
174
+ const session = this.getSessionByName(name);
175
+ if (!session)
176
+ throw new Error(`Could not open workspace "${name}".`);
177
+ this.db.exec("COMMIT");
178
+ return session;
179
+ }
180
+ catch (error) {
181
+ this.db.exec("ROLLBACK");
182
+ throw error;
183
+ }
184
+ }
126
185
  createSession(name) {
127
186
  return this.bootstrapSession(name, name, true);
128
187
  }
@@ -238,15 +297,15 @@ export class StateStore {
238
297
  const timestamp = this.now().toISOString();
239
298
  this.db
240
299
  .prepare(`INSERT INTO profiles
241
- (name, target, secret_env, credential_ref, read_only, created_at, updated_at)
242
- VALUES (?, ?, ?, ?, ?, ?, ?)`)
243
- .run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
300
+ (name, target, secret_env, credential_ref, password_ref, read_only, created_at, updated_at)
301
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
302
+ .run(input.name, input.target ?? null, input.secretEnv ?? null, input.credentialRef ?? null, input.passwordRef ?? null, input.readOnly ? 1 : 0, timestamp, timestamp);
244
303
  return this.getProfile(input.name);
245
304
  }
246
305
  updateProfile(input) {
247
306
  const result = this.db.prepare(`UPDATE profiles
248
- SET target = ?, secret_env = ?, credential_ref = ?, read_only = ?, updated_at = ?
249
- WHERE name = ?`).run(input.target, input.secretEnv, input.credentialRef, input.readOnly ? 1 : 0, this.now().toISOString(), input.name);
307
+ SET target = ?, secret_env = ?, credential_ref = ?, password_ref = ?, read_only = ?, updated_at = ?
308
+ WHERE name = ?`).run(input.target, input.secretEnv, input.credentialRef, input.passwordRef, input.readOnly ? 1 : 0, this.now().toISOString(), input.name);
250
309
  return Number(result.changes) === 1 ? this.getProfile(input.name) : undefined;
251
310
  }
252
311
  getProfile(name) {
@@ -278,14 +337,15 @@ export class StateStore {
278
337
  this.db.exec("COMMIT");
279
338
  return undefined;
280
339
  }
281
- const id = this.nextId("conn");
282
340
  const timestamp = this.now().toISOString();
283
- this.db
284
- .prepare(`INSERT INTO connections
285
- (id, session_id, name, driver, database_name, source, secret_env,
286
- credential_ref, read_only, version, created_at)
287
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
288
- .run(id, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.credentialRef ?? null, input.readOnly ? 1 : 0, timestamp);
341
+ const id = this.insertWithRandomId("conn", "connections", (candidate) => {
342
+ this.db
343
+ .prepare(`INSERT INTO connections
344
+ (id, session_id, name, driver, database_name, source, secret_env,
345
+ credential_ref, password_ref, read_only, version, created_at)
346
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)`)
347
+ .run(candidate, input.sessionId, input.name, input.driver, input.databaseName, input.source, input.secretEnv ?? null, input.credentialRef ?? null, input.passwordRef ?? null, input.readOnly ? 1 : 0, timestamp);
348
+ });
289
349
  this.allocateConnectionAlias(id);
290
350
  this.db
291
351
  .prepare(`UPDATE sessions
@@ -362,16 +422,17 @@ export class StateStore {
362
422
  return `sv_${row.version}`;
363
423
  }
364
424
  saveResult(input) {
365
- const id = this.nextId("q");
366
425
  this.db.exec("BEGIN IMMEDIATE");
367
426
  try {
368
- this.db
369
- .prepare(`INSERT INTO results
370
- (id, session_id, connection_id, fingerprint, sql, parameters,
371
- rows_json, columns_json, row_count, state_version, state_signature,
372
- state_confidence, expires_at, created_at)
373
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
374
- .run(id, input.sessionId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), JSON.stringify(toJsonSafe(input.rows)), JSON.stringify(input.columns), input.rows.length, input.stateVersion, input.stateSignature, input.stateConfidence, input.expiresAt, this.now().toISOString());
427
+ const id = this.insertWithRandomId("q", "results", (candidate) => {
428
+ this.db
429
+ .prepare(`INSERT INTO results
430
+ (id, session_id, connection_id, fingerprint, sql, parameters,
431
+ rows_json, columns_json, row_count, state_version, state_signature,
432
+ state_confidence, expires_at, created_at)
433
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
434
+ .run(candidate, input.sessionId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), JSON.stringify(toJsonSafe(input.rows)), JSON.stringify(input.columns), input.rows.length, input.stateVersion, input.stateSignature, input.stateConfidence, input.expiresAt, this.now().toISOString());
435
+ });
375
436
  this.enforceResultQuota(id);
376
437
  this.allocateGeneratedAlias(input.sessionId, id);
377
438
  const result = this.getResult(id);
@@ -466,14 +527,15 @@ export class StateStore {
466
527
  }
467
528
  }
468
529
  saveOperation(input) {
469
- const id = this.nextId("op");
470
- this.db
471
- .prepare(`INSERT INTO operations
472
- (id, session_id, actor_id, connection_id, fingerprint, sql, parameters,
473
- statement_type, affected_rows, status, transaction_id, replay_of,
474
- idempotency_key, state_version_before, state_version_after, created_at)
475
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
476
- .run(id, input.sessionId, input.actorId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.affectedRows ?? null, input.status, input.transactionId ?? null, input.replayOf ?? null, input.idempotencyKey ?? null, input.stateVersionBefore, input.stateVersionAfter ?? null, this.now().toISOString());
530
+ const id = this.insertWithRandomId("op", "operations", (candidate) => {
531
+ this.db
532
+ .prepare(`INSERT INTO operations
533
+ (id, session_id, actor_id, connection_id, fingerprint, sql, parameters,
534
+ statement_type, affected_rows, status, transaction_id, replay_of,
535
+ idempotency_key, state_version_before, state_version_after, created_at)
536
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
537
+ .run(candidate, input.sessionId, input.actorId, input.connectionId, input.fingerprint, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.affectedRows ?? null, input.status, input.transactionId ?? null, input.replayOf ?? null, input.idempotencyKey ?? null, input.stateVersionBefore, input.stateVersionAfter ?? null, this.now().toISOString());
538
+ });
477
539
  return this.getOperation(id);
478
540
  }
479
541
  getOperation(id) {
@@ -584,7 +646,6 @@ export class StateStore {
584
646
  .run(operationId);
585
647
  }
586
648
  createTransaction(input) {
587
- const id = this.nextId("tx");
588
649
  const timestamp = this.now().toISOString();
589
650
  this.db.exec("BEGIN IMMEDIATE");
590
651
  try {
@@ -604,12 +665,14 @@ export class StateStore {
604
665
  this.db.exec("COMMIT");
605
666
  return undefined;
606
667
  }
607
- this.db
608
- .prepare(`INSERT INTO transactions
609
- (id, session_id, owner_actor_id, connection_id, state,
610
- isolation_level, start_version, created_at)
611
- VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`)
612
- .run(id, input.sessionId, input.actorId, input.connectionId, input.isolation, `sv_${eligible.version}`, timestamp);
668
+ const id = this.insertWithRandomId("tx", "transactions", (candidate) => {
669
+ this.db
670
+ .prepare(`INSERT INTO transactions
671
+ (id, session_id, owner_actor_id, connection_id, state,
672
+ isolation_level, start_version, created_at)
673
+ VALUES (?, ?, ?, ?, 'active', ?, ?, ?)`)
674
+ .run(candidate, input.sessionId, input.actorId, input.connectionId, input.isolation, `sv_${eligible.version}`, timestamp);
675
+ });
613
676
  this.db
614
677
  .prepare(`UPDATE sessions
615
678
  SET active_transaction_id = ?, updated_at = ?
@@ -630,7 +693,7 @@ export class StateStore {
630
693
  }
631
694
  transactionOperations(transactionId) {
632
695
  return this.db
633
- .prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at")
696
+ .prepare("SELECT * FROM operations WHERE transaction_id = ? ORDER BY created_at, rowid")
634
697
  .all(transactionId);
635
698
  }
636
699
  validatedTransactionOperations(transactionId) {
@@ -804,14 +867,15 @@ export class StateStore {
804
867
  }
805
868
  }
806
869
  savePlan(input) {
807
- const id = this.nextId("p");
808
- this.db
809
- .prepare(`INSERT INTO plans
810
- (id, session_id, owner_actor_id, connection_id, sql, parameters,
811
- statement_type, state_version, state_signature, destructive,
812
- allow_unbounded, allow_destructive, expires_at, created_at)
813
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
814
- .run(id, input.sessionId, input.ownerActorId, input.connectionId, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.stateVersion, input.stateSignature, input.destructive ? 1 : 0, input.allowUnbounded ? 1 : 0, input.allowDestructive ? 1 : 0, input.expiresAt, this.now().toISOString());
870
+ const id = this.insertWithRandomId("p", "plans", (candidate) => {
871
+ this.db
872
+ .prepare(`INSERT INTO plans
873
+ (id, session_id, owner_actor_id, connection_id, sql, parameters,
874
+ statement_type, state_version, state_signature, destructive,
875
+ allow_unbounded, allow_destructive, expires_at, created_at)
876
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
877
+ .run(candidate, input.sessionId, input.ownerActorId, input.connectionId, input.sql, JSON.stringify(toJsonSafe(input.parameters)), input.statementType, input.stateVersion, input.stateSignature, input.destructive ? 1 : 0, input.allowUnbounded ? 1 : 0, input.allowDestructive ? 1 : 0, input.expiresAt, this.now().toISOString());
878
+ });
815
879
  return this.getPlan(id);
816
880
  }
817
881
  getPlan(id) {
@@ -876,13 +940,17 @@ export class StateStore {
876
940
  }
877
941
  }
878
942
  addHistory(input) {
879
- const id = input.id ?? this.nextId("cmd");
880
- this.db
881
- .prepare(`INSERT INTO history
882
- (id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
883
- executed, cached, success, error_code)
884
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
885
- .run(id, this.now().toISOString(), input.sessionId, input.actorId, input.origin ?? "legacy", input.category ?? "management", input.internal ? 1 : 0, input.command, boundedHistorySql(input.sql), input.target?.slice(0, 1024) ?? null, input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
943
+ const insert = (id) => {
944
+ this.db
945
+ .prepare(`INSERT INTO history
946
+ (id, timestamp, session_id, actor_id, origin, category, internal, command, sql, target, handle,
947
+ executed, cached, success, error_code)
948
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
949
+ .run(id, this.now().toISOString(), input.sessionId, input.actorId, input.origin ?? "legacy", input.category ?? "management", input.internal ? 1 : 0, input.command, boundedHistorySql(input.sql), input.target?.slice(0, 1024) ?? null, input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
950
+ };
951
+ const id = input.id ?? this.insertWithRandomId("cmd", "history", insert);
952
+ if (input.id !== undefined)
953
+ insert(id);
886
954
  this.db
887
955
  .prepare(`DELETE FROM history
888
956
  WHERE rowid IN (
@@ -1120,8 +1188,18 @@ function outcomeJson(outcome) {
1120
1188
  return outcome === undefined ? null : JSON.stringify(toJsonSafe(outcome));
1121
1189
  }
1122
1190
  function randomBase32Alias() {
1191
+ return randomBase32(10);
1192
+ }
1193
+ function randomBase32Id() {
1194
+ return randomBase32(26);
1195
+ }
1196
+ function randomBase32(length) {
1123
1197
  const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
1124
- return [...randomBytes(10)].map((value) => alphabet[value & 31]).join("");
1198
+ return [...randomBytes(length)].map((value) => alphabet[value & 31]).join("");
1199
+ }
1200
+ function isIdCollision(error, table) {
1201
+ return error instanceof Error &&
1202
+ error.message.includes(`UNIQUE constraint failed: ${table}.id`);
1125
1203
  }
1126
1204
  function boundedHistorySql(sql) {
1127
1205
  if (sql === undefined)
@@ -11,7 +11,7 @@ export interface CommandExecutionContext {
11
11
  }
12
12
  export type CredentialAccess = "read" | "write";
13
13
  export type CredentialOperation = "connect" | "query" | "inspect" | "plan" | "exec" | "apply" | "transaction.commit";
14
- export type CredentialSource = "secret_env" | "credential_ref";
14
+ export type CredentialSource = "secret_env" | "credential_ref" | "password_ref";
15
15
  interface CredentialRequestBase {
16
16
  reference: string;
17
17
  actorId: string;
@@ -39,6 +39,9 @@ export type CredentialRequest = CredentialRequestBase & ({
39
39
  source?: "secret_env";
40
40
  } | {
41
41
  source: "credential_ref";
42
+ } | {
43
+ source: "password_ref";
44
+ target: string;
42
45
  });
43
46
  export type CredentialResolver = (request: CredentialRequest) => string | undefined | Promise<string | undefined>;
44
47
  export type StateConfidence = "authoritative" | "transaction_snapshot" | "database_reported" | "local" | "ttl_based" | "unknown";
@@ -271,6 +274,10 @@ export interface StateQLOptions extends ExecutionOptions {
271
274
  export type StateQLActorOptions = Omit<StateQLOptions, "session" | "actor"> & {
272
275
  actor: string;
273
276
  };
277
+ /** Trusted-host options for opening one actor in a named shared workspace. */
278
+ export type StateQLWorkspaceOptions = StateQLActorOptions & {
279
+ workspace: string;
280
+ };
274
281
  export interface QueryOptions extends ExecutionOptions {
275
282
  params?: SqlParameters;
276
283
  cache?: "auto" | "bypass" | "require";
@@ -300,16 +307,19 @@ export interface ConnectOptions extends ExecutionOptions {
300
307
  secretEnv?: string;
301
308
  profile?: string;
302
309
  credentialRef?: string;
310
+ passwordRef?: string;
303
311
  }
304
312
  export interface ProfileOptions {
305
313
  readOnly?: boolean;
306
314
  secretEnv?: string;
307
315
  credentialRef?: string;
316
+ passwordRef?: string;
308
317
  }
309
318
  export interface ProfileUpdateOptions {
310
319
  target?: string | null;
311
320
  secretEnv?: string | null;
312
321
  credentialRef?: string | null;
322
+ passwordRef?: string | null;
313
323
  readOnly?: boolean;
314
324
  }
315
325
  export interface RedisQueryOptions extends ExecutionOptions {
@@ -353,6 +363,7 @@ export interface BatchCommand {
353
363
  read_only?: boolean;
354
364
  secret_env?: string;
355
365
  credential_ref?: string;
366
+ password_ref?: string | null;
356
367
  profile?: string;
357
368
  replay?: boolean;
358
369
  idempotency_key?: string;
@@ -398,6 +409,7 @@ export interface ProfileData {
398
409
  target: string | null;
399
410
  secret_env: string | null;
400
411
  credential_ref: string | null;
412
+ password_ref: string | null;
401
413
  read_only: boolean;
402
414
  }
403
415
  export interface ProfilesData {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",