@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,370 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { Hono } from "hono";
3
+ import { describeRoute, resolver, validator } from "hono-openapi";
4
+
5
+ import { wallet, transaction } from "@intx/db/schema";
6
+ import { parseWalletRow, parseTransactionRow } from "@intx/db";
7
+ import type { DB } from "@intx/db";
8
+ import {
9
+ CreateWallet,
10
+ UpdateWallet,
11
+ WalletResponse,
12
+ TransactionResponse,
13
+ ErrorResponse,
14
+ paginatedSchema,
15
+ } from "@intx/types";
16
+
17
+ import type { TenantEnv } from "../context";
18
+ import { first, ts } 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
+ function formatWallet(row: typeof wallet.$inferSelect) {
31
+ const parsed = parseWalletRow(row);
32
+ return {
33
+ id: parsed.id,
34
+ tenantId: parsed.tenantId,
35
+ name: parsed.name,
36
+ backendType: parsed.backendType,
37
+ currency: parsed.currency,
38
+ balance: parsed.balance,
39
+ config: parsed.config ?? undefined,
40
+ createdAt: ts(parsed.createdAt),
41
+ updatedAt: ts(parsed.updatedAt),
42
+ };
43
+ }
44
+
45
+ function formatTransaction(row: typeof transaction.$inferSelect) {
46
+ const parsed = parseTransactionRow(row);
47
+ return {
48
+ id: parsed.id,
49
+ walletId: parsed.walletId,
50
+ agentId: parsed.agentId ?? null,
51
+ direction: parsed.direction,
52
+ amount: parsed.amount,
53
+ currency: parsed.currency,
54
+ recipientId: parsed.recipientId ?? null,
55
+ senderId: parsed.senderId ?? null,
56
+ requestId: parsed.requestId ?? null,
57
+ status: parsed.status,
58
+ createdAt: ts(parsed.createdAt),
59
+ };
60
+ }
61
+
62
+ export type CreateWalletRoutesDeps = {
63
+ db: DB["db"];
64
+ requireGrant: RequireGrant;
65
+ };
66
+
67
+ export function createWalletRoutes({
68
+ db,
69
+ requireGrant,
70
+ }: CreateWalletRoutesDeps): Hono<TenantEnv> {
71
+ const app = new Hono<TenantEnv>();
72
+
73
+ app.get(
74
+ "/",
75
+ requireGrant("wallet:*", "read"),
76
+ describeRoute({
77
+ tags: ["Wallets"],
78
+ summary: "List wallets in the tenant",
79
+ parameters: [...pageParameters],
80
+ responses: {
81
+ 200: {
82
+ description: "List of wallets",
83
+ content: {
84
+ "application/json": {
85
+ schema: resolver(paginatedSchema(WalletResponse)),
86
+ },
87
+ },
88
+ },
89
+ },
90
+ }),
91
+ async (c) => {
92
+ const tenantCtx = c.get("tenant");
93
+ const { limit, cursor } = parsePageParams({
94
+ cursor: c.req.query("cursor"),
95
+ limit: c.req.query("limit"),
96
+ });
97
+
98
+ const conditions = [eq(wallet.tenantId, tenantCtx.id)];
99
+ if (cursor) {
100
+ conditions.push(cursorCondition(wallet.createdAt, wallet.id, cursor));
101
+ }
102
+
103
+ const rows = await db.query.wallet.findMany({
104
+ where: and(...conditions),
105
+ orderBy: pageOrder(wallet.createdAt, wallet.id),
106
+ limit,
107
+ });
108
+
109
+ return c.json(paginatedResponse(rows.map(formatWallet), rows, limit));
110
+ },
111
+ );
112
+
113
+ app.post(
114
+ "/",
115
+ requireGrant("wallet:*", "create"),
116
+ describeRoute({
117
+ tags: ["Wallets"],
118
+ summary: "Create a wallet",
119
+ description:
120
+ "Creates a wallet with the specified payment backend and currency. Access for agents is managed through grants.",
121
+ responses: {
122
+ 201: {
123
+ description: "Wallet created",
124
+ content: {
125
+ "application/json": { schema: resolver(WalletResponse) },
126
+ },
127
+ },
128
+ 400: {
129
+ description: "Validation error",
130
+ content: {
131
+ "application/json": { schema: resolver(ErrorResponse) },
132
+ },
133
+ },
134
+ },
135
+ }),
136
+ validator("json", CreateWallet),
137
+ async (c) => {
138
+ const tenantCtx = c.get("tenant");
139
+ const body = c.req.valid("json");
140
+
141
+ const now = new Date();
142
+ const row = first(
143
+ await db
144
+ .insert(wallet)
145
+ .values({
146
+ id: generateId("wallet"),
147
+ tenantId: tenantCtx.id,
148
+ name: body.name,
149
+ backendType: body.backendType,
150
+ currency: body.currency,
151
+ balance: "0",
152
+ config: body.config ?? null,
153
+ createdAt: now,
154
+ updatedAt: now,
155
+ })
156
+ .returning(),
157
+ );
158
+
159
+ return c.json(formatWallet(row), 201);
160
+ },
161
+ );
162
+
163
+ app.get(
164
+ "/:walletId",
165
+ requireGrant(idResource("wallet", "walletId"), "read"),
166
+ describeRoute({
167
+ tags: ["Wallets"],
168
+ summary: "Get wallet details",
169
+ description: "Returns wallet details including current balance.",
170
+ responses: {
171
+ 200: {
172
+ description: "Wallet details",
173
+ content: {
174
+ "application/json": { schema: resolver(WalletResponse) },
175
+ },
176
+ },
177
+ 404: {
178
+ description: "Wallet not found",
179
+ content: {
180
+ "application/json": { schema: resolver(ErrorResponse) },
181
+ },
182
+ },
183
+ },
184
+ }),
185
+ async (c) => {
186
+ const tenantCtx = c.get("tenant");
187
+ const walletId = c.req.param("walletId");
188
+
189
+ const row = await db.query.wallet.findFirst({
190
+ where: and(eq(wallet.id, walletId), eq(wallet.tenantId, tenantCtx.id)),
191
+ });
192
+
193
+ if (!row) {
194
+ return c.json(
195
+ { error: { code: "not_found", message: "Wallet not found" } },
196
+ 404,
197
+ );
198
+ }
199
+
200
+ return c.json(formatWallet(row));
201
+ },
202
+ );
203
+
204
+ app.patch(
205
+ "/:walletId",
206
+ requireGrant(idResource("wallet", "walletId"), "manage"),
207
+ describeRoute({
208
+ tags: ["Wallets"],
209
+ summary: "Update wallet config",
210
+ responses: {
211
+ 200: {
212
+ description: "Wallet updated",
213
+ content: {
214
+ "application/json": { schema: resolver(WalletResponse) },
215
+ },
216
+ },
217
+ 404: {
218
+ description: "Wallet not found",
219
+ content: {
220
+ "application/json": { schema: resolver(ErrorResponse) },
221
+ },
222
+ },
223
+ },
224
+ }),
225
+ validator("json", UpdateWallet),
226
+ async (c) => {
227
+ const tenantCtx = c.get("tenant");
228
+ const walletId = c.req.param("walletId");
229
+ const body = c.req.valid("json");
230
+
231
+ const updates: Record<string, unknown> = { updatedAt: new Date() };
232
+ if (body.name !== undefined) updates["name"] = body.name;
233
+ if (body.config !== undefined) updates["config"] = body.config;
234
+
235
+ const [updated] = await db
236
+ .update(wallet)
237
+ .set(updates)
238
+ .where(and(eq(wallet.id, walletId), eq(wallet.tenantId, tenantCtx.id)))
239
+ .returning();
240
+
241
+ if (!updated) {
242
+ return c.json(
243
+ { error: { code: "not_found", message: "Wallet not found" } },
244
+ 404,
245
+ );
246
+ }
247
+
248
+ return c.json(formatWallet(updated));
249
+ },
250
+ );
251
+
252
+ app.delete(
253
+ "/:walletId",
254
+ requireGrant(idResource("wallet", "walletId"), "manage"),
255
+ describeRoute({
256
+ tags: ["Wallets"],
257
+ summary: "Deactivate a wallet",
258
+ responses: {
259
+ 204: {
260
+ description: "Wallet deactivated",
261
+ },
262
+ 404: {
263
+ description: "Wallet not found",
264
+ content: {
265
+ "application/json": { schema: resolver(ErrorResponse) },
266
+ },
267
+ },
268
+ },
269
+ }),
270
+ async (c) => {
271
+ const tenantCtx = c.get("tenant");
272
+ const walletId = c.req.param("walletId");
273
+
274
+ const deleted = await db
275
+ .delete(wallet)
276
+ .where(and(eq(wallet.id, walletId), eq(wallet.tenantId, tenantCtx.id)))
277
+ .returning();
278
+
279
+ if (deleted.length === 0) {
280
+ return c.json(
281
+ { error: { code: "not_found", message: "Wallet not found" } },
282
+ 404,
283
+ );
284
+ }
285
+
286
+ return c.body(null, 204);
287
+ },
288
+ );
289
+
290
+ app.get(
291
+ "/:walletId/transactions",
292
+ requireGrant(idResource("wallet", "walletId"), "read"),
293
+ describeRoute({
294
+ tags: ["Wallets"],
295
+ summary: "List transactions",
296
+ description:
297
+ "Transaction history for a wallet. Filterable by agent, date range, and status.",
298
+ parameters: [
299
+ { name: "agentId", in: "query", schema: { type: "string" } },
300
+ { name: "startTime", in: "query", schema: { type: "string" } },
301
+ { name: "endTime", in: "query", schema: { type: "string" } },
302
+ {
303
+ name: "status",
304
+ in: "query",
305
+ schema: { type: "string", enum: ["pending", "completed", "failed"] },
306
+ },
307
+ ...pageParameters,
308
+ ],
309
+ responses: {
310
+ 200: {
311
+ description: "List of transactions",
312
+ content: {
313
+ "application/json": {
314
+ schema: resolver(paginatedSchema(TransactionResponse)),
315
+ },
316
+ },
317
+ },
318
+ },
319
+ }),
320
+ async (c) => {
321
+ const tenantCtx = c.get("tenant");
322
+ const walletId = c.req.param("walletId");
323
+
324
+ const walletRow = await db.query.wallet.findFirst({
325
+ where: and(eq(wallet.id, walletId), eq(wallet.tenantId, tenantCtx.id)),
326
+ });
327
+
328
+ if (!walletRow) {
329
+ return c.json(
330
+ { error: { code: "not_found", message: "Wallet not found" } },
331
+ 404,
332
+ );
333
+ }
334
+
335
+ const agentId = c.req.query("agentId");
336
+ const status = c.req.query("status");
337
+ const { limit, cursor } = parsePageParams({
338
+ cursor: c.req.query("cursor"),
339
+ limit: c.req.query("limit"),
340
+ });
341
+
342
+ const conditions = [eq(transaction.walletId, walletId)];
343
+ if (agentId) conditions.push(eq(transaction.agentId, agentId));
344
+ if (
345
+ status === "pending" ||
346
+ status === "completed" ||
347
+ status === "failed"
348
+ ) {
349
+ conditions.push(eq(transaction.status, status));
350
+ }
351
+ if (cursor) {
352
+ conditions.push(
353
+ cursorCondition(transaction.createdAt, transaction.id, cursor),
354
+ );
355
+ }
356
+
357
+ const rows = await db.query.transaction.findMany({
358
+ where: and(...conditions),
359
+ orderBy: pageOrder(transaction.createdAt, transaction.id),
360
+ limit,
361
+ });
362
+
363
+ return c.json(
364
+ paginatedResponse(rows.map(formatTransaction), rows, limit),
365
+ );
366
+ },
367
+ );
368
+
369
+ return app;
370
+ }
package/src/session.ts ADDED
@@ -0,0 +1,44 @@
1
+ // Hub-owned session contract.
2
+ //
3
+ // SessionUser and SessionInfo mirror the structural shape that better-auth
4
+ // currently returns (see @better-auth/core/dist/db/schema/{user,session}),
5
+ // but the hub does not reference Auth["$Infer"]. This breaks the type-level
6
+ // dependency on better-auth so a third-party identity provider can be
7
+ // plugged in by satisfying the GetSession contract.
8
+ //
9
+ // The shapes are kept intentionally hand-written: deriving with Pick<>
10
+ // against the better-auth types would re-introduce the type dependency.
11
+ // The trade-off is that a field added upstream in better-auth would not
12
+ // surface here automatically.
13
+ //
14
+ // The optional `?: T | null | undefined` shape (rather than `?: T | null`)
15
+ // is deliberate. Under exactOptionalPropertyTypes, the latter rejects an
16
+ // explicit `undefined` assignment, but the inferred return of
17
+ // z.string().nullish() in better-auth's user/session schemas is
18
+ // `string | null | undefined`. Without the trailing `| undefined`, the
19
+ // adapter in apps/hub cannot pass the result through structurally.
20
+
21
+ export type SessionUser = {
22
+ id: string;
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ email: string;
26
+ emailVerified: boolean;
27
+ name: string;
28
+ image?: string | null | undefined;
29
+ };
30
+
31
+ export type SessionInfo = {
32
+ id: string;
33
+ createdAt: Date;
34
+ updatedAt: Date;
35
+ userId: string;
36
+ expiresAt: Date;
37
+ token: string;
38
+ ipAddress?: string | null | undefined;
39
+ userAgent?: string | null | undefined;
40
+ };
41
+
42
+ export type GetSession = (
43
+ headers: Headers,
44
+ ) => Promise<{ user: SessionUser; session: SessionInfo } | null>;