@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,337 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ import { createInMemoryGrantStore } from "@intx/authz";
4
+ import type { GrantRule } from "@intx/types/authz";
5
+
6
+ import { createApp } from "../app";
7
+ import {
8
+ createSidecarEmitter,
9
+ type EventCollectorRegistry,
10
+ type SessionService,
11
+ type SidecarRouter,
12
+ } from "@intx/hub-sessions";
13
+ import type { GetSession } from "../session";
14
+
15
+ const TENANT_ID = "tnt_test";
16
+ const PRINCIPAL_ID = "prn_test";
17
+ const USER_ID = "usr_test";
18
+
19
+ const testTenant = {
20
+ id: TENANT_ID,
21
+ name: "Test",
22
+ slug: "test",
23
+ domain: "test.example.com",
24
+ parentId: null,
25
+ config: null,
26
+ createdAt: new Date("2025-01-01"),
27
+ updatedAt: new Date("2025-01-01"),
28
+ };
29
+
30
+ const testPrincipal = {
31
+ id: PRINCIPAL_ID,
32
+ tenantId: TENANT_ID,
33
+ kind: "user" as const,
34
+ refId: USER_ID,
35
+ status: "active" as const,
36
+ createdAt: new Date("2025-01-01"),
37
+ updatedAt: new Date("2025-01-01"),
38
+ };
39
+
40
+ function makeGrant(overrides: Partial<GrantRule> = {}): GrantRule {
41
+ return {
42
+ id: "grant-test",
43
+ resource: "agent:*",
44
+ action: "create",
45
+ effect: "allow",
46
+ origin: "system",
47
+ conditions: null,
48
+ expiresAt: null,
49
+ roleId: null,
50
+ principalId: PRINCIPAL_ID,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ type InsertCapture = {
56
+ table: string;
57
+ rows: Record<string, unknown>[];
58
+ };
59
+
60
+ function tableName(table: unknown): string {
61
+ // Drizzle PgTable objects carry their name on a Symbol; the test only
62
+ // needs a stable string label, so we cooperate with the Drizzle API by
63
+ // reading a known own property when present and falling back to a
64
+ // marker that lets a misconfigured test fail loudly.
65
+ if (table && typeof table === "object") {
66
+ const sym = Object.getOwnPropertySymbols(table).find(
67
+ (s) => s.description === "drizzle:Name",
68
+ );
69
+ if (sym) {
70
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- drizzle stores the table name keyed by a documented symbol
71
+ const value = (table as Record<symbol, unknown>)[sym];
72
+ if (typeof value === "string") return value;
73
+ }
74
+ }
75
+ return "unknown";
76
+ }
77
+
78
+ type MockDBOpts = {
79
+ tenant?: typeof testTenant;
80
+ principal?: typeof testPrincipal;
81
+ roles?: { id: string; name: string; tenantId: string }[];
82
+ inserts: InsertCapture[];
83
+ };
84
+
85
+ function notImplemented(path: string) {
86
+ return () => {
87
+ throw new Error(`mock: ${path} not implemented`);
88
+ };
89
+ }
90
+
91
+ function createMockDB(opts: MockDBOpts) {
92
+ const roles = opts.roles ?? [];
93
+
94
+ function insertChain(table: unknown) {
95
+ const name = tableName(table);
96
+ return {
97
+ values: (
98
+ rowsOrRow: Record<string, unknown> | Record<string, unknown>[],
99
+ ) => {
100
+ const rows = Array.isArray(rowsOrRow) ? rowsOrRow : [rowsOrRow];
101
+ opts.inserts.push({ table: name, rows });
102
+ return {
103
+ returning: () => Promise.resolve(rows),
104
+ then: (resolve: (v: undefined) => unknown) => resolve(undefined),
105
+ };
106
+ },
107
+ };
108
+ }
109
+
110
+ const txLike = {
111
+ insert: insertChain,
112
+ };
113
+
114
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- drizzle PgDatabase type cannot be structurally satisfied in tests
115
+ return {
116
+ query: {
117
+ tenant: {
118
+ findFirst: async () => opts.tenant,
119
+ findMany: notImplemented("db.query.tenant.findMany"),
120
+ },
121
+ principal: {
122
+ findFirst: async () => opts.principal,
123
+ findMany: notImplemented("db.query.principal.findMany"),
124
+ },
125
+ role: {
126
+ findFirst: notImplemented("db.query.role.findFirst"),
127
+ findMany: async () => roles,
128
+ },
129
+ agent: {
130
+ findFirst: notImplemented("db.query.agent.findFirst"),
131
+ findMany: notImplemented("db.query.agent.findMany"),
132
+ },
133
+ agentRole: {
134
+ findFirst: notImplemented("db.query.agentRole.findFirst"),
135
+ findMany: notImplemented("db.query.agentRole.findMany"),
136
+ },
137
+ },
138
+ transaction: async (fn: (tx: typeof txLike) => Promise<unknown>) =>
139
+ fn(txLike),
140
+ insert: insertChain,
141
+ } as unknown as Parameters<typeof createApp>[0]["db"];
142
+ }
143
+
144
+ function createMockGetSession(userId: string): GetSession {
145
+ const now = new Date("2025-01-01");
146
+ return async () => ({
147
+ user: {
148
+ id: userId,
149
+ email: "test@example.com",
150
+ emailVerified: true,
151
+ name: "Test User",
152
+ createdAt: now,
153
+ updatedAt: now,
154
+ },
155
+ session: {
156
+ id: "session_test",
157
+ userId,
158
+ token: "tok_test",
159
+ expiresAt: new Date("2999-01-01"),
160
+ createdAt: now,
161
+ updatedAt: now,
162
+ },
163
+ });
164
+ }
165
+
166
+ function createMockSidecarRouter(): SidecarRouter {
167
+ function notImpl(name: string): never {
168
+ throw new Error(`mock: sidecarRouter.${name} not implemented`);
169
+ }
170
+ return {
171
+ handleOpen: () => notImpl("handleOpen"),
172
+ handleMessage: () => notImpl("handleMessage"),
173
+ handleClose: () => notImpl("handleClose"),
174
+ routeMail: () => notImpl("routeMail"),
175
+ sendAgentDeploy: () => notImpl("sendAgentDeploy"),
176
+ sendAgentUndeploy: () => notImpl("sendAgentUndeploy"),
177
+ sendSessionStart: () => notImpl("sendSessionStart"),
178
+ sendSessionAbort: () => notImpl("sendSessionAbort"),
179
+ sendGrantsUpdate: () => notImpl("sendGrantsUpdate"),
180
+ sendSourcesUpdate: () => notImpl("sendSourcesUpdate"),
181
+ sendPack: () => notImpl("sendPack"),
182
+ sendSyncRequest: () => notImpl("sendSyncRequest"),
183
+ subscribeAgent: () => notImpl("subscribeAgent"),
184
+ dispatchAgentEvent: () => undefined,
185
+ getConnectedSidecars: () => [],
186
+ getRoutableAddresses: () => [],
187
+ getConnectorState: () => null,
188
+ events: createSidecarEmitter(),
189
+ };
190
+ }
191
+
192
+ function createMockSessionService(): SessionService {
193
+ return {
194
+ launchSession: () => {
195
+ throw new Error("mock: sessionService.launchSession not implemented");
196
+ },
197
+ sendUserMessage: () => {
198
+ throw new Error("mock: sessionService.sendUserMessage not implemented");
199
+ },
200
+ endSession: () => {
201
+ throw new Error("mock: sessionService.endSession not implemented");
202
+ },
203
+ };
204
+ }
205
+
206
+ function createMockEventCollectors(): EventCollectorRegistry {
207
+ return {
208
+ create: notImplemented("eventCollectors.create"),
209
+ dispatch: notImplemented("eventCollectors.dispatch"),
210
+ abandon: notImplemented("eventCollectors.abandon"),
211
+ has: () => false,
212
+ getStatus: () => undefined,
213
+ getAccumulatedText: () => undefined,
214
+ getCurrentTurnId: () => undefined,
215
+ getLastTurnId: () => undefined,
216
+ };
217
+ }
218
+
219
+ type TestAppOpts = {
220
+ db: MockDBOpts;
221
+ grants?: GrantRule[];
222
+ };
223
+
224
+ function createTestApp(opts: TestAppOpts) {
225
+ const db = createMockDB(opts.db);
226
+
227
+ return createApp({
228
+ getSession: createMockGetSession(USER_ID),
229
+ authHandler: () => new Response("", { status: 404 }),
230
+ db,
231
+ grantStore: createInMemoryGrantStore(opts.grants ?? [makeGrant()]),
232
+ sidecarRouter: createMockSidecarRouter(),
233
+ sessionService: createMockSessionService(),
234
+ eventCollectors: createMockEventCollectors(),
235
+ assetService: null,
236
+ repoStore: null,
237
+ });
238
+ }
239
+
240
+ const agentsURL = `/api/tenants/${TENANT_ID}/agents/definitions`;
241
+
242
+ describe("POST /agents/definitions", () => {
243
+ test("seeds a creator-level agent-state read grant on the new agent", async () => {
244
+ const inserts: InsertCapture[] = [];
245
+
246
+ const app = createTestApp({
247
+ db: {
248
+ tenant: testTenant,
249
+ principal: testPrincipal,
250
+ inserts,
251
+ },
252
+ });
253
+
254
+ const res = await app.request(agentsURL, {
255
+ method: "POST",
256
+ headers: { "content-type": "application/json" },
257
+ body: JSON.stringify({ name: "Demo" }),
258
+ });
259
+
260
+ expect(res.status).toBe(201);
261
+
262
+ const grantInserts = inserts.filter((i) => i.table === "grant");
263
+ expect(grantInserts).toHaveLength(1);
264
+
265
+ const insertedRows = grantInserts[0]?.rows ?? [];
266
+ expect(insertedRows).toHaveLength(1);
267
+
268
+ const agentInserts = inserts.filter((i) => i.table === "agent");
269
+ expect(agentInserts).toHaveLength(1);
270
+ const agentRow = agentInserts[0]?.rows[0];
271
+ expect(agentRow).toBeDefined();
272
+ const agentId = agentRow?.["id"];
273
+ if (typeof agentId !== "string") {
274
+ throw new Error("expected captured agent insert to carry a string id");
275
+ }
276
+
277
+ const grantRow = insertedRows[0];
278
+ expect(grantRow).toMatchObject({
279
+ tenantId: TENANT_ID,
280
+ principalId: PRINCIPAL_ID,
281
+ resource: `agent-state:${agentId}`,
282
+ action: "read",
283
+ effect: "allow",
284
+ origin: "creator",
285
+ });
286
+ });
287
+
288
+ test("grant insert is ordered relative to the agent insert", async () => {
289
+ const inserts: InsertCapture[] = [];
290
+
291
+ const app = createTestApp({
292
+ db: {
293
+ tenant: testTenant,
294
+ principal: testPrincipal,
295
+ inserts,
296
+ },
297
+ });
298
+
299
+ const res = await app.request(agentsURL, {
300
+ method: "POST",
301
+ headers: { "content-type": "application/json" },
302
+ body: JSON.stringify({ name: "Demo" }),
303
+ });
304
+
305
+ expect(res.status).toBe(201);
306
+
307
+ const tables = inserts.map((i) => i.table);
308
+ const agentIdx = tables.indexOf("agent");
309
+ const grantIdx = tables.indexOf("grant");
310
+ expect(agentIdx).toBeGreaterThanOrEqual(0);
311
+ expect(grantIdx).toBeGreaterThan(agentIdx);
312
+ });
313
+
314
+ test("missing create grant rejects the request before any insert runs", async () => {
315
+ const inserts: InsertCapture[] = [];
316
+
317
+ const app = createTestApp({
318
+ db: {
319
+ tenant: testTenant,
320
+ principal: testPrincipal,
321
+ inserts,
322
+ },
323
+ grants: [],
324
+ });
325
+
326
+ const res = await app.request(agentsURL, {
327
+ method: "POST",
328
+ headers: { "content-type": "application/json" },
329
+ body: JSON.stringify({ name: "Demo" }),
330
+ });
331
+
332
+ expect(res.status).toBe(403);
333
+
334
+ const grantInserts = inserts.filter((i) => i.table === "grant");
335
+ expect(grantInserts).toHaveLength(0);
336
+ });
337
+ });