@fadhilp/stateql 0.11.0 → 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
@@ -479,16 +479,40 @@ context as `options.executionContext` for all commands in that batch.
479
479
 
480
480
  ### Actor workspaces
481
481
 
482
- `StateQL.forActor(...)` resolves the actor's attached session directly from
483
- StateQL storage, avoiding a duplicate actor-to-session mapping in integrations.
484
- On first use, it creates a legacy-compatible session named after the actor. Use
485
- `new StateQL({ session, actor })` when the session is already known.
486
-
487
- Membership is managed only through the library API, not batch commands:
488
- `linkActor(session, actorId)`, `unlinkActor(session, actorId)`,
489
- `listActors(session)`, and `resolveActor(actorId)`. An existing member must link
490
- an actor before that actor opens an existing workspace. Integrations should ask
491
- for user confirmation before changing membership or the shared connection.
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
+ ```
495
+
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.
492
516
 
493
517
  ### Harness credential resolution
494
518
 
@@ -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";
@@ -1,9 +1,11 @@
1
1
  import { type TableChange } from "./table-editor.js";
2
- import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, CommandExecutionContext, CommandOrigin, BatchOptions, CapabilitiesData, CatalogObject, DescribeObjectData, ListObjectsData, ListObjectsFilter, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, HistoryOptions, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, ProfileUpdateOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, RedisCommand, RedisExecOptions, RedisPlanOptions, RedisQueryOptions, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, Column, StateQLActorOptions, StateQLOptions, StateQLSnapshot, StateQLSnapshotOptions, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
2
+ import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, CommandExecutionContext, CommandOrigin, BatchOptions, CapabilitiesData, CatalogObject, DescribeObjectData, ListObjectsData, ListObjectsFilter, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, HistoryOptions, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, ProfileUpdateOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, RedisCommand, RedisExecOptions, RedisPlanOptions, RedisQueryOptions, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, Column, StateQLActorOptions, StateQLOptions, StateQLWorkspaceOptions, StateQLSnapshot, StateQLSnapshotOptions, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
3
3
  export declare class StateQL {
4
4
  /** Runtime contract marker for passwordRef/password_ref support. */
5
5
  static readonly passwordReferenceVersion: 1;
6
6
  static forActor(options: StateQLActorOptions): StateQL;
7
+ /** Opens one actor in a named shared workspace for a trusted library host. */
8
+ static forWorkspace(options: StateQLWorkspaceOptions): StateQL;
7
9
  private readonly store;
8
10
  private readonly sessionName;
9
11
  private readonly actorId;
@@ -17,6 +17,7 @@ import { compactRows, defaultHome, hash, isSqlParameters, parseJson, redact, } f
17
17
  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
+ const WORKSPACE_BOOTSTRAP = Symbol("StateQL.workspaceBootstrap");
20
21
  export class StateQL {
21
22
  /** Runtime contract marker for passwordRef/password_ref support. */
22
23
  static passwordReferenceVersion = 1;
@@ -37,6 +38,21 @@ export class StateQL {
37
38
  store.close();
38
39
  }
39
40
  }
41
+ /** Opens one actor in a named shared workspace for a trusted library host. */
42
+ static forWorkspace(options) {
43
+ if (!options.workspace.trim()) {
44
+ throw new StateQLError("INVALID_COMMAND", "Workspace name is required.");
45
+ }
46
+ if (!options.actor.trim()) {
47
+ throw new StateQLError("INVALID_COMMAND", "Actor ID is required.");
48
+ }
49
+ const { workspace, ...actorOptions } = options;
50
+ return new StateQL({
51
+ ...actorOptions,
52
+ session: workspace,
53
+ [WORKSPACE_BOOTSTRAP]: true,
54
+ });
55
+ }
40
56
  store;
41
57
  sessionName;
42
58
  actorId;
@@ -80,7 +96,12 @@ export class StateQL {
80
96
  }
81
97
  const store = new StateStore(options.home ?? defaultHome(), this.now, maxStateBytes);
82
98
  try {
83
- store.bootstrapSession(this.sessionName, this.actorId, options.actor === undefined);
99
+ if (options[WORKSPACE_BOOTSTRAP]) {
100
+ store.bootstrapWorkspace(this.sessionName, this.actorId);
101
+ }
102
+ else {
103
+ store.bootstrapSession(this.sessionName, this.actorId, options.actor === undefined);
104
+ }
84
105
  }
85
106
  catch (error) {
86
107
  store.close();
@@ -132,6 +132,7 @@ export declare class StateStore {
132
132
  private insertWithRandomId;
133
133
  ensureSession(name?: string): SessionRecord;
134
134
  bootstrapSession(name: string, actorId: string, ensureLegacyMembership: boolean): SessionRecord;
135
+ bootstrapWorkspace(name: string, actorId: string): SessionRecord;
135
136
  createSession(name: string): SessionRecord;
136
137
  isSessionMember(sessionId: string, actorId: string): boolean;
137
138
  linkActor(sessionId: string, requestingActorId: string, actorId: string): "linked" | "already_linked" | "actor_conflict" | "denied";
package/dist/src/store.js CHANGED
@@ -132,6 +132,56 @@ export class StateStore {
132
132
  throw error;
133
133
  }
134
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
+ }
135
185
  createSession(name) {
136
186
  return this.bootstrapSession(name, name, true);
137
187
  }
@@ -274,6 +274,10 @@ export interface StateQLOptions extends ExecutionOptions {
274
274
  export type StateQLActorOptions = Omit<StateQLOptions, "session" | "actor"> & {
275
275
  actor: string;
276
276
  };
277
+ /** Trusted-host options for opening one actor in a named shared workspace. */
278
+ export type StateQLWorkspaceOptions = StateQLActorOptions & {
279
+ workspace: string;
280
+ };
277
281
  export interface QueryOptions extends ExecutionOptions {
278
282
  params?: SqlParameters;
279
283
  cache?: "auto" | "bypass" | "require";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",