@haverstack/record-adapter-sqlite 0.29.0 → 0.31.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/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { StackRecord, JournalOptions, RecordChanges, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, JournalQuery, RecordJournalEntry, StackType, Association, TokenSession } from '@haverstack/core';
2
- import { StackRecordAdapter, AdapterCapabilities } from '@haverstack/core/adapter';
3
- import { StackTokenStore, TokenInfo } from '@haverstack/core/wire';
1
+ import { StackCapabilities, StackRecord, RecordChangeSet, RecordVersion, ActorOptions, TypeId, StackQuery, QueryResult, FileId, JournalQuery, RecordJournalEntry, StackType, Association, Actor } from '@haverstack/core';
2
+ import { StackRecordAdapter, JournalOptions } from '@haverstack/core/adapter';
3
+ import { StackTokenStore, TokenSession, TokenInfo } from '@haverstack/core/wire';
4
4
 
5
5
  /**
6
6
  * Normalizes a SQLite binding's call convention (array-bind-then-step,
@@ -60,33 +60,33 @@ type StackConfig = {
60
60
  */
61
61
 
62
62
  declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
63
- readonly capabilities: AdapterCapabilities;
63
+ readonly capabilities: StackCapabilities;
64
64
  readonly ownerEntityId: string;
65
65
  readonly timezone: string | undefined;
66
66
  private readonly record;
67
67
  protected constructor(exec: SqlExecutor, config: StackConfig);
68
68
  createRecord(record: StackRecord, opts?: JournalOptions): Promise<StackRecord>;
69
69
  getRecord(id: string): Promise<StackRecord | null>;
70
- mutateRecord(id: string, changes: RecordChanges, opts?: {
71
- expectedVersion?: number;
70
+ mutateRecord(id: string, changes: RecordChangeSet, opts?: {
71
+ ifVersion?: number;
72
72
  snapshot?: RecordVersion;
73
73
  bumpsVersion?: boolean;
74
74
  } & ActorOptions & JournalOptions): Promise<StackRecord>;
75
75
  deleteRecord(id: string, opts?: {
76
- hard?: boolean;
77
- expectedVersion?: number;
76
+ purge?: boolean;
77
+ ifVersion?: number;
78
78
  snapshot?: RecordVersion;
79
79
  } & ActorOptions & JournalOptions): Promise<StackRecord | null>;
80
80
  undeleteRecord(id: string, opts?: {
81
- expectedVersion?: number;
81
+ ifVersion?: number;
82
82
  snapshot?: RecordVersion;
83
83
  } & ActorOptions & JournalOptions): Promise<StackRecord>;
84
84
  restoreVersion(id: string, version: number, opts?: {
85
- expectedVersion?: number;
85
+ ifVersion?: number;
86
86
  snapshot?: RecordVersion;
87
87
  } & ActorOptions & JournalOptions): Promise<StackRecord>;
88
88
  commitMigration(id: string, toTypeId: TypeId, content: Record<string, unknown>, opts?: {
89
- expectedVersion?: number;
89
+ ifVersion?: number;
90
90
  snapshot?: RecordVersion;
91
91
  } & ActorOptions & JournalOptions): Promise<StackRecord>;
92
92
  queryRecords(query: StackQuery): Promise<QueryResult>;
@@ -120,7 +120,7 @@ declare abstract class SharedSqlRecordAdapter implements StackRecordAdapter {
120
120
 
121
121
  /** Conventional sibling path for a token store beside a stack's main .db file. */
122
122
  declare const defaultTokenStorePath: (dbPath: string) => string;
123
- type NativeTokenStoreOptions = {
123
+ type NativeTokenStoreOpenOptions = {
124
124
  /** Absolute path to the token store file. Created if it doesn't exist. */
125
125
  path: string;
126
126
  /** Bypass the storage-ownership lock check. See docs/spec/adapters.md § Concurrency & storage ownership. */
@@ -132,9 +132,8 @@ declare class NativeTokenStore implements StackTokenStore {
132
132
  private tokens;
133
133
  private constructor();
134
134
  /** Opens the token store, creating the file and schema if needed. */
135
- static open(opts: NativeTokenStoreOptions): Promise<NativeTokenStore>;
136
- createToken(principalId: string, opts?: {
137
- onBehalfOf?: string;
135
+ static open(opts: NativeTokenStoreOpenOptions): Promise<NativeTokenStore>;
136
+ createToken(actor: Actor, opts?: {
138
137
  label?: string;
139
138
  expiresAt?: Date;
140
139
  }): Promise<{
@@ -174,17 +173,17 @@ declare class NativeTokenStore implements StackTokenStore {
174
173
  * spread-args run/get/all calls to it).
175
174
  */
176
175
 
177
- type NativeRecordInitializeOptions = {
176
+ type NativeSQLiteRecordAdapterInitializeOptions = {
178
177
  /** Absolute path to the .db file. Must not already exist. */
179
178
  path: string;
180
179
  /** IANA timezone string e.g. "America/New_York". Optional passthrough app metadata — no default. */
181
180
  timezone?: string;
182
181
  /** Entity ID of the stack owner. */
183
- entityId: string;
184
- /** Bypass the storage-ownership lock check. See NativeRecordOpenOptions.force. */
182
+ ownerEntityId: string;
183
+ /** Bypass the storage-ownership lock check. See NativeSQLiteRecordAdapterOpenOptions.force. */
185
184
  force?: boolean;
186
185
  };
187
- type NativeRecordOpenOptions = {
186
+ type NativeSQLiteRecordAdapterOpenOptions = {
188
187
  /** Absolute path to an existing .db file. */
189
188
  path: string;
190
189
  /**
@@ -210,15 +209,15 @@ declare class NativeSQLiteRecordAdapter extends SharedSqlRecordAdapter {
210
209
  * Initialize a new stack database. Fails if the file already exists —
211
210
  * use open() for existing databases.
212
211
  */
213
- static initialize(opts: NativeRecordInitializeOptions): Promise<NativeSQLiteRecordAdapter>;
212
+ static initialize(opts: NativeSQLiteRecordAdapterInitializeOptions): Promise<NativeSQLiteRecordAdapter>;
214
213
  /**
215
214
  * Open an existing stack database. Fails if the file does not exist —
216
215
  * use initialize() for new databases.
217
216
  */
218
- static open(opts: NativeRecordOpenOptions): Promise<NativeSQLiteRecordAdapter>;
217
+ static open(opts: NativeSQLiteRecordAdapterOpenOptions): Promise<NativeSQLiteRecordAdapter>;
219
218
  /** Folds the WAL back into the main file — useful before copying/backing up the database. */
220
219
  flush(): Promise<void>;
221
220
  close(): Promise<void>;
222
221
  }
223
222
 
224
- export { type NativeRecordInitializeOptions, type NativeRecordOpenOptions, NativeSQLiteRecordAdapter, NativeTokenStore, type NativeTokenStoreOptions, defaultTokenStorePath };
223
+ export { NativeSQLiteRecordAdapter, type NativeSQLiteRecordAdapterInitializeOptions, type NativeSQLiteRecordAdapterOpenOptions, NativeTokenStore, type NativeTokenStoreOpenOptions, defaultTokenStorePath };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync, writeFileSync, unlinkSync, readFileSync } from 'fs';
2
- import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackQueryError, NATIVE_SORT_FIELDS } from '@haverstack/core';
2
+ import { StackNotFoundError, StackConflictError, applyMergePatch, StackVersionConflictError, StackBadRequestError, NATIVE_SORT_FIELDS } from '@haverstack/core';
3
3
  import { contentSortEntry, parseContentFilterKey, contentSortKey } from '@haverstack/core/adapter';
4
4
  import { randomBytes, createHash } from 'crypto';
5
5
 
@@ -75,6 +75,7 @@ var RECORD_SCHEMA_SQL = `
75
75
  content TEXT NOT NULL CHECK (json_valid(content)),
76
76
  updated_at INTEGER NOT NULL,
77
77
  entity_id TEXT,
78
+ principal_id TEXT,
78
79
  updated_by TEXT,
79
80
  updated_via TEXT,
80
81
  PRIMARY KEY (record_id, version)
@@ -91,7 +92,7 @@ var RECORD_SCHEMA_SQL = `
91
92
  at INTEGER NOT NULL,
92
93
  -- No 'purged': a purge destroys the log rather than appending to it,
93
94
  -- so the constraint names exactly the kinds an entry can carry.
94
- -- See docs/spec/journal.md \xA7 A hard delete destroys the journal.
95
+ -- See docs/spec/journal.md \xA7 A purge destroys the journal.
95
96
  kind TEXT NOT NULL CHECK (kind IN ('created', 'changed', 'deleted')),
96
97
  ops TEXT NOT NULL CHECK (json_valid(ops)),
97
98
  version INTEGER NOT NULL,
@@ -217,7 +218,7 @@ var fromBase64 = (cursor) => {
217
218
  };
218
219
  var encodeCursor = (decoded) => toBase64(JSON.stringify(decoded.kind === "absent" ? { k: decoded.kind, f: decoded.field, i: decoded.id } : { k: decoded.kind, f: decoded.field, i: decoded.id, v: decoded.value }));
219
220
  var decodeCursor = (cursor) => {
220
- const malformed = () => new StackQueryError(`Invalid cursor: malformed "${cursor}"`);
221
+ const malformed = () => new StackBadRequestError(`Invalid cursor: malformed "${cursor}"`);
221
222
  let parsed;
222
223
  try {
223
224
  parsed = JSON.parse(fromBase64(cursor));
@@ -238,11 +239,11 @@ var decodeCursor = (cursor) => {
238
239
  }
239
240
  if (k === "num" || k === "native") {
240
241
  if (typeof v !== "number" || !isFinite(v)) {
241
- throw new StackQueryError(`Invalid cursor: non-numeric sort value`);
242
+ throw new StackBadRequestError(`Invalid cursor: non-numeric sort value`);
242
243
  }
243
244
  if (k === "native") {
244
245
  if (!SORT_FIELDS.includes(f)) {
245
- throw new StackQueryError(`Invalid cursor: unknown sort field "${f}"`);
246
+ throw new StackBadRequestError(`Invalid cursor: unknown sort field "${f}"`);
246
247
  }
247
248
  return { kind: "native", field: f, value: v, id: i };
248
249
  }
@@ -376,7 +377,7 @@ var atBudget = (page, rows) => ({
376
377
  var sqlDirection = (query) => {
377
378
  const dir = query.sort?.direction ?? "desc";
378
379
  if (dir !== "asc" && dir !== "desc") {
379
- throw new StackQueryError(`Invalid sort direction "${dir}": expected "asc" or "desc".`);
380
+ throw new StackBadRequestError(`Invalid sort direction "${dir}": expected "asc" or "desc".`);
380
381
  }
381
382
  return dir === "asc" ? "ASC" : "DESC";
382
383
  };
@@ -401,7 +402,7 @@ var contentPathExists = (segments, leaf, params, nextAlias) => {
401
402
  };
402
403
  var equalsLeaf = (alias) => `${alias}.type NOT IN ('object', 'array') AND ${alias}.value = ?`;
403
404
  var SORT_ALIAS = "cs";
404
- var cursorMismatch = (cursor, field) => new StackQueryError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
405
+ var cursorMismatch = (cursor, field) => new StackBadRequestError(`Cursor sort field "${cursor.field}" does not match query sort field "${field}"`);
405
406
  var nativeCursor = (cursor, field) => {
406
407
  if (cursor.kind !== "native" || cursor.field !== field)
407
408
  throw cursorMismatch(cursor, field);
@@ -449,8 +450,8 @@ var recordConditions = (query) => {
449
450
  }
450
451
  }
451
452
  inFilter("app_id", f.appId);
452
- inFilter("entity_id", f.entityId);
453
- inFilter("principal_id", f.principalId);
453
+ inFilter("entity_id", f.createdBy?.subjectId);
454
+ inFilter("principal_id", f.createdBy?.principalId);
454
455
  rangeFilter("created_at", f.createdAt);
455
456
  rangeFilter("updated_at", f.updatedAt);
456
457
  const hasAssociation = (kind, clauses) => `r.id IN (SELECT a.record_id FROM associations a WHERE a.kind = '${kind}'` + clauses.map((c) => ` AND ${c}`).join("") + `)`;
@@ -460,14 +461,22 @@ var recordConditions = (query) => {
460
461
  params.push(tag);
461
462
  }
462
463
  }
463
- if (f.hasAttachment) {
464
- conditions.push(hasAssociation("attachment", ["a.label = ?"]));
465
- params.push(f.hasAttachment);
464
+ if (f.attachment) {
465
+ const clauses = [];
466
+ if (f.attachment.label !== void 0) {
467
+ clauses.push("a.label = ?");
468
+ params.push(f.attachment.label);
469
+ }
470
+ if (f.attachment.fileId !== void 0) {
471
+ clauses.push("a.file_id = ?");
472
+ params.push(f.attachment.fileId);
473
+ }
474
+ conditions.push(hasAssociation("attachment", clauses));
466
475
  }
467
- if (f.attachmentFileId) {
476
+ if (f.referencesFileId) {
468
477
  conditions.push(`(${hasAssociation("attachment", ["a.file_id = ?"])}
469
478
  OR r.id IN (SELECT ci.record_id FROM content_index ci WHERE ci.file_id = ?))`);
470
- params.push(f.attachmentFileId, f.attachmentFileId);
479
+ params.push(f.referencesFileId, f.referencesFileId);
471
480
  }
472
481
  if (f.relatedTo) {
473
482
  const clauses = [];
@@ -478,11 +487,11 @@ var recordConditions = (query) => {
478
487
  }
479
488
  if (target) {
480
489
  clauses.push("a.related_scope = ?");
481
- params.push(target.scope);
482
- if (target.scope === "record") {
490
+ params.push(target.kind);
491
+ if (target.kind === "record") {
483
492
  clauses.push("a.related_ns = ?", "a.related_id = ?", "a.related_stack = ?");
484
493
  params.push("", target.recordId, target.stackUrl ?? "");
485
- } else if (target.scope === "entity") {
494
+ } else if (target.kind === "entity") {
486
495
  clauses.push("a.related_ns = ?", "a.related_id = ?");
487
496
  params.push("", target.entityId);
488
497
  } else {
@@ -587,6 +596,10 @@ var buildQueryPlan = (query) => {
587
596
 
588
597
  // ../sqlite-shared/dist/mappers.js
589
598
  var toMs = (d) => d.getTime();
599
+ var actorFromColumns = (subject, principal) => subject == null ? void 0 : {
600
+ subjectId: subject,
601
+ ...principal != null && { principalId: principal }
602
+ };
590
603
  var fromMs = (ms) => new Date(ms);
591
604
  var partitionAssociations = (associations) => ({
592
605
  associations: associations.filter((a) => a.kind !== "permission" && a.kind !== "anyone"),
@@ -603,16 +616,14 @@ var rowToRecord = (row, associations) => {
603
616
  };
604
617
  if (row.parent_id != null)
605
618
  record.parentId = row.parent_id;
606
- if (row.entity_id != null)
607
- record.entityId = row.entity_id;
608
619
  if (row.app_id != null)
609
620
  record.appId = row.app_id;
610
- if (row.principal_id != null)
611
- record.principalId = row.principal_id;
612
- if (row.updated_by != null)
613
- record.updatedBy = row.updated_by;
614
- if (row.updated_via != null)
615
- record.updatedVia = row.updated_via;
621
+ const createdBy = actorFromColumns(row.entity_id, row.principal_id);
622
+ if (createdBy)
623
+ record.createdBy = createdBy;
624
+ const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
625
+ if (updatedBy)
626
+ record.updatedBy = updatedBy;
616
627
  if (row.deleted_at != null)
617
628
  record.deletedAt = fromMs(row.deleted_at);
618
629
  if (row.unlisted_at != null)
@@ -661,31 +672,31 @@ var associationKeyColumns = (a) => {
661
672
  return [a.fileId, "", "", "", "", ""];
662
673
  if (a.kind === "permission") {
663
674
  const g = a.grantee;
664
- return g.scope === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
675
+ return g.kind === "entity" ? ["", "entity", g.entityId, "", "", ""] : ["", "group", g.groupId, "", "", g.role];
665
676
  }
666
677
  if (a.kind !== "relationship")
667
678
  return ["", "", "", "", "", ""];
668
679
  const t = a.target;
669
- if (t.scope === "entity")
680
+ if (t.kind === "entity")
670
681
  return ["", "entity", t.entityId, "", "", ""];
671
- if (t.scope === "external")
682
+ if (t.kind === "external")
672
683
  return ["", "external", t.id, t.ns, "", ""];
673
684
  return ["", "record", t.recordId, "", t.stackUrl ?? "", ""];
674
685
  };
675
686
  var rowToGrantee = (row) => row.related_scope === "group" ? {
676
- scope: "group",
687
+ kind: "group",
677
688
  groupId: row.related_id,
678
689
  role: row.related_role
679
- } : { scope: "entity", entityId: row.related_id };
690
+ } : { kind: "entity", entityId: row.related_id };
680
691
  var rowToTarget = (row) => {
681
692
  const id = row.related_id;
682
693
  if (row.related_scope === "entity")
683
- return { scope: "entity", entityId: id };
694
+ return { kind: "entity", entityId: id };
684
695
  if (row.related_scope === "external") {
685
- return { scope: "external", ns: row.related_ns, id };
696
+ return { kind: "external", ns: row.related_ns, id };
686
697
  }
687
698
  const stackUrl = row.related_stack;
688
- return { scope: "record", recordId: id, ...stackUrl && { stackUrl } };
699
+ return { kind: "record", recordId: id, ...stackUrl && { stackUrl } };
689
700
  };
690
701
  var rowToType = (row) => {
691
702
  const t = {
@@ -708,12 +719,12 @@ var rowToVersion = (row) => {
708
719
  content: JSON.parse(row.content),
709
720
  updatedAt: fromMs(row.updated_at)
710
721
  };
711
- if (row.entity_id != null)
712
- v.entityId = row.entity_id;
713
- if (row.updated_by != null)
714
- v.updatedBy = row.updated_by;
715
- if (row.updated_via != null)
716
- v.updatedVia = row.updated_via;
722
+ const createdBy = actorFromColumns(row.entity_id, row.principal_id);
723
+ if (createdBy)
724
+ v.createdBy = createdBy;
725
+ const updatedBy = actorFromColumns(row.updated_by, row.updated_via);
726
+ if (updatedBy)
727
+ v.updatedBy = updatedBy;
717
728
  return v;
718
729
  };
719
730
  var rowToJournalEntry = (row) => {
@@ -762,13 +773,13 @@ var readStackConfig = (exec) => {
762
773
  throw new Error("Stack database is missing its config record.");
763
774
  return config;
764
775
  };
765
- var asStackQueryError = (query, run) => {
776
+ var asStackBadRequestError = (query, run) => {
766
777
  try {
767
778
  return run();
768
779
  } catch (err) {
769
780
  if (!query.filter?.search)
770
781
  throw err;
771
- throw new StackQueryError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
782
+ throw new StackBadRequestError(`Search text could not be parsed: ${err instanceof Error ? err.message : String(err)}`);
772
783
  }
773
784
  };
774
785
  var versionConflict = (id, expected, actual) => new StackVersionConflictError(`Record "${id}" is at version ${actual}, expected ${expected}`, id, expected, actual);
@@ -777,6 +788,7 @@ var VERSION_COLUMNS = [
777
788
  "content",
778
789
  "updated_at",
779
790
  "entity_id",
791
+ "principal_id",
780
792
  "updated_by",
781
793
  "updated_via"
782
794
  ];
@@ -784,9 +796,10 @@ var versionRowValues = (version) => [
784
796
  version.typeId,
785
797
  JSON.stringify(version.content),
786
798
  toMs(version.updatedAt),
787
- version.entityId ?? null,
788
- version.updatedBy ?? null,
789
- version.updatedVia ?? null
799
+ version.createdBy?.subjectId ?? null,
800
+ version.createdBy?.principalId ?? null,
801
+ version.updatedBy?.subjectId ?? null,
802
+ version.updatedBy?.principalId ?? null
790
803
  ];
791
804
  var indexedFieldKinds = (schema) => {
792
805
  const kinds = /* @__PURE__ */ new Map();
@@ -815,7 +828,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
815
828
  /**
816
829
  * Precondition check for a mutation that has already read the record —
817
830
  * either because it can't fold the check into its own statement
818
- * (hardDeleteRecord: children reference the row, so the DELETE can't
831
+ * (purgeRecord: children reference the row, so the DELETE can't
819
832
  * carry it), or because it has to settle the precondition *before*
820
833
  * fts5Strategy.remove() runs, which needs the records row still holding
821
834
  * the old content. In the latter case versionedUpdate() re-checks it
@@ -844,7 +857,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
844
857
  /**
845
858
  * The one shape every mutating UPDATE in this class has: whatever columns
846
859
  * the caller is changing, then the version bump and the actor/timestamp
847
- * stamp that every mutation owes, gated on the opt-in `expectedVersion`
860
+ * stamp that every mutation owes, gated on the opt-in `ifVersion`
848
861
  * precondition and failing loudly when nothing matched.
849
862
  *
850
863
  * The guard rides in the WHERE clause even where the caller has already
@@ -853,17 +866,17 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
853
866
  * rather than silently overwritten.
854
867
  */
855
868
  versionedUpdate(id, sets, values, opts, now = toMs(/* @__PURE__ */ new Date())) {
856
- const guarded = opts.expectedVersion !== void 0;
869
+ const guarded = opts.ifVersion !== void 0;
857
870
  const changed = this.exec.run(`UPDATE records SET ${[...sets, "version = version + 1", "updated_at = ?", "updated_by = ?", "updated_via = ?"].join(", ")} WHERE id = ?${guarded ? " AND version = ?" : ""}`, [
858
871
  ...values,
859
872
  now,
860
- opts.updatedBy ?? null,
861
- opts.updatedVia ?? null,
873
+ opts.actor?.subjectId ?? null,
874
+ opts.actor?.principalId ?? null,
862
875
  id,
863
- ...guarded ? [opts.expectedVersion] : []
876
+ ...guarded ? [opts.ifVersion] : []
864
877
  ]);
865
878
  if (changed === 0)
866
- this.throwVersionConflict(id, opts.expectedVersion);
879
+ this.throwVersionConflict(id, opts.ifVersion);
867
880
  }
868
881
  /**
869
882
  * Read a record back after its own mutation committed, for the caller to
@@ -912,11 +925,11 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
912
925
  JSON.stringify(record.content),
913
926
  record.version,
914
927
  record.parentId ?? null,
915
- record.entityId ?? null,
928
+ record.createdBy?.subjectId ?? null,
916
929
  record.appId ?? null,
917
- record.principalId ?? null,
918
- record.updatedBy ?? null,
919
- record.updatedVia ?? null,
930
+ record.createdBy?.principalId ?? null,
931
+ record.updatedBy?.subjectId ?? null,
932
+ record.updatedBy?.principalId ?? null,
920
933
  record.deletedAt ? toMs(record.deletedAt) : null,
921
934
  record.unlistedAt ? toMs(record.unlistedAt) : null
922
935
  ]);
@@ -969,13 +982,13 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
969
982
  * `associations`, `parentId` and/or `unlisted` — Stack computes this from
970
983
  * which ops the set actually moves. Such a write still lands its columns,
971
984
  * but leaves `version`, `updatedAt` and the actor stamps exactly as they
972
- * stood and takes no snapshot. `expectedVersion`, if given, is re-checked
985
+ * stood and takes no snapshot. `ifVersion`, if given, is re-checked
973
986
  * synchronously inside the transaction — the CAS guard the bumping path
974
987
  * gets from its UPDATE's WHERE clause, restated as a read because the
975
988
  * UPDATE here carries no version predicate. See
976
989
  * docs/spec/versioning.md § Version history.
977
990
  *
978
- * The expectedVersion check for a bumping write is standalone rather than
991
+ * The ifVersion check for a bumping write is standalone rather than
979
992
  * folded into the UPDATE alone, because a content change needs
980
993
  * fts.remove() to run *before* the records-table content changes. The
981
994
  * guard is still in the UPDATE's WHERE, so a writer that slipped in
@@ -985,7 +998,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
985
998
  const existing = await this.getRecord(id);
986
999
  if (!existing)
987
1000
  throw new StackNotFoundError(`Record not found: "${id}"`);
988
- this.checkExpectedVersion(existing, opts.expectedVersion);
1001
+ this.checkExpectedVersion(existing, opts.ifVersion);
989
1002
  const now = toMs(/* @__PURE__ */ new Date());
990
1003
  const sets = [];
991
1004
  const values = [];
@@ -1002,7 +1015,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1002
1015
  const current = this.readRecord(id);
1003
1016
  if (!current)
1004
1017
  throw new StackNotFoundError(`Record not found: "${id}"`);
1005
- this.checkExpectedVersion(current, opts.expectedVersion);
1018
+ this.checkExpectedVersion(current, opts.ifVersion);
1006
1019
  if (sets.length > 0) {
1007
1020
  this.exec.run(`UPDATE records SET ${sets.join(", ")} WHERE id = ?`, [...values, id]);
1008
1021
  }
@@ -1032,8 +1045,8 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1032
1045
  return this.reread(id, "mutateRecord");
1033
1046
  }
1034
1047
  async deleteRecord(id, opts = {}) {
1035
- if (opts.hard) {
1036
- return this.exec.transaction(() => this.hardDeleteRecord(id, opts.expectedVersion));
1048
+ if (opts.purge) {
1049
+ return this.exec.transaction(() => this.purgeRecord(id, opts.ifVersion));
1037
1050
  }
1038
1051
  const now = toMs(/* @__PURE__ */ new Date());
1039
1052
  this.exec.transaction(() => {
@@ -1049,18 +1062,18 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1049
1062
  * index rows, and row, returning the record as it stood — the last copy that will ever
1050
1063
  * exist, read here so that nothing can overtake it between the read and
1051
1064
  * the deletes. Null when there was no such record. No write() call —
1052
- * callers batch it. expectedVersion is checked first so a lost CAS race
1065
+ * callers batch it. ifVersion is checked first so a lost CAS race
1053
1066
  * leaves nothing touched (children reference the row, so the check can't
1054
1067
  * fold into the final DELETE).
1055
1068
  */
1056
- hardDeleteRecord(id, expectedVersion) {
1069
+ purgeRecord(id, ifVersion) {
1057
1070
  const purged = this.readRecord(id);
1058
1071
  if (!purged) {
1059
- if (expectedVersion !== void 0)
1072
+ if (ifVersion !== void 0)
1060
1073
  throw new StackNotFoundError(`Record not found: "${id}"`);
1061
1074
  return null;
1062
1075
  }
1063
- this.checkExpectedVersion(purged, expectedVersion);
1076
+ this.checkExpectedVersion(purged, ifVersion);
1064
1077
  fts5Strategy.remove(this.exec, id);
1065
1078
  this.exec.run("DELETE FROM associations WHERE record_id = ?", [id]);
1066
1079
  this.exec.run("DELETE FROM versions WHERE record_id = ?", [id]);
@@ -1082,7 +1095,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1082
1095
  const existing = await this.getRecord(id);
1083
1096
  if (!existing)
1084
1097
  throw new StackNotFoundError(`Record not found: "${id}"`);
1085
- this.checkExpectedVersion(existing, opts.expectedVersion);
1098
+ this.checkExpectedVersion(existing, opts.ifVersion);
1086
1099
  const target = await this.getVersion(id, version);
1087
1100
  if (!target)
1088
1101
  throw new StackNotFoundError(`Version not found: "${id}"@${version}`);
@@ -1098,7 +1111,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1098
1111
  const existing = await this.getRecord(id);
1099
1112
  if (!existing)
1100
1113
  throw new StackNotFoundError(`Record not found: "${id}"`);
1101
- this.checkExpectedVersion(existing, opts.expectedVersion);
1114
+ this.checkExpectedVersion(existing, opts.ifVersion);
1102
1115
  this.exec.transaction(() => {
1103
1116
  if (opts.snapshot)
1104
1117
  this.snapshotBeforeMutation(id, opts.snapshot);
@@ -1133,7 +1146,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1133
1146
  if (budget <= 0)
1134
1147
  break;
1135
1148
  const statement2 = atBudget(page2, budget);
1136
- rows.push(...asStackQueryError(query, () => this.exec.all(statement2.sql, statement2.params)));
1149
+ rows.push(...asStackBadRequestError(query, () => this.exec.all(statement2.sql, statement2.params)));
1137
1150
  }
1138
1151
  const hasMore = rows.length > limit;
1139
1152
  const page = hasMore ? rows.slice(0, limit) : rows;
@@ -1146,7 +1159,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1146
1159
  return { records, cursor };
1147
1160
  }
1148
1161
  /**
1149
- * Atomically verify fileId is unreferenced, then hard-delete its
1162
+ * Atomically verify fileId is unreferenced, then purge its
1150
1163
  * metadata records across every typeId in `metadataTypeIds` (see
1151
1164
  * deleteUnreferencedAttachmentRecords on the adapter contract). A real
1152
1165
  * SQL transaction of synchronous calls — no `await` between the check
@@ -1168,7 +1181,7 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1168
1181
  WHERE type_id IN (${placeholders}) AND json_extract(content, '$.fileId') = ?`, [...metadataTypeIds, fileId]);
1169
1182
  const deleted = [];
1170
1183
  for (const row of metaRows) {
1171
- const purged = this.hardDeleteRecord(row.id);
1184
+ const purged = this.purgeRecord(row.id);
1172
1185
  if (purged)
1173
1186
  deleted.push(purged);
1174
1187
  }
@@ -1290,9 +1303,9 @@ var SharedSqlRecordLogic = class _SharedSqlRecordLogic {
1290
1303
  }
1291
1304
  const conditions = ["record_id = ?"];
1292
1305
  const values = [id];
1293
- if (query.sinceSeq !== void 0) {
1306
+ if (query.afterSeq !== void 0) {
1294
1307
  conditions.push("seq > ?");
1295
- values.push(query.sinceSeq);
1308
+ values.push(query.afterSeq);
1296
1309
  }
1297
1310
  const rows = this.exec.all(`SELECT * FROM journal WHERE ${conditions.join(" AND ")} ORDER BY seq ASC${query.limit !== void 0 ? " LIMIT ?" : ""}`, query.limit !== void 0 ? [...values, query.limit] : values);
1298
1311
  return rows.map(rowToJournalEntry);
@@ -1612,17 +1625,17 @@ var SharedTokenLogic = class {
1612
1625
  get exec() {
1613
1626
  return this.deps.exec;
1614
1627
  }
1615
- async createToken(principalId, opts = {}) {
1628
+ async createToken(actor, opts = {}) {
1616
1629
  const id = randomBytes(8).toString("hex");
1617
1630
  const token = randomBytes(32).toString("hex");
1618
1631
  const tokenHash = hashToken(token);
1619
1632
  this.exec.run("INSERT INTO tokens (id, token_hash, principal_id, subject_id, label, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)", [
1620
1633
  id,
1621
1634
  tokenHash,
1622
- principalId,
1623
1635
  // Stored rather than left null for an undelegated token, so a
1624
1636
  // reader never has to know which column stands in for the other.
1625
- opts.onBehalfOf ?? principalId,
1637
+ actor.principalId ?? actor.subjectId,
1638
+ actor.subjectId,
1626
1639
  opts.label ?? null,
1627
1640
  toMs(/* @__PURE__ */ new Date()),
1628
1641
  opts.expiresAt ? toMs(opts.expiresAt) : null
@@ -1705,8 +1718,8 @@ var NativeTokenStore = class _NativeTokenStore {
1705
1718
  store.tokens = new SharedTokenLogic({ exec: new NativeSqliteExecutor(store.db) });
1706
1719
  return store;
1707
1720
  }
1708
- createToken(principalId, opts) {
1709
- return this.tokens.createToken(principalId, opts);
1721
+ createToken(actor, opts) {
1722
+ return this.tokens.createToken(actor, opts);
1710
1723
  }
1711
1724
  lookupToken(token) {
1712
1725
  return this.tokens.lookupToken(token);
@@ -1756,7 +1769,7 @@ var NativeSQLiteRecordAdapter = class _NativeSQLiteRecordAdapter extends SharedS
1756
1769
  );
1757
1770
  }
1758
1771
  const [db, exec] = _NativeSQLiteRecordAdapter.attach(opts.path, opts.force);
1759
- const config = insertConfigRecord(exec, opts.entityId, opts.timezone);
1772
+ const config = insertConfigRecord(exec, opts.ownerEntityId, opts.timezone);
1760
1773
  return new _NativeSQLiteRecordAdapter(opts.path, db, exec, config);
1761
1774
  }
1762
1775
  /**