@intx/hub-api 0.1.2
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/README.md +29 -0
- package/package.json +28 -0
- package/src/app.test.ts +225 -0
- package/src/app.ts +382 -0
- package/src/auth.ts +21 -0
- package/src/context.ts +38 -0
- package/src/format.ts +9 -0
- package/src/git-http/advertise-refs.test.ts +459 -0
- package/src/git-http/advertise-refs.ts +226 -0
- package/src/git-http/pkt-line.test.ts +220 -0
- package/src/git-http/pkt-line.ts +235 -0
- package/src/git-http/receive-pack.test.ts +397 -0
- package/src/git-http/receive-pack.ts +261 -0
- package/src/git-http/side-band-64k.test.ts +181 -0
- package/src/git-http/side-band-64k.ts +134 -0
- package/src/git-http/upload-pack.test.ts +545 -0
- package/src/git-http/upload-pack.ts +396 -0
- package/src/index.ts +23 -0
- package/src/middleware/git-token-auth.test.ts +587 -0
- package/src/middleware/git-token-auth.ts +315 -0
- package/src/middleware/grant.ts +106 -0
- package/src/middleware/session.ts +13 -0
- package/src/middleware/tenant.test.ts +192 -0
- package/src/middleware/tenant.ts +101 -0
- package/src/openapi.ts +66 -0
- package/src/pagination.ts +117 -0
- package/src/routes/agent-data.ts +179 -0
- package/src/routes/agent-state-git.ts +562 -0
- package/src/routes/agents.test.ts +337 -0
- package/src/routes/agents.ts +704 -0
- package/src/routes/approvals.ts +130 -0
- package/src/routes/assets.test.ts +567 -0
- package/src/routes/assets.ts +592 -0
- package/src/routes/credentials.ts +435 -0
- package/src/routes/git-tokens.test.ts +709 -0
- package/src/routes/git-tokens.ts +771 -0
- package/src/routes/grants.ts +509 -0
- package/src/routes/instances.test.ts +1103 -0
- package/src/routes/instances.ts +1797 -0
- package/src/routes/me.ts +405 -0
- package/src/routes/oauth-clients.ts +349 -0
- package/src/routes/observability.ts +146 -0
- package/src/routes/offerings.ts +382 -0
- package/src/routes/principals.ts +515 -0
- package/src/routes/providers.ts +351 -0
- package/src/routes/roles.ts +452 -0
- package/src/routes/sidecars.ts +221 -0
- package/src/routes/tenant-federation.ts +225 -0
- package/src/routes/tenants.ts +369 -0
- package/src/routes/wallets.ts +370 -0
- package/src/session.ts +44 -0
- package/src/timeline-reconstruction.test.ts +786 -0
- package/src/timeline-reconstruction.ts +383 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { eq, and, isNull } from "drizzle-orm";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
4
|
+
|
|
5
|
+
import { credential, provider } from "@intx/db/schema";
|
|
6
|
+
import {
|
|
7
|
+
getAncestorChain,
|
|
8
|
+
resolveCredentialByName,
|
|
9
|
+
parseCredentialRow,
|
|
10
|
+
} from "@intx/db";
|
|
11
|
+
import type { DB } from "@intx/db";
|
|
12
|
+
import {
|
|
13
|
+
CreateCredential,
|
|
14
|
+
UpdateCredential,
|
|
15
|
+
CredentialResponse,
|
|
16
|
+
ErrorResponse,
|
|
17
|
+
paginatedSchema,
|
|
18
|
+
} from "@intx/types";
|
|
19
|
+
|
|
20
|
+
import type { TenantEnv } from "../context";
|
|
21
|
+
import { first, ts } from "../format";
|
|
22
|
+
import { generateId } from "@intx/hub-common";
|
|
23
|
+
import { idResource } from "../middleware/grant";
|
|
24
|
+
import type { RequireGrant } from "../middleware/grant";
|
|
25
|
+
import {
|
|
26
|
+
parsePageParams,
|
|
27
|
+
cursorCondition,
|
|
28
|
+
pageOrder,
|
|
29
|
+
paginatedResponse,
|
|
30
|
+
pageParameters,
|
|
31
|
+
} from "../pagination";
|
|
32
|
+
import { pushSourceUpdates, type SidecarRouter } from "@intx/hub-sessions";
|
|
33
|
+
|
|
34
|
+
function formatCredential(row: typeof credential.$inferSelect) {
|
|
35
|
+
const parsed = parseCredentialRow(row);
|
|
36
|
+
return {
|
|
37
|
+
id: parsed.id,
|
|
38
|
+
tenantId: parsed.tenantId,
|
|
39
|
+
providerId: parsed.providerId,
|
|
40
|
+
principalId: parsed.principalId ?? null,
|
|
41
|
+
oauthClientId: parsed.oauthClientId ?? null,
|
|
42
|
+
name: parsed.name,
|
|
43
|
+
type: parsed.type,
|
|
44
|
+
description: parsed.description ?? null,
|
|
45
|
+
scopes: parsed.scopes ?? null,
|
|
46
|
+
expiresAt: parsed.expiresAt ? ts(parsed.expiresAt) : null,
|
|
47
|
+
status: parsed.status,
|
|
48
|
+
metadata: parsed.metadata,
|
|
49
|
+
createdAt: ts(parsed.createdAt),
|
|
50
|
+
updatedAt: ts(parsed.updatedAt),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type CreateCredentialRoutesDeps = {
|
|
55
|
+
db: DB["db"];
|
|
56
|
+
sidecarRouter: SidecarRouter;
|
|
57
|
+
requireGrant: RequireGrant;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function createCredentialRoutes({
|
|
61
|
+
db,
|
|
62
|
+
sidecarRouter,
|
|
63
|
+
requireGrant,
|
|
64
|
+
}: CreateCredentialRoutesDeps): Hono<TenantEnv> {
|
|
65
|
+
const app = new Hono<TenantEnv>();
|
|
66
|
+
|
|
67
|
+
app.get(
|
|
68
|
+
"/",
|
|
69
|
+
requireGrant("credential:*", "read"),
|
|
70
|
+
describeRoute({
|
|
71
|
+
tags: ["Credentials"],
|
|
72
|
+
summary: "List credentials",
|
|
73
|
+
description:
|
|
74
|
+
"Lists credential metadata. Secrets are never returned. Filterable by owner type.",
|
|
75
|
+
parameters: [
|
|
76
|
+
{
|
|
77
|
+
name: "owner",
|
|
78
|
+
in: "query",
|
|
79
|
+
schema: { type: "string", enum: ["me", "org", "all"] },
|
|
80
|
+
},
|
|
81
|
+
...pageParameters,
|
|
82
|
+
],
|
|
83
|
+
responses: {
|
|
84
|
+
200: {
|
|
85
|
+
description: "List of credentials",
|
|
86
|
+
content: {
|
|
87
|
+
"application/json": {
|
|
88
|
+
schema: resolver(paginatedSchema(CredentialResponse)),
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
async (c) => {
|
|
95
|
+
const tenantCtx = c.get("tenant");
|
|
96
|
+
const principalCtx = c.get("principal");
|
|
97
|
+
const owner = c.req.query("owner") ?? "all";
|
|
98
|
+
const { limit, cursor } = parsePageParams({
|
|
99
|
+
cursor: c.req.query("cursor"),
|
|
100
|
+
limit: c.req.query("limit"),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const conditions = [eq(credential.tenantId, tenantCtx.id)];
|
|
104
|
+
|
|
105
|
+
if (owner === "me") {
|
|
106
|
+
conditions.push(eq(credential.principalId, principalCtx.id));
|
|
107
|
+
} else if (owner === "org") {
|
|
108
|
+
conditions.push(isNull(credential.principalId));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (cursor) {
|
|
112
|
+
conditions.push(
|
|
113
|
+
cursorCondition(credential.createdAt, credential.id, cursor),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rows = await db.query.credential.findMany({
|
|
118
|
+
where: and(...conditions),
|
|
119
|
+
orderBy: pageOrder(credential.createdAt, credential.id),
|
|
120
|
+
limit,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return c.json(paginatedResponse(rows.map(formatCredential), rows, limit));
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
app.post(
|
|
128
|
+
"/",
|
|
129
|
+
requireGrant("credential:*", "create"),
|
|
130
|
+
describeRoute({
|
|
131
|
+
tags: ["Credentials"],
|
|
132
|
+
summary: "Store a credential",
|
|
133
|
+
description:
|
|
134
|
+
"Stores a credential (API key, OAuth token, etc.). The secret is stored securely and never returned in subsequent reads. A provider must be specified.",
|
|
135
|
+
responses: {
|
|
136
|
+
201: {
|
|
137
|
+
description: "Credential stored",
|
|
138
|
+
content: {
|
|
139
|
+
"application/json": { schema: resolver(CredentialResponse) },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
400: {
|
|
143
|
+
description: "Validation error",
|
|
144
|
+
content: {
|
|
145
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
404: {
|
|
149
|
+
description: "Provider not found",
|
|
150
|
+
content: {
|
|
151
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
409: {
|
|
155
|
+
description: "Credential name already exists in this tenant",
|
|
156
|
+
content: {
|
|
157
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
validator("json", CreateCredential),
|
|
163
|
+
async (c) => {
|
|
164
|
+
const tenantCtx = c.get("tenant");
|
|
165
|
+
const body = c.req.valid("json");
|
|
166
|
+
|
|
167
|
+
const providerRow = await db.query.provider.findFirst({
|
|
168
|
+
where: eq(provider.id, body.providerId),
|
|
169
|
+
});
|
|
170
|
+
if (!providerRow) {
|
|
171
|
+
return c.json(
|
|
172
|
+
{ error: { code: "not_found", message: "Provider not found" } },
|
|
173
|
+
404,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const chain = await getAncestorChain(db, tenantCtx.id);
|
|
178
|
+
if (!chain.includes(providerRow.tenantId)) {
|
|
179
|
+
return c.json(
|
|
180
|
+
{ error: { code: "not_found", message: "Provider not found" } },
|
|
181
|
+
404,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const existing = await db.query.credential.findFirst({
|
|
186
|
+
where: and(
|
|
187
|
+
eq(credential.tenantId, tenantCtx.id),
|
|
188
|
+
eq(credential.name, body.name),
|
|
189
|
+
),
|
|
190
|
+
});
|
|
191
|
+
if (existing) {
|
|
192
|
+
return c.json(
|
|
193
|
+
{
|
|
194
|
+
error: {
|
|
195
|
+
code: "conflict",
|
|
196
|
+
message: "Credential name already exists in this tenant",
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
409,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const now = new Date();
|
|
204
|
+
const row = first(
|
|
205
|
+
await db
|
|
206
|
+
.insert(credential)
|
|
207
|
+
.values({
|
|
208
|
+
id: generateId("credential"),
|
|
209
|
+
tenantId: tenantCtx.id,
|
|
210
|
+
providerId: body.providerId,
|
|
211
|
+
principalId: body.principalId ?? null,
|
|
212
|
+
oauthClientId: body.oauthClientId ?? null,
|
|
213
|
+
name: body.name,
|
|
214
|
+
type: body.type,
|
|
215
|
+
description: body.description ?? null,
|
|
216
|
+
secret: body.secret,
|
|
217
|
+
refreshSecret: body.refreshSecret ?? null,
|
|
218
|
+
scopes: body.scopes ?? null,
|
|
219
|
+
expiresAt: body.expiresAt ? new Date(body.expiresAt) : null,
|
|
220
|
+
metadata: body.metadata ?? null,
|
|
221
|
+
createdAt: now,
|
|
222
|
+
updatedAt: now,
|
|
223
|
+
})
|
|
224
|
+
.returning(),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return c.json(formatCredential(row), 201);
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
app.get(
|
|
232
|
+
"/resolve/:name",
|
|
233
|
+
requireGrant("credential:*", "read"),
|
|
234
|
+
describeRoute({
|
|
235
|
+
tags: ["Credentials"],
|
|
236
|
+
summary: "Resolve a credential by name",
|
|
237
|
+
description:
|
|
238
|
+
"Resolves a credential by name, walking the tenant hierarchy. Returns metadata only (no secret). Useful for discovering which credential an agent would get.",
|
|
239
|
+
responses: {
|
|
240
|
+
200: {
|
|
241
|
+
description: "Credential metadata",
|
|
242
|
+
content: {
|
|
243
|
+
"application/json": { schema: resolver(CredentialResponse) },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
404: {
|
|
247
|
+
description: "Credential not found",
|
|
248
|
+
content: {
|
|
249
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
async (c) => {
|
|
255
|
+
const tenantCtx = c.get("tenant");
|
|
256
|
+
const name = c.req.param("name");
|
|
257
|
+
|
|
258
|
+
const row = await resolveCredentialByName(db, tenantCtx.id, name);
|
|
259
|
+
|
|
260
|
+
if (!row) {
|
|
261
|
+
return c.json(
|
|
262
|
+
{ error: { code: "not_found", message: "Credential not found" } },
|
|
263
|
+
404,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return c.json(formatCredential(row));
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
app.get(
|
|
272
|
+
"/:credentialId",
|
|
273
|
+
requireGrant(idResource("credential", "credentialId"), "read"),
|
|
274
|
+
describeRoute({
|
|
275
|
+
tags: ["Credentials"],
|
|
276
|
+
summary: "Get credential metadata",
|
|
277
|
+
description:
|
|
278
|
+
"Returns credential metadata. The secret is never included. Supports hierarchy-aware access.",
|
|
279
|
+
responses: {
|
|
280
|
+
200: {
|
|
281
|
+
description: "Credential metadata",
|
|
282
|
+
content: {
|
|
283
|
+
"application/json": { schema: resolver(CredentialResponse) },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
404: {
|
|
287
|
+
description: "Credential not found",
|
|
288
|
+
content: {
|
|
289
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
async (c) => {
|
|
295
|
+
const tenantCtx = c.get("tenant");
|
|
296
|
+
const credentialId = c.req.param("credentialId");
|
|
297
|
+
|
|
298
|
+
const row = await db.query.credential.findFirst({
|
|
299
|
+
where: eq(credential.id, credentialId),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (!row) {
|
|
303
|
+
return c.json(
|
|
304
|
+
{ error: { code: "not_found", message: "Credential not found" } },
|
|
305
|
+
404,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const chain = await getAncestorChain(db, tenantCtx.id);
|
|
310
|
+
if (!chain.includes(row.tenantId)) {
|
|
311
|
+
return c.json(
|
|
312
|
+
{ error: { code: "not_found", message: "Credential not found" } },
|
|
313
|
+
404,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return c.json(formatCredential(row));
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
app.patch(
|
|
322
|
+
"/:credentialId",
|
|
323
|
+
requireGrant(idResource("credential", "credentialId"), "manage"),
|
|
324
|
+
describeRoute({
|
|
325
|
+
tags: ["Credentials"],
|
|
326
|
+
summary: "Rotate or update a credential",
|
|
327
|
+
description: "Only credentials owned by this tenant can be updated.",
|
|
328
|
+
responses: {
|
|
329
|
+
200: {
|
|
330
|
+
description: "Credential updated",
|
|
331
|
+
content: {
|
|
332
|
+
"application/json": { schema: resolver(CredentialResponse) },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
404: {
|
|
336
|
+
description: "Credential not found",
|
|
337
|
+
content: {
|
|
338
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
}),
|
|
343
|
+
validator("json", UpdateCredential),
|
|
344
|
+
async (c) => {
|
|
345
|
+
const tenantCtx = c.get("tenant");
|
|
346
|
+
const credentialId = c.req.param("credentialId");
|
|
347
|
+
const body = c.req.valid("json");
|
|
348
|
+
|
|
349
|
+
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
350
|
+
if (body.name !== undefined) updates["name"] = body.name;
|
|
351
|
+
if (body.description !== undefined)
|
|
352
|
+
updates["description"] = body.description;
|
|
353
|
+
if (body.secret !== undefined) updates["secret"] = body.secret;
|
|
354
|
+
if (body.refreshSecret !== undefined)
|
|
355
|
+
updates["refreshSecret"] = body.refreshSecret;
|
|
356
|
+
if (body.scopes !== undefined) updates["scopes"] = body.scopes;
|
|
357
|
+
if (body.expiresAt !== undefined)
|
|
358
|
+
updates["expiresAt"] = body.expiresAt ? new Date(body.expiresAt) : null;
|
|
359
|
+
if (body.status !== undefined) updates["status"] = body.status;
|
|
360
|
+
if (body.metadata !== undefined) updates["metadata"] = body.metadata;
|
|
361
|
+
|
|
362
|
+
const [updated] = await db
|
|
363
|
+
.update(credential)
|
|
364
|
+
.set(updates)
|
|
365
|
+
.where(
|
|
366
|
+
and(
|
|
367
|
+
eq(credential.id, credentialId),
|
|
368
|
+
eq(credential.tenantId, tenantCtx.id),
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
.returning();
|
|
372
|
+
|
|
373
|
+
if (!updated) {
|
|
374
|
+
return c.json(
|
|
375
|
+
{ error: { code: "not_found", message: "Credential not found" } },
|
|
376
|
+
404,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// If the secret was updated, push new inference sources to running
|
|
381
|
+
// instances.
|
|
382
|
+
if (body.secret !== undefined) {
|
|
383
|
+
void pushSourceUpdates(db, sidecarRouter, updated.tenantId);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return c.json(formatCredential(updated));
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
app.delete(
|
|
391
|
+
"/:credentialId",
|
|
392
|
+
requireGrant(idResource("credential", "credentialId"), "manage"),
|
|
393
|
+
describeRoute({
|
|
394
|
+
tags: ["Credentials"],
|
|
395
|
+
summary: "Revoke a credential",
|
|
396
|
+
description: "Only credentials owned by this tenant can be revoked.",
|
|
397
|
+
responses: {
|
|
398
|
+
204: {
|
|
399
|
+
description: "Credential revoked",
|
|
400
|
+
},
|
|
401
|
+
404: {
|
|
402
|
+
description: "Credential not found",
|
|
403
|
+
content: {
|
|
404
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
async (c) => {
|
|
410
|
+
const tenantCtx = c.get("tenant");
|
|
411
|
+
const credentialId = c.req.param("credentialId");
|
|
412
|
+
|
|
413
|
+
const deleted = await db
|
|
414
|
+
.delete(credential)
|
|
415
|
+
.where(
|
|
416
|
+
and(
|
|
417
|
+
eq(credential.id, credentialId),
|
|
418
|
+
eq(credential.tenantId, tenantCtx.id),
|
|
419
|
+
),
|
|
420
|
+
)
|
|
421
|
+
.returning();
|
|
422
|
+
|
|
423
|
+
if (deleted.length === 0) {
|
|
424
|
+
return c.json(
|
|
425
|
+
{ error: { code: "not_found", message: "Credential not found" } },
|
|
426
|
+
404,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return c.body(null, 204);
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
return app;
|
|
435
|
+
}
|