@sixb/sqlite 0.0.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. package/src/workflow-run-storage.ts +1148 -0
@@ -0,0 +1,338 @@
1
+ import type { Principal } from "@sixb/core"
2
+ import type {
3
+ AccessTokenRecord,
4
+ GroupMembershipRecord,
5
+ GroupMembershipSource,
6
+ InvitationRecord,
7
+ InvitationStatus,
8
+ MagicLinkRecord,
9
+ OidcAuthorizationAttemptRecord,
10
+ ServiceAccountGroupMembershipRecord,
11
+ ServiceAccountRecord,
12
+ ServiceAccountStatus,
13
+ SessionRecord,
14
+ UserIdentityRecord,
15
+ UserRecord,
16
+ UserStatus,
17
+ } from "@sixb/core/storage"
18
+
19
+ export interface SqliteAuthUserRow {
20
+ readonly project_id: string
21
+ readonly id: string
22
+ readonly email: string
23
+ readonly display_name: string | null
24
+ readonly avatar_url: string | null
25
+ readonly status: UserStatus
26
+ readonly created_at: string
27
+ readonly updated_at: string
28
+ }
29
+
30
+ export interface SqliteAuthUserIdentityRow {
31
+ readonly project_id: string
32
+ readonly strategy_id: string
33
+ readonly subject: string
34
+ readonly user_id: string
35
+ readonly claims: string | null
36
+ readonly created_at: string
37
+ readonly updated_at: string
38
+ }
39
+
40
+ export interface SqliteAuthServiceAccountRow {
41
+ readonly project_id: string
42
+ readonly id: string
43
+ readonly name: string
44
+ readonly description: string | null
45
+ readonly status: ServiceAccountStatus
46
+ readonly created_by_principal_type: Principal["type"] | null
47
+ readonly created_by_principal_id: string | null
48
+ readonly created_by_session_id: string | null
49
+ readonly created_at: string
50
+ readonly updated_at: string
51
+ }
52
+
53
+ export interface SqliteAuthServiceAccountGroupMembershipRow {
54
+ readonly project_id: string
55
+ readonly service_account_id: string
56
+ readonly group_id: string
57
+ readonly source: GroupMembershipSource
58
+ readonly created_at: string
59
+ }
60
+
61
+ export interface SqliteAuthSessionRow {
62
+ readonly project_id: string
63
+ readonly id: string
64
+ readonly user_id: string
65
+ readonly strategy_id: string
66
+ readonly audience: SessionRecord["audience"]
67
+ readonly token_hash: string
68
+ readonly created_at: string
69
+ readonly expires_at: string
70
+ readonly absolute_expires_at: string | null
71
+ readonly revoked_at: string | null
72
+ readonly last_seen_at: string | null
73
+ readonly user_agent: string | null
74
+ readonly ip_address: string | null
75
+ }
76
+
77
+ export interface SqliteAuthAccessTokenRow {
78
+ readonly project_id: string
79
+ readonly id: string
80
+ readonly name: string
81
+ readonly kind: AccessTokenRecord["kind"]
82
+ readonly subject_type: AccessTokenRecord["subjectType"]
83
+ readonly subject_id: string
84
+ readonly token_hash: string
85
+ readonly group_ids: string | null
86
+ readonly created_by_principal_type: Principal["type"] | null
87
+ readonly created_by_principal_id: string | null
88
+ readonly created_by_session_id: string | null
89
+ readonly created_at: string
90
+ readonly expires_at: string
91
+ readonly revoked_at: string | null
92
+ readonly last_used_at: string | null
93
+ readonly last_used_user_agent: string | null
94
+ readonly last_used_ip_address: string | null
95
+ }
96
+
97
+ export interface SqliteAuthInvitationRow {
98
+ readonly project_id: string
99
+ readonly id: string
100
+ readonly email: string
101
+ readonly status: InvitationStatus
102
+ readonly created_by_principal_type: Principal["type"] | null
103
+ readonly created_by_principal_id: string | null
104
+ readonly created_by_session_id: string | null
105
+ readonly created_at: string
106
+ readonly updated_at: string
107
+ readonly expires_at: string
108
+ readonly accepted_at: string | null
109
+ readonly revoked_at: string | null
110
+ }
111
+
112
+ export interface SqliteAuthGroupMembershipRow {
113
+ readonly project_id: string
114
+ readonly user_id: string
115
+ readonly group_id: string
116
+ readonly source: GroupMembershipSource
117
+ readonly created_at: string
118
+ }
119
+
120
+ export interface SqliteAuthMagicLinkRow {
121
+ readonly project_id: string
122
+ readonly id: string
123
+ readonly strategy_id: string
124
+ readonly audience: MagicLinkRecord["audience"]
125
+ readonly email: string
126
+ readonly token_hash: string
127
+ readonly return_to: string | null
128
+ readonly created_at: string
129
+ readonly expires_at: string
130
+ readonly consumed_at: string | null
131
+ readonly revoked_at: string | null
132
+ }
133
+
134
+ export interface SqliteAuthOidcAttemptRow {
135
+ readonly project_id: string
136
+ readonly id: string
137
+ readonly strategy_id: string
138
+ readonly audience: OidcAuthorizationAttemptRecord["audience"]
139
+ readonly state_hash: string
140
+ readonly nonce_hash: string
141
+ readonly code_verifier: string
142
+ readonly return_to: string | null
143
+ readonly created_at: string
144
+ readonly expires_at: string
145
+ readonly consumed_at: string | null
146
+ }
147
+
148
+ export function rowToUserRecord(row: SqliteAuthUserRow): UserRecord {
149
+ return {
150
+ id: row.id,
151
+ projectId: row.project_id,
152
+ email: row.email,
153
+ displayName: row.display_name ?? undefined,
154
+ avatarUrl: row.avatar_url ?? undefined,
155
+ status: row.status,
156
+ createdAt: new Date(row.created_at),
157
+ updatedAt: new Date(row.updated_at),
158
+ }
159
+ }
160
+
161
+ export function rowToIdentityRecord(row: SqliteAuthUserIdentityRow): UserIdentityRecord {
162
+ return {
163
+ projectId: row.project_id,
164
+ strategyId: row.strategy_id,
165
+ subject: row.subject,
166
+ userId: row.user_id,
167
+ claims: parseOptionalRecord(row.claims),
168
+ createdAt: new Date(row.created_at),
169
+ updatedAt: new Date(row.updated_at),
170
+ }
171
+ }
172
+
173
+ export function rowToServiceAccountRecord(row: SqliteAuthServiceAccountRow): ServiceAccountRecord {
174
+ return {
175
+ id: row.id,
176
+ projectId: row.project_id,
177
+ name: row.name,
178
+ description: row.description ?? undefined,
179
+ status: row.status,
180
+ createdByPrincipal:
181
+ row.created_by_principal_type && row.created_by_principal_id
182
+ ? {
183
+ type: row.created_by_principal_type,
184
+ id: row.created_by_principal_id,
185
+ }
186
+ : undefined,
187
+ createdBySessionId: row.created_by_session_id ?? undefined,
188
+ createdAt: new Date(row.created_at),
189
+ updatedAt: new Date(row.updated_at),
190
+ }
191
+ }
192
+
193
+ export function rowToServiceAccountGroupMembershipRecord(
194
+ row: SqliteAuthServiceAccountGroupMembershipRow
195
+ ): ServiceAccountGroupMembershipRecord {
196
+ return {
197
+ projectId: row.project_id,
198
+ serviceAccountId: row.service_account_id,
199
+ groupId: row.group_id,
200
+ source: row.source,
201
+ createdAt: new Date(row.created_at),
202
+ }
203
+ }
204
+
205
+ export function rowToSessionRecord(row: SqliteAuthSessionRow): SessionRecord {
206
+ return {
207
+ id: row.id,
208
+ projectId: row.project_id,
209
+ userId: row.user_id,
210
+ strategyId: row.strategy_id,
211
+ audience: row.audience,
212
+ tokenHash: row.token_hash,
213
+ createdAt: new Date(row.created_at),
214
+ expiresAt: new Date(row.expires_at),
215
+ absoluteExpiresAt: row.absolute_expires_at ? new Date(row.absolute_expires_at) : undefined,
216
+ revokedAt: row.revoked_at ? new Date(row.revoked_at) : undefined,
217
+ lastSeenAt: row.last_seen_at ? new Date(row.last_seen_at) : undefined,
218
+ userAgent: row.user_agent ?? undefined,
219
+ ipAddress: row.ip_address ?? undefined,
220
+ }
221
+ }
222
+
223
+ export function rowToAccessTokenRecord(row: SqliteAuthAccessTokenRow): AccessTokenRecord {
224
+ return {
225
+ id: row.id,
226
+ projectId: row.project_id,
227
+ name: row.name,
228
+ kind: row.kind,
229
+ subjectType: row.subject_type,
230
+ subjectId: row.subject_id,
231
+ tokenHash: row.token_hash,
232
+ groupIds: parseOptionalStringArray(row.group_ids),
233
+ createdByPrincipal:
234
+ row.created_by_principal_type && row.created_by_principal_id
235
+ ? {
236
+ type: row.created_by_principal_type,
237
+ id: row.created_by_principal_id,
238
+ }
239
+ : undefined,
240
+ createdBySessionId: row.created_by_session_id ?? undefined,
241
+ createdAt: new Date(row.created_at),
242
+ expiresAt: new Date(row.expires_at),
243
+ revokedAt: row.revoked_at ? new Date(row.revoked_at) : undefined,
244
+ lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : undefined,
245
+ lastUsedUserAgent: row.last_used_user_agent ?? undefined,
246
+ lastUsedIpAddress: row.last_used_ip_address ?? undefined,
247
+ }
248
+ }
249
+
250
+ export function rowToInvitationRecord(
251
+ row: SqliteAuthInvitationRow,
252
+ groupIds: readonly string[]
253
+ ): InvitationRecord {
254
+ return {
255
+ id: row.id,
256
+ projectId: row.project_id,
257
+ email: row.email,
258
+ groupIds,
259
+ status: row.status,
260
+ createdByPrincipal:
261
+ row.created_by_principal_type && row.created_by_principal_id
262
+ ? {
263
+ type: row.created_by_principal_type,
264
+ id: row.created_by_principal_id,
265
+ }
266
+ : undefined,
267
+ createdBySessionId: row.created_by_session_id ?? undefined,
268
+ createdAt: new Date(row.created_at),
269
+ updatedAt: new Date(row.updated_at),
270
+ expiresAt: new Date(row.expires_at),
271
+ acceptedAt: row.accepted_at ? new Date(row.accepted_at) : undefined,
272
+ revokedAt: row.revoked_at ? new Date(row.revoked_at) : undefined,
273
+ }
274
+ }
275
+
276
+ export function rowToGroupMembershipRecord(
277
+ row: SqliteAuthGroupMembershipRow
278
+ ): GroupMembershipRecord {
279
+ return {
280
+ projectId: row.project_id,
281
+ userId: row.user_id,
282
+ groupId: row.group_id,
283
+ source: row.source,
284
+ createdAt: new Date(row.created_at),
285
+ }
286
+ }
287
+
288
+ export function rowToMagicLinkRecord(row: SqliteAuthMagicLinkRow): MagicLinkRecord {
289
+ return {
290
+ id: row.id,
291
+ projectId: row.project_id,
292
+ strategyId: row.strategy_id,
293
+ audience: row.audience,
294
+ email: row.email,
295
+ tokenHash: row.token_hash,
296
+ returnTo: row.return_to ?? undefined,
297
+ createdAt: new Date(row.created_at),
298
+ expiresAt: new Date(row.expires_at),
299
+ consumedAt: row.consumed_at ? new Date(row.consumed_at) : undefined,
300
+ revokedAt: row.revoked_at ? new Date(row.revoked_at) : undefined,
301
+ }
302
+ }
303
+
304
+ export function rowToOidcAuthorizationAttemptRecord(
305
+ row: SqliteAuthOidcAttemptRow
306
+ ): OidcAuthorizationAttemptRecord {
307
+ return {
308
+ id: row.id,
309
+ projectId: row.project_id,
310
+ strategyId: row.strategy_id,
311
+ audience: row.audience,
312
+ stateHash: row.state_hash,
313
+ nonceHash: row.nonce_hash,
314
+ codeVerifier: row.code_verifier,
315
+ returnTo: row.return_to ?? undefined,
316
+ createdAt: new Date(row.created_at),
317
+ expiresAt: new Date(row.expires_at),
318
+ consumedAt: row.consumed_at ? new Date(row.consumed_at) : undefined,
319
+ }
320
+ }
321
+
322
+ export function serializeOptionalRecord(
323
+ value: Readonly<Record<string, unknown>> | undefined
324
+ ): string | null {
325
+ return value ? JSON.stringify(value) : null
326
+ }
327
+
328
+ export function serializeOptionalStringArray(value: readonly string[] | undefined): string | null {
329
+ return value === undefined ? null : JSON.stringify(value)
330
+ }
331
+
332
+ function parseOptionalRecord(value: string | null): Readonly<Record<string, unknown>> | undefined {
333
+ return value ? (JSON.parse(value) as Readonly<Record<string, unknown>>) : undefined
334
+ }
335
+
336
+ function parseOptionalStringArray(value: string | null): readonly string[] | undefined {
337
+ return value === null ? undefined : (JSON.parse(value) as readonly string[])
338
+ }
@@ -0,0 +1,161 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type {
3
+ AuthServiceAccountGroupMembershipStore,
4
+ ReconcileAuthServiceAccountGroupMembershipsInput,
5
+ ServiceAccountGroupMembershipRecord,
6
+ UpsertAuthServiceAccountGroupMembershipInput,
7
+ } from "@sixb/core/storage"
8
+ import { AuthStorageError } from "@sixb/core/storage"
9
+ import type {
10
+ SqliteAuthServiceAccountGroupMembershipRow,
11
+ SqliteAuthServiceAccountRow,
12
+ } from "./rows"
13
+ import { rowToServiceAccountGroupMembershipRecord } from "./rows"
14
+ import { assertNonEmpty, dateOrNow, normalizeGroupIds, toIso } from "./shared"
15
+
16
+ export class SqliteAuthServiceAccountGroupMembershipStore
17
+ implements AuthServiceAccountGroupMembershipStore
18
+ {
19
+ constructor(private readonly db: Database) {}
20
+
21
+ async upsert(
22
+ input: UpsertAuthServiceAccountGroupMembershipInput
23
+ ): Promise<ServiceAccountGroupMembershipRecord> {
24
+ const projectId = assertNonEmpty(input.projectId, "Project id")
25
+ const serviceAccountId = assertNonEmpty(input.serviceAccountId, "Service account id")
26
+ const groupId = assertNonEmpty(input.groupId, "Group id")
27
+ const serviceAccount = this.db
28
+ .query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?")
29
+ .get(projectId, serviceAccountId) as SqliteAuthServiceAccountRow | null
30
+ if (!serviceAccount) {
31
+ throw new AuthStorageError(
32
+ "missing_service_account",
33
+ `[Sixb] Service account '${serviceAccountId}' not found for project '${projectId}'.`
34
+ )
35
+ }
36
+
37
+ this.db
38
+ .query(
39
+ `
40
+ INSERT OR IGNORE INTO auth_service_account_group_memberships (
41
+ project_id,
42
+ service_account_id,
43
+ group_id,
44
+ source,
45
+ created_at
46
+ ) VALUES (?, ?, ?, ?, ?)
47
+ `
48
+ )
49
+ .run(projectId, serviceAccountId, groupId, input.source, toIso(dateOrNow(input.createdAt)))
50
+
51
+ const row = this.db
52
+ .query(
53
+ `
54
+ SELECT *
55
+ FROM auth_service_account_group_memberships
56
+ WHERE project_id = ?
57
+ AND service_account_id = ?
58
+ AND group_id = ?
59
+ `
60
+ )
61
+ .get(projectId, serviceAccountId, groupId) as SqliteAuthServiceAccountGroupMembershipRow
62
+ return rowToServiceAccountGroupMembershipRecord(row)
63
+ }
64
+
65
+ async reconcileForServiceAccount(
66
+ input: ReconcileAuthServiceAccountGroupMembershipsInput
67
+ ): Promise<readonly ServiceAccountGroupMembershipRecord[]> {
68
+ const projectId = assertNonEmpty(input.projectId, "Project id")
69
+ const serviceAccountId = assertNonEmpty(input.serviceAccountId, "Service account id")
70
+ const serviceAccount = this.db
71
+ .query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?")
72
+ .get(projectId, serviceAccountId) as SqliteAuthServiceAccountRow | null
73
+ if (!serviceAccount) {
74
+ throw new AuthStorageError(
75
+ "missing_service_account",
76
+ `[Sixb] Service account '${serviceAccountId}' not found for project '${projectId}'.`
77
+ )
78
+ }
79
+
80
+ const groupIds = normalizeGroupIds(input.groupIds)
81
+ const desired = new Set(groupIds)
82
+ const existing = await this.listForServiceAccount({ projectId, serviceAccountId })
83
+ for (const membership of existing) {
84
+ if (membership.source === input.source && !desired.has(membership.groupId)) {
85
+ this.db
86
+ .query(
87
+ `
88
+ DELETE FROM auth_service_account_group_memberships
89
+ WHERE project_id = ?
90
+ AND service_account_id = ?
91
+ AND group_id = ?
92
+ AND source = ?
93
+ `
94
+ )
95
+ .run(projectId, serviceAccountId, membership.groupId, input.source)
96
+ }
97
+ }
98
+
99
+ const updatedAt = toIso(dateOrNow(input.updatedAt))
100
+ for (const groupId of groupIds) {
101
+ this.db
102
+ .query(
103
+ `
104
+ INSERT INTO auth_service_account_group_memberships (
105
+ project_id,
106
+ service_account_id,
107
+ group_id,
108
+ source,
109
+ created_at
110
+ ) VALUES (?, ?, ?, ?, ?)
111
+ ON CONFLICT(project_id, service_account_id, group_id) DO UPDATE
112
+ SET source = excluded.source
113
+ `
114
+ )
115
+ .run(projectId, serviceAccountId, groupId, input.source, updatedAt)
116
+ }
117
+
118
+ return this.listForServiceAccount({ projectId, serviceAccountId })
119
+ }
120
+
121
+ async listForServiceAccount(params: {
122
+ readonly projectId: string
123
+ readonly serviceAccountId: string
124
+ }): Promise<readonly ServiceAccountGroupMembershipRecord[]> {
125
+ const rows = this.db
126
+ .query(
127
+ `
128
+ SELECT *
129
+ FROM auth_service_account_group_memberships
130
+ WHERE project_id = ?
131
+ AND service_account_id = ?
132
+ ORDER BY group_id ASC
133
+ `
134
+ )
135
+ .all(
136
+ params.projectId,
137
+ params.serviceAccountId
138
+ ) as SqliteAuthServiceAccountGroupMembershipRow[]
139
+
140
+ return rows.map(rowToServiceAccountGroupMembershipRecord)
141
+ }
142
+
143
+ async listForGroup(params: {
144
+ readonly projectId: string
145
+ readonly groupId: string
146
+ }): Promise<readonly ServiceAccountGroupMembershipRecord[]> {
147
+ const rows = this.db
148
+ .query(
149
+ `
150
+ SELECT *
151
+ FROM auth_service_account_group_memberships
152
+ WHERE project_id = ?
153
+ AND group_id = ?
154
+ ORDER BY service_account_id ASC
155
+ `
156
+ )
157
+ .all(params.projectId, params.groupId) as SqliteAuthServiceAccountGroupMembershipRow[]
158
+
159
+ return rows.map(rowToServiceAccountGroupMembershipRecord)
160
+ }
161
+ }
@@ -0,0 +1,168 @@
1
+ import type { Database } from "bun:sqlite"
2
+ import type {
3
+ AuthServiceAccountStore,
4
+ CreateAuthServiceAccountInput,
5
+ ListAuthServiceAccountsInput,
6
+ ListAuthServiceAccountsResult,
7
+ ServiceAccountRecord,
8
+ UpdateAuthServiceAccountInput,
9
+ } from "@sixb/core/storage"
10
+ import { AuthStorageError } from "@sixb/core/storage"
11
+ import type { SqliteAuthServiceAccountRow } from "./rows"
12
+ import { rowToServiceAccountRecord } from "./rows"
13
+ import {
14
+ appendPagination,
15
+ assertNonEmpty,
16
+ dateOrNow,
17
+ hasEmptyFilter,
18
+ mapUniqueConstraintError,
19
+ type SqliteValue,
20
+ toIso,
21
+ } from "./shared"
22
+
23
+ export class SqliteAuthServiceAccountStore implements AuthServiceAccountStore {
24
+ constructor(private readonly db: Database) {}
25
+
26
+ async create(input: CreateAuthServiceAccountInput): Promise<ServiceAccountRecord> {
27
+ const id = assertNonEmpty(input.id, "Service account id")
28
+ const projectId = assertNonEmpty(input.projectId, "Project id")
29
+ const name = assertNonEmpty(input.name, "Service account name")
30
+ const createdAt = dateOrNow(input.createdAt)
31
+ const updatedAt = input.updatedAt ? new Date(input.updatedAt) : createdAt
32
+
33
+ try {
34
+ this.db
35
+ .query(
36
+ `
37
+ INSERT INTO auth_service_accounts (
38
+ project_id,
39
+ id,
40
+ name,
41
+ description,
42
+ status,
43
+ created_by_principal_type,
44
+ created_by_principal_id,
45
+ created_by_session_id,
46
+ created_at,
47
+ updated_at
48
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
49
+ `
50
+ )
51
+ .run(
52
+ projectId,
53
+ id,
54
+ name,
55
+ input.description ?? null,
56
+ input.status ?? "active",
57
+ input.createdByPrincipal?.type ?? null,
58
+ input.createdByPrincipal?.id ?? null,
59
+ input.createdBySessionId ?? null,
60
+ toIso(createdAt),
61
+ toIso(updatedAt)
62
+ )
63
+ } catch (error) {
64
+ mapUniqueConstraintError(
65
+ error,
66
+ "duplicate_service_account",
67
+ `[Sixb] Service account '${id}' already exists for project '${projectId}'.`
68
+ )
69
+ }
70
+
71
+ return {
72
+ id,
73
+ projectId,
74
+ name,
75
+ description: input.description,
76
+ status: input.status ?? "active",
77
+ createdByPrincipal: input.createdByPrincipal,
78
+ createdBySessionId: input.createdBySessionId,
79
+ createdAt,
80
+ updatedAt,
81
+ }
82
+ }
83
+
84
+ async getById(params: {
85
+ readonly projectId: string
86
+ readonly id: string
87
+ }): Promise<ServiceAccountRecord | null> {
88
+ const row = this.db
89
+ .query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?")
90
+ .get(params.projectId, params.id) as SqliteAuthServiceAccountRow | null
91
+ return row ? rowToServiceAccountRecord(row) : null
92
+ }
93
+
94
+ async update(input: UpdateAuthServiceAccountInput): Promise<ServiceAccountRecord> {
95
+ const existing = await this.getById(input)
96
+ if (!existing) {
97
+ throw new AuthStorageError(
98
+ "missing_service_account",
99
+ `[Sixb] Service account '${input.id}' not found for project '${input.projectId}'.`
100
+ )
101
+ }
102
+
103
+ const updatedAt = dateOrNow(input.updatedAt)
104
+ const name =
105
+ input.name === undefined ? existing.name : assertNonEmpty(input.name, "Service account name")
106
+ const description = input.description === undefined ? existing.description : input.description
107
+ const status = input.status ?? existing.status
108
+ this.db
109
+ .query(
110
+ `
111
+ UPDATE auth_service_accounts
112
+ SET name = ?,
113
+ description = ?,
114
+ status = ?,
115
+ updated_at = ?
116
+ WHERE project_id = ?
117
+ AND id = ?
118
+ `
119
+ )
120
+ .run(name, description ?? null, status, toIso(updatedAt), input.projectId, input.id)
121
+
122
+ return {
123
+ ...existing,
124
+ name,
125
+ description,
126
+ status,
127
+ updatedAt,
128
+ }
129
+ }
130
+
131
+ async list(input: ListAuthServiceAccountsInput): Promise<ListAuthServiceAccountsResult> {
132
+ if (hasEmptyFilter(input.statuses)) {
133
+ return { serviceAccounts: [], hasMore: false, total: 0 }
134
+ }
135
+
136
+ const whereClauses = ["project_id = ?"]
137
+ const args: SqliteValue[] = [input.projectId]
138
+ if (input.statuses) {
139
+ whereClauses.push(`status IN (${input.statuses.map(() => "?").join(", ")})`)
140
+ args.push(...input.statuses)
141
+ }
142
+
143
+ const where = `WHERE ${whereClauses.join(" AND ")}`
144
+ const order = input.order === "desc" ? "DESC" : "ASC"
145
+ const totalRow = this.db
146
+ .query(`SELECT COUNT(*) AS count FROM auth_service_accounts ${where}`)
147
+ .get(...args) as { readonly count: number }
148
+ const queryArgs = [...args]
149
+ const query = appendPagination(
150
+ `
151
+ SELECT *
152
+ FROM auth_service_accounts
153
+ ${where}
154
+ ORDER BY created_at ${order}, id ${order}
155
+ `,
156
+ queryArgs,
157
+ input
158
+ )
159
+ const rows = this.db.query(query).all(...queryArgs) as SqliteAuthServiceAccountRow[]
160
+
161
+ return {
162
+ serviceAccounts: rows.map(rowToServiceAccountRecord),
163
+ hasMore:
164
+ input.limit === undefined ? false : (input.offset ?? 0) + input.limit < totalRow.count,
165
+ total: totalRow.count,
166
+ }
167
+ }
168
+ }