@shipfox/api-auth 18.0.0 → 19.0.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +34 -0
- package/README.md +24 -9
- package/dist/core/administration.d.ts +15 -0
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +44 -2
- package/dist/core/administration.js.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js +5 -3
- package/dist/core/auth.js.map +1 -1
- package/dist/core/entities/agent-access.d.ts +75 -0
- package/dist/core/entities/agent-access.d.ts.map +1 -0
- package/dist/core/entities/agent-access.js +3 -0
- package/dist/core/entities/agent-access.js.map +1 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/db/admin-user-moderation.d.ts.map +1 -1
- package/dist/db/admin-user-moderation.js +30 -26
- package/dist/db/admin-user-moderation.js.map +1 -1
- package/dist/db/admin-user-summary.d.ts +22 -0
- package/dist/db/admin-user-summary.d.ts.map +1 -1
- package/dist/db/admin-user-summary.js +75 -1
- package/dist/db/admin-user-summary.js.map +1 -1
- package/dist/db/admin-users.d.ts +11 -0
- package/dist/db/admin-users.d.ts.map +1 -1
- package/dist/db/admin-users.js +4 -1
- package/dist/db/admin-users.js.map +1 -1
- package/dist/db/agent-access.d.ts +159 -0
- package/dist/db/agent-access.d.ts.map +1 -0
- package/dist/db/agent-access.js +267 -0
- package/dist/db/agent-access.js.map +1 -0
- package/dist/db/db.d.ts +2604 -238
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +7 -0
- package/dist/db/db.js.map +1 -1
- package/dist/db/schema/agent-access.d.ts +1204 -0
- package/dist/db/schema/agent-access.d.ts.map +1 -0
- package/dist/db/schema/agent-access.js +267 -0
- package/dist/db/schema/agent-access.js.map +1 -0
- package/dist/db/schema/users.d.ts.map +1 -1
- package/dist/db/schema/users.js +8 -3
- package/dist/db/schema/users.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +123 -20
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.js +6 -1
- package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +10 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/presentation/routes/registration/signup.d.ts.map +1 -1
- package/dist/presentation/routes/registration/signup.js +59 -50
- package/dist/presentation/routes/registration/signup.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0003_brainy_luckman.sql +6 -0
- package/drizzle/0004_yummy_runaways.sql +110 -0
- package/drizzle/meta/0003_snapshot.json +844 -0
- package/drizzle/meta/0004_snapshot.json +1751 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +7 -7
- package/src/core/admin-role.test.ts +70 -0
- package/src/core/administration.ts +78 -1
- package/src/core/auth.ts +3 -5
- package/src/core/entities/agent-access.ts +80 -0
- package/src/core/errors.ts +7 -0
- package/src/db/admin-user-moderation.ts +53 -45
- package/src/db/admin-user-summary.test.ts +228 -0
- package/src/db/admin-user-summary.ts +143 -1
- package/src/db/admin-users.ts +21 -1
- package/src/db/agent-access.test.ts +370 -0
- package/src/db/agent-access.ts +629 -0
- package/src/db/db.ts +14 -0
- package/src/db/schema/agent-access.ts +273 -0
- package/src/db/schema/users.ts +9 -3
- package/src/index.ts +6 -0
- package/src/metrics/instance.ts +1 -0
- package/src/presentation/routes/administration.test.ts +359 -2
- package/src/presentation/routes/administration.ts +142 -20
- package/src/presentation/routes/email-verification/verify-email-resend.ts +7 -1
- package/src/presentation/routes/rate-limit.ts +4 -0
- package/src/presentation/routes/registration/signup.ts +52 -41
- package/test/globalSetup.ts +1 -1
- package/test/routes.ts +3 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import {and, asc, eq, gt, inArray, isNull, lt, or, sql} from 'drizzle-orm';
|
|
2
|
+
import type {
|
|
3
|
+
AgentAuthorizationCode,
|
|
4
|
+
AgentAuthorizationRequest,
|
|
5
|
+
AgentClient,
|
|
6
|
+
AgentGrant,
|
|
7
|
+
AgentPersonalAccessToken,
|
|
8
|
+
AgentRefreshToken,
|
|
9
|
+
} from '#core/entities/agent-access.js';
|
|
10
|
+
import {db} from './db.js';
|
|
11
|
+
import {
|
|
12
|
+
agentAuthorizationCodes,
|
|
13
|
+
agentAuthorizationRequests,
|
|
14
|
+
agentClients,
|
|
15
|
+
agentGrants,
|
|
16
|
+
agentPersonalAccessTokens,
|
|
17
|
+
agentRefreshTokens,
|
|
18
|
+
toAgentAuthorizationCode,
|
|
19
|
+
toAgentAuthorizationRequest,
|
|
20
|
+
toAgentClient,
|
|
21
|
+
toAgentGrant,
|
|
22
|
+
toAgentPersonalAccessToken,
|
|
23
|
+
toAgentRefreshToken,
|
|
24
|
+
} from './schema/agent-access.js';
|
|
25
|
+
|
|
26
|
+
export type AgentAccessTx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
27
|
+
|
|
28
|
+
type AgentAccessExecutor = ReturnType<typeof db> | AgentAccessTx;
|
|
29
|
+
|
|
30
|
+
export interface CreateAgentClientParams {
|
|
31
|
+
clientId: string;
|
|
32
|
+
name: string;
|
|
33
|
+
redirectUris: string[];
|
|
34
|
+
kind: 'registered' | 'cimd';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function createAgentClient(params: CreateAgentClientParams): Promise<AgentClient> {
|
|
38
|
+
return await db().transaction((tx) => createAgentClientTx(tx, params));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createAgentClientTx(
|
|
42
|
+
tx: AgentAccessTx,
|
|
43
|
+
params: CreateAgentClientParams,
|
|
44
|
+
): Promise<AgentClient> {
|
|
45
|
+
const rows = await tx
|
|
46
|
+
.insert(agentClients)
|
|
47
|
+
.values(params)
|
|
48
|
+
.onConflictDoUpdate({
|
|
49
|
+
target: agentClients.clientId,
|
|
50
|
+
set: {
|
|
51
|
+
lastSeenAt: sql`now()`,
|
|
52
|
+
unreferencedAt: null,
|
|
53
|
+
updatedAt: sql`now()`,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
.returning();
|
|
57
|
+
const row = rows[0];
|
|
58
|
+
if (!row) throw new Error('Upsert returned no rows');
|
|
59
|
+
return toAgentClient(row);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function findAgentClientByClientId(params: {
|
|
63
|
+
clientId: string;
|
|
64
|
+
executor?: AgentAccessExecutor;
|
|
65
|
+
}): Promise<AgentClient | undefined> {
|
|
66
|
+
const executor = params.executor ?? db();
|
|
67
|
+
const rows = await executor
|
|
68
|
+
.select()
|
|
69
|
+
.from(agentClients)
|
|
70
|
+
.where(eq(agentClients.clientId, params.clientId))
|
|
71
|
+
.limit(1);
|
|
72
|
+
const row = rows[0];
|
|
73
|
+
return row ? toAgentClient(row) : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function markAgentClientReferenced(
|
|
77
|
+
tx: AgentAccessTx,
|
|
78
|
+
params: {clientUuid: string},
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
await tx
|
|
81
|
+
.update(agentClients)
|
|
82
|
+
.set({unreferencedAt: null, lastSeenAt: sql`now()`, updatedAt: sql`now()`})
|
|
83
|
+
.where(eq(agentClients.id, params.clientUuid));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function markAgentClientUnreferenced(
|
|
87
|
+
tx: AgentAccessTx,
|
|
88
|
+
params: {clientUuid: string},
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
await tx
|
|
91
|
+
.update(agentClients)
|
|
92
|
+
.set({unreferencedAt: sql`now()`, updatedAt: sql`now()`})
|
|
93
|
+
.where(eq(agentClients.id, params.clientUuid));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CreateAgentAuthorizationRequestParams {
|
|
97
|
+
/** Internal `auth_agent_clients.id`, not the public OAuth `client_id`. */
|
|
98
|
+
clientId: string;
|
|
99
|
+
redirectUri: string;
|
|
100
|
+
resource: string;
|
|
101
|
+
scopes: string[];
|
|
102
|
+
codeChallenge: string;
|
|
103
|
+
state: string | null;
|
|
104
|
+
expiresAt: Date;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function createAgentAuthorizationRequest(
|
|
108
|
+
params: CreateAgentAuthorizationRequestParams,
|
|
109
|
+
): Promise<AgentAuthorizationRequest> {
|
|
110
|
+
return await db().transaction((tx) => createAgentAuthorizationRequestTx(tx, params));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function createAgentAuthorizationRequestTx(
|
|
114
|
+
tx: AgentAccessTx,
|
|
115
|
+
params: CreateAgentAuthorizationRequestParams,
|
|
116
|
+
): Promise<AgentAuthorizationRequest> {
|
|
117
|
+
const rows = await tx.insert(agentAuthorizationRequests).values(params).returning();
|
|
118
|
+
const row = rows[0];
|
|
119
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
120
|
+
return toAgentAuthorizationRequest(row);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function findPendingAgentAuthorizationRequest(params: {
|
|
124
|
+
id: string;
|
|
125
|
+
executor?: AgentAccessExecutor;
|
|
126
|
+
}): Promise<AgentAuthorizationRequest | undefined> {
|
|
127
|
+
const executor = params.executor ?? db();
|
|
128
|
+
const rows = await executor
|
|
129
|
+
.select()
|
|
130
|
+
.from(agentAuthorizationRequests)
|
|
131
|
+
.where(
|
|
132
|
+
and(
|
|
133
|
+
eq(agentAuthorizationRequests.id, params.id),
|
|
134
|
+
isNull(agentAuthorizationRequests.consumedAt),
|
|
135
|
+
gt(agentAuthorizationRequests.expiresAt, sql`now()`),
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
.limit(1);
|
|
139
|
+
const row = rows[0];
|
|
140
|
+
return row ? toAgentAuthorizationRequest(row) : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function consumeAgentAuthorizationRequest(params: {
|
|
144
|
+
id: string;
|
|
145
|
+
}): Promise<AgentAuthorizationRequest | undefined> {
|
|
146
|
+
return await db().transaction((tx) => consumeAgentAuthorizationRequestTx(tx, params));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Claims a pending request in the caller's transaction. The expiry and
|
|
151
|
+
* consumed-at predicates are part of the update so concurrent approval and
|
|
152
|
+
* denial can have only one winner.
|
|
153
|
+
*/
|
|
154
|
+
export async function consumeAgentAuthorizationRequestTx(
|
|
155
|
+
tx: AgentAccessTx,
|
|
156
|
+
params: {id: string},
|
|
157
|
+
): Promise<AgentAuthorizationRequest | undefined> {
|
|
158
|
+
const rows = await tx
|
|
159
|
+
.update(agentAuthorizationRequests)
|
|
160
|
+
.set({consumedAt: sql`now()`, updatedAt: sql`now()`})
|
|
161
|
+
.where(
|
|
162
|
+
and(
|
|
163
|
+
eq(agentAuthorizationRequests.id, params.id),
|
|
164
|
+
isNull(agentAuthorizationRequests.consumedAt),
|
|
165
|
+
gt(agentAuthorizationRequests.expiresAt, sql`now()`),
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
.returning();
|
|
169
|
+
const row = rows[0];
|
|
170
|
+
return row ? toAgentAuthorizationRequest(row) : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface CreateAgentGrantParams {
|
|
174
|
+
userId: string;
|
|
175
|
+
workspaceId: string;
|
|
176
|
+
/** Internal `auth_agent_clients.id`, not the public OAuth `client_id`. */
|
|
177
|
+
clientId: string;
|
|
178
|
+
scopes: string[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export async function createAgentGrant(params: CreateAgentGrantParams): Promise<AgentGrant> {
|
|
182
|
+
return await db().transaction((tx) => createAgentGrantTx(tx, params));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function createAgentGrantTx(
|
|
186
|
+
tx: AgentAccessTx,
|
|
187
|
+
params: CreateAgentGrantParams,
|
|
188
|
+
): Promise<AgentGrant> {
|
|
189
|
+
const rows = await tx
|
|
190
|
+
.insert(agentGrants)
|
|
191
|
+
.values(params)
|
|
192
|
+
.onConflictDoUpdate({
|
|
193
|
+
target: [agentGrants.userId, agentGrants.workspaceId, agentGrants.clientId],
|
|
194
|
+
targetWhere: sql`${agentGrants.revokedAt} IS NULL AND ${agentGrants.terminalAt} IS NULL`,
|
|
195
|
+
set: {
|
|
196
|
+
scopes: params.scopes,
|
|
197
|
+
updatedAt: sql`now()`,
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
.returning();
|
|
201
|
+
const row = rows[0];
|
|
202
|
+
if (!row) throw new Error('Upsert returned no rows');
|
|
203
|
+
|
|
204
|
+
// Refresh tokens derive their scopes from the grant. Reauthorization must
|
|
205
|
+
// invalidate existing tokens before the grant's scopes can be widened.
|
|
206
|
+
await tx
|
|
207
|
+
.update(agentRefreshTokens)
|
|
208
|
+
.set({revokedAt: sql`now()`, updatedAt: sql`now()`})
|
|
209
|
+
.where(
|
|
210
|
+
and(
|
|
211
|
+
eq(agentRefreshTokens.grantId, row.id),
|
|
212
|
+
isNull(agentRefreshTokens.rotatedAt),
|
|
213
|
+
isNull(agentRefreshTokens.revokedAt),
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await markAgentClientReferenced(tx, {clientUuid: params.clientId});
|
|
218
|
+
return toAgentGrant(row);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function findAgentGrant(params: {
|
|
222
|
+
id: string;
|
|
223
|
+
executor?: AgentAccessExecutor;
|
|
224
|
+
}): Promise<AgentGrant | undefined> {
|
|
225
|
+
const executor = params.executor ?? db();
|
|
226
|
+
const rows = await executor
|
|
227
|
+
.select()
|
|
228
|
+
.from(agentGrants)
|
|
229
|
+
.where(eq(agentGrants.id, params.id))
|
|
230
|
+
.limit(1);
|
|
231
|
+
const row = rows[0];
|
|
232
|
+
return row ? toAgentGrant(row) : undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Locks one grant row until the surrounding transaction ends. Code exchange
|
|
237
|
+
* and lifecycle transitions must use this same primitive before inspecting or
|
|
238
|
+
* changing grant state. Callers must keep the transaction database-only while
|
|
239
|
+
* this lock is held; external I/O must happen after commit.
|
|
240
|
+
*/
|
|
241
|
+
export async function lockAgentGrant(
|
|
242
|
+
tx: AgentAccessTx,
|
|
243
|
+
params: {grantId: string},
|
|
244
|
+
): Promise<AgentGrant | undefined> {
|
|
245
|
+
const rows = await tx
|
|
246
|
+
.select()
|
|
247
|
+
.from(agentGrants)
|
|
248
|
+
.where(eq(agentGrants.id, params.grantId))
|
|
249
|
+
.for('update')
|
|
250
|
+
.limit(1);
|
|
251
|
+
const row = rows[0];
|
|
252
|
+
return row ? toAgentGrant(row) : undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface CreateAgentAuthorizationCodeParams {
|
|
256
|
+
grantId: string;
|
|
257
|
+
hashedCode: string;
|
|
258
|
+
codeChallenge: string;
|
|
259
|
+
redirectUri: string;
|
|
260
|
+
resource: string;
|
|
261
|
+
expiresAt: Date;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function createAgentAuthorizationCode(
|
|
265
|
+
params: CreateAgentAuthorizationCodeParams,
|
|
266
|
+
): Promise<AgentAuthorizationCode> {
|
|
267
|
+
return await db().transaction((tx) => createAgentAuthorizationCodeTx(tx, params));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function createAgentAuthorizationCodeTx(
|
|
271
|
+
tx: AgentAccessTx,
|
|
272
|
+
params: CreateAgentAuthorizationCodeParams,
|
|
273
|
+
): Promise<AgentAuthorizationCode> {
|
|
274
|
+
const rows = await tx.insert(agentAuthorizationCodes).values(params).returning();
|
|
275
|
+
const row = rows[0];
|
|
276
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
277
|
+
return toAgentAuthorizationCode(row);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function findAgentAuthorizationCodeByHash(params: {
|
|
281
|
+
hashedCode: string;
|
|
282
|
+
executor?: AgentAccessExecutor;
|
|
283
|
+
}): Promise<AgentAuthorizationCode | undefined> {
|
|
284
|
+
const executor = params.executor ?? db();
|
|
285
|
+
const rows = await executor
|
|
286
|
+
.select()
|
|
287
|
+
.from(agentAuthorizationCodes)
|
|
288
|
+
.where(eq(agentAuthorizationCodes.hashedCode, params.hashedCode))
|
|
289
|
+
.limit(1);
|
|
290
|
+
const row = rows[0];
|
|
291
|
+
return row ? toAgentAuthorizationCode(row) : undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function findActiveAgentAuthorizationCodeByHash(params: {
|
|
295
|
+
hashedCode: string;
|
|
296
|
+
executor?: AgentAccessExecutor;
|
|
297
|
+
}): Promise<AgentAuthorizationCode | undefined> {
|
|
298
|
+
const executor = params.executor ?? db();
|
|
299
|
+
const rows = await executor
|
|
300
|
+
.select()
|
|
301
|
+
.from(agentAuthorizationCodes)
|
|
302
|
+
.where(
|
|
303
|
+
and(
|
|
304
|
+
eq(agentAuthorizationCodes.hashedCode, params.hashedCode),
|
|
305
|
+
isNull(agentAuthorizationCodes.consumedAt),
|
|
306
|
+
gt(agentAuthorizationCodes.expiresAt, sql`now()`),
|
|
307
|
+
),
|
|
308
|
+
)
|
|
309
|
+
.limit(1);
|
|
310
|
+
const row = rows[0];
|
|
311
|
+
return row ? toAgentAuthorizationCode(row) : undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function consumeAgentAuthorizationCode(params: {
|
|
315
|
+
hashedCode: string;
|
|
316
|
+
}): Promise<AgentAuthorizationCode | undefined> {
|
|
317
|
+
return await db().transaction((tx) => consumeAgentAuthorizationCodeTx(tx, params));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Claims an authorization code exactly once, including its 60-second TTL. */
|
|
321
|
+
export async function consumeAgentAuthorizationCodeTx(
|
|
322
|
+
tx: AgentAccessTx,
|
|
323
|
+
params: {hashedCode: string},
|
|
324
|
+
): Promise<AgentAuthorizationCode | undefined> {
|
|
325
|
+
const rows = await tx
|
|
326
|
+
.update(agentAuthorizationCodes)
|
|
327
|
+
.set({consumedAt: sql`now()`, updatedAt: sql`now()`})
|
|
328
|
+
.where(
|
|
329
|
+
and(
|
|
330
|
+
eq(agentAuthorizationCodes.hashedCode, params.hashedCode),
|
|
331
|
+
isNull(agentAuthorizationCodes.consumedAt),
|
|
332
|
+
gt(agentAuthorizationCodes.expiresAt, sql`now()`),
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
.returning();
|
|
336
|
+
const row = rows[0];
|
|
337
|
+
return row ? toAgentAuthorizationCode(row) : undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface CreateAgentRefreshTokenParams {
|
|
341
|
+
grantId: string;
|
|
342
|
+
hashedToken: string;
|
|
343
|
+
expiresAt: Date;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export async function createAgentRefreshToken(
|
|
347
|
+
params: CreateAgentRefreshTokenParams,
|
|
348
|
+
): Promise<AgentRefreshToken> {
|
|
349
|
+
return await db().transaction((tx) => createAgentRefreshTokenTx(tx, params));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function createAgentRefreshTokenTx(
|
|
353
|
+
tx: AgentAccessTx,
|
|
354
|
+
params: CreateAgentRefreshTokenParams,
|
|
355
|
+
): Promise<AgentRefreshToken> {
|
|
356
|
+
const rows = await tx.insert(agentRefreshTokens).values(params).returning();
|
|
357
|
+
const row = rows[0];
|
|
358
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
359
|
+
return toAgentRefreshToken(row);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function findAgentRefreshTokenByHash(params: {
|
|
363
|
+
hashedToken: string;
|
|
364
|
+
executor?: AgentAccessExecutor;
|
|
365
|
+
}): Promise<AgentRefreshToken | undefined> {
|
|
366
|
+
const executor = params.executor ?? db();
|
|
367
|
+
const rows = await executor
|
|
368
|
+
.select()
|
|
369
|
+
.from(agentRefreshTokens)
|
|
370
|
+
.where(eq(agentRefreshTokens.hashedToken, params.hashedToken))
|
|
371
|
+
.limit(1);
|
|
372
|
+
const row = rows[0];
|
|
373
|
+
return row ? toAgentRefreshToken(row) : undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function findActiveAgentRefreshTokenByHash(params: {
|
|
377
|
+
hashedToken: string;
|
|
378
|
+
executor?: AgentAccessExecutor;
|
|
379
|
+
}): Promise<AgentRefreshToken | undefined> {
|
|
380
|
+
const executor = params.executor ?? db();
|
|
381
|
+
const rows = await executor
|
|
382
|
+
.select()
|
|
383
|
+
.from(agentRefreshTokens)
|
|
384
|
+
.where(
|
|
385
|
+
and(
|
|
386
|
+
eq(agentRefreshTokens.hashedToken, params.hashedToken),
|
|
387
|
+
isNull(agentRefreshTokens.rotatedAt),
|
|
388
|
+
isNull(agentRefreshTokens.revokedAt),
|
|
389
|
+
gt(agentRefreshTokens.expiresAt, sql`now()`),
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
.limit(1);
|
|
393
|
+
const row = rows[0];
|
|
394
|
+
return row ? toAgentRefreshToken(row) : undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Rotates a refresh token atomically; a concurrent rotation cannot win twice. */
|
|
398
|
+
export async function rotateAgentRefreshToken(params: {
|
|
399
|
+
hashedToken: string;
|
|
400
|
+
replacementHashedToken: string;
|
|
401
|
+
replacementExpiresAt: Date;
|
|
402
|
+
}): Promise<AgentRefreshToken | undefined> {
|
|
403
|
+
return await db().transaction((tx) => rotateAgentRefreshTokenTx(tx, params));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function rotateAgentRefreshTokenTx(
|
|
407
|
+
tx: AgentAccessTx,
|
|
408
|
+
params: {
|
|
409
|
+
hashedToken: string;
|
|
410
|
+
replacementHashedToken: string;
|
|
411
|
+
replacementExpiresAt: Date;
|
|
412
|
+
},
|
|
413
|
+
): Promise<AgentRefreshToken | undefined> {
|
|
414
|
+
const rows = await tx
|
|
415
|
+
.update(agentRefreshTokens)
|
|
416
|
+
.set({rotatedAt: sql`now()`, lastUsedAt: sql`now()`, updatedAt: sql`now()`})
|
|
417
|
+
.where(
|
|
418
|
+
and(
|
|
419
|
+
eq(agentRefreshTokens.hashedToken, params.hashedToken),
|
|
420
|
+
isNull(agentRefreshTokens.rotatedAt),
|
|
421
|
+
isNull(agentRefreshTokens.revokedAt),
|
|
422
|
+
gt(agentRefreshTokens.expiresAt, sql`now()`),
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
.returning();
|
|
426
|
+
const row = rows[0];
|
|
427
|
+
if (!row) return undefined;
|
|
428
|
+
|
|
429
|
+
return await createAgentRefreshTokenTx(tx, {
|
|
430
|
+
grantId: row.grantId,
|
|
431
|
+
hashedToken: params.replacementHashedToken,
|
|
432
|
+
expiresAt: params.replacementExpiresAt,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export async function revokeAgentRefreshToken(params: {
|
|
437
|
+
id: string;
|
|
438
|
+
}): Promise<AgentRefreshToken | undefined> {
|
|
439
|
+
return await db().transaction(async (tx) => {
|
|
440
|
+
const rows = await tx
|
|
441
|
+
.update(agentRefreshTokens)
|
|
442
|
+
.set({revokedAt: sql`now()`, updatedAt: sql`now()`})
|
|
443
|
+
.where(and(eq(agentRefreshTokens.id, params.id), isNull(agentRefreshTokens.revokedAt)))
|
|
444
|
+
.returning();
|
|
445
|
+
const row = rows[0];
|
|
446
|
+
return row ? toAgentRefreshToken(row) : undefined;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export interface CreateAgentPersonalAccessTokenParams {
|
|
451
|
+
userId: string;
|
|
452
|
+
workspaceId: string;
|
|
453
|
+
hashedToken: string;
|
|
454
|
+
prefix: string;
|
|
455
|
+
name: string;
|
|
456
|
+
scopes: string[];
|
|
457
|
+
expiresAt: Date;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function createAgentPersonalAccessToken(
|
|
461
|
+
params: CreateAgentPersonalAccessTokenParams,
|
|
462
|
+
): Promise<AgentPersonalAccessToken> {
|
|
463
|
+
return await db().transaction((tx) => createAgentPersonalAccessTokenTx(tx, params));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export async function createAgentPersonalAccessTokenTx(
|
|
467
|
+
tx: AgentAccessTx,
|
|
468
|
+
params: CreateAgentPersonalAccessTokenParams,
|
|
469
|
+
): Promise<AgentPersonalAccessToken> {
|
|
470
|
+
const rows = await tx.insert(agentPersonalAccessTokens).values(params).returning();
|
|
471
|
+
const row = rows[0];
|
|
472
|
+
if (!row) throw new Error('Insert returned no rows');
|
|
473
|
+
return toAgentPersonalAccessToken(row);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export async function findAgentPersonalAccessTokenByHash(params: {
|
|
477
|
+
hashedToken: string;
|
|
478
|
+
executor?: AgentAccessExecutor;
|
|
479
|
+
}): Promise<AgentPersonalAccessToken | undefined> {
|
|
480
|
+
const executor = params.executor ?? db();
|
|
481
|
+
const rows = await executor
|
|
482
|
+
.select()
|
|
483
|
+
.from(agentPersonalAccessTokens)
|
|
484
|
+
.where(eq(agentPersonalAccessTokens.hashedToken, params.hashedToken))
|
|
485
|
+
.limit(1);
|
|
486
|
+
const row = rows[0];
|
|
487
|
+
return row ? toAgentPersonalAccessToken(row) : undefined;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export async function findActiveAgentPersonalAccessTokenByHash(params: {
|
|
491
|
+
hashedToken: string;
|
|
492
|
+
executor?: AgentAccessExecutor;
|
|
493
|
+
}): Promise<AgentPersonalAccessToken | undefined> {
|
|
494
|
+
const executor = params.executor ?? db();
|
|
495
|
+
const rows = await executor
|
|
496
|
+
.select()
|
|
497
|
+
.from(agentPersonalAccessTokens)
|
|
498
|
+
.where(
|
|
499
|
+
and(
|
|
500
|
+
eq(agentPersonalAccessTokens.hashedToken, params.hashedToken),
|
|
501
|
+
isNull(agentPersonalAccessTokens.revokedAt),
|
|
502
|
+
gt(agentPersonalAccessTokens.expiresAt, sql`now()`),
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
.limit(1);
|
|
506
|
+
const row = rows[0];
|
|
507
|
+
return row ? toAgentPersonalAccessToken(row) : undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function markAgentPersonalAccessTokenUsed(params: {
|
|
511
|
+
id: string;
|
|
512
|
+
}): Promise<AgentPersonalAccessToken | undefined> {
|
|
513
|
+
const rows = await db()
|
|
514
|
+
.update(agentPersonalAccessTokens)
|
|
515
|
+
.set({lastUsedAt: sql`now()`, updatedAt: sql`now()`})
|
|
516
|
+
.where(eq(agentPersonalAccessTokens.id, params.id))
|
|
517
|
+
.returning();
|
|
518
|
+
const row = rows[0];
|
|
519
|
+
return row ? toAgentPersonalAccessToken(row) : undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function revokeAgentPersonalAccessToken(params: {
|
|
523
|
+
id: string;
|
|
524
|
+
}): Promise<AgentPersonalAccessToken | undefined> {
|
|
525
|
+
const rows = await db()
|
|
526
|
+
.update(agentPersonalAccessTokens)
|
|
527
|
+
.set({revokedAt: sql`now()`, updatedAt: sql`now()`})
|
|
528
|
+
.where(
|
|
529
|
+
and(eq(agentPersonalAccessTokens.id, params.id), isNull(agentPersonalAccessTokens.revokedAt)),
|
|
530
|
+
)
|
|
531
|
+
.returning();
|
|
532
|
+
const row = rows[0];
|
|
533
|
+
return row ? toAgentPersonalAccessToken(row) : undefined;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export interface PruneAgentAccessParams {
|
|
537
|
+
retentionDays?: number;
|
|
538
|
+
limit?: number;
|
|
539
|
+
now?: Date;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** Deletes terminal credentials and ephemeral authorization rows in bounded batches. */
|
|
543
|
+
export async function pruneAgentAccess(params: PruneAgentAccessParams = {}): Promise<number> {
|
|
544
|
+
const now = params.now ?? new Date();
|
|
545
|
+
const retentionDays = params.retentionDays ?? 7;
|
|
546
|
+
const limit = params.limit ?? 1000;
|
|
547
|
+
const cutoff = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000);
|
|
548
|
+
|
|
549
|
+
return await db().transaction(async (tx) => {
|
|
550
|
+
const requestIds = tx
|
|
551
|
+
.select({id: agentAuthorizationRequests.id})
|
|
552
|
+
.from(agentAuthorizationRequests)
|
|
553
|
+
.where(
|
|
554
|
+
or(
|
|
555
|
+
lt(agentAuthorizationRequests.consumedAt, cutoff),
|
|
556
|
+
and(
|
|
557
|
+
isNull(agentAuthorizationRequests.consumedAt),
|
|
558
|
+
lt(agentAuthorizationRequests.expiresAt, cutoff),
|
|
559
|
+
),
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
.orderBy(asc(agentAuthorizationRequests.expiresAt), asc(agentAuthorizationRequests.id))
|
|
563
|
+
.limit(limit);
|
|
564
|
+
const codeIds = tx
|
|
565
|
+
.select({id: agentAuthorizationCodes.id})
|
|
566
|
+
.from(agentAuthorizationCodes)
|
|
567
|
+
.where(
|
|
568
|
+
or(
|
|
569
|
+
lt(agentAuthorizationCodes.consumedAt, cutoff),
|
|
570
|
+
and(
|
|
571
|
+
isNull(agentAuthorizationCodes.consumedAt),
|
|
572
|
+
lt(agentAuthorizationCodes.expiresAt, cutoff),
|
|
573
|
+
),
|
|
574
|
+
),
|
|
575
|
+
)
|
|
576
|
+
.orderBy(asc(agentAuthorizationCodes.expiresAt), asc(agentAuthorizationCodes.id))
|
|
577
|
+
.limit(limit);
|
|
578
|
+
const refreshTokenIds = tx
|
|
579
|
+
.select({id: agentRefreshTokens.id})
|
|
580
|
+
.from(agentRefreshTokens)
|
|
581
|
+
.where(
|
|
582
|
+
or(
|
|
583
|
+
lt(agentRefreshTokens.rotatedAt, cutoff),
|
|
584
|
+
and(isNull(agentRefreshTokens.rotatedAt), lt(agentRefreshTokens.revokedAt, cutoff)),
|
|
585
|
+
and(
|
|
586
|
+
isNull(agentRefreshTokens.rotatedAt),
|
|
587
|
+
isNull(agentRefreshTokens.revokedAt),
|
|
588
|
+
lt(agentRefreshTokens.expiresAt, cutoff),
|
|
589
|
+
),
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
.orderBy(asc(agentRefreshTokens.expiresAt), asc(agentRefreshTokens.id))
|
|
593
|
+
.limit(limit);
|
|
594
|
+
const patIds = tx
|
|
595
|
+
.select({id: agentPersonalAccessTokens.id})
|
|
596
|
+
.from(agentPersonalAccessTokens)
|
|
597
|
+
.where(
|
|
598
|
+
or(
|
|
599
|
+
lt(agentPersonalAccessTokens.revokedAt, cutoff),
|
|
600
|
+
and(
|
|
601
|
+
isNull(agentPersonalAccessTokens.revokedAt),
|
|
602
|
+
lt(agentPersonalAccessTokens.expiresAt, cutoff),
|
|
603
|
+
),
|
|
604
|
+
),
|
|
605
|
+
)
|
|
606
|
+
.orderBy(asc(agentPersonalAccessTokens.expiresAt), asc(agentPersonalAccessTokens.id))
|
|
607
|
+
.limit(limit);
|
|
608
|
+
|
|
609
|
+
const [requests, codes, refreshTokens, pats] = await Promise.all([
|
|
610
|
+
tx
|
|
611
|
+
.delete(agentAuthorizationRequests)
|
|
612
|
+
.where(inArray(agentAuthorizationRequests.id, requestIds))
|
|
613
|
+
.returning({id: agentAuthorizationRequests.id}),
|
|
614
|
+
tx
|
|
615
|
+
.delete(agentAuthorizationCodes)
|
|
616
|
+
.where(inArray(agentAuthorizationCodes.id, codeIds))
|
|
617
|
+
.returning({id: agentAuthorizationCodes.id}),
|
|
618
|
+
tx
|
|
619
|
+
.delete(agentRefreshTokens)
|
|
620
|
+
.where(inArray(agentRefreshTokens.id, refreshTokenIds))
|
|
621
|
+
.returning({id: agentRefreshTokens.id}),
|
|
622
|
+
tx
|
|
623
|
+
.delete(agentPersonalAccessTokens)
|
|
624
|
+
.where(inArray(agentPersonalAccessTokens.id, patIds))
|
|
625
|
+
.returning({id: agentPersonalAccessTokens.id}),
|
|
626
|
+
]);
|
|
627
|
+
return requests.length + codes.length + refreshTokens.length + pats.length;
|
|
628
|
+
});
|
|
629
|
+
}
|
package/src/db/db.ts
CHANGED
|
@@ -2,6 +2,14 @@ import {drizzle, type NodePgDatabase} from '@shipfox/node-drizzle';
|
|
|
2
2
|
import {pgClient} from '@shipfox/node-postgres';
|
|
3
3
|
import {adminCommandResults} from './schema/admin-command-results.js';
|
|
4
4
|
import {adminGrants} from './schema/admin-grants.js';
|
|
5
|
+
import {
|
|
6
|
+
agentAuthorizationCodes,
|
|
7
|
+
agentAuthorizationRequests,
|
|
8
|
+
agentClients,
|
|
9
|
+
agentGrants,
|
|
10
|
+
agentPersonalAccessTokens,
|
|
11
|
+
agentRefreshTokens,
|
|
12
|
+
} from './schema/agent-access.js';
|
|
5
13
|
import {authOutbox} from './schema/outbox.js';
|
|
6
14
|
import {passwordResets} from './schema/password-resets.js';
|
|
7
15
|
import {authRateLimits} from './schema/rate-limits.js';
|
|
@@ -10,6 +18,12 @@ import {users} from './schema/users.js';
|
|
|
10
18
|
|
|
11
19
|
export const schema = {
|
|
12
20
|
adminGrants,
|
|
21
|
+
agentClients,
|
|
22
|
+
agentAuthorizationRequests,
|
|
23
|
+
agentGrants,
|
|
24
|
+
agentAuthorizationCodes,
|
|
25
|
+
agentRefreshTokens,
|
|
26
|
+
agentPersonalAccessTokens,
|
|
13
27
|
adminCommandResults,
|
|
14
28
|
users,
|
|
15
29
|
passwordResets,
|