@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,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>;
|