@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.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/action-run-storage.d.ts +29 -0
- package/dist/action-run-storage.d.ts.map +1 -0
- package/dist/agents/agent-storage.d.ts +25 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +14 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +26 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +14 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +46 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +28 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +134 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +131 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10204 -0
- package/dist/index.js.map +54 -0
- package/dist/latest-run-query.d.ts +22 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +18 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/object-query-compiler.d.ts +44 -0
- package/dist/object-query-compiler.d.ts.map +1 -0
- package/dist/object-storage.d.ts +93 -0
- package/dist/object-storage.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +14 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +49 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +63 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +32 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +14 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +91 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +26 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts +26 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -0
- package/dist/projection-run-storage.d.ts +32 -0
- package/dist/projection-run-storage.d.ts.map +1 -0
- package/dist/rules-storage.d.ts +33 -0
- package/dist/rules-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +27 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/sync-run-storage.d.ts +23 -0
- package/dist/sync-run-storage.d.ts.map +1 -0
- package/dist/timeseries-storage.d.ts +41 -0
- package/dist/timeseries-storage.d.ts.map +1 -0
- package/dist/transactions.d.ts +15 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/webhook-delivery-storage.d.ts +27 -0
- package/dist/webhook-delivery-storage.d.ts.map +1 -0
- package/dist/webhook-run-storage.d.ts +22 -0
- package/dist/webhook-run-storage.d.ts.map +1 -0
- package/dist/workflow-intervention-storage.d.ts +26 -0
- package/dist/workflow-intervention-storage.d.ts.map +1 -0
- package/dist/workflow-run-storage.d.ts +63 -0
- package/dist/workflow-run-storage.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/action-run-storage.ts +761 -0
- package/src/agents/agent-storage.ts +46 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +162 -0
- package/src/agents/rows.ts +205 -0
- package/src/agents/runs.ts +389 -0
- package/src/agents/threads.ts +133 -0
- package/src/auth-storage/access-tokens.ts +289 -0
- package/src/auth-storage/auth-storage.ts +784 -0
- package/src/auth-storage/group-memberships.ts +52 -0
- package/src/auth-storage/identities.ts +98 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +258 -0
- package/src/auth-storage/magic-links.ts +149 -0
- package/src/auth-storage/oidc-attempts.ts +109 -0
- package/src/auth-storage/rows.ts +338 -0
- package/src/auth-storage/service-account-group-memberships.ts +161 -0
- package/src/auth-storage/service-accounts.ts +168 -0
- package/src/auth-storage/sessions.ts +230 -0
- package/src/auth-storage/shared.ts +707 -0
- package/src/auth-storage/users.ts +226 -0
- package/src/index.ts +292 -0
- package/src/latest-run-query.ts +49 -0
- package/src/migrations/001-initial-schema.sql +1068 -0
- package/src/migrations.ts +245 -0
- package/src/object-query-compiler.ts +1198 -0
- package/src/object-storage.ts +605 -0
- package/src/ontology-storage/commits.ts +127 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +393 -0
- package/src/ontology-storage/materialization-state.ts +901 -0
- package/src/ontology-storage/materialization-writer.ts +394 -0
- package/src/ontology-storage/materializations.ts +838 -0
- package/src/ontology-storage/outbox.ts +237 -0
- package/src/ontology-storage/shared.ts +269 -0
- package/src/ontology-storage/sources.ts +653 -0
- package/src/pipeline-run-storage.ts +506 -0
- package/src/projection-run-storage.ts +592 -0
- package/src/rules-storage.ts +375 -0
- package/src/run-list-query.ts +98 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +3 -0
- package/src/sync-run-storage.ts +299 -0
- package/src/timeseries-storage.ts +212 -0
- package/src/transactions.ts +96 -0
- package/src/validation.ts +30 -0
- package/src/webhook-delivery-storage.ts +272 -0
- package/src/webhook-run-storage.ts +240 -0
- package/src/workflow-intervention-storage.ts +398 -0
- package/src/workflow-run-storage.ts +1148 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { AuthSessionAudience } from "@sixb/core"
|
|
3
|
+
import type {
|
|
4
|
+
CompleteAuthSessionInput,
|
|
5
|
+
CreateAuthSessionInput,
|
|
6
|
+
GroupMembershipRecord,
|
|
7
|
+
InvitationRecord,
|
|
8
|
+
MagicLinkRecord,
|
|
9
|
+
OidcAuthorizationAttemptRecord,
|
|
10
|
+
UpsertAuthGroupMembershipInput,
|
|
11
|
+
UserRecord,
|
|
12
|
+
} from "@sixb/core/storage"
|
|
13
|
+
import { AuthStorageError } from "@sixb/core/storage"
|
|
14
|
+
import { isUniqueConstraintError } from "../storage-errors"
|
|
15
|
+
import type {
|
|
16
|
+
SqliteAuthGroupMembershipRow,
|
|
17
|
+
SqliteAuthInvitationRow,
|
|
18
|
+
SqliteAuthMagicLinkRow,
|
|
19
|
+
SqliteAuthOidcAttemptRow,
|
|
20
|
+
SqliteAuthSessionRow,
|
|
21
|
+
SqliteAuthUserIdentityRow,
|
|
22
|
+
SqliteAuthUserRow,
|
|
23
|
+
} from "./rows"
|
|
24
|
+
import {
|
|
25
|
+
rowToGroupMembershipRecord,
|
|
26
|
+
rowToInvitationRecord,
|
|
27
|
+
rowToMagicLinkRecord,
|
|
28
|
+
rowToOidcAuthorizationAttemptRecord,
|
|
29
|
+
rowToSessionRecord,
|
|
30
|
+
rowToUserRecord,
|
|
31
|
+
} from "./rows"
|
|
32
|
+
|
|
33
|
+
export type SqliteValue = string | number | null
|
|
34
|
+
|
|
35
|
+
export function assertNonEmpty(value: string, label: string): string {
|
|
36
|
+
const normalized = value.trim()
|
|
37
|
+
if (!normalized) {
|
|
38
|
+
throw new AuthStorageError("invalid_input", `[Sixb] ${label} must be a non-empty string.`)
|
|
39
|
+
}
|
|
40
|
+
return normalized
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeEmail(email: string): string {
|
|
44
|
+
return assertNonEmpty(email, "Email").toLowerCase()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeGroupIds(groupIds: readonly string[] | undefined): readonly string[] {
|
|
48
|
+
return [...new Set((groupIds ?? []).map((groupId) => assertNonEmpty(groupId, "Group id")))]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function cloneRecord<T>(record: T): T {
|
|
52
|
+
return structuredClone(record)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function dateOrNow(value: Date | undefined): Date {
|
|
56
|
+
return value ? new Date(value) : new Date()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function toIso(value: Date): string {
|
|
60
|
+
return value.toISOString()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function hasEmptyFilter(values: readonly unknown[] | undefined): boolean {
|
|
64
|
+
return values !== undefined && values.length === 0
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function appendPagination(
|
|
68
|
+
query: string,
|
|
69
|
+
args: SqliteValue[],
|
|
70
|
+
input: { readonly limit?: number; readonly offset?: number }
|
|
71
|
+
): string {
|
|
72
|
+
const offset = input.offset ?? 0
|
|
73
|
+
|
|
74
|
+
if (input.limit !== undefined) {
|
|
75
|
+
args.push(input.limit, offset)
|
|
76
|
+
return `${query} LIMIT ? OFFSET ?`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (offset > 0) {
|
|
80
|
+
args.push(offset)
|
|
81
|
+
return `${query} LIMIT -1 OFFSET ?`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return query
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getUserRowById(
|
|
88
|
+
db: Database,
|
|
89
|
+
params: { readonly projectId: string; readonly id: string }
|
|
90
|
+
): SqliteAuthUserRow | null {
|
|
91
|
+
return db
|
|
92
|
+
.query("SELECT * FROM auth_users WHERE project_id = ? AND id = ?")
|
|
93
|
+
.get(params.projectId, params.id) as SqliteAuthUserRow | null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getUserById(
|
|
97
|
+
db: Database,
|
|
98
|
+
params: { readonly projectId: string; readonly id: string }
|
|
99
|
+
): UserRecord | null {
|
|
100
|
+
const row = getUserRowById(db, params)
|
|
101
|
+
return row ? rowToUserRecord(row) : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getUserRowByEmail(
|
|
105
|
+
db: Database,
|
|
106
|
+
params: { readonly projectId: string; readonly email: string }
|
|
107
|
+
): SqliteAuthUserRow | null {
|
|
108
|
+
return db
|
|
109
|
+
.query("SELECT * FROM auth_users WHERE project_id = ? AND email = ?")
|
|
110
|
+
.get(params.projectId, normalizeEmail(params.email)) as SqliteAuthUserRow | null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getUserByEmail(
|
|
114
|
+
db: Database,
|
|
115
|
+
params: { readonly projectId: string; readonly email: string }
|
|
116
|
+
): UserRecord | null {
|
|
117
|
+
const row = getUserRowByEmail(db, params)
|
|
118
|
+
return row ? rowToUserRecord(row) : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getIdentityRowBySubject(
|
|
122
|
+
db: Database,
|
|
123
|
+
params: {
|
|
124
|
+
readonly projectId: string
|
|
125
|
+
readonly strategyId: string
|
|
126
|
+
readonly subject: string
|
|
127
|
+
}
|
|
128
|
+
): SqliteAuthUserIdentityRow | null {
|
|
129
|
+
return db
|
|
130
|
+
.query(
|
|
131
|
+
`
|
|
132
|
+
SELECT *
|
|
133
|
+
FROM auth_user_identities
|
|
134
|
+
WHERE project_id = ?
|
|
135
|
+
AND strategy_id = ?
|
|
136
|
+
AND subject = ?
|
|
137
|
+
`
|
|
138
|
+
)
|
|
139
|
+
.get(params.projectId, params.strategyId, params.subject) as SqliteAuthUserIdentityRow | null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function getSessionRowById(
|
|
143
|
+
db: Database,
|
|
144
|
+
params: { readonly projectId: string; readonly id: string }
|
|
145
|
+
): SqliteAuthSessionRow | null {
|
|
146
|
+
return db
|
|
147
|
+
.query("SELECT * FROM auth_sessions WHERE project_id = ? AND id = ?")
|
|
148
|
+
.get(params.projectId, params.id) as SqliteAuthSessionRow | null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function getSessionById(
|
|
152
|
+
db: Database,
|
|
153
|
+
params: { readonly projectId: string; readonly id: string }
|
|
154
|
+
): ReturnType<typeof rowToSessionRecord> | null {
|
|
155
|
+
const row = getSessionRowById(db, params)
|
|
156
|
+
return row ? rowToSessionRecord(row) : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function requireSessionById(
|
|
160
|
+
db: Database,
|
|
161
|
+
params: { readonly projectId: string; readonly id: string }
|
|
162
|
+
): ReturnType<typeof rowToSessionRecord> {
|
|
163
|
+
const session = getSessionById(db, params)
|
|
164
|
+
if (!session) {
|
|
165
|
+
throw new AuthStorageError(
|
|
166
|
+
"missing_session",
|
|
167
|
+
`[Sixb] Session '${params.id}' not found for project '${params.projectId}'.`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return session
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getInvitationGroupIds(
|
|
174
|
+
db: Database,
|
|
175
|
+
params: { readonly projectId: string; readonly invitationId: string }
|
|
176
|
+
): readonly string[] {
|
|
177
|
+
const rows = db
|
|
178
|
+
.query(
|
|
179
|
+
`
|
|
180
|
+
SELECT group_id
|
|
181
|
+
FROM auth_invitation_groups
|
|
182
|
+
WHERE project_id = ?
|
|
183
|
+
AND invitation_id = ?
|
|
184
|
+
ORDER BY position ASC, group_id ASC
|
|
185
|
+
`
|
|
186
|
+
)
|
|
187
|
+
.all(params.projectId, params.invitationId) as Array<{ readonly group_id: string }>
|
|
188
|
+
|
|
189
|
+
return rows.map((row) => row.group_id)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function getInvitationById(
|
|
193
|
+
db: Database,
|
|
194
|
+
params: { readonly projectId: string; readonly id: string }
|
|
195
|
+
): InvitationRecord | null {
|
|
196
|
+
const row = db
|
|
197
|
+
.query("SELECT * FROM auth_invitations WHERE project_id = ? AND id = ?")
|
|
198
|
+
.get(params.projectId, params.id) as SqliteAuthInvitationRow | null
|
|
199
|
+
|
|
200
|
+
return row ? invitationRowToRecord(db, row) : null
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function requireInvitationById(
|
|
204
|
+
db: Database,
|
|
205
|
+
params: { readonly projectId: string; readonly id: string }
|
|
206
|
+
): InvitationRecord {
|
|
207
|
+
const invitation = getInvitationById(db, params)
|
|
208
|
+
if (!invitation) {
|
|
209
|
+
throw new AuthStorageError(
|
|
210
|
+
"missing_invitation",
|
|
211
|
+
`[Sixb] Invitation '${params.id}' not found for project '${params.projectId}'.`
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
return invitation
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function getActiveInvitationByEmail(
|
|
218
|
+
db: Database,
|
|
219
|
+
params: { readonly projectId: string; readonly email: string; readonly now: Date }
|
|
220
|
+
): InvitationRecord | null {
|
|
221
|
+
const row = db
|
|
222
|
+
.query(
|
|
223
|
+
`
|
|
224
|
+
SELECT *
|
|
225
|
+
FROM auth_invitations
|
|
226
|
+
WHERE project_id = ?
|
|
227
|
+
AND email = ?
|
|
228
|
+
AND status = 'pending'
|
|
229
|
+
AND expires_at > ?
|
|
230
|
+
ORDER BY created_at DESC, id DESC
|
|
231
|
+
LIMIT 1
|
|
232
|
+
`
|
|
233
|
+
)
|
|
234
|
+
.get(
|
|
235
|
+
params.projectId,
|
|
236
|
+
normalizeEmail(params.email),
|
|
237
|
+
toIso(params.now)
|
|
238
|
+
) as SqliteAuthInvitationRow | null
|
|
239
|
+
|
|
240
|
+
return row ? invitationRowToRecord(db, row) : null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function replaceInvitationGroups(
|
|
244
|
+
db: Database,
|
|
245
|
+
params: {
|
|
246
|
+
readonly projectId: string
|
|
247
|
+
readonly invitationId: string
|
|
248
|
+
readonly groupIds: readonly string[]
|
|
249
|
+
}
|
|
250
|
+
): void {
|
|
251
|
+
db.query("DELETE FROM auth_invitation_groups WHERE project_id = ? AND invitation_id = ?").run(
|
|
252
|
+
params.projectId,
|
|
253
|
+
params.invitationId
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
for (const [index, groupId] of params.groupIds.entries()) {
|
|
257
|
+
db.query(
|
|
258
|
+
`
|
|
259
|
+
INSERT INTO auth_invitation_groups (
|
|
260
|
+
project_id,
|
|
261
|
+
invitation_id,
|
|
262
|
+
group_id,
|
|
263
|
+
position
|
|
264
|
+
) VALUES (?, ?, ?, ?)
|
|
265
|
+
`
|
|
266
|
+
).run(params.projectId, params.invitationId, groupId, index)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function getMagicLinkRowById(
|
|
271
|
+
db: Database,
|
|
272
|
+
params: { readonly projectId: string; readonly id: string }
|
|
273
|
+
): SqliteAuthMagicLinkRow | null {
|
|
274
|
+
return db
|
|
275
|
+
.query("SELECT * FROM auth_magic_links WHERE project_id = ? AND id = ?")
|
|
276
|
+
.get(params.projectId, params.id) as SqliteAuthMagicLinkRow | null
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function getMagicLinkById(
|
|
280
|
+
db: Database,
|
|
281
|
+
params: { readonly projectId: string; readonly id: string }
|
|
282
|
+
): MagicLinkRecord | null {
|
|
283
|
+
const row = getMagicLinkRowById(db, params)
|
|
284
|
+
return row ? rowToMagicLinkRecord(row) : null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function consumeMagicLink(
|
|
288
|
+
db: Database,
|
|
289
|
+
params: {
|
|
290
|
+
readonly projectId: string
|
|
291
|
+
readonly id: string
|
|
292
|
+
readonly tokenHash: string
|
|
293
|
+
readonly consumedAt: Date
|
|
294
|
+
}
|
|
295
|
+
): MagicLinkRecord {
|
|
296
|
+
const projectId = assertNonEmpty(params.projectId, "Project id")
|
|
297
|
+
const id = assertNonEmpty(params.id, "Magic link id")
|
|
298
|
+
const tokenHash = assertNonEmpty(params.tokenHash, "Magic link token hash")
|
|
299
|
+
const row = getMagicLinkRowById(db, { projectId, id })
|
|
300
|
+
|
|
301
|
+
if (!row) {
|
|
302
|
+
throw new AuthStorageError(
|
|
303
|
+
"missing_magic_link",
|
|
304
|
+
`[Sixb] Magic link '${id}' not found for project '${projectId}'.`
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (row.token_hash !== tokenHash || row.consumed_at || row.revoked_at) {
|
|
309
|
+
throw new AuthStorageError(
|
|
310
|
+
"invalid_magic_link",
|
|
311
|
+
`[Sixb] Magic link '${id}' is not valid for project '${projectId}'.`
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (new Date(row.expires_at) <= params.consumedAt) {
|
|
316
|
+
throw new AuthStorageError(
|
|
317
|
+
"expired_magic_link",
|
|
318
|
+
`[Sixb] Magic link '${id}' is expired for project '${projectId}'.`
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
db.query(
|
|
323
|
+
`
|
|
324
|
+
UPDATE auth_magic_links
|
|
325
|
+
SET consumed_at = ?
|
|
326
|
+
WHERE project_id = ?
|
|
327
|
+
AND id = ?
|
|
328
|
+
`
|
|
329
|
+
).run(toIso(params.consumedAt), projectId, id)
|
|
330
|
+
|
|
331
|
+
return rowToMagicLinkRecord({
|
|
332
|
+
...row,
|
|
333
|
+
consumed_at: toIso(params.consumedAt),
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function getOidcAttemptRowById(
|
|
338
|
+
db: Database,
|
|
339
|
+
params: { readonly projectId: string; readonly id: string }
|
|
340
|
+
): SqliteAuthOidcAttemptRow | null {
|
|
341
|
+
return db
|
|
342
|
+
.query("SELECT * FROM auth_oidc_authorization_attempts WHERE project_id = ? AND id = ?")
|
|
343
|
+
.get(params.projectId, params.id) as SqliteAuthOidcAttemptRow | null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function getOidcAttemptById(
|
|
347
|
+
db: Database,
|
|
348
|
+
params: { readonly projectId: string; readonly id: string }
|
|
349
|
+
): OidcAuthorizationAttemptRecord | null {
|
|
350
|
+
const row = getOidcAttemptRowById(db, params)
|
|
351
|
+
return row ? rowToOidcAuthorizationAttemptRecord(row) : null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function consumeOidcAttempt(
|
|
355
|
+
db: Database,
|
|
356
|
+
params: {
|
|
357
|
+
readonly projectId: string
|
|
358
|
+
readonly id: string
|
|
359
|
+
readonly stateHash: string
|
|
360
|
+
readonly consumedAt: Date
|
|
361
|
+
}
|
|
362
|
+
): OidcAuthorizationAttemptRecord {
|
|
363
|
+
const projectId = assertNonEmpty(params.projectId, "Project id")
|
|
364
|
+
const id = assertNonEmpty(params.id, "OIDC authorization attempt id")
|
|
365
|
+
const stateHash = assertNonEmpty(params.stateHash, "OIDC state hash")
|
|
366
|
+
const row = getOidcAttemptRowById(db, { projectId, id })
|
|
367
|
+
|
|
368
|
+
if (!row) {
|
|
369
|
+
throw new AuthStorageError(
|
|
370
|
+
"missing_oidc_attempt",
|
|
371
|
+
`[Sixb] OIDC authorization attempt '${id}' not found for project '${projectId}'.`
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (row.state_hash !== stateHash || row.consumed_at) {
|
|
376
|
+
throw new AuthStorageError(
|
|
377
|
+
"invalid_oidc_attempt",
|
|
378
|
+
`[Sixb] OIDC authorization attempt '${id}' is not valid for project '${projectId}'.`
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (new Date(row.expires_at) <= params.consumedAt) {
|
|
383
|
+
throw new AuthStorageError(
|
|
384
|
+
"expired_oidc_attempt",
|
|
385
|
+
`[Sixb] OIDC authorization attempt '${id}' is expired for project '${projectId}'.`
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
db.query(
|
|
390
|
+
`
|
|
391
|
+
UPDATE auth_oidc_authorization_attempts
|
|
392
|
+
SET consumed_at = ?
|
|
393
|
+
WHERE project_id = ?
|
|
394
|
+
AND id = ?
|
|
395
|
+
`
|
|
396
|
+
).run(toIso(params.consumedAt), projectId, id)
|
|
397
|
+
|
|
398
|
+
return rowToOidcAuthorizationAttemptRecord({
|
|
399
|
+
...row,
|
|
400
|
+
consumed_at: toIso(params.consumedAt),
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function validateCompleteSessionInput(
|
|
405
|
+
db: Database,
|
|
406
|
+
projectId: string,
|
|
407
|
+
session: CompleteAuthSessionInput
|
|
408
|
+
): void {
|
|
409
|
+
const sessionId = assertNonEmpty(session.id, "Session id")
|
|
410
|
+
assertNonEmpty(session.audience, "Session audience")
|
|
411
|
+
assertNonEmpty(session.tokenHash, "Session token hash")
|
|
412
|
+
assertSessionIdAvailable(db, projectId, sessionId)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function assertSessionIdAvailable(db: Database, projectId: string, sessionId: string): void {
|
|
416
|
+
if (getSessionRowById(db, { projectId, id: sessionId })) {
|
|
417
|
+
throw new AuthStorageError(
|
|
418
|
+
"duplicate_session",
|
|
419
|
+
`[Sixb] Session '${sessionId}' already exists for project '${projectId}'.`
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function createSession(
|
|
425
|
+
db: Database,
|
|
426
|
+
input: CreateAuthSessionInput
|
|
427
|
+
): ReturnType<typeof rowToSessionRecord> {
|
|
428
|
+
const id = assertNonEmpty(input.id, "Session id")
|
|
429
|
+
const projectId = assertNonEmpty(input.projectId, "Project id")
|
|
430
|
+
const userId = assertNonEmpty(input.userId, "User id")
|
|
431
|
+
const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
|
|
432
|
+
const audience = assertNonEmpty(input.audience, "Session audience")
|
|
433
|
+
const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash")
|
|
434
|
+
|
|
435
|
+
assertSessionIdAvailable(db, projectId, id)
|
|
436
|
+
|
|
437
|
+
db.query(
|
|
438
|
+
`
|
|
439
|
+
INSERT INTO auth_sessions (
|
|
440
|
+
project_id,
|
|
441
|
+
id,
|
|
442
|
+
user_id,
|
|
443
|
+
strategy_id,
|
|
444
|
+
audience,
|
|
445
|
+
token_hash,
|
|
446
|
+
created_at,
|
|
447
|
+
expires_at,
|
|
448
|
+
absolute_expires_at,
|
|
449
|
+
user_agent,
|
|
450
|
+
ip_address
|
|
451
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
452
|
+
`
|
|
453
|
+
).run(
|
|
454
|
+
projectId,
|
|
455
|
+
id,
|
|
456
|
+
userId,
|
|
457
|
+
strategyId,
|
|
458
|
+
audience,
|
|
459
|
+
tokenHash,
|
|
460
|
+
toIso(input.createdAt),
|
|
461
|
+
toIso(input.expiresAt),
|
|
462
|
+
input.absoluteExpiresAt ? toIso(input.absoluteExpiresAt) : null,
|
|
463
|
+
input.userAgent ?? null,
|
|
464
|
+
input.ipAddress ?? null
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
const session = getSessionById(db, { projectId, id })
|
|
468
|
+
if (!session) {
|
|
469
|
+
throw new AuthStorageError(
|
|
470
|
+
"missing_session",
|
|
471
|
+
`[Sixb] Failed to load session '${id}' for project '${projectId}'.`
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
return session
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export function revokeActiveSessionsForUser(
|
|
478
|
+
db: Database,
|
|
479
|
+
params: {
|
|
480
|
+
readonly projectId: string
|
|
481
|
+
readonly userId: string
|
|
482
|
+
readonly audience?: AuthSessionAudience
|
|
483
|
+
readonly revokedAt: Date
|
|
484
|
+
}
|
|
485
|
+
): readonly ReturnType<typeof rowToSessionRecord>[] {
|
|
486
|
+
const revokedAt = toIso(params.revokedAt)
|
|
487
|
+
const audienceCondition = params.audience === undefined ? "" : "AND audience = ?"
|
|
488
|
+
const selectArgs =
|
|
489
|
+
params.audience === undefined
|
|
490
|
+
? [params.projectId, params.userId, revokedAt, revokedAt]
|
|
491
|
+
: [params.projectId, params.userId, params.audience, revokedAt, revokedAt]
|
|
492
|
+
const updateArgs =
|
|
493
|
+
params.audience === undefined
|
|
494
|
+
? [revokedAt, params.projectId, params.userId, revokedAt, revokedAt]
|
|
495
|
+
: [revokedAt, params.projectId, params.userId, params.audience, revokedAt, revokedAt]
|
|
496
|
+
const rows = db
|
|
497
|
+
.query(
|
|
498
|
+
`
|
|
499
|
+
SELECT *
|
|
500
|
+
FROM auth_sessions
|
|
501
|
+
WHERE project_id = ?
|
|
502
|
+
AND user_id = ?
|
|
503
|
+
${audienceCondition}
|
|
504
|
+
AND revoked_at IS NULL
|
|
505
|
+
AND expires_at > ?
|
|
506
|
+
AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
|
|
507
|
+
ORDER BY created_at ASC, id ASC
|
|
508
|
+
`
|
|
509
|
+
)
|
|
510
|
+
.all(...selectArgs) as SqliteAuthSessionRow[]
|
|
511
|
+
|
|
512
|
+
db.query(
|
|
513
|
+
`
|
|
514
|
+
UPDATE auth_sessions
|
|
515
|
+
SET revoked_at = ?
|
|
516
|
+
WHERE project_id = ?
|
|
517
|
+
AND user_id = ?
|
|
518
|
+
${audienceCondition}
|
|
519
|
+
AND revoked_at IS NULL
|
|
520
|
+
AND expires_at > ?
|
|
521
|
+
AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
|
|
522
|
+
`
|
|
523
|
+
).run(...updateArgs)
|
|
524
|
+
|
|
525
|
+
return rows.map((row) =>
|
|
526
|
+
rowToSessionRecord({
|
|
527
|
+
...row,
|
|
528
|
+
revoked_at: revokedAt,
|
|
529
|
+
})
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function upsertGroupMembership(
|
|
534
|
+
db: Database,
|
|
535
|
+
input: UpsertAuthGroupMembershipInput
|
|
536
|
+
): GroupMembershipRecord {
|
|
537
|
+
const projectId = assertNonEmpty(input.projectId, "Project id")
|
|
538
|
+
const userId = assertNonEmpty(input.userId, "User id")
|
|
539
|
+
const groupId = assertNonEmpty(input.groupId, "Group id")
|
|
540
|
+
if (!getUserRowById(db, { projectId, id: userId })) {
|
|
541
|
+
throw new AuthStorageError(
|
|
542
|
+
"missing_user",
|
|
543
|
+
`[Sixb] User '${userId}' not found for project '${projectId}'.`
|
|
544
|
+
)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const existing = getGroupMembership(db, { projectId, userId, groupId })
|
|
548
|
+
|
|
549
|
+
if (existing) {
|
|
550
|
+
return existing
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const createdAt = dateOrNow(input.createdAt)
|
|
554
|
+
db.query(
|
|
555
|
+
`
|
|
556
|
+
INSERT INTO auth_group_memberships (
|
|
557
|
+
project_id,
|
|
558
|
+
user_id,
|
|
559
|
+
group_id,
|
|
560
|
+
source,
|
|
561
|
+
created_at
|
|
562
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
563
|
+
`
|
|
564
|
+
).run(projectId, userId, groupId, input.source, toIso(createdAt))
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
projectId,
|
|
568
|
+
userId,
|
|
569
|
+
groupId,
|
|
570
|
+
source: input.source,
|
|
571
|
+
createdAt,
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function removeGroupMembership(
|
|
576
|
+
db: Database,
|
|
577
|
+
params: { readonly projectId: string; readonly userId: string; readonly groupId: string }
|
|
578
|
+
): GroupMembershipRecord | null {
|
|
579
|
+
const projectId = assertNonEmpty(params.projectId, "Project id")
|
|
580
|
+
const userId = assertNonEmpty(params.userId, "User id")
|
|
581
|
+
const groupId = assertNonEmpty(params.groupId, "Group id")
|
|
582
|
+
|
|
583
|
+
const existing = getGroupMembership(db, { projectId, userId, groupId })
|
|
584
|
+
if (!existing) {
|
|
585
|
+
return null
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
db.query(
|
|
589
|
+
`
|
|
590
|
+
DELETE FROM auth_group_memberships
|
|
591
|
+
WHERE project_id = ?
|
|
592
|
+
AND user_id = ?
|
|
593
|
+
AND group_id = ?
|
|
594
|
+
`
|
|
595
|
+
).run(projectId, userId, groupId)
|
|
596
|
+
|
|
597
|
+
return existing
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export function getGroupMembership(
|
|
601
|
+
db: Database,
|
|
602
|
+
params: { readonly projectId: string; readonly userId: string; readonly groupId: string }
|
|
603
|
+
): GroupMembershipRecord | null {
|
|
604
|
+
const row = db
|
|
605
|
+
.query(
|
|
606
|
+
`
|
|
607
|
+
SELECT *
|
|
608
|
+
FROM auth_group_memberships
|
|
609
|
+
WHERE project_id = ?
|
|
610
|
+
AND user_id = ?
|
|
611
|
+
AND group_id = ?
|
|
612
|
+
`
|
|
613
|
+
)
|
|
614
|
+
.get(params.projectId, params.userId, params.groupId) as SqliteAuthGroupMembershipRow | null
|
|
615
|
+
|
|
616
|
+
return row ? rowToGroupMembershipRecord(row) : null
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function listMembershipsForUser(
|
|
620
|
+
db: Database,
|
|
621
|
+
params: { readonly projectId: string; readonly userId: string }
|
|
622
|
+
): readonly GroupMembershipRecord[] {
|
|
623
|
+
const rows = db
|
|
624
|
+
.query(
|
|
625
|
+
`
|
|
626
|
+
SELECT *
|
|
627
|
+
FROM auth_group_memberships
|
|
628
|
+
WHERE project_id = ?
|
|
629
|
+
AND user_id = ?
|
|
630
|
+
ORDER BY group_id ASC
|
|
631
|
+
`
|
|
632
|
+
)
|
|
633
|
+
.all(params.projectId, params.userId) as SqliteAuthGroupMembershipRow[]
|
|
634
|
+
|
|
635
|
+
return rows.map(rowToGroupMembershipRecord)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export function revokeActiveMagicLinksForEmail(
|
|
639
|
+
db: Database,
|
|
640
|
+
params: { readonly projectId: string; readonly email: string; readonly revokedAt: Date }
|
|
641
|
+
): readonly MagicLinkRecord[] {
|
|
642
|
+
const email = normalizeEmail(params.email)
|
|
643
|
+
const revokedAt = toIso(params.revokedAt)
|
|
644
|
+
const rows = db
|
|
645
|
+
.query(
|
|
646
|
+
`
|
|
647
|
+
SELECT *
|
|
648
|
+
FROM auth_magic_links
|
|
649
|
+
WHERE project_id = ?
|
|
650
|
+
AND email = ?
|
|
651
|
+
AND consumed_at IS NULL
|
|
652
|
+
AND revoked_at IS NULL
|
|
653
|
+
AND expires_at > ?
|
|
654
|
+
ORDER BY created_at ASC, id ASC
|
|
655
|
+
`
|
|
656
|
+
)
|
|
657
|
+
.all(params.projectId, email, revokedAt) as SqliteAuthMagicLinkRow[]
|
|
658
|
+
|
|
659
|
+
db.query(
|
|
660
|
+
`
|
|
661
|
+
UPDATE auth_magic_links
|
|
662
|
+
SET revoked_at = ?
|
|
663
|
+
WHERE project_id = ?
|
|
664
|
+
AND email = ?
|
|
665
|
+
AND consumed_at IS NULL
|
|
666
|
+
AND revoked_at IS NULL
|
|
667
|
+
AND expires_at > ?
|
|
668
|
+
`
|
|
669
|
+
).run(revokedAt, params.projectId, email, revokedAt)
|
|
670
|
+
|
|
671
|
+
return rows.map((row) =>
|
|
672
|
+
rowToMagicLinkRecord({
|
|
673
|
+
...row,
|
|
674
|
+
revoked_at: revokedAt,
|
|
675
|
+
})
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
export function mapUniqueConstraintError(
|
|
680
|
+
error: unknown,
|
|
681
|
+
code:
|
|
682
|
+
| "duplicate_access_token"
|
|
683
|
+
| "duplicate_identity"
|
|
684
|
+
| "duplicate_invitation"
|
|
685
|
+
| "duplicate_magic_link"
|
|
686
|
+
| "duplicate_oidc_attempt"
|
|
687
|
+
| "duplicate_service_account"
|
|
688
|
+
| "duplicate_session"
|
|
689
|
+
| "duplicate_user",
|
|
690
|
+
message: string
|
|
691
|
+
): never {
|
|
692
|
+
if (isUniqueConstraintError(error)) {
|
|
693
|
+
throw new AuthStorageError(code, message)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
throw error
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function invitationRowToRecord(db: Database, row: SqliteAuthInvitationRow): InvitationRecord {
|
|
700
|
+
return rowToInvitationRecord(
|
|
701
|
+
row,
|
|
702
|
+
getInvitationGroupIds(db, {
|
|
703
|
+
projectId: row.project_id,
|
|
704
|
+
invitationId: row.id,
|
|
705
|
+
})
|
|
706
|
+
)
|
|
707
|
+
}
|