@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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/agents/agent-storage.d.ts +20 -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 +13 -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 +25 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +13 -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 +41 -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 +29 -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 +138 -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 +159 -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 +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11957 -0
- package/dist/index.js.map +55 -0
- package/dist/latest-run-query.d.ts +24 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +20 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +11 -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 +63 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +69 -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 +33 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +12 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +95 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +25 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts +21 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -0
- package/dist/pg-client.d.ts +58 -0
- package/dist/pg-client.d.ts.map +1 -0
- package/dist/pg-object-query-compiler.d.ts +55 -0
- package/dist/pg-object-query-compiler.d.ts.map +1 -0
- package/dist/pg-object-storage.d.ts +90 -0
- package/dist/pg-object-storage.d.ts.map +1 -0
- package/dist/pg-pipeline-run-storage.d.ts +18 -0
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
- package/dist/pg-projection-run-storage.d.ts +19 -0
- package/dist/pg-projection-run-storage.d.ts.map +1 -0
- package/dist/pg-rules-storage.d.ts +25 -0
- package/dist/pg-rules-storage.d.ts.map +1 -0
- package/dist/pg-sync-run-storage.d.ts +15 -0
- package/dist/pg-sync-run-storage.d.ts.map +1 -0
- package/dist/pg-timeseries-storage.d.ts +32 -0
- package/dist/pg-timeseries-storage.d.ts.map +1 -0
- package/dist/pg-webhook-delivery-storage.d.ts +17 -0
- package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
- package/dist/pg-webhook-run-storage.d.ts +14 -0
- package/dist/pg-webhook-run-storage.d.ts.map +1 -0
- package/dist/pg-workflow-intervention-storage.d.ts +16 -0
- package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
- package/dist/pg-workflow-run-storage.d.ts +49 -0
- package/dist/pg-workflow-run-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +30 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +16 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/transactions.d.ts +10 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/package.json +60 -0
- package/src/agents/agent-storage.ts +26 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +144 -0
- package/src/agents/rows.ts +227 -0
- package/src/agents/runs.ts +330 -0
- package/src/agents/threads.ts +116 -0
- package/src/auth-storage/access-tokens.ts +254 -0
- package/src/auth-storage/auth-storage.ts +798 -0
- package/src/auth-storage/group-memberships.ts +47 -0
- package/src/auth-storage/identities.ts +85 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +280 -0
- package/src/auth-storage/magic-links.ts +140 -0
- package/src/auth-storage/oidc-attempts.ts +93 -0
- package/src/auth-storage/rows.ts +352 -0
- package/src/auth-storage/service-account-group-memberships.ts +150 -0
- package/src/auth-storage/service-accounts.ts +154 -0
- package/src/auth-storage/sessions.ts +193 -0
- package/src/auth-storage/shared.ts +799 -0
- package/src/auth-storage/users.ts +196 -0
- package/src/index.ts +367 -0
- package/src/latest-run-query.ts +42 -0
- package/src/migrations/001-initial-schema.sql +1195 -0
- package/src/migrations.ts +280 -0
- package/src/ontology-storage/commits.ts +115 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +426 -0
- package/src/ontology-storage/materialization-state.ts +1109 -0
- package/src/ontology-storage/materialization-writer.ts +509 -0
- package/src/ontology-storage/materializations.ts +939 -0
- package/src/ontology-storage/outbox.ts +216 -0
- package/src/ontology-storage/shared.ts +293 -0
- package/src/ontology-storage/sources.ts +658 -0
- package/src/pg-action-run-storage.ts +657 -0
- package/src/pg-client.ts +124 -0
- package/src/pg-object-query-compiler.ts +1545 -0
- package/src/pg-object-storage.ts +638 -0
- package/src/pg-pipeline-run-storage.ts +457 -0
- package/src/pg-projection-run-storage.ts +526 -0
- package/src/pg-rules-storage.ts +308 -0
- package/src/pg-sync-run-storage.ts +263 -0
- package/src/pg-timeseries-storage.ts +180 -0
- package/src/pg-webhook-delivery-storage.ts +180 -0
- package/src/pg-webhook-run-storage.ts +185 -0
- package/src/pg-workflow-intervention-storage.ts +339 -0
- package/src/pg-workflow-run-storage.ts +1065 -0
- package/src/run-list-query.ts +108 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +72 -0
- 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,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
|
+
}
|