@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,382 @@
|
|
|
1
|
+
import { eq, and, ilike } from "drizzle-orm";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute, resolver, validator } from "hono-openapi";
|
|
4
|
+
|
|
5
|
+
import { agent, offering } from "@intx/db/schema";
|
|
6
|
+
import { parseOfferingRow } from "@intx/db";
|
|
7
|
+
import type { DB } from "@intx/db";
|
|
8
|
+
import {
|
|
9
|
+
CreateOffering,
|
|
10
|
+
UpdateOffering,
|
|
11
|
+
OfferingDetail,
|
|
12
|
+
ModelInfo,
|
|
13
|
+
ErrorResponse,
|
|
14
|
+
paginatedSchema,
|
|
15
|
+
} from "@intx/types";
|
|
16
|
+
|
|
17
|
+
import type { TenantEnv, AppEnv } from "../context";
|
|
18
|
+
import { first } from "../format";
|
|
19
|
+
import { generateId } from "@intx/hub-common";
|
|
20
|
+
import { idResource } from "../middleware/grant";
|
|
21
|
+
import type { RequireGrant } from "../middleware/grant";
|
|
22
|
+
import {
|
|
23
|
+
parsePageParams,
|
|
24
|
+
cursorCondition,
|
|
25
|
+
pageOrder,
|
|
26
|
+
paginatedResponse,
|
|
27
|
+
pageParameters,
|
|
28
|
+
} from "../pagination";
|
|
29
|
+
|
|
30
|
+
export function formatOffering(
|
|
31
|
+
row: typeof offering.$inferSelect,
|
|
32
|
+
agentName: string,
|
|
33
|
+
) {
|
|
34
|
+
const parsed = parseOfferingRow(row);
|
|
35
|
+
return {
|
|
36
|
+
id: parsed.id,
|
|
37
|
+
agentId: parsed.agentId,
|
|
38
|
+
agentName,
|
|
39
|
+
tenantId: parsed.tenantId,
|
|
40
|
+
name: parsed.name,
|
|
41
|
+
description: parsed.description ?? null,
|
|
42
|
+
pricing: parsed.pricing ?? undefined,
|
|
43
|
+
schema: parsed.schema,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type CreateOfferingRoutesDeps = {
|
|
48
|
+
db: DB["db"];
|
|
49
|
+
requireGrant: RequireGrant;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function createOfferingRoutes({
|
|
53
|
+
db,
|
|
54
|
+
requireGrant,
|
|
55
|
+
}: CreateOfferingRoutesDeps): Hono<TenantEnv> {
|
|
56
|
+
const app = new Hono<TenantEnv>();
|
|
57
|
+
|
|
58
|
+
app.get(
|
|
59
|
+
"/",
|
|
60
|
+
requireGrant("offering:*", "read"),
|
|
61
|
+
describeRoute({
|
|
62
|
+
tags: ["Discovery"],
|
|
63
|
+
summary: "Search offerings",
|
|
64
|
+
description:
|
|
65
|
+
"Searches offerings across discoverable agents in the tenant and federated tenants. Filterable by offering name, pricing range, and payment method.",
|
|
66
|
+
parameters: [
|
|
67
|
+
{ name: "name", in: "query", schema: { type: "string" } },
|
|
68
|
+
{ name: "minPrice", in: "query", schema: { type: "string" } },
|
|
69
|
+
{ name: "maxPrice", in: "query", schema: { type: "string" } },
|
|
70
|
+
{ name: "paymentMethod", in: "query", schema: { type: "string" } },
|
|
71
|
+
...pageParameters,
|
|
72
|
+
],
|
|
73
|
+
responses: {
|
|
74
|
+
200: {
|
|
75
|
+
description: "List of offerings",
|
|
76
|
+
content: {
|
|
77
|
+
"application/json": {
|
|
78
|
+
schema: resolver(paginatedSchema(OfferingDetail)),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
async (c) => {
|
|
85
|
+
const tenantCtx = c.get("tenant");
|
|
86
|
+
const name = c.req.query("name");
|
|
87
|
+
const { limit, cursor } = parsePageParams({
|
|
88
|
+
cursor: c.req.query("cursor"),
|
|
89
|
+
limit: c.req.query("limit"),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const conditions = [eq(offering.tenantId, tenantCtx.id)];
|
|
93
|
+
if (name) {
|
|
94
|
+
conditions.push(ilike(offering.name, `%${name}%`));
|
|
95
|
+
}
|
|
96
|
+
if (cursor) {
|
|
97
|
+
conditions.push(
|
|
98
|
+
cursorCondition(offering.createdAt, offering.id, cursor),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rows = await db.query.offering.findMany({
|
|
103
|
+
where: and(...conditions),
|
|
104
|
+
orderBy: pageOrder(offering.createdAt, offering.id),
|
|
105
|
+
limit,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const agentIds = [...new Set(rows.map((r) => r.agentId))];
|
|
109
|
+
const agentNames = new Map<string, string>();
|
|
110
|
+
if (agentIds.length > 0) {
|
|
111
|
+
const agents = await db.query.agent.findMany({
|
|
112
|
+
where: (a, { inArray }) => inArray(a.id, agentIds),
|
|
113
|
+
});
|
|
114
|
+
for (const a of agents) {
|
|
115
|
+
agentNames.set(a.id, a.name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const items = rows.map((r) =>
|
|
120
|
+
formatOffering(r, agentNames.get(r.agentId) ?? r.agentId),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
return c.json(paginatedResponse(items, rows, limit));
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
app.post(
|
|
128
|
+
"/",
|
|
129
|
+
requireGrant("offering:*", "create"),
|
|
130
|
+
describeRoute({
|
|
131
|
+
tags: ["Discovery"],
|
|
132
|
+
summary: "Register an offering",
|
|
133
|
+
description:
|
|
134
|
+
"Registers an offering for an agent. The agent must belong to the tenant.",
|
|
135
|
+
responses: {
|
|
136
|
+
201: {
|
|
137
|
+
description: "Offering registered",
|
|
138
|
+
content: {
|
|
139
|
+
"application/json": { schema: resolver(OfferingDetail) },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
400: {
|
|
143
|
+
description: "Validation error",
|
|
144
|
+
content: {
|
|
145
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
404: {
|
|
149
|
+
description: "Agent not found",
|
|
150
|
+
content: {
|
|
151
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
}),
|
|
156
|
+
validator("json", CreateOffering),
|
|
157
|
+
async (c) => {
|
|
158
|
+
const tenantCtx = c.get("tenant");
|
|
159
|
+
const body = c.req.valid("json");
|
|
160
|
+
|
|
161
|
+
const agentRow = await db.query.agent.findFirst({
|
|
162
|
+
where: and(
|
|
163
|
+
eq(agent.id, body.agentId),
|
|
164
|
+
eq(agent.tenantId, tenantCtx.id),
|
|
165
|
+
),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!agentRow) {
|
|
169
|
+
return c.json(
|
|
170
|
+
{
|
|
171
|
+
error: {
|
|
172
|
+
code: "not_found",
|
|
173
|
+
message: "Agent not found in this tenant",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
404,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const now = new Date();
|
|
181
|
+
const row = first(
|
|
182
|
+
await db
|
|
183
|
+
.insert(offering)
|
|
184
|
+
.values({
|
|
185
|
+
id: generateId("offering"),
|
|
186
|
+
agentId: body.agentId,
|
|
187
|
+
tenantId: tenantCtx.id,
|
|
188
|
+
name: body.name,
|
|
189
|
+
description: body.description ?? null,
|
|
190
|
+
pricing: body.pricing ?? null,
|
|
191
|
+
schema: body.schema ?? null,
|
|
192
|
+
createdAt: now,
|
|
193
|
+
updatedAt: now,
|
|
194
|
+
})
|
|
195
|
+
.returning(),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return c.json(formatOffering(row, agentRow.name), 201);
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
app.get(
|
|
203
|
+
"/:offeringId",
|
|
204
|
+
requireGrant(idResource("offering", "offeringId"), "read"),
|
|
205
|
+
describeRoute({
|
|
206
|
+
tags: ["Discovery"],
|
|
207
|
+
summary: "Get offering details",
|
|
208
|
+
description:
|
|
209
|
+
"Returns pricing, agent info, and request/response type information.",
|
|
210
|
+
responses: {
|
|
211
|
+
200: {
|
|
212
|
+
description: "Offering details",
|
|
213
|
+
content: {
|
|
214
|
+
"application/json": { schema: resolver(OfferingDetail) },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
404: {
|
|
218
|
+
description: "Offering not found",
|
|
219
|
+
content: {
|
|
220
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
async (c) => {
|
|
226
|
+
const tenantCtx = c.get("tenant");
|
|
227
|
+
const offeringId = c.req.param("offeringId");
|
|
228
|
+
|
|
229
|
+
const row = await db.query.offering.findFirst({
|
|
230
|
+
where: and(
|
|
231
|
+
eq(offering.id, offeringId),
|
|
232
|
+
eq(offering.tenantId, tenantCtx.id),
|
|
233
|
+
),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!row) {
|
|
237
|
+
return c.json(
|
|
238
|
+
{ error: { code: "not_found", message: "Offering not found" } },
|
|
239
|
+
404,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const agentRow = await db.query.agent.findFirst({
|
|
244
|
+
where: eq(agent.id, row.agentId),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return c.json(formatOffering(row, agentRow?.name ?? row.agentId));
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
app.patch(
|
|
252
|
+
"/:offeringId",
|
|
253
|
+
requireGrant(idResource("offering", "offeringId"), "manage"),
|
|
254
|
+
describeRoute({
|
|
255
|
+
tags: ["Discovery"],
|
|
256
|
+
summary: "Update an offering",
|
|
257
|
+
responses: {
|
|
258
|
+
200: {
|
|
259
|
+
description: "Offering updated",
|
|
260
|
+
content: {
|
|
261
|
+
"application/json": { schema: resolver(OfferingDetail) },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
404: {
|
|
265
|
+
description: "Offering not found",
|
|
266
|
+
content: {
|
|
267
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
validator("json", UpdateOffering),
|
|
273
|
+
async (c) => {
|
|
274
|
+
const tenantCtx = c.get("tenant");
|
|
275
|
+
const offeringId = c.req.param("offeringId");
|
|
276
|
+
const body = c.req.valid("json");
|
|
277
|
+
|
|
278
|
+
const updates: Record<string, unknown> = { updatedAt: new Date() };
|
|
279
|
+
if (body.name !== undefined) updates["name"] = body.name;
|
|
280
|
+
if (body.description !== undefined)
|
|
281
|
+
updates["description"] = body.description;
|
|
282
|
+
if (body.pricing !== undefined) updates["pricing"] = body.pricing;
|
|
283
|
+
if (body.schema !== undefined) updates["schema"] = body.schema;
|
|
284
|
+
|
|
285
|
+
const [updated] = await db
|
|
286
|
+
.update(offering)
|
|
287
|
+
.set(updates)
|
|
288
|
+
.where(
|
|
289
|
+
and(eq(offering.id, offeringId), eq(offering.tenantId, tenantCtx.id)),
|
|
290
|
+
)
|
|
291
|
+
.returning();
|
|
292
|
+
|
|
293
|
+
if (!updated) {
|
|
294
|
+
return c.json(
|
|
295
|
+
{ error: { code: "not_found", message: "Offering not found" } },
|
|
296
|
+
404,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const agentRow = await db.query.agent.findFirst({
|
|
301
|
+
where: eq(agent.id, updated.agentId),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return c.json(formatOffering(updated, agentRow?.name ?? updated.agentId));
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
app.delete(
|
|
309
|
+
"/:offeringId",
|
|
310
|
+
requireGrant(idResource("offering", "offeringId"), "manage"),
|
|
311
|
+
describeRoute({
|
|
312
|
+
tags: ["Discovery"],
|
|
313
|
+
summary: "Remove an offering",
|
|
314
|
+
responses: {
|
|
315
|
+
204: {
|
|
316
|
+
description: "Offering removed",
|
|
317
|
+
},
|
|
318
|
+
404: {
|
|
319
|
+
description: "Offering not found",
|
|
320
|
+
content: {
|
|
321
|
+
"application/json": { schema: resolver(ErrorResponse) },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
async (c) => {
|
|
327
|
+
const tenantCtx = c.get("tenant");
|
|
328
|
+
const offeringId = c.req.param("offeringId");
|
|
329
|
+
|
|
330
|
+
const deleted = await db
|
|
331
|
+
.delete(offering)
|
|
332
|
+
.where(
|
|
333
|
+
and(eq(offering.id, offeringId), eq(offering.tenantId, tenantCtx.id)),
|
|
334
|
+
)
|
|
335
|
+
.returning();
|
|
336
|
+
|
|
337
|
+
if (deleted.length === 0) {
|
|
338
|
+
return c.json(
|
|
339
|
+
{ error: { code: "not_found", message: "Offering not found" } },
|
|
340
|
+
404,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return c.body(null, 204);
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return app;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Models endpoint is global (not tenant-scoped) -- remains a stub for now
|
|
352
|
+
// since model discovery requires external provider integration
|
|
353
|
+
export function createModelRoutes(): Hono<AppEnv> {
|
|
354
|
+
const modelsApp = new Hono<AppEnv>();
|
|
355
|
+
|
|
356
|
+
modelsApp.get(
|
|
357
|
+
"/",
|
|
358
|
+
describeRoute({
|
|
359
|
+
tags: ["Discovery"],
|
|
360
|
+
summary: "List available models",
|
|
361
|
+
description:
|
|
362
|
+
"Lists available models across configured providers with capabilities, pricing, and limits.",
|
|
363
|
+
responses: {
|
|
364
|
+
200: {
|
|
365
|
+
description: "List of models",
|
|
366
|
+
content: {
|
|
367
|
+
"application/json": {
|
|
368
|
+
schema: resolver(ModelInfo.array()),
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
(c) =>
|
|
375
|
+
c.json(
|
|
376
|
+
{ error: { code: "not_implemented", message: "Not implemented" } },
|
|
377
|
+
501,
|
|
378
|
+
),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
return modelsApp;
|
|
382
|
+
}
|