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