@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,784 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
AuthStorage,
|
|
4
|
+
CompleteMagicLinkSignInInput,
|
|
5
|
+
CompleteOidcSignInInput,
|
|
6
|
+
CompleteSignInResult,
|
|
7
|
+
GroupMembershipRecord,
|
|
8
|
+
InvitationRecord,
|
|
9
|
+
SessionRecord,
|
|
10
|
+
SuspendUserAndRevokeSessionsInput,
|
|
11
|
+
UserIdentityRecord,
|
|
12
|
+
UserRecord,
|
|
13
|
+
} from "@sixb/core/storage"
|
|
14
|
+
import { AuthStorageError } from "@sixb/core/storage"
|
|
15
|
+
import { installFreshSqliteSchema } from "../migrations"
|
|
16
|
+
import {
|
|
17
|
+
closeSqliteStoreConnection,
|
|
18
|
+
openSqliteStoreConnection,
|
|
19
|
+
runImmediateTransaction,
|
|
20
|
+
type SqliteStoreConnection,
|
|
21
|
+
} from "../transactions"
|
|
22
|
+
import { SqliteAuthAccessTokenStore } from "./access-tokens"
|
|
23
|
+
import { SqliteAuthGroupMembershipStore } from "./group-memberships"
|
|
24
|
+
import { SqliteAuthUserIdentityStore } from "./identities"
|
|
25
|
+
import { SqliteAuthInvitationStore } from "./invitations"
|
|
26
|
+
import { SqliteAuthMagicLinkStore } from "./magic-links"
|
|
27
|
+
import { SqliteAuthOidcAuthorizationAttemptStore } from "./oidc-attempts"
|
|
28
|
+
import type { SqliteAuthMagicLinkRow, SqliteAuthOidcAttemptRow } from "./rows"
|
|
29
|
+
import { rowToIdentityRecord, rowToUserRecord, serializeOptionalRecord } from "./rows"
|
|
30
|
+
import { SqliteAuthServiceAccountGroupMembershipStore } from "./service-account-group-memberships"
|
|
31
|
+
import { SqliteAuthServiceAccountStore } from "./service-accounts"
|
|
32
|
+
import { SqliteAuthSessionStore } from "./sessions"
|
|
33
|
+
import {
|
|
34
|
+
assertNonEmpty,
|
|
35
|
+
consumeMagicLink,
|
|
36
|
+
consumeOidcAttempt,
|
|
37
|
+
createSession,
|
|
38
|
+
getActiveInvitationByEmail,
|
|
39
|
+
getIdentityRowBySubject,
|
|
40
|
+
getMagicLinkRowById,
|
|
41
|
+
getOidcAttemptRowById,
|
|
42
|
+
getUserRowByEmail,
|
|
43
|
+
getUserRowById,
|
|
44
|
+
mapUniqueConstraintError,
|
|
45
|
+
normalizeEmail,
|
|
46
|
+
normalizeGroupIds,
|
|
47
|
+
revokeActiveSessionsForUser,
|
|
48
|
+
toIso,
|
|
49
|
+
upsertGroupMembership,
|
|
50
|
+
validateCompleteSessionInput,
|
|
51
|
+
} from "./shared"
|
|
52
|
+
import { SqliteAuthUserStore } from "./users"
|
|
53
|
+
|
|
54
|
+
interface AuthTransactionError {
|
|
55
|
+
readonly error: AuthStorageError
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SqliteAuthStorageOptions {
|
|
59
|
+
/** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
|
|
60
|
+
path?: string
|
|
61
|
+
/** Internal shared connection used by bundled SqliteStorage. */
|
|
62
|
+
connection?: SqliteStoreConnection
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class SqliteAuthStorage implements AuthStorage {
|
|
66
|
+
private readonly connection: SqliteStoreConnection
|
|
67
|
+
private readonly db: Database
|
|
68
|
+
|
|
69
|
+
readonly users: SqliteAuthUserStore
|
|
70
|
+
readonly identities: SqliteAuthUserIdentityStore
|
|
71
|
+
readonly serviceAccounts: SqliteAuthServiceAccountStore
|
|
72
|
+
readonly serviceAccountGroupMemberships: SqliteAuthServiceAccountGroupMembershipStore
|
|
73
|
+
readonly sessions: SqliteAuthSessionStore
|
|
74
|
+
readonly accessTokens: SqliteAuthAccessTokenStore
|
|
75
|
+
readonly invitations: SqliteAuthInvitationStore
|
|
76
|
+
readonly groupMemberships: SqliteAuthGroupMembershipStore
|
|
77
|
+
readonly magicLinks: SqliteAuthMagicLinkStore
|
|
78
|
+
readonly oidcAuthorizationAttempts: SqliteAuthOidcAuthorizationAttemptStore
|
|
79
|
+
|
|
80
|
+
constructor(options: SqliteAuthStorageOptions = {}) {
|
|
81
|
+
this.connection = openSqliteStoreConnection(options)
|
|
82
|
+
this.db = this.connection.db
|
|
83
|
+
|
|
84
|
+
if (this.connection.installFreshSchema) {
|
|
85
|
+
installFreshSqliteSchema(this.db)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.users = new SqliteAuthUserStore(this.db)
|
|
89
|
+
this.identities = new SqliteAuthUserIdentityStore(this.db)
|
|
90
|
+
this.serviceAccounts = new SqliteAuthServiceAccountStore(this.db)
|
|
91
|
+
this.serviceAccountGroupMemberships = new SqliteAuthServiceAccountGroupMembershipStore(this.db)
|
|
92
|
+
this.sessions = new SqliteAuthSessionStore(this.db)
|
|
93
|
+
this.accessTokens = new SqliteAuthAccessTokenStore(this.db)
|
|
94
|
+
this.invitations = new SqliteAuthInvitationStore(this.db)
|
|
95
|
+
this.groupMemberships = new SqliteAuthGroupMembershipStore(this.db)
|
|
96
|
+
this.magicLinks = new SqliteAuthMagicLinkStore(this.db)
|
|
97
|
+
this.oidcAuthorizationAttempts = new SqliteAuthOidcAuthorizationAttemptStore(this.db)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async completeMagicLinkSignIn(
|
|
101
|
+
input: CompleteMagicLinkSignInInput
|
|
102
|
+
): Promise<CompleteSignInResult> {
|
|
103
|
+
const result = runImmediateTransaction(
|
|
104
|
+
this.db,
|
|
105
|
+
(): CompleteSignInResult | AuthTransactionError => {
|
|
106
|
+
const projectId = assertNonEmpty(input.projectId, "Project id")
|
|
107
|
+
const completedAt = new Date(input.completedAt)
|
|
108
|
+
const magicLink = getMagicLinkRowById(this.db, {
|
|
109
|
+
projectId,
|
|
110
|
+
id: input.magicLinkId,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
this.assertMagicLinkUsable(
|
|
114
|
+
projectId,
|
|
115
|
+
input.magicLinkId,
|
|
116
|
+
input.tokenHash,
|
|
117
|
+
completedAt,
|
|
118
|
+
magicLink
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const existingUserRow = getUserRowByEmail(this.db, {
|
|
122
|
+
projectId,
|
|
123
|
+
email: magicLink.email,
|
|
124
|
+
})
|
|
125
|
+
const activeInvitation = getActiveInvitationByEmail(this.db, {
|
|
126
|
+
projectId,
|
|
127
|
+
email: magicLink.email,
|
|
128
|
+
now: completedAt,
|
|
129
|
+
})
|
|
130
|
+
const shouldCreateUser = !existingUserRow
|
|
131
|
+
const manualGroupIds = normalizeGroupIds(input.manualGroupIds)
|
|
132
|
+
|
|
133
|
+
if (existingUserRow?.status === "suspended") {
|
|
134
|
+
consumeMagicLink(this.db, {
|
|
135
|
+
projectId,
|
|
136
|
+
id: input.magicLinkId,
|
|
137
|
+
tokenHash: input.tokenHash,
|
|
138
|
+
consumedAt: completedAt,
|
|
139
|
+
})
|
|
140
|
+
return {
|
|
141
|
+
error: new AuthStorageError(
|
|
142
|
+
"suspended_user",
|
|
143
|
+
`[Sixb] User '${existingUserRow.id}' is suspended for project '${projectId}'.`
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
|
|
149
|
+
consumeMagicLink(this.db, {
|
|
150
|
+
projectId,
|
|
151
|
+
id: input.magicLinkId,
|
|
152
|
+
tokenHash: input.tokenHash,
|
|
153
|
+
consumedAt: completedAt,
|
|
154
|
+
})
|
|
155
|
+
return {
|
|
156
|
+
error: new AuthStorageError(
|
|
157
|
+
"user_creation_not_allowed",
|
|
158
|
+
`[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
|
|
159
|
+
),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
shouldCreateUser &&
|
|
165
|
+
!activeInvitation &&
|
|
166
|
+
input.allowUserCreationWithoutInvitation &&
|
|
167
|
+
input.requireNoActiveUsersForUserCreation &&
|
|
168
|
+
hasActiveUsers(this.db, projectId)
|
|
169
|
+
) {
|
|
170
|
+
consumeMagicLink(this.db, {
|
|
171
|
+
projectId,
|
|
172
|
+
id: input.magicLinkId,
|
|
173
|
+
tokenHash: input.tokenHash,
|
|
174
|
+
consumedAt: completedAt,
|
|
175
|
+
})
|
|
176
|
+
return {
|
|
177
|
+
error: new AuthStorageError(
|
|
178
|
+
"user_creation_not_allowed",
|
|
179
|
+
`[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
|
|
180
|
+
),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
validateCompleteSessionInput(this.db, projectId, input.session)
|
|
185
|
+
assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience)
|
|
186
|
+
|
|
187
|
+
let newUserId: string | undefined
|
|
188
|
+
if (shouldCreateUser) {
|
|
189
|
+
newUserId = assertNonEmpty(input.newUserId, "User id")
|
|
190
|
+
if (getUserRowById(this.db, { projectId, id: newUserId })) {
|
|
191
|
+
throw new AuthStorageError(
|
|
192
|
+
"duplicate_user",
|
|
193
|
+
`[Sixb] User '${newUserId}' already exists for project '${projectId}'.`
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
consumeMagicLink(this.db, {
|
|
199
|
+
projectId,
|
|
200
|
+
id: input.magicLinkId,
|
|
201
|
+
tokenHash: input.tokenHash,
|
|
202
|
+
consumedAt: completedAt,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const user = existingUserRow
|
|
206
|
+
? rowToUserRecord(existingUserRow)
|
|
207
|
+
: this.insertUser({
|
|
208
|
+
id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
|
|
209
|
+
projectId,
|
|
210
|
+
email: magicLink.email,
|
|
211
|
+
displayName: input.newUserDisplayName,
|
|
212
|
+
avatarUrl: input.newUserAvatarUrl,
|
|
213
|
+
createdAt: completedAt,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const invitation = this.acceptInvitationAndApplyGroups({
|
|
217
|
+
activeInvitation,
|
|
218
|
+
completedAt,
|
|
219
|
+
projectId,
|
|
220
|
+
user,
|
|
221
|
+
})
|
|
222
|
+
const groupMemberships = this.applyManualGroups({
|
|
223
|
+
completedAt,
|
|
224
|
+
existing: invitation.groupMemberships,
|
|
225
|
+
groupIds: manualGroupIds,
|
|
226
|
+
projectId,
|
|
227
|
+
userId: user.id,
|
|
228
|
+
})
|
|
229
|
+
const session = this.createSignInSession({
|
|
230
|
+
projectId,
|
|
231
|
+
strategyId: magicLink.strategy_id,
|
|
232
|
+
session: input.session,
|
|
233
|
+
userId: user.id,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
user,
|
|
238
|
+
session,
|
|
239
|
+
invitation: invitation.invitation,
|
|
240
|
+
groupMemberships,
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if ("error" in result) {
|
|
246
|
+
throw result.error
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return result
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async completeOidcSignIn(input: CompleteOidcSignInInput): Promise<CompleteSignInResult> {
|
|
253
|
+
const result = runImmediateTransaction(
|
|
254
|
+
this.db,
|
|
255
|
+
(): CompleteSignInResult | AuthTransactionError => {
|
|
256
|
+
const projectId = assertNonEmpty(input.projectId, "Project id")
|
|
257
|
+
const completedAt = new Date(input.completedAt)
|
|
258
|
+
const attempt = getOidcAttemptRowById(this.db, {
|
|
259
|
+
projectId,
|
|
260
|
+
id: input.oidcAuthorizationAttemptId,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
this.assertOidcAttemptUsable(
|
|
264
|
+
projectId,
|
|
265
|
+
input.oidcAuthorizationAttemptId,
|
|
266
|
+
input.stateHash,
|
|
267
|
+
completedAt,
|
|
268
|
+
attempt
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const subject = assertNonEmpty(input.subject, "Subject")
|
|
272
|
+
const email = normalizeEmail(input.email)
|
|
273
|
+
const identity = getIdentityRowBySubject(this.db, {
|
|
274
|
+
projectId,
|
|
275
|
+
strategyId: attempt.strategy_id,
|
|
276
|
+
subject,
|
|
277
|
+
})
|
|
278
|
+
const activeInvitation = identity
|
|
279
|
+
? null
|
|
280
|
+
: getActiveInvitationByEmail(this.db, {
|
|
281
|
+
projectId,
|
|
282
|
+
email,
|
|
283
|
+
now: completedAt,
|
|
284
|
+
})
|
|
285
|
+
let userRow = identity
|
|
286
|
+
? getUserRowById(this.db, { projectId, id: identity.user_id })
|
|
287
|
+
: getUserRowByEmail(this.db, { projectId, email })
|
|
288
|
+
const shouldCreateUser = !identity && !userRow
|
|
289
|
+
const manualGroupIds = normalizeGroupIds(input.manualGroupIds)
|
|
290
|
+
|
|
291
|
+
if (identity && !userRow) {
|
|
292
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
293
|
+
return {
|
|
294
|
+
error: new AuthStorageError(
|
|
295
|
+
"missing_user",
|
|
296
|
+
`[Sixb] User '${identity.user_id}' not found for linked OIDC identity.`
|
|
297
|
+
),
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!identity && userRow && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
|
|
302
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
303
|
+
return {
|
|
304
|
+
error: new AuthStorageError(
|
|
305
|
+
"email_link_not_allowed",
|
|
306
|
+
`[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`
|
|
307
|
+
),
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (userRow?.status === "suspended") {
|
|
312
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
313
|
+
return {
|
|
314
|
+
error: new AuthStorageError(
|
|
315
|
+
"suspended_user",
|
|
316
|
+
`[Sixb] User '${userRow.id}' is suspended for project '${projectId}'.`
|
|
317
|
+
),
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (shouldCreateUser && !input.emailVerified) {
|
|
322
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
323
|
+
return {
|
|
324
|
+
error: new AuthStorageError(
|
|
325
|
+
"user_creation_not_allowed",
|
|
326
|
+
`[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
|
|
327
|
+
),
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
|
|
332
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
333
|
+
return {
|
|
334
|
+
error: new AuthStorageError(
|
|
335
|
+
"user_creation_not_allowed",
|
|
336
|
+
`[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
|
|
337
|
+
),
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
shouldCreateUser &&
|
|
343
|
+
!activeInvitation &&
|
|
344
|
+
input.allowUserCreationWithoutInvitation &&
|
|
345
|
+
input.requireNoActiveUsersForUserCreation &&
|
|
346
|
+
hasActiveUsers(this.db, projectId)
|
|
347
|
+
) {
|
|
348
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
349
|
+
return {
|
|
350
|
+
error: new AuthStorageError(
|
|
351
|
+
"user_creation_not_allowed",
|
|
352
|
+
`[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
|
|
353
|
+
),
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
validateCompleteSessionInput(this.db, projectId, input.session)
|
|
358
|
+
assertSignInSessionAudience(projectId, input.session.audience, attempt.audience)
|
|
359
|
+
|
|
360
|
+
let newUserId: string | undefined
|
|
361
|
+
if (shouldCreateUser) {
|
|
362
|
+
newUserId = assertNonEmpty(input.newUserId, "User id")
|
|
363
|
+
if (getUserRowById(this.db, { projectId, id: newUserId })) {
|
|
364
|
+
throw new AuthStorageError(
|
|
365
|
+
"duplicate_user",
|
|
366
|
+
`[Sixb] User '${newUserId}' already exists for project '${projectId}'.`
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.consumeOidcAttempt(input, completedAt, projectId)
|
|
372
|
+
|
|
373
|
+
if (!userRow) {
|
|
374
|
+
const user = this.insertUser({
|
|
375
|
+
id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
|
|
376
|
+
projectId,
|
|
377
|
+
email,
|
|
378
|
+
displayName: input.displayName,
|
|
379
|
+
avatarUrl: input.avatarUrl,
|
|
380
|
+
createdAt: completedAt,
|
|
381
|
+
})
|
|
382
|
+
userRow = {
|
|
383
|
+
project_id: user.projectId,
|
|
384
|
+
id: user.id,
|
|
385
|
+
email: user.email,
|
|
386
|
+
display_name: user.displayName ?? null,
|
|
387
|
+
avatar_url: user.avatarUrl ?? null,
|
|
388
|
+
status: user.status,
|
|
389
|
+
created_at: toIso(user.createdAt),
|
|
390
|
+
updated_at: toIso(user.updatedAt),
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const user = rowToUserRecord(userRow)
|
|
395
|
+
const invitation = this.acceptInvitationAndApplyGroups({
|
|
396
|
+
activeInvitation,
|
|
397
|
+
completedAt,
|
|
398
|
+
projectId,
|
|
399
|
+
user,
|
|
400
|
+
})
|
|
401
|
+
const groupMemberships = this.applyManualGroups({
|
|
402
|
+
completedAt,
|
|
403
|
+
existing: invitation.groupMemberships,
|
|
404
|
+
groupIds: manualGroupIds,
|
|
405
|
+
projectId,
|
|
406
|
+
userId: user.id,
|
|
407
|
+
})
|
|
408
|
+
const nextIdentity = this.upsertIdentity({
|
|
409
|
+
projectId,
|
|
410
|
+
strategyId: attempt.strategy_id,
|
|
411
|
+
subject,
|
|
412
|
+
userId: user.id,
|
|
413
|
+
claims: input.claims,
|
|
414
|
+
createdAt: identity ? new Date(identity.created_at) : completedAt,
|
|
415
|
+
updatedAt: completedAt,
|
|
416
|
+
})
|
|
417
|
+
const session = this.createSignInSession({
|
|
418
|
+
projectId,
|
|
419
|
+
strategyId: attempt.strategy_id,
|
|
420
|
+
session: input.session,
|
|
421
|
+
userId: user.id,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
user,
|
|
426
|
+
session,
|
|
427
|
+
identity: nextIdentity,
|
|
428
|
+
invitation: invitation.invitation,
|
|
429
|
+
groupMemberships,
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if ("error" in result) {
|
|
435
|
+
throw result.error
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return result
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async suspendUserAndRevokeSessions(
|
|
442
|
+
input: SuspendUserAndRevokeSessionsInput
|
|
443
|
+
): Promise<UserRecord> {
|
|
444
|
+
return runImmediateTransaction(this.db, () => {
|
|
445
|
+
const existing = getUserRowById(this.db, {
|
|
446
|
+
projectId: input.projectId,
|
|
447
|
+
id: input.userId,
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
if (!existing) {
|
|
451
|
+
throw new AuthStorageError(
|
|
452
|
+
"missing_user",
|
|
453
|
+
`[Sixb] User '${input.userId}' not found for project '${input.projectId}'.`
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
this.db
|
|
458
|
+
.query(
|
|
459
|
+
`
|
|
460
|
+
UPDATE auth_users
|
|
461
|
+
SET status = 'suspended',
|
|
462
|
+
updated_at = ?
|
|
463
|
+
WHERE project_id = ?
|
|
464
|
+
AND id = ?
|
|
465
|
+
`
|
|
466
|
+
)
|
|
467
|
+
.run(toIso(input.suspendedAt), input.projectId, input.userId)
|
|
468
|
+
revokeActiveSessionsForUser(this.db, {
|
|
469
|
+
projectId: input.projectId,
|
|
470
|
+
userId: input.userId,
|
|
471
|
+
revokedAt: input.suspendedAt,
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
return rowToUserRecord({
|
|
475
|
+
...existing,
|
|
476
|
+
status: "suspended",
|
|
477
|
+
updated_at: toIso(input.suspendedAt),
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
close(): void {
|
|
483
|
+
closeSqliteStoreConnection(this.connection)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private assertMagicLinkUsable(
|
|
487
|
+
projectId: string,
|
|
488
|
+
id: string,
|
|
489
|
+
tokenHash: string,
|
|
490
|
+
completedAt: Date,
|
|
491
|
+
row: SqliteAuthMagicLinkRow | null
|
|
492
|
+
): asserts row is SqliteAuthMagicLinkRow {
|
|
493
|
+
if (!row) {
|
|
494
|
+
throw new AuthStorageError(
|
|
495
|
+
"missing_magic_link",
|
|
496
|
+
`[Sixb] Magic link '${id}' not found for project '${projectId}'.`
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (row.token_hash !== tokenHash || row.consumed_at || row.revoked_at) {
|
|
501
|
+
throw new AuthStorageError(
|
|
502
|
+
"invalid_magic_link",
|
|
503
|
+
`[Sixb] Magic link '${id}' is not valid for project '${projectId}'.`
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (new Date(row.expires_at) <= completedAt) {
|
|
508
|
+
throw new AuthStorageError(
|
|
509
|
+
"expired_magic_link",
|
|
510
|
+
`[Sixb] Magic link '${id}' is expired for project '${projectId}'.`
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private assertOidcAttemptUsable(
|
|
516
|
+
projectId: string,
|
|
517
|
+
id: string,
|
|
518
|
+
stateHash: string,
|
|
519
|
+
completedAt: Date,
|
|
520
|
+
row: SqliteAuthOidcAttemptRow | null
|
|
521
|
+
): asserts row is SqliteAuthOidcAttemptRow {
|
|
522
|
+
if (!row) {
|
|
523
|
+
throw new AuthStorageError(
|
|
524
|
+
"missing_oidc_attempt",
|
|
525
|
+
`[Sixb] OIDC authorization attempt '${id}' not found for project '${projectId}'.`
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (row.state_hash !== stateHash || row.consumed_at) {
|
|
530
|
+
throw new AuthStorageError(
|
|
531
|
+
"invalid_oidc_attempt",
|
|
532
|
+
`[Sixb] OIDC authorization attempt '${id}' is not valid for project '${projectId}'.`
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (new Date(row.expires_at) <= completedAt) {
|
|
537
|
+
throw new AuthStorageError(
|
|
538
|
+
"expired_oidc_attempt",
|
|
539
|
+
`[Sixb] OIDC authorization attempt '${id}' is expired for project '${projectId}'.`
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private insertUser(input: {
|
|
545
|
+
readonly id: string
|
|
546
|
+
readonly projectId: string
|
|
547
|
+
readonly email: string
|
|
548
|
+
readonly displayName?: string
|
|
549
|
+
readonly avatarUrl?: string
|
|
550
|
+
readonly createdAt: Date
|
|
551
|
+
}): UserRecord {
|
|
552
|
+
try {
|
|
553
|
+
this.db
|
|
554
|
+
.query(
|
|
555
|
+
`
|
|
556
|
+
INSERT INTO auth_users (
|
|
557
|
+
project_id,
|
|
558
|
+
id,
|
|
559
|
+
email,
|
|
560
|
+
display_name,
|
|
561
|
+
avatar_url,
|
|
562
|
+
status,
|
|
563
|
+
created_at,
|
|
564
|
+
updated_at
|
|
565
|
+
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?)
|
|
566
|
+
`
|
|
567
|
+
)
|
|
568
|
+
.run(
|
|
569
|
+
input.projectId,
|
|
570
|
+
input.id,
|
|
571
|
+
input.email,
|
|
572
|
+
input.displayName ?? null,
|
|
573
|
+
input.avatarUrl ?? null,
|
|
574
|
+
toIso(input.createdAt),
|
|
575
|
+
toIso(input.createdAt)
|
|
576
|
+
)
|
|
577
|
+
} catch (error) {
|
|
578
|
+
mapUniqueConstraintError(
|
|
579
|
+
error,
|
|
580
|
+
"duplicate_user",
|
|
581
|
+
`[Sixb] User '${input.id}' already exists for project '${input.projectId}'.`
|
|
582
|
+
)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
id: input.id,
|
|
587
|
+
projectId: input.projectId,
|
|
588
|
+
email: input.email,
|
|
589
|
+
displayName: input.displayName,
|
|
590
|
+
avatarUrl: input.avatarUrl,
|
|
591
|
+
status: "active",
|
|
592
|
+
createdAt: new Date(input.createdAt),
|
|
593
|
+
updatedAt: new Date(input.createdAt),
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private acceptInvitationAndApplyGroups(input: {
|
|
598
|
+
readonly activeInvitation: InvitationRecord | null
|
|
599
|
+
readonly completedAt: Date
|
|
600
|
+
readonly projectId: string
|
|
601
|
+
readonly user: UserRecord
|
|
602
|
+
}): {
|
|
603
|
+
readonly invitation?: InvitationRecord
|
|
604
|
+
readonly groupMemberships: readonly GroupMembershipRecord[]
|
|
605
|
+
} {
|
|
606
|
+
if (!input.activeInvitation) {
|
|
607
|
+
return { groupMemberships: [] }
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
this.db
|
|
611
|
+
.query(
|
|
612
|
+
`
|
|
613
|
+
UPDATE auth_invitations
|
|
614
|
+
SET status = 'accepted',
|
|
615
|
+
accepted_at = ?,
|
|
616
|
+
updated_at = ?
|
|
617
|
+
WHERE project_id = ?
|
|
618
|
+
AND id = ?
|
|
619
|
+
`
|
|
620
|
+
)
|
|
621
|
+
.run(
|
|
622
|
+
toIso(input.completedAt),
|
|
623
|
+
toIso(input.completedAt),
|
|
624
|
+
input.projectId,
|
|
625
|
+
input.activeInvitation.id
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
const invitation: InvitationRecord = {
|
|
629
|
+
...input.activeInvitation,
|
|
630
|
+
status: "accepted",
|
|
631
|
+
acceptedAt: new Date(input.completedAt),
|
|
632
|
+
updatedAt: new Date(input.completedAt),
|
|
633
|
+
}
|
|
634
|
+
const groupMemberships = input.activeInvitation.groupIds.map((groupId) =>
|
|
635
|
+
upsertGroupMembership(this.db, {
|
|
636
|
+
projectId: input.projectId,
|
|
637
|
+
userId: input.user.id,
|
|
638
|
+
groupId,
|
|
639
|
+
source: "invitation",
|
|
640
|
+
createdAt: input.completedAt,
|
|
641
|
+
})
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return { invitation, groupMemberships }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private applyManualGroups(input: {
|
|
648
|
+
readonly completedAt: Date
|
|
649
|
+
readonly existing: readonly GroupMembershipRecord[]
|
|
650
|
+
readonly groupIds: readonly string[]
|
|
651
|
+
readonly projectId: string
|
|
652
|
+
readonly userId: string
|
|
653
|
+
}): readonly GroupMembershipRecord[] {
|
|
654
|
+
const groupMemberships = [...input.existing]
|
|
655
|
+
|
|
656
|
+
for (const groupId of input.groupIds) {
|
|
657
|
+
groupMemberships.push(
|
|
658
|
+
upsertGroupMembership(this.db, {
|
|
659
|
+
projectId: input.projectId,
|
|
660
|
+
userId: input.userId,
|
|
661
|
+
groupId,
|
|
662
|
+
source: "manual",
|
|
663
|
+
createdAt: input.completedAt,
|
|
664
|
+
})
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return groupMemberships
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private createSignInSession(input: {
|
|
672
|
+
readonly projectId: string
|
|
673
|
+
readonly strategyId: string
|
|
674
|
+
readonly session: CompleteMagicLinkSignInInput["session"]
|
|
675
|
+
readonly userId: string
|
|
676
|
+
}): SessionRecord {
|
|
677
|
+
return createSession(this.db, {
|
|
678
|
+
id: input.session.id,
|
|
679
|
+
projectId: input.projectId,
|
|
680
|
+
userId: input.userId,
|
|
681
|
+
strategyId: input.strategyId,
|
|
682
|
+
audience: input.session.audience,
|
|
683
|
+
tokenHash: input.session.tokenHash,
|
|
684
|
+
createdAt: input.session.createdAt,
|
|
685
|
+
expiresAt: input.session.expiresAt,
|
|
686
|
+
absoluteExpiresAt: input.session.absoluteExpiresAt,
|
|
687
|
+
userAgent: input.session.userAgent,
|
|
688
|
+
ipAddress: input.session.ipAddress,
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private consumeOidcAttempt(
|
|
693
|
+
input: CompleteOidcSignInInput,
|
|
694
|
+
completedAt: Date,
|
|
695
|
+
projectId: string
|
|
696
|
+
): void {
|
|
697
|
+
consumeOidcAttempt(this.db, {
|
|
698
|
+
projectId,
|
|
699
|
+
id: input.oidcAuthorizationAttemptId,
|
|
700
|
+
stateHash: input.stateHash,
|
|
701
|
+
consumedAt: completedAt,
|
|
702
|
+
})
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private upsertIdentity(input: {
|
|
706
|
+
readonly projectId: string
|
|
707
|
+
readonly strategyId: string
|
|
708
|
+
readonly subject: string
|
|
709
|
+
readonly userId: string
|
|
710
|
+
readonly claims?: Readonly<Record<string, unknown>>
|
|
711
|
+
readonly createdAt: Date
|
|
712
|
+
readonly updatedAt: Date
|
|
713
|
+
}): UserIdentityRecord {
|
|
714
|
+
this.db
|
|
715
|
+
.query(
|
|
716
|
+
`
|
|
717
|
+
INSERT INTO auth_user_identities (
|
|
718
|
+
project_id,
|
|
719
|
+
strategy_id,
|
|
720
|
+
subject,
|
|
721
|
+
user_id,
|
|
722
|
+
claims,
|
|
723
|
+
created_at,
|
|
724
|
+
updated_at
|
|
725
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
726
|
+
ON CONFLICT(project_id, strategy_id, subject)
|
|
727
|
+
DO UPDATE SET
|
|
728
|
+
user_id = excluded.user_id,
|
|
729
|
+
claims = excluded.claims,
|
|
730
|
+
updated_at = excluded.updated_at
|
|
731
|
+
`
|
|
732
|
+
)
|
|
733
|
+
.run(
|
|
734
|
+
input.projectId,
|
|
735
|
+
input.strategyId,
|
|
736
|
+
input.subject,
|
|
737
|
+
input.userId,
|
|
738
|
+
serializeOptionalRecord(input.claims),
|
|
739
|
+
toIso(input.createdAt),
|
|
740
|
+
toIso(input.updatedAt)
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
return rowToIdentityRecord({
|
|
744
|
+
project_id: input.projectId,
|
|
745
|
+
strategy_id: input.strategyId,
|
|
746
|
+
subject: input.subject,
|
|
747
|
+
user_id: input.userId,
|
|
748
|
+
claims: serializeOptionalRecord(input.claims),
|
|
749
|
+
created_at: toIso(input.createdAt),
|
|
750
|
+
updated_at: toIso(input.updatedAt),
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function hasActiveUsers(db: Database, projectId: string): boolean {
|
|
756
|
+
const row = db
|
|
757
|
+
.query(
|
|
758
|
+
`
|
|
759
|
+
SELECT 1 AS active
|
|
760
|
+
FROM auth_users
|
|
761
|
+
WHERE project_id = ?
|
|
762
|
+
AND status = 'active'
|
|
763
|
+
LIMIT 1
|
|
764
|
+
`
|
|
765
|
+
)
|
|
766
|
+
.get(projectId) as { readonly active: number } | null
|
|
767
|
+
|
|
768
|
+
return row !== null
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function assertSignInSessionAudience(
|
|
772
|
+
projectId: string,
|
|
773
|
+
sessionAudience: string,
|
|
774
|
+
storedAudience: string
|
|
775
|
+
): void {
|
|
776
|
+
if (sessionAudience === storedAudience) {
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
throw new AuthStorageError(
|
|
781
|
+
"invalid_input",
|
|
782
|
+
`[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
|
|
783
|
+
)
|
|
784
|
+
}
|