@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.
Files changed (55) hide show
  1. package/README.md +29 -0
  2. package/package.json +28 -0
  3. package/src/app.test.ts +225 -0
  4. package/src/app.ts +382 -0
  5. package/src/auth.ts +21 -0
  6. package/src/context.ts +38 -0
  7. package/src/format.ts +9 -0
  8. package/src/git-http/advertise-refs.test.ts +459 -0
  9. package/src/git-http/advertise-refs.ts +226 -0
  10. package/src/git-http/pkt-line.test.ts +220 -0
  11. package/src/git-http/pkt-line.ts +235 -0
  12. package/src/git-http/receive-pack.test.ts +397 -0
  13. package/src/git-http/receive-pack.ts +261 -0
  14. package/src/git-http/side-band-64k.test.ts +181 -0
  15. package/src/git-http/side-band-64k.ts +134 -0
  16. package/src/git-http/upload-pack.test.ts +545 -0
  17. package/src/git-http/upload-pack.ts +396 -0
  18. package/src/index.ts +23 -0
  19. package/src/middleware/git-token-auth.test.ts +587 -0
  20. package/src/middleware/git-token-auth.ts +315 -0
  21. package/src/middleware/grant.ts +106 -0
  22. package/src/middleware/session.ts +13 -0
  23. package/src/middleware/tenant.test.ts +192 -0
  24. package/src/middleware/tenant.ts +101 -0
  25. package/src/openapi.ts +66 -0
  26. package/src/pagination.ts +117 -0
  27. package/src/routes/agent-data.ts +179 -0
  28. package/src/routes/agent-state-git.ts +562 -0
  29. package/src/routes/agents.test.ts +337 -0
  30. package/src/routes/agents.ts +704 -0
  31. package/src/routes/approvals.ts +130 -0
  32. package/src/routes/assets.test.ts +567 -0
  33. package/src/routes/assets.ts +592 -0
  34. package/src/routes/credentials.ts +435 -0
  35. package/src/routes/git-tokens.test.ts +709 -0
  36. package/src/routes/git-tokens.ts +771 -0
  37. package/src/routes/grants.ts +509 -0
  38. package/src/routes/instances.test.ts +1103 -0
  39. package/src/routes/instances.ts +1797 -0
  40. package/src/routes/me.ts +405 -0
  41. package/src/routes/oauth-clients.ts +349 -0
  42. package/src/routes/observability.ts +146 -0
  43. package/src/routes/offerings.ts +382 -0
  44. package/src/routes/principals.ts +515 -0
  45. package/src/routes/providers.ts +351 -0
  46. package/src/routes/roles.ts +452 -0
  47. package/src/routes/sidecars.ts +221 -0
  48. package/src/routes/tenant-federation.ts +225 -0
  49. package/src/routes/tenants.ts +369 -0
  50. package/src/routes/wallets.ts +370 -0
  51. package/src/session.ts +44 -0
  52. package/src/timeline-reconstruction.test.ts +786 -0
  53. package/src/timeline-reconstruction.ts +383 -0
  54. package/tsconfig.json +4 -0
  55. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,349 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { Hono } from "hono";
3
+ import { describeRoute, resolver, validator } from "hono-openapi";
4
+
5
+ import { oauthClient, provider } from "@intx/db/schema";
6
+ import { getAncestorChain, parseOAuthClientRow } from "@intx/db";
7
+ import type { DB } from "@intx/db";
8
+ import {
9
+ CreateOAuthClient,
10
+ UpdateOAuthClient,
11
+ OAuthClientResponse,
12
+ ErrorResponse,
13
+ paginatedSchema,
14
+ } from "@intx/types";
15
+
16
+ import type { TenantEnv } from "../context";
17
+ import { first, ts } from "../format";
18
+ import { generateId } from "@intx/hub-common";
19
+ import { idResource } from "../middleware/grant";
20
+ import type { RequireGrant } from "../middleware/grant";
21
+ import {
22
+ parsePageParams,
23
+ cursorCondition,
24
+ pageOrder,
25
+ paginatedResponse,
26
+ pageParameters,
27
+ } from "../pagination";
28
+
29
+ function formatOAuthClient(row: typeof oauthClient.$inferSelect) {
30
+ const parsed = parseOAuthClientRow(row);
31
+ return {
32
+ id: parsed.id,
33
+ tenantId: parsed.tenantId,
34
+ providerId: parsed.providerId,
35
+ name: parsed.name,
36
+ redirectUris: parsed.redirectUris ?? null,
37
+ defaultScopes: parsed.defaultScopes ?? null,
38
+ metadata: parsed.metadata,
39
+ createdAt: ts(parsed.createdAt),
40
+ updatedAt: ts(parsed.updatedAt),
41
+ };
42
+ }
43
+
44
+ export type CreateOAuthClientRoutesDeps = {
45
+ db: DB["db"];
46
+ requireGrant: RequireGrant;
47
+ };
48
+
49
+ export function createOAuthClientRoutes({
50
+ db,
51
+ requireGrant,
52
+ }: CreateOAuthClientRoutesDeps): Hono<TenantEnv> {
53
+ const app = new Hono<TenantEnv>();
54
+
55
+ app.get(
56
+ "/",
57
+ requireGrant("oauth_client:*", "read"),
58
+ describeRoute({
59
+ tags: ["OAuth Clients"],
60
+ summary: "List OAuth client registrations",
61
+ description:
62
+ "Lists OAuth client registrations for the tenant. Secrets are never returned.",
63
+ parameters: [...pageParameters],
64
+ responses: {
65
+ 200: {
66
+ description: "List of OAuth clients",
67
+ content: {
68
+ "application/json": {
69
+ schema: resolver(paginatedSchema(OAuthClientResponse)),
70
+ },
71
+ },
72
+ },
73
+ },
74
+ }),
75
+ async (c) => {
76
+ const tenantCtx = c.get("tenant");
77
+ const { limit, cursor } = parsePageParams({
78
+ cursor: c.req.query("cursor"),
79
+ limit: c.req.query("limit"),
80
+ });
81
+
82
+ const conditions = [eq(oauthClient.tenantId, tenantCtx.id)];
83
+ if (cursor) {
84
+ conditions.push(
85
+ cursorCondition(oauthClient.createdAt, oauthClient.id, cursor),
86
+ );
87
+ }
88
+
89
+ const rows = await db.query.oauthClient.findMany({
90
+ where: and(...conditions),
91
+ orderBy: pageOrder(oauthClient.createdAt, oauthClient.id),
92
+ limit,
93
+ });
94
+
95
+ return c.json(
96
+ paginatedResponse(rows.map(formatOAuthClient), rows, limit),
97
+ );
98
+ },
99
+ );
100
+
101
+ app.post(
102
+ "/",
103
+ requireGrant("oauth_client:*", "create"),
104
+ describeRoute({
105
+ tags: ["OAuth Clients"],
106
+ summary: "Register an OAuth client",
107
+ description:
108
+ "Registers an OAuth client (client_id/client_secret) for a provider. The provider must exist in the tenant or its ancestors.",
109
+ responses: {
110
+ 201: {
111
+ description: "OAuth client registered",
112
+ content: {
113
+ "application/json": { schema: resolver(OAuthClientResponse) },
114
+ },
115
+ },
116
+ 400: {
117
+ description: "Validation error",
118
+ content: {
119
+ "application/json": { schema: resolver(ErrorResponse) },
120
+ },
121
+ },
122
+ 404: {
123
+ description: "Provider not found",
124
+ content: {
125
+ "application/json": { schema: resolver(ErrorResponse) },
126
+ },
127
+ },
128
+ 409: {
129
+ description:
130
+ "OAuth client already exists for this provider in this tenant",
131
+ content: {
132
+ "application/json": { schema: resolver(ErrorResponse) },
133
+ },
134
+ },
135
+ },
136
+ }),
137
+ validator("json", CreateOAuthClient),
138
+ async (c) => {
139
+ const tenantCtx = c.get("tenant");
140
+ const body = c.req.valid("json");
141
+
142
+ const providerRow = await db.query.provider.findFirst({
143
+ where: eq(provider.id, body.providerId),
144
+ });
145
+ if (!providerRow) {
146
+ return c.json(
147
+ { error: { code: "not_found", message: "Provider not found" } },
148
+ 404,
149
+ );
150
+ }
151
+
152
+ const chain = await getAncestorChain(db, tenantCtx.id);
153
+ if (!chain.includes(providerRow.tenantId)) {
154
+ return c.json(
155
+ { error: { code: "not_found", message: "Provider not found" } },
156
+ 404,
157
+ );
158
+ }
159
+
160
+ const existing = await db.query.oauthClient.findFirst({
161
+ where: and(
162
+ eq(oauthClient.tenantId, tenantCtx.id),
163
+ eq(oauthClient.providerId, body.providerId),
164
+ ),
165
+ });
166
+ if (existing) {
167
+ return c.json(
168
+ {
169
+ error: {
170
+ code: "conflict",
171
+ message:
172
+ "OAuth client already exists for this provider in this tenant",
173
+ },
174
+ },
175
+ 409,
176
+ );
177
+ }
178
+
179
+ const now = new Date();
180
+ const row = first(
181
+ await db
182
+ .insert(oauthClient)
183
+ .values({
184
+ id: generateId("oauthClient"),
185
+ tenantId: tenantCtx.id,
186
+ providerId: body.providerId,
187
+ name: body.name,
188
+ clientId: body.clientId,
189
+ clientSecret: body.clientSecret,
190
+ redirectUris: body.redirectUris ?? null,
191
+ defaultScopes: body.defaultScopes ?? null,
192
+ metadata: body.metadata ?? null,
193
+ createdAt: now,
194
+ updatedAt: now,
195
+ })
196
+ .returning(),
197
+ );
198
+
199
+ return c.json(formatOAuthClient(row), 201);
200
+ },
201
+ );
202
+
203
+ app.get(
204
+ "/:oauthClientId",
205
+ requireGrant(idResource("oauth_client", "oauthClientId"), "read"),
206
+ describeRoute({
207
+ tags: ["OAuth Clients"],
208
+ summary: "Get OAuth client details",
209
+ description: "Returns OAuth client metadata. Secrets are never included.",
210
+ responses: {
211
+ 200: {
212
+ description: "OAuth client details",
213
+ content: {
214
+ "application/json": { schema: resolver(OAuthClientResponse) },
215
+ },
216
+ },
217
+ 404: {
218
+ description: "OAuth client 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 oauthClientId = c.req.param("oauthClientId");
228
+
229
+ const row = await db.query.oauthClient.findFirst({
230
+ where: and(
231
+ eq(oauthClient.id, oauthClientId),
232
+ eq(oauthClient.tenantId, tenantCtx.id),
233
+ ),
234
+ });
235
+
236
+ if (!row) {
237
+ return c.json(
238
+ { error: { code: "not_found", message: "OAuth client not found" } },
239
+ 404,
240
+ );
241
+ }
242
+
243
+ return c.json(formatOAuthClient(row));
244
+ },
245
+ );
246
+
247
+ app.patch(
248
+ "/:oauthClientId",
249
+ requireGrant(idResource("oauth_client", "oauthClientId"), "manage"),
250
+ describeRoute({
251
+ tags: ["OAuth Clients"],
252
+ summary: "Update an OAuth client registration",
253
+ responses: {
254
+ 200: {
255
+ description: "OAuth client updated",
256
+ content: {
257
+ "application/json": { schema: resolver(OAuthClientResponse) },
258
+ },
259
+ },
260
+ 404: {
261
+ description: "OAuth client not found",
262
+ content: {
263
+ "application/json": { schema: resolver(ErrorResponse) },
264
+ },
265
+ },
266
+ },
267
+ }),
268
+ validator("json", UpdateOAuthClient),
269
+ async (c) => {
270
+ const tenantCtx = c.get("tenant");
271
+ const oauthClientId = c.req.param("oauthClientId");
272
+ const body = c.req.valid("json");
273
+
274
+ const updates: Record<string, unknown> = { updatedAt: new Date() };
275
+ if (body.name !== undefined) updates["name"] = body.name;
276
+ if (body.clientId !== undefined) updates["clientId"] = body.clientId;
277
+ if (body.clientSecret !== undefined)
278
+ updates["clientSecret"] = body.clientSecret;
279
+ if (body.redirectUris !== undefined)
280
+ updates["redirectUris"] = body.redirectUris;
281
+ if (body.defaultScopes !== undefined)
282
+ updates["defaultScopes"] = body.defaultScopes;
283
+ if (body.metadata !== undefined) updates["metadata"] = body.metadata;
284
+
285
+ const [updated] = await db
286
+ .update(oauthClient)
287
+ .set(updates)
288
+ .where(
289
+ and(
290
+ eq(oauthClient.id, oauthClientId),
291
+ eq(oauthClient.tenantId, tenantCtx.id),
292
+ ),
293
+ )
294
+ .returning();
295
+
296
+ if (!updated) {
297
+ return c.json(
298
+ { error: { code: "not_found", message: "OAuth client not found" } },
299
+ 404,
300
+ );
301
+ }
302
+
303
+ return c.json(formatOAuthClient(updated));
304
+ },
305
+ );
306
+
307
+ app.delete(
308
+ "/:oauthClientId",
309
+ requireGrant(idResource("oauth_client", "oauthClientId"), "manage"),
310
+ describeRoute({
311
+ tags: ["OAuth Clients"],
312
+ summary: "Remove an OAuth client registration",
313
+ responses: {
314
+ 204: { description: "OAuth client removed" },
315
+ 404: {
316
+ description: "OAuth client not found",
317
+ content: {
318
+ "application/json": { schema: resolver(ErrorResponse) },
319
+ },
320
+ },
321
+ },
322
+ }),
323
+ async (c) => {
324
+ const tenantCtx = c.get("tenant");
325
+ const oauthClientId = c.req.param("oauthClientId");
326
+
327
+ const deleted = await db
328
+ .delete(oauthClient)
329
+ .where(
330
+ and(
331
+ eq(oauthClient.id, oauthClientId),
332
+ eq(oauthClient.tenantId, tenantCtx.id),
333
+ ),
334
+ )
335
+ .returning();
336
+
337
+ if (deleted.length === 0) {
338
+ return c.json(
339
+ { error: { code: "not_found", message: "OAuth client not found" } },
340
+ 404,
341
+ );
342
+ }
343
+
344
+ return c.body(null, 204);
345
+ },
346
+ );
347
+
348
+ return app;
349
+ }
@@ -0,0 +1,146 @@
1
+ import { Hono } from "hono";
2
+ import { describeRoute, resolver } from "hono-openapi";
3
+ import {
4
+ LogEntry,
5
+ MetricsResponse,
6
+ TraceResponse,
7
+ SpanResponse,
8
+ ErrorResponse,
9
+ } from "@intx/types";
10
+
11
+ import type { AppEnv } from "../context";
12
+
13
+ export function createObservabilityRoutes(): Hono<AppEnv> {
14
+ const app = new Hono<AppEnv>();
15
+
16
+ app.get(
17
+ "/agents/:agentId/logs",
18
+ describeRoute({
19
+ tags: ["Observability"],
20
+ summary: "Get agent logs",
21
+ description:
22
+ "Structured logs for an agent. Filterable by level and time range.",
23
+ parameters: [
24
+ {
25
+ name: "level",
26
+ in: "query",
27
+ schema: { type: "string", enum: ["debug", "info", "warn", "error"] },
28
+ },
29
+ { name: "startTime", in: "query", schema: { type: "string" } },
30
+ { name: "endTime", in: "query", schema: { type: "string" } },
31
+ ],
32
+ responses: {
33
+ 200: {
34
+ description: "Log entries",
35
+ content: {
36
+ "application/json": {
37
+ schema: resolver(LogEntry.array()),
38
+ },
39
+ },
40
+ },
41
+ 404: {
42
+ description: "Agent not found",
43
+ content: {
44
+ "application/json": { schema: resolver(ErrorResponse) },
45
+ },
46
+ },
47
+ },
48
+ }),
49
+ (c) =>
50
+ c.json(
51
+ { error: { code: "not_implemented", message: "Not implemented" } },
52
+ 501,
53
+ ),
54
+ );
55
+
56
+ app.get(
57
+ "/agents/:agentId/metrics",
58
+ describeRoute({
59
+ tags: ["Observability"],
60
+ summary: "Get agent metrics",
61
+ description:
62
+ "Returns throughput, latency, error rates, token usage, and cost metrics.",
63
+ responses: {
64
+ 200: {
65
+ description: "Agent metrics",
66
+ content: {
67
+ "application/json": { schema: resolver(MetricsResponse) },
68
+ },
69
+ },
70
+ 404: {
71
+ description: "Agent not found",
72
+ content: {
73
+ "application/json": { schema: resolver(ErrorResponse) },
74
+ },
75
+ },
76
+ },
77
+ }),
78
+ (c) =>
79
+ c.json(
80
+ { error: { code: "not_implemented", message: "Not implemented" } },
81
+ 501,
82
+ ),
83
+ );
84
+
85
+ app.get(
86
+ "/traces",
87
+ describeRoute({
88
+ tags: ["Observability"],
89
+ summary: "Query distributed traces",
90
+ description:
91
+ "Searches traces within the tenant. Filterable by agent, session, time range, and trace ID.",
92
+ parameters: [
93
+ { name: "agentId", in: "query", schema: { type: "string" } },
94
+ { name: "sessionId", in: "query", schema: { type: "string" } },
95
+ { name: "traceId", in: "query", schema: { type: "string" } },
96
+ { name: "startTime", in: "query", schema: { type: "string" } },
97
+ { name: "endTime", in: "query", schema: { type: "string" } },
98
+ ],
99
+ responses: {
100
+ 200: {
101
+ description: "List of traces",
102
+ content: {
103
+ "application/json": {
104
+ schema: resolver(SpanResponse.array()),
105
+ },
106
+ },
107
+ },
108
+ },
109
+ }),
110
+ (c) =>
111
+ c.json(
112
+ { error: { code: "not_implemented", message: "Not implemented" } },
113
+ 501,
114
+ ),
115
+ );
116
+
117
+ app.get(
118
+ "/traces/:traceId",
119
+ describeRoute({
120
+ tags: ["Observability"],
121
+ summary: "Get a full trace",
122
+ description: "Returns all spans in a trace across agent boundaries.",
123
+ responses: {
124
+ 200: {
125
+ description: "Trace with spans",
126
+ content: {
127
+ "application/json": { schema: resolver(TraceResponse) },
128
+ },
129
+ },
130
+ 404: {
131
+ description: "Trace not found",
132
+ content: {
133
+ "application/json": { schema: resolver(ErrorResponse) },
134
+ },
135
+ },
136
+ },
137
+ }),
138
+ (c) =>
139
+ c.json(
140
+ { error: { code: "not_implemented", message: "Not implemented" } },
141
+ 501,
142
+ ),
143
+ );
144
+
145
+ return app;
146
+ }