@sixb/pg 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 +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,47 @@
1
+ import type {
2
+ AuthGroupMembershipStore,
3
+ GroupMembershipRecord,
4
+ UpsertAuthGroupMembershipInput,
5
+ } from "@sixb/core/storage"
6
+ import type { PgStoreClient } from "../transactions"
7
+ import type { PgAuthGroupMembershipRow } from "./rows"
8
+ import { rowToGroupMembershipRecord } from "./rows"
9
+ import { listMembershipsForUser, removeGroupMembership, upsertGroupMembership } from "./shared"
10
+
11
+ export class PgAuthGroupMembershipStore implements AuthGroupMembershipStore {
12
+ constructor(private readonly sql: PgStoreClient) {}
13
+
14
+ async upsert(input: UpsertAuthGroupMembershipInput): Promise<GroupMembershipRecord> {
15
+ return upsertGroupMembership(this.sql, input)
16
+ }
17
+
18
+ async remove(params: {
19
+ readonly projectId: string
20
+ readonly userId: string
21
+ readonly groupId: string
22
+ }): Promise<GroupMembershipRecord | null> {
23
+ return removeGroupMembership(this.sql, params)
24
+ }
25
+
26
+ async listForUser(params: {
27
+ readonly projectId: string
28
+ readonly userId: string
29
+ }): Promise<readonly GroupMembershipRecord[]> {
30
+ return listMembershipsForUser(this.sql, params)
31
+ }
32
+
33
+ async listForGroup(params: {
34
+ readonly projectId: string
35
+ readonly groupId: string
36
+ }): Promise<readonly GroupMembershipRecord[]> {
37
+ const rows = await this.sql<PgAuthGroupMembershipRow[]>`
38
+ SELECT *
39
+ FROM auth_group_memberships
40
+ WHERE project_id = ${params.projectId}
41
+ AND group_id = ${params.groupId}
42
+ ORDER BY user_id ASC
43
+ `
44
+
45
+ return rows.map(rowToGroupMembershipRecord)
46
+ }
47
+ }
@@ -0,0 +1,85 @@
1
+ import type {
2
+ AuthUserIdentityStore,
3
+ UpsertAuthUserIdentityInput,
4
+ UserIdentityRecord,
5
+ } from "@sixb/core/storage"
6
+ import type { PgStoreClient } from "../transactions"
7
+ import type { PgAuthUserIdentityRow } from "./rows"
8
+ import { rowToIdentityRecord, serializeOptionalRecord } from "./rows"
9
+ import { assertNonEmpty, dateOrNow, getIdentityRowBySubject, requireUserById } from "./shared"
10
+
11
+ export class PgAuthUserIdentityStore implements AuthUserIdentityStore {
12
+ constructor(private readonly sql: PgStoreClient) {}
13
+
14
+ async upsert(input: UpsertAuthUserIdentityInput): Promise<UserIdentityRecord> {
15
+ const projectId = assertNonEmpty(input.projectId, "Project id")
16
+ const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
17
+ const subject = assertNonEmpty(input.subject, "Subject")
18
+ const userId = assertNonEmpty(input.userId, "User id")
19
+ const existing = await getIdentityRowBySubject(this.sql, {
20
+ projectId,
21
+ strategyId,
22
+ subject,
23
+ })
24
+ const createdAt = existing ? new Date(existing.created_at) : dateOrNow(input.createdAt)
25
+ const updatedAt = input.updatedAt
26
+ ? new Date(input.updatedAt)
27
+ : existing
28
+ ? new Date()
29
+ : createdAt
30
+
31
+ await requireUserById(this.sql, { projectId, id: userId })
32
+
33
+ const [row] = await this.sql<PgAuthUserIdentityRow[]>`
34
+ INSERT INTO auth_user_identities (
35
+ project_id,
36
+ strategy_id,
37
+ subject,
38
+ user_id,
39
+ claims,
40
+ created_at,
41
+ updated_at
42
+ ) VALUES (
43
+ ${projectId},
44
+ ${strategyId},
45
+ ${subject},
46
+ ${userId},
47
+ ${serializeOptionalRecord(input.claims)}::text::jsonb,
48
+ ${createdAt},
49
+ ${updatedAt}
50
+ )
51
+ ON CONFLICT (project_id, strategy_id, subject)
52
+ DO UPDATE SET
53
+ user_id = excluded.user_id,
54
+ claims = excluded.claims,
55
+ updated_at = excluded.updated_at
56
+ RETURNING *
57
+ `
58
+
59
+ return rowToIdentityRecord(row)
60
+ }
61
+
62
+ async getBySubject(params: {
63
+ readonly projectId: string
64
+ readonly strategyId: string
65
+ readonly subject: string
66
+ }): Promise<UserIdentityRecord | null> {
67
+ const row = await getIdentityRowBySubject(this.sql, params)
68
+ return row ? rowToIdentityRecord(row) : null
69
+ }
70
+
71
+ async listForUser(params: {
72
+ readonly projectId: string
73
+ readonly userId: string
74
+ }): Promise<readonly UserIdentityRecord[]> {
75
+ const rows = await this.sql<PgAuthUserIdentityRow[]>`
76
+ SELECT *
77
+ FROM auth_user_identities
78
+ WHERE project_id = ${params.projectId}
79
+ AND user_id = ${params.userId}
80
+ ORDER BY created_at ASC, strategy_id ASC, subject ASC
81
+ `
82
+
83
+ return rows.map(rowToIdentityRecord)
84
+ }
85
+ }
@@ -0,0 +1,2 @@
1
+ export type { PgAuthStorageOptions } from "./auth-storage"
2
+ export { PgAuthStorage } from "./auth-storage"
@@ -0,0 +1,280 @@
1
+ import type {
2
+ AuthInvitationStore,
3
+ CreateOrUpdateAuthInvitationInput,
4
+ InvitationRecord,
5
+ ListAuthInvitationsInput,
6
+ ListAuthInvitationsResult,
7
+ } from "@sixb/core/storage"
8
+ import { AuthStorageError } from "@sixb/core/storage"
9
+ import type { SQLClient } from "../pg-client"
10
+ import {
11
+ authLockKey,
12
+ lockAdvisoryKeys,
13
+ type PgStoreClient,
14
+ runPgTransaction,
15
+ } from "../transactions"
16
+ import type { PgAuthInvitationRow } from "./rows"
17
+ import { rowToInvitationRecord } from "./rows"
18
+ import {
19
+ appendPagination,
20
+ assertNonEmpty,
21
+ dateOrNow,
22
+ getActiveInvitationByEmail,
23
+ getInvitationById,
24
+ getInvitationGroupIds,
25
+ hasEmptyFilter,
26
+ invitationCreatorColumns,
27
+ mapUniqueConstraintError,
28
+ normalizeEmail,
29
+ normalizeGroupIds,
30
+ type PgValue,
31
+ placeholders,
32
+ replaceInvitationGroups,
33
+ requireInvitationById,
34
+ requireSessionById,
35
+ requireUserById,
36
+ } from "./shared"
37
+
38
+ export class PgAuthInvitationStore implements AuthInvitationStore {
39
+ constructor(private readonly sql: PgStoreClient) {}
40
+
41
+ async createOrUpdateActive(input: CreateOrUpdateAuthInvitationInput): Promise<InvitationRecord> {
42
+ return runPgTransaction(this.sql, async (tx) => {
43
+ const id = assertNonEmpty(input.id, "Invitation id")
44
+ const projectId = assertNonEmpty(input.projectId, "Project id")
45
+ const email = normalizeEmail(input.email)
46
+ const groupIds = normalizeGroupIds(input.groupIds)
47
+ const now = dateOrNow(input.updatedAt ?? input.createdAt)
48
+ const creator = invitationCreatorColumns(input.createdByPrincipal)
49
+
50
+ await lockAdvisoryKeys(tx, [authLockKey("invitations", projectId, email)])
51
+ await this.validateCreator(tx, projectId, input)
52
+
53
+ const existingActive = await getActiveInvitationByEmail(
54
+ tx,
55
+ {
56
+ projectId,
57
+ email,
58
+ now,
59
+ },
60
+ { forUpdate: true }
61
+ )
62
+
63
+ if (existingActive) {
64
+ await tx`
65
+ UPDATE auth_invitations
66
+ SET created_by_user_id = ${creator.createdByUserId},
67
+ created_by_service_account_id = ${creator.createdByServiceAccountId},
68
+ created_by_system_id = ${creator.createdBySystemId},
69
+ created_by_session_id = ${input.createdBySessionId ?? null},
70
+ expires_at = ${input.expiresAt},
71
+ updated_at = ${now}
72
+ WHERE project_id = ${projectId}
73
+ AND id = ${existingActive.id}
74
+ `
75
+ await replaceInvitationGroups(tx, {
76
+ projectId,
77
+ invitationId: existingActive.id,
78
+ groupIds,
79
+ })
80
+
81
+ return requireInvitationById(tx, { projectId, id: existingActive.id })
82
+ }
83
+
84
+ if (await getInvitationById(tx, { projectId, id })) {
85
+ throw new AuthStorageError(
86
+ "duplicate_invitation",
87
+ `[Sixb] Invitation '${id}' already exists but is not active for project '${projectId}'.`
88
+ )
89
+ }
90
+
91
+ const createdAt = dateOrNow(input.createdAt)
92
+ try {
93
+ await tx`
94
+ INSERT INTO auth_invitations (
95
+ project_id,
96
+ id,
97
+ email,
98
+ status,
99
+ created_by_user_id,
100
+ created_by_service_account_id,
101
+ created_by_system_id,
102
+ created_by_session_id,
103
+ created_at,
104
+ updated_at,
105
+ expires_at
106
+ ) VALUES (
107
+ ${projectId},
108
+ ${id},
109
+ ${email},
110
+ ${"pending"},
111
+ ${creator.createdByUserId},
112
+ ${creator.createdByServiceAccountId},
113
+ ${creator.createdBySystemId},
114
+ ${input.createdBySessionId ?? null},
115
+ ${createdAt},
116
+ ${now},
117
+ ${input.expiresAt}
118
+ )
119
+ `
120
+ } catch (error) {
121
+ mapUniqueConstraintError(
122
+ error,
123
+ "duplicate_invitation",
124
+ `[Sixb] Invitation '${id}' already exists for project '${projectId}'.`
125
+ )
126
+ }
127
+
128
+ await replaceInvitationGroups(tx, { projectId, invitationId: id, groupIds })
129
+ return requireInvitationById(tx, { projectId, id })
130
+ })
131
+ }
132
+
133
+ async getById(params: {
134
+ readonly projectId: string
135
+ readonly id: string
136
+ }): Promise<InvitationRecord | null> {
137
+ return getInvitationById(this.sql, params)
138
+ }
139
+
140
+ async getActiveByEmail(params: {
141
+ readonly projectId: string
142
+ readonly email: string
143
+ readonly now: Date
144
+ }): Promise<InvitationRecord | null> {
145
+ return getActiveInvitationByEmail(this.sql, params)
146
+ }
147
+
148
+ async list(input: ListAuthInvitationsInput): Promise<ListAuthInvitationsResult> {
149
+ if (hasEmptyFilter(input.statuses) || hasEmptyFilter(input.groupIds)) {
150
+ return { invitations: [], hasMore: false, total: 0 }
151
+ }
152
+
153
+ const whereClauses = ["project_id = $1"]
154
+ const params: PgValue[] = [input.projectId]
155
+ let nextIndex = 2
156
+
157
+ if (input.email) {
158
+ whereClauses.push(`email = $${nextIndex++}`)
159
+ params.push(normalizeEmail(input.email))
160
+ }
161
+
162
+ if (input.statuses) {
163
+ const statusPlaceholders = placeholders(nextIndex, input.statuses.length)
164
+ nextIndex += input.statuses.length
165
+ whereClauses.push(`status IN (${statusPlaceholders.join(", ")})`)
166
+ params.push(...input.statuses)
167
+ }
168
+
169
+ const groupIds = input.groupIds ? normalizeGroupIds(input.groupIds) : null
170
+ if (groupIds) {
171
+ const groupPlaceholders = placeholders(nextIndex, groupIds.length)
172
+ nextIndex += groupIds.length
173
+ whereClauses.push(`
174
+ EXISTS (
175
+ SELECT 1
176
+ FROM auth_invitation_groups invitation_groups
177
+ WHERE invitation_groups.project_id = auth_invitations.project_id
178
+ AND invitation_groups.invitation_id = auth_invitations.id
179
+ AND invitation_groups.group_id IN (${groupPlaceholders.join(", ")})
180
+ )
181
+ `)
182
+ params.push(...groupIds)
183
+ }
184
+
185
+ const where = `WHERE ${whereClauses.join(" AND ")}`
186
+ const order = input.order === "asc" ? "ASC" : "DESC"
187
+ const [totalRow] = await this.sql.unsafe<{ readonly count: string | number }[]>(
188
+ `SELECT COUNT(*)::bigint AS count FROM auth_invitations ${where}`,
189
+ [...params]
190
+ )
191
+
192
+ const queryParams = [...params]
193
+ const query = appendPagination(
194
+ `
195
+ SELECT *
196
+ FROM auth_invitations
197
+ ${where}
198
+ ORDER BY created_at ${order}, id ${order}
199
+ `,
200
+ queryParams,
201
+ nextIndex,
202
+ input
203
+ )
204
+ const rows = await this.sql.unsafe<PgAuthInvitationRow[]>(query, queryParams)
205
+ const invitations = await Promise.all(
206
+ rows.map(async (row) =>
207
+ rowToInvitationRecord(
208
+ row,
209
+ await getInvitationGroupIds(this.sql, {
210
+ projectId: row.project_id,
211
+ invitationId: row.id,
212
+ })
213
+ )
214
+ )
215
+ )
216
+ const total = Number(totalRow?.count ?? 0)
217
+
218
+ return {
219
+ invitations,
220
+ hasMore: (input.offset ?? 0) + rows.length < total,
221
+ total,
222
+ }
223
+ }
224
+
225
+ async accept(params: {
226
+ readonly projectId: string
227
+ readonly id: string
228
+ readonly acceptedAt: Date
229
+ }): Promise<InvitationRecord> {
230
+ return runPgTransaction(this.sql, async (tx) => {
231
+ await requireInvitationById(tx, params, { forUpdate: true })
232
+
233
+ await tx`
234
+ UPDATE auth_invitations
235
+ SET status = 'accepted',
236
+ accepted_at = ${params.acceptedAt},
237
+ updated_at = ${params.acceptedAt}
238
+ WHERE project_id = ${params.projectId}
239
+ AND id = ${params.id}
240
+ `
241
+
242
+ return requireInvitationById(tx, params)
243
+ })
244
+ }
245
+
246
+ async revoke(params: {
247
+ readonly projectId: string
248
+ readonly id: string
249
+ readonly revokedAt: Date
250
+ }): Promise<InvitationRecord> {
251
+ return runPgTransaction(this.sql, async (tx) => {
252
+ await requireInvitationById(tx, params, { forUpdate: true })
253
+
254
+ await tx`
255
+ UPDATE auth_invitations
256
+ SET status = 'revoked',
257
+ revoked_at = ${params.revokedAt},
258
+ updated_at = ${params.revokedAt}
259
+ WHERE project_id = ${params.projectId}
260
+ AND id = ${params.id}
261
+ `
262
+
263
+ return requireInvitationById(tx, params)
264
+ })
265
+ }
266
+
267
+ private async validateCreator(
268
+ sql: SQLClient,
269
+ projectId: string,
270
+ input: CreateOrUpdateAuthInvitationInput
271
+ ): Promise<void> {
272
+ if (input.createdByPrincipal?.type === "user") {
273
+ await requireUserById(sql, { projectId, id: input.createdByPrincipal.id })
274
+ }
275
+
276
+ if (input.createdBySessionId) {
277
+ await requireSessionById(sql, { projectId, id: input.createdBySessionId })
278
+ }
279
+ }
280
+ }
@@ -0,0 +1,140 @@
1
+ import { resolveAuthSessionAudience } from "@sixb/core"
2
+ import type {
3
+ AuthMagicLinkStore,
4
+ CreateAuthMagicLinkInput,
5
+ MagicLinkRecord,
6
+ } from "@sixb/core/storage"
7
+ import { AuthStorageError } from "@sixb/core/storage"
8
+ import {
9
+ authLockKey,
10
+ lockAdvisoryKeys,
11
+ type PgStoreClient,
12
+ runPgTransaction,
13
+ } from "../transactions"
14
+ import type { PgAuthMagicLinkRow } from "./rows"
15
+ import { rowToMagicLinkRecord } from "./rows"
16
+ import {
17
+ assertNonEmpty,
18
+ consumeMagicLink,
19
+ getMagicLinkById,
20
+ getMagicLinkRowById,
21
+ mapUniqueConstraintError,
22
+ normalizeEmail,
23
+ revokeActiveMagicLinksForEmail,
24
+ } from "./shared"
25
+
26
+ export class PgAuthMagicLinkStore implements AuthMagicLinkStore {
27
+ constructor(private readonly sql: PgStoreClient) {}
28
+
29
+ async create(input: CreateAuthMagicLinkInput): Promise<MagicLinkRecord> {
30
+ return runPgTransaction(this.sql, async (tx) => {
31
+ const id = assertNonEmpty(input.id, "Magic link id")
32
+ const projectId = assertNonEmpty(input.projectId, "Project id")
33
+ const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
34
+ const audience = resolveAuthSessionAudience(input.audience)
35
+ const email = normalizeEmail(input.email)
36
+ const tokenHash = assertNonEmpty(input.tokenHash, "Magic link token hash")
37
+
38
+ await lockAdvisoryKeys(tx, [authLockKey("magic-links", projectId, email)])
39
+
40
+ if (await getMagicLinkRowById(tx, { projectId, id })) {
41
+ throw new AuthStorageError(
42
+ "duplicate_magic_link",
43
+ `[Sixb] Magic link '${id}' already exists for project '${projectId}'.`
44
+ )
45
+ }
46
+
47
+ await revokeActiveMagicLinksForEmail(tx, {
48
+ projectId,
49
+ email,
50
+ revokedAt: input.createdAt,
51
+ })
52
+
53
+ try {
54
+ const [row] = await tx<PgAuthMagicLinkRow[]>`
55
+ INSERT INTO auth_magic_links (
56
+ project_id,
57
+ id,
58
+ strategy_id,
59
+ audience,
60
+ email,
61
+ token_hash,
62
+ return_to,
63
+ created_at,
64
+ expires_at
65
+ ) VALUES (
66
+ ${projectId},
67
+ ${id},
68
+ ${strategyId},
69
+ ${audience},
70
+ ${email},
71
+ ${tokenHash},
72
+ ${input.returnTo ?? null},
73
+ ${input.createdAt},
74
+ ${input.expiresAt}
75
+ )
76
+ RETURNING *
77
+ `
78
+
79
+ return rowToMagicLinkRecord(row)
80
+ } catch (error) {
81
+ mapUniqueConstraintError(
82
+ error,
83
+ "duplicate_magic_link",
84
+ `[Sixb] Magic link '${id}' already exists for project '${projectId}'.`
85
+ )
86
+ }
87
+ })
88
+ }
89
+
90
+ async getById(params: {
91
+ readonly projectId: string
92
+ readonly id: string
93
+ }): Promise<MagicLinkRecord | null> {
94
+ return getMagicLinkById(this.sql, params)
95
+ }
96
+
97
+ async getActiveByEmail(params: {
98
+ readonly projectId: string
99
+ readonly email: string
100
+ readonly now: Date
101
+ }): Promise<MagicLinkRecord | null> {
102
+ const [row] = await this.sql<PgAuthMagicLinkRow[]>`
103
+ SELECT *
104
+ FROM auth_magic_links
105
+ WHERE project_id = ${params.projectId}
106
+ AND email = ${normalizeEmail(params.email)}
107
+ AND consumed_at IS NULL
108
+ AND revoked_at IS NULL
109
+ AND expires_at > ${params.now}
110
+ ORDER BY created_at DESC, id DESC
111
+ LIMIT 1
112
+ `
113
+
114
+ return row ? rowToMagicLinkRecord(row) : null
115
+ }
116
+
117
+ async consume(params: {
118
+ readonly projectId: string
119
+ readonly id: string
120
+ readonly tokenHash: string
121
+ readonly consumedAt: Date
122
+ }): Promise<MagicLinkRecord> {
123
+ return runPgTransaction(this.sql, (tx) => consumeMagicLink(tx, params))
124
+ }
125
+
126
+ async revokeActiveForEmail(params: {
127
+ readonly projectId: string
128
+ readonly email: string
129
+ readonly revokedAt: Date
130
+ }): Promise<readonly MagicLinkRecord[]> {
131
+ return runPgTransaction(this.sql, async (tx) => {
132
+ const email = normalizeEmail(params.email)
133
+ await lockAdvisoryKeys(tx, [authLockKey("magic-links", params.projectId, email)])
134
+ return revokeActiveMagicLinksForEmail(tx, {
135
+ ...params,
136
+ email,
137
+ })
138
+ })
139
+ }
140
+ }
@@ -0,0 +1,93 @@
1
+ import { resolveAuthSessionAudience } from "@sixb/core"
2
+ import type {
3
+ AuthOidcAuthorizationAttemptStore,
4
+ CreateOidcAuthorizationAttemptInput,
5
+ OidcAuthorizationAttemptRecord,
6
+ } from "@sixb/core/storage"
7
+ import { AuthStorageError } from "@sixb/core/storage"
8
+ import { type PgStoreClient, runPgTransaction } from "../transactions"
9
+ import type { PgAuthOidcAttemptRow } from "./rows"
10
+ import { rowToOidcAuthorizationAttemptRecord } from "./rows"
11
+ import {
12
+ assertNonEmpty,
13
+ consumeOidcAttempt,
14
+ getOidcAttemptById,
15
+ getOidcAttemptRowById,
16
+ mapUniqueConstraintError,
17
+ } from "./shared"
18
+
19
+ export class PgAuthOidcAuthorizationAttemptStore implements AuthOidcAuthorizationAttemptStore {
20
+ constructor(private readonly sql: PgStoreClient) {}
21
+
22
+ async create(
23
+ input: CreateOidcAuthorizationAttemptInput
24
+ ): Promise<OidcAuthorizationAttemptRecord> {
25
+ const id = assertNonEmpty(input.id, "OIDC authorization attempt id")
26
+ const projectId = assertNonEmpty(input.projectId, "Project id")
27
+ const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
28
+ const audience = resolveAuthSessionAudience(input.audience)
29
+ const stateHash = assertNonEmpty(input.stateHash, "OIDC state hash")
30
+ const nonceHash = assertNonEmpty(input.nonceHash, "OIDC nonce hash")
31
+ const codeVerifier = assertNonEmpty(input.codeVerifier, "OIDC code verifier")
32
+
33
+ if (await getOidcAttemptRowById(this.sql, { projectId, id })) {
34
+ throw new AuthStorageError(
35
+ "duplicate_oidc_attempt",
36
+ `[Sixb] OIDC authorization attempt '${id}' already exists for project '${projectId}'.`
37
+ )
38
+ }
39
+
40
+ try {
41
+ const [row] = await this.sql<PgAuthOidcAttemptRow[]>`
42
+ INSERT INTO auth_oidc_authorization_attempts (
43
+ project_id,
44
+ id,
45
+ strategy_id,
46
+ audience,
47
+ state_hash,
48
+ nonce_hash,
49
+ code_verifier,
50
+ return_to,
51
+ created_at,
52
+ expires_at
53
+ ) VALUES (
54
+ ${projectId},
55
+ ${id},
56
+ ${strategyId},
57
+ ${audience},
58
+ ${stateHash},
59
+ ${nonceHash},
60
+ ${codeVerifier},
61
+ ${input.returnTo ?? null},
62
+ ${input.createdAt},
63
+ ${input.expiresAt}
64
+ )
65
+ RETURNING *
66
+ `
67
+
68
+ return rowToOidcAuthorizationAttemptRecord(row)
69
+ } catch (error) {
70
+ mapUniqueConstraintError(
71
+ error,
72
+ "duplicate_oidc_attempt",
73
+ `[Sixb] OIDC authorization attempt '${id}' already exists for project '${projectId}'.`
74
+ )
75
+ }
76
+ }
77
+
78
+ async getById(params: {
79
+ readonly projectId: string
80
+ readonly id: string
81
+ }): Promise<OidcAuthorizationAttemptRecord | null> {
82
+ return getOidcAttemptById(this.sql, params)
83
+ }
84
+
85
+ async consume(params: {
86
+ readonly projectId: string
87
+ readonly id: string
88
+ readonly stateHash: string
89
+ readonly consumedAt: Date
90
+ }): Promise<OidcAuthorizationAttemptRecord> {
91
+ return runPgTransaction(this.sql, (tx) => consumeOidcAttempt(tx, params))
92
+ }
93
+ }