@sixb/sqlite 0.1.6 → 0.1.8

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.
Files changed (91) hide show
  1. package/dist/agents/rows.d.ts +7 -4
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/ai-cost-storage.d.ts +2 -1
  9. package/dist/ai-cost-storage.d.ts.map +1 -1
  10. package/dist/ai-limit-storage.d.ts +35 -0
  11. package/dist/ai-limit-storage.d.ts.map +1 -0
  12. package/dist/ai-usage-storage.d.ts +1 -0
  13. package/dist/ai-usage-storage.d.ts.map +1 -1
  14. package/dist/auth-storage/access-tokens.d.ts +1 -1
  15. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  16. package/dist/auth-storage/auth-storage.d.ts +4 -1
  17. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  18. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  19. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  20. package/dist/auth-storage/rows.d.ts +19 -1
  21. package/dist/auth-storage/rows.d.ts.map +1 -1
  22. package/dist/auth-storage/shared.d.ts +1 -1
  23. package/dist/auth-storage/shared.d.ts.map +1 -1
  24. package/dist/execution-storage.d.ts +3 -2
  25. package/dist/execution-storage.d.ts.map +1 -1
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3443 -789
  29. package/dist/index.js.map +29 -20
  30. package/dist/migrations.d.ts.map +1 -1
  31. package/dist/objects/index.d.ts +3 -0
  32. package/dist/objects/index.d.ts.map +1 -0
  33. package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
  34. package/dist/objects/query-compiler.d.ts.map +1 -0
  35. package/dist/objects/read-scope.d.ts +17 -0
  36. package/dist/objects/read-scope.d.ts.map +1 -0
  37. package/dist/objects/reader.d.ts +51 -0
  38. package/dist/objects/reader.d.ts.map +1 -0
  39. package/dist/objects/rows.d.ts +33 -0
  40. package/dist/objects/rows.d.ts.map +1 -0
  41. package/dist/objects/selected-reader.d.ts +9 -0
  42. package/dist/objects/selected-reader.d.ts.map +1 -0
  43. package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
  44. package/dist/objects/storage.d.ts.map +1 -0
  45. package/dist/share-grant-storage.d.ts +20 -0
  46. package/dist/share-grant-storage.d.ts.map +1 -0
  47. package/dist/share-session-storage.d.ts +19 -0
  48. package/dist/share-session-storage.d.ts.map +1 -0
  49. package/dist/storage-errors.d.ts +1 -0
  50. package/dist/storage-errors.d.ts.map +1 -1
  51. package/dist/transactions.d.ts +8 -0
  52. package/dist/transactions.d.ts.map +1 -1
  53. package/package.json +3 -3
  54. package/src/agents/rows.ts +36 -9
  55. package/src/agents/runs.ts +152 -13
  56. package/src/agents/threads.ts +2 -17
  57. package/src/ai-cost-groups.ts +100 -0
  58. package/src/ai-cost-storage.ts +67 -25
  59. package/src/ai-limit-storage.ts +981 -0
  60. package/src/ai-usage-storage.ts +24 -1
  61. package/src/auth-storage/access-tokens.ts +77 -68
  62. package/src/auth-storage/auth-storage.ts +80 -1
  63. package/src/auth-storage/device-authorizations.ts +153 -0
  64. package/src/auth-storage/rows.ts +41 -0
  65. package/src/auth-storage/shared.ts +1 -0
  66. package/src/execution-storage.ts +22 -3
  67. package/src/index.ts +24 -3
  68. package/src/migrations/029-model-accounting.sql +79 -0
  69. package/src/migrations/030-ai-usage-limits.sql +100 -0
  70. package/src/migrations/031-subagent-runs.sql +231 -0
  71. package/src/migrations/032-conversation-run-spec.sql +78 -0
  72. package/src/migrations/033-retire-agent-definitions.sql +89 -0
  73. package/src/migrations/034-device-authorizations.sql +22 -0
  74. package/src/migrations/035-share-grants.sql +66 -0
  75. package/src/migrations/036-share-sessions.sql +226 -0
  76. package/src/migrations.ts +22 -0
  77. package/src/objects/index.ts +2 -0
  78. package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
  79. package/src/objects/read-scope.ts +316 -0
  80. package/src/objects/reader.ts +526 -0
  81. package/src/objects/rows.ts +123 -0
  82. package/src/objects/selected-reader.ts +119 -0
  83. package/src/objects/storage.ts +296 -0
  84. package/src/share-grant-storage.ts +252 -0
  85. package/src/share-session-storage.ts +189 -0
  86. package/src/storage-errors.ts +4 -0
  87. package/src/transactions.ts +32 -0
  88. package/src/workflow-run-storage.ts +8 -8
  89. package/dist/object-query-compiler.d.ts.map +0 -1
  90. package/dist/object-storage.d.ts.map +0 -1
  91. package/src/object-storage.ts +0 -709
@@ -0,0 +1,119 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type {
3
+ CompiledSelectedObjectReadScope,
4
+ ObjectQueryCapabilities,
5
+ ObjectReadExecutionLimits,
6
+ ObjectReadStorage,
7
+ } from "@sixb/core/storage"
8
+ import {
9
+ assertObjectReaderProject,
10
+ assertObjectReadFacetCount,
11
+ assertObjectReadOutputWithinLimit,
12
+ ObjectReadLimitExceededError,
13
+ snapshotObjectReadExecutionLimits,
14
+ } from "@sixb/core/storage"
15
+ import { runDeferredReadTransaction } from "../transactions"
16
+ import {
17
+ compileSqliteSelectedObjectReadSource,
18
+ type SqliteSelectedObjectReadSource,
19
+ } from "./read-scope"
20
+ import { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
21
+
22
+ /** Keep the traversal probe and every terminal statement in the same synchronous snapshot. */
23
+ export function createSqliteSelectedReader(
24
+ db: Database,
25
+ params: {
26
+ projectId: string
27
+ scope: CompiledSelectedObjectReadScope
28
+ limits: ObjectReadExecutionLimits
29
+ },
30
+ queryCapabilities: () => ObjectQueryCapabilities
31
+ ): ObjectReadStorage {
32
+ const projectId = params.projectId
33
+ const limits = snapshotObjectReadExecutionLimits(params.limits)
34
+ const source = compileSqliteSelectedObjectReadSource(
35
+ projectId,
36
+ params.scope,
37
+ limits.maxTraversalFacts
38
+ )
39
+ const reader = new SqliteObjectReader(db, source)
40
+ const assertProject = (actualProjectId: string): void =>
41
+ assertObjectReaderProject(projectId, actualProjectId)
42
+ const read = <T>(run: () => T): T =>
43
+ runDeferredReadTransaction(db, () => {
44
+ assertTraversalBudget(db, source, limits.maxTraversalFacts)
45
+ const value = run()
46
+ assertObjectReadOutputWithinLimit(value, limits)
47
+ return value
48
+ })
49
+ const readMap = <TKey, TValue>(run: () => Map<TKey, TValue>): Map<TKey, TValue> =>
50
+ runDeferredReadTransaction(db, () => {
51
+ assertTraversalBudget(db, source, limits.maxTraversalFacts)
52
+ const value = run()
53
+ assertObjectReadOutputWithinLimit([...value.entries()], limits)
54
+ return value
55
+ })
56
+
57
+ return Object.freeze({
58
+ queryCapabilities,
59
+ queryObjects: async (input) => {
60
+ assertProject(input.projectId)
61
+ return read(() => reader.queryObjects(input))
62
+ },
63
+ countObjects: async (input) => {
64
+ assertProject(input.projectId)
65
+ return read(() => reader.countObjects(input))
66
+ },
67
+ existsObjects: async (input) => {
68
+ assertProject(input.projectId)
69
+ return read(() => reader.existsObjects(input))
70
+ },
71
+ facetObjects: async (input) => {
72
+ assertProject(input.projectId)
73
+ assertObjectReadFacetCount(input.facets.length)
74
+ return read(() => reader.facetObjects(input))
75
+ },
76
+ getByPrimaryId: async (input) => {
77
+ assertProject(input.projectId)
78
+ return read(() => reader.getByPrimaryId(input))
79
+ },
80
+ selectsObjectProperties: async (input) => {
81
+ assertProject(input.projectId)
82
+ return read(() => reader.selectsObjectProperties(input))
83
+ },
84
+ listLinks: async (input) => {
85
+ assertProject(input.projectId)
86
+ return read(() => reader.listLinks(input))
87
+ },
88
+ getByPrimaryIdBatch: async (input) => {
89
+ assertProject(input.projectId)
90
+ return readMap(() => reader.getByPrimaryIdBatch(input))
91
+ },
92
+ listLinksBatch: async (input) => {
93
+ assertProject(input.projectId)
94
+ return readMap(() => reader.listLinksBatch(input))
95
+ },
96
+ queryLinks: async (input) => {
97
+ assertProject(input.projectId)
98
+ assertLinkQueryLimit(input.limit)
99
+ return read(() => reader.queryLinks(input))
100
+ },
101
+ list: async (input) => {
102
+ assertProject(input.projectId)
103
+ return read(() => reader.list(input))
104
+ },
105
+ } satisfies ObjectReadStorage)
106
+ }
107
+
108
+ function assertTraversalBudget(
109
+ db: Database,
110
+ source: SqliteSelectedObjectReadSource,
111
+ maxTraversalFacts: number
112
+ ): void {
113
+ const row = db.query(source.traversalProbe.sql).get(...source.traversalProbe.args) as {
114
+ total: number | bigint
115
+ }
116
+ if (BigInt(row.total) > BigInt(maxTraversalFacts)) {
117
+ throw new ObjectReadLimitExceededError("traversalFacts", maxTraversalFacts)
118
+ }
119
+ }
@@ -0,0 +1,296 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type {
3
+ CompiledSelectedObjectReadScope,
4
+ CountObjectsInput,
5
+ CountObjectsResult,
6
+ ExistsObjectsInput,
7
+ ExistsObjectsResult,
8
+ FacetObjectsInput,
9
+ FacetObjectsResult,
10
+ LinkBatchKey,
11
+ LinkDirection,
12
+ ObjectBatchKey,
13
+ ObjectLinkRow,
14
+ ObjectQueryCapabilities,
15
+ ObjectReadExecutionLimits,
16
+ ObjectReadStorage,
17
+ ObjectRow,
18
+ ObjectStorage,
19
+ QueryObjectLinksInput,
20
+ QueryObjectLinksResult,
21
+ QueryObjectsInput,
22
+ QueryObjectsResult,
23
+ } from "@sixb/core/storage"
24
+ import { installFreshSqliteSchema } from "../migrations"
25
+ import {
26
+ closeSqliteStoreConnection,
27
+ openSqliteStoreConnection,
28
+ type SqliteStoreConnection,
29
+ } from "../transactions"
30
+ import { DEFAULT_OBJECT_QUERY_SOURCE } from "./query-compiler"
31
+ import { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
32
+ import { type DatabaseRow, type LinkDatabaseRow, rowToLink, rowToObject } from "./rows"
33
+ import { createSqliteSelectedReader } from "./selected-reader"
34
+
35
+ export interface SqliteObjectStorageOptions {
36
+ /** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
37
+ path?: string
38
+ /** Internal shared connection used by bundled SqliteStorage. */
39
+ connection?: SqliteStoreConnection
40
+ }
41
+
42
+ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
43
+ queryObjects: true,
44
+ countObjects: true,
45
+ existsObjects: true,
46
+ facetObjects: true,
47
+ nodes: {
48
+ start: true,
49
+ refs: true,
50
+ filter: true,
51
+ text: true,
52
+ sort: true,
53
+ limit: true,
54
+ page: true,
55
+ traverse: true,
56
+ set: true,
57
+ project: true,
58
+ expand: true,
59
+ },
60
+ predicateOps: {
61
+ and: true,
62
+ or: true,
63
+ not: true,
64
+ eq: true,
65
+ neq: true,
66
+ lt: true,
67
+ lte: true,
68
+ gt: true,
69
+ gte: true,
70
+ in: true,
71
+ exists: true,
72
+ contains: true,
73
+ },
74
+ sortKinds: {
75
+ property: true,
76
+ },
77
+ traversalDirections: {
78
+ outgoing: true,
79
+ incoming: true,
80
+ },
81
+ setOps: {
82
+ union: true,
83
+ intersect: true,
84
+ subtract: true,
85
+ },
86
+ scalarOperations: {
87
+ string: { equality: true, ordering: true },
88
+ uuid: { equality: true, ordering: true },
89
+ boolean: { equality: true },
90
+ integer: { equality: true, ordering: true },
91
+ double: { equality: true, ordering: true },
92
+ decimal: { equality: true },
93
+ date: { equality: true, ordering: true },
94
+ timestamp: { equality: true, ordering: true },
95
+ },
96
+ limits: {
97
+ totalCount: true,
98
+ stablePageTokens: true,
99
+ },
100
+ notes: [
101
+ "SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
102
+ "expand hydrates linked objects in-database (top-N per parent via row_number() + json_group_array); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
103
+ "Ordered decimal predicates and sorting use the bounded core fallback because SQLite has no native exact decimal type; canonical decimal equality remains pushdown-safe.",
104
+ "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
105
+ ],
106
+ }
107
+
108
+ /**
109
+ * SQLite-based ObjectStorage implementation.
110
+ *
111
+ * Stores object projections and links. V1 object-query IR pushdown covers the
112
+ * scalar JSON-property and link-traversal subset declared by queryCapabilities().
113
+ */
114
+ export class SqliteObjectStorage implements ObjectStorage {
115
+ private readonly connection: SqliteStoreConnection
116
+ private readonly db: Database
117
+ private readonly reader: SqliteObjectReader
118
+
119
+ constructor(options: SqliteObjectStorageOptions = {}) {
120
+ this.connection = openSqliteStoreConnection(options)
121
+ this.db = this.connection.db
122
+ this.reader = new SqliteObjectReader(this.db, DEFAULT_OBJECT_QUERY_SOURCE)
123
+
124
+ if (this.connection.installFreshSchema) {
125
+ installFreshSqliteSchema(this.db)
126
+ }
127
+ }
128
+
129
+ queryCapabilities(): ObjectQueryCapabilities {
130
+ return SQLITE_OBJECT_QUERY_CAPABILITIES
131
+ }
132
+
133
+ createSelectedReadScope(params: {
134
+ projectId: string
135
+ scope: CompiledSelectedObjectReadScope
136
+ limits: ObjectReadExecutionLimits
137
+ }): ObjectReadStorage {
138
+ return createSqliteSelectedReader(this.db, params, () => this.queryCapabilities())
139
+ }
140
+
141
+ async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
142
+ return this.reader.queryObjects(params)
143
+ }
144
+
145
+ async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
146
+ return this.reader.countObjects(params)
147
+ }
148
+
149
+ async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
150
+ return this.reader.existsObjects(params)
151
+ }
152
+
153
+ async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
154
+ return this.reader.facetObjects(params)
155
+ }
156
+
157
+ async getByPrimaryId(params: {
158
+ projectId: string
159
+ objectTypeId: string
160
+ primaryId: string
161
+ }): Promise<ObjectRow | null> {
162
+ return this.reader.getByPrimaryId(params)
163
+ }
164
+
165
+ async selectsObjectProperties(
166
+ params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
167
+ ): Promise<readonly boolean[]> {
168
+ return this.reader.selectsObjectProperties(params)
169
+ }
170
+
171
+ async listLinks(params: {
172
+ projectId: string
173
+ objectTypeId: string
174
+ objectId: string
175
+ linkId?: string
176
+ direction?: LinkDirection
177
+ }): Promise<readonly ObjectLinkRow[]> {
178
+ return this.reader.listLinks(params)
179
+ }
180
+
181
+ async getByPrimaryIdBatch(params: {
182
+ projectId: string
183
+ items: readonly { objectTypeId: string; primaryId: string }[]
184
+ }): Promise<Map<ObjectBatchKey, ObjectRow>> {
185
+ return this.reader.getByPrimaryIdBatch(params)
186
+ }
187
+
188
+ async listLinksBatch(params: {
189
+ projectId: string
190
+ direction?: LinkDirection
191
+ items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
192
+ }): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
193
+ return this.reader.listLinksBatch(params)
194
+ }
195
+
196
+ async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
197
+ assertLinkQueryLimit(params.limit)
198
+ return this.reader.queryLinks(params)
199
+ }
200
+
201
+ async listIncidentLinksBatch(params: {
202
+ projectId: string
203
+ items: readonly { objectTypeId: string; objectId: string }[]
204
+ }): Promise<readonly ObjectLinkRow[]> {
205
+ if (params.items.length === 0) return []
206
+
207
+ const rows = this.db
208
+ .query(
209
+ `
210
+ WITH requested AS (
211
+ SELECT
212
+ json_extract(value, '$.objectTypeId') AS object_type_id,
213
+ json_extract(value, '$.objectId') AS object_id
214
+ FROM json_each(?)
215
+ )
216
+ SELECT link.*
217
+ FROM links AS link
218
+ JOIN requested
219
+ ON requested.object_type_id = link.source_type_id
220
+ AND requested.object_id = link.source_id
221
+ WHERE link.project_id = ?
222
+ UNION
223
+ SELECT link.*
224
+ FROM links AS link
225
+ JOIN requested
226
+ ON requested.object_type_id = link.target_type_id
227
+ AND requested.object_id = link.target_id
228
+ WHERE link.project_id = ?
229
+ `
230
+ )
231
+ .all(JSON.stringify(params.items), params.projectId, params.projectId) as LinkDatabaseRow[]
232
+
233
+ return rows.map((row) => rowToLink(row))
234
+ }
235
+
236
+ async listByPrimaryIdPage(params: {
237
+ projectId: string
238
+ objectTypeId: string
239
+ afterPrimaryId?: string
240
+ limit: number
241
+ }): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
242
+ assertReconciliationPageLimit(params.limit)
243
+ const rows = this.db
244
+ .query(
245
+ `
246
+ SELECT * FROM objects
247
+ WHERE project_id = ? AND object_type_id = ?
248
+ AND (? IS NULL OR primary_id > ?)
249
+ ORDER BY primary_id ASC
250
+ LIMIT ?
251
+ `
252
+ )
253
+ .all(
254
+ params.projectId,
255
+ params.objectTypeId,
256
+ params.afterPrimaryId ?? null,
257
+ params.afterPrimaryId ?? null,
258
+ params.limit + 1
259
+ ) as DatabaseRow[]
260
+ const hasMore = rows.length > params.limit
261
+ const objects = rows.slice(0, params.limit).map((row) => rowToObject(row))
262
+ const last = objects.at(-1)
263
+ return {
264
+ objects,
265
+ ...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
266
+ }
267
+ }
268
+
269
+ async list(params: {
270
+ projectId: string
271
+ objectTypeId?: string | readonly string[]
272
+ primaryIdPrefix?: string
273
+ primaryIdSuffix?: string
274
+ updatedAfter?: Date
275
+ updatedBefore?: Date
276
+ createdAfter?: Date
277
+ createdBefore?: Date
278
+ limit?: number
279
+ offset?: number
280
+ orderBy?: "createdAt" | "updatedAt" | "primaryId"
281
+ order?: "asc" | "desc"
282
+ }): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
283
+ return this.reader.list(params)
284
+ }
285
+
286
+ /** Close the connection only when this provider owns it. */
287
+ close(): void {
288
+ closeSqliteStoreConnection(this.connection)
289
+ }
290
+ }
291
+
292
+ function assertReconciliationPageLimit(limit: number): void {
293
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
294
+ throw new Error("Object reconciliation page limit must be a positive safe integer.")
295
+ }
296
+ }
@@ -0,0 +1,252 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import {
3
+ normalizeGetShareGrantByIdInput,
4
+ normalizeListShareGrantsInput,
5
+ normalizeRevokeShareGrantInput,
6
+ normalizeShareGrantCreate,
7
+ parseShareGrantRecord,
8
+ } from "@sixb/core/internal/share-grant-storage-provider"
9
+ import type {
10
+ CreateShareGrantInput,
11
+ GetShareGrantByIdInput,
12
+ ListShareGrantsInput,
13
+ ListShareGrantsResult,
14
+ RevokeShareGrantInput,
15
+ ShareGrantRecord,
16
+ ShareGrantStorage,
17
+ } from "@sixb/core/storage"
18
+ import { ShareGrantStorageError } from "@sixb/core/storage"
19
+ import { installFreshSqliteSchema } from "./migrations"
20
+ import { isUniqueConstraintError } from "./storage-errors"
21
+ import {
22
+ closeSqliteStoreConnection,
23
+ openSqliteStoreConnection,
24
+ type SqliteStoreConnection,
25
+ } from "./transactions"
26
+
27
+ export interface SqliteShareGrantStorageOptions {
28
+ readonly path?: string
29
+ readonly connection?: SqliteStoreConnection
30
+ }
31
+
32
+ /** SQLite persistence for issued Share grants. Secret handling remains in Core. */
33
+ export class SqliteShareGrantStorage implements ShareGrantStorage {
34
+ private readonly connection: SqliteStoreConnection
35
+ private readonly db: Database
36
+
37
+ constructor(options: SqliteShareGrantStorageOptions = {}) {
38
+ this.connection = openSqliteStoreConnection(options)
39
+ this.db = this.connection.db
40
+ if (this.connection.installFreshSchema) installFreshSqliteSchema(this.db)
41
+ }
42
+
43
+ async create(input: CreateShareGrantInput): Promise<ShareGrantRecord> {
44
+ const record = normalizeShareGrantCreate(input)
45
+ try {
46
+ this.db
47
+ .query(
48
+ `
49
+ INSERT INTO share_grants (
50
+ project_id, id, definition_id, target_object_type_id, target_primary_id,
51
+ issued_by_type, issued_by_id, authority_version, authority_snapshot,
52
+ authority_digest, token_hash, destination_path, created_at, expires_at
53
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
54
+ `
55
+ )
56
+ .run(
57
+ record.projectId,
58
+ record.id,
59
+ record.definitionId,
60
+ record.target.objectTypeId,
61
+ record.target.primaryId,
62
+ record.issuedBy.type,
63
+ record.issuedBy.id,
64
+ record.authoritySnapshot.version,
65
+ JSON.stringify(record.authoritySnapshot),
66
+ record.authorityDigest,
67
+ record.tokenHash,
68
+ record.destinationPath,
69
+ record.createdAt.toISOString(),
70
+ record.expiresAt.toISOString()
71
+ )
72
+ } catch (error) {
73
+ if (isUniqueConstraintError(error)) {
74
+ throw new ShareGrantStorageError(
75
+ "duplicate",
76
+ `[SixbSqlite] Share grant '${record.id}' conflicts with an existing record.`,
77
+ { cause: error }
78
+ )
79
+ }
80
+ throw error
81
+ }
82
+ return this.require(record.projectId, record.id, "create")
83
+ }
84
+
85
+ async getById(input: GetShareGrantByIdInput): Promise<ShareGrantRecord | null> {
86
+ const normalized = normalizeGetShareGrantByIdInput(input)
87
+ return this.getRow(normalized.projectId, normalized.id)
88
+ }
89
+
90
+ async list(input: ListShareGrantsInput): Promise<ListShareGrantsResult> {
91
+ const normalized = normalizeListShareGrantsInput(input)
92
+ const where = ["project_id = ?"]
93
+ const args: (string | number)[] = [normalized.projectId]
94
+ if (normalized.definitionId !== undefined) {
95
+ where.push("definition_id = ?")
96
+ args.push(normalized.definitionId)
97
+ }
98
+ if (normalized.target !== undefined) {
99
+ where.push("target_object_type_id = ?", "target_primary_id = ?")
100
+ args.push(normalized.target.objectTypeId, normalized.target.primaryId)
101
+ }
102
+ if (!normalized.includeRevoked) where.push("revoked_at IS NULL")
103
+ if (!normalized.includeExpired) {
104
+ where.push("expires_at > ?")
105
+ args.push(normalized.now.toISOString())
106
+ }
107
+
108
+ const predicate = where.join(" AND ")
109
+ const totalRow = this.db
110
+ .query(`SELECT COUNT(*) AS total FROM share_grants WHERE ${predicate}`)
111
+ .get(...args) as { readonly total: number }
112
+ const rows = this.db
113
+ .query(
114
+ `
115
+ SELECT * FROM share_grants
116
+ WHERE ${predicate}
117
+ ORDER BY created_at DESC, id DESC
118
+ LIMIT ? OFFSET ?
119
+ `
120
+ )
121
+ .all(...args, normalized.limit, normalized.offset) as SqliteShareGrantRow[]
122
+ const grants = rows.map(rowToRecord)
123
+ return {
124
+ grants,
125
+ total: totalRow.total,
126
+ hasMore: normalized.offset + grants.length < totalRow.total,
127
+ }
128
+ }
129
+
130
+ async revoke(input: RevokeShareGrantInput): Promise<ShareGrantRecord | null> {
131
+ const normalized = normalizeRevokeShareGrantInput(input)
132
+ const current = this.getRow(normalized.projectId, normalized.id)
133
+ if (!current || current.revokedAt) return current
134
+
135
+ const revocation = normalizeRevokeShareGrantInput(normalized, current.createdAt)
136
+ this.db
137
+ .query(
138
+ `
139
+ UPDATE share_grants
140
+ SET revoked_at = ?, revoked_by_type = ?, revoked_by_id = ?
141
+ WHERE project_id = ? AND id = ? AND revoked_at IS NULL
142
+ `
143
+ )
144
+ .run(
145
+ revocation.revokedAt.toISOString(),
146
+ revocation.revokedBy.type,
147
+ revocation.revokedBy.id,
148
+ revocation.projectId,
149
+ revocation.id
150
+ )
151
+ return this.require(revocation.projectId, revocation.id, "revoke")
152
+ }
153
+
154
+ close(): void {
155
+ closeSqliteStoreConnection(this.connection)
156
+ }
157
+
158
+ private getRow(projectId: string, id: string): ShareGrantRecord | null {
159
+ const row = this.db
160
+ .query("SELECT * FROM share_grants WHERE project_id = ? AND id = ?")
161
+ .get(projectId, id) as SqliteShareGrantRow | null
162
+ return row ? rowToRecord(row) : null
163
+ }
164
+
165
+ private require(projectId: string, id: string, operation: string): ShareGrantRecord {
166
+ const record = this.getRow(projectId, id)
167
+ if (!record) {
168
+ throw new ShareGrantStorageError(
169
+ "invalid_record",
170
+ `[SixbSqlite] Share grant '${id}' disappeared after ${operation}.`
171
+ )
172
+ }
173
+ return record
174
+ }
175
+ }
176
+
177
+ interface SqliteShareGrantRow {
178
+ readonly project_id: unknown
179
+ readonly id: unknown
180
+ readonly definition_id: unknown
181
+ readonly target_object_type_id: unknown
182
+ readonly target_primary_id: unknown
183
+ readonly issued_by_type: unknown
184
+ readonly issued_by_id: unknown
185
+ readonly authority_version: unknown
186
+ readonly authority_snapshot: unknown
187
+ readonly authority_digest: unknown
188
+ readonly token_hash: unknown
189
+ readonly destination_path: unknown
190
+ readonly created_at: unknown
191
+ readonly expires_at: unknown
192
+ readonly revoked_at: unknown
193
+ readonly revoked_by_type: unknown
194
+ readonly revoked_by_id: unknown
195
+ }
196
+
197
+ function rowToRecord(row: SqliteShareGrantRow): ShareGrantRecord {
198
+ const authoritySnapshot = parseStoredJson(row.authority_snapshot, "authority snapshot")
199
+ if (
200
+ typeof row.authority_version !== "number" ||
201
+ !isRecord(authoritySnapshot) ||
202
+ authoritySnapshot.version !== row.authority_version
203
+ ) {
204
+ throw invalidRecord("Share authority version does not match its snapshot.")
205
+ }
206
+
207
+ const hasRevokedAt = row.revoked_at !== null
208
+ const hasRevokedBy = row.revoked_by_type !== null || row.revoked_by_id !== null
209
+ if (hasRevokedAt !== hasRevokedBy) {
210
+ throw invalidRecord("Share revocation time and actor must be stored together.")
211
+ }
212
+
213
+ return parseShareGrantRecord({
214
+ projectId: row.project_id,
215
+ id: row.id,
216
+ definitionId: row.definition_id,
217
+ target: {
218
+ objectTypeId: row.target_object_type_id,
219
+ primaryId: row.target_primary_id,
220
+ },
221
+ issuedBy: { type: row.issued_by_type, id: row.issued_by_id },
222
+ authoritySnapshot,
223
+ authorityDigest: row.authority_digest,
224
+ tokenHash: row.token_hash,
225
+ destinationPath: row.destination_path,
226
+ createdAt: row.created_at,
227
+ expiresAt: row.expires_at,
228
+ ...(hasRevokedAt ? { revokedAt: row.revoked_at } : {}),
229
+ ...(hasRevokedBy ? { revokedBy: { type: row.revoked_by_type, id: row.revoked_by_id } } : {}),
230
+ })
231
+ }
232
+
233
+ function parseStoredJson(value: unknown, field: string): unknown {
234
+ if (typeof value !== "string") throw invalidRecord(`Share ${field} must be stored as JSON text.`)
235
+ try {
236
+ return JSON.parse(value)
237
+ } catch (cause) {
238
+ throw new ShareGrantStorageError(
239
+ "invalid_record",
240
+ `[SixbSqlite] Share ${field} contains invalid JSON.`,
241
+ { cause }
242
+ )
243
+ }
244
+ }
245
+
246
+ function isRecord(value: unknown): value is Record<string, unknown> {
247
+ return typeof value === "object" && value !== null && !Array.isArray(value)
248
+ }
249
+
250
+ function invalidRecord(message: string): ShareGrantStorageError {
251
+ return new ShareGrantStorageError("invalid_record", `[SixbSqlite] ${message}`)
252
+ }