@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,351 @@
1
+ import { eq, and } from "drizzle-orm";
2
+ import { Hono } from "hono";
3
+ import { describeRoute, resolver, validator } from "hono-openapi";
4
+
5
+ import { provider } from "@intx/db/schema";
6
+ import { getAncestorChain, parseProviderRow } from "@intx/db";
7
+ import type { DB } from "@intx/db";
8
+ import {
9
+ CreateProvider,
10
+ UpdateProvider,
11
+ ProviderResponse,
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 formatProvider(row: typeof provider.$inferSelect) {
30
+ const parsed = parseProviderRow(row);
31
+ return {
32
+ id: parsed.id,
33
+ tenantId: parsed.tenantId,
34
+ name: parsed.name,
35
+ plugin: parsed.plugin,
36
+ authorizationUrl: parsed.authorizationUrl ?? null,
37
+ tokenUrl: parsed.tokenUrl ?? null,
38
+ userInfoUrl: parsed.userInfoUrl ?? null,
39
+ scopes: parsed.scopes ?? null,
40
+ metadata: parsed.metadata,
41
+ createdAt: ts(parsed.createdAt),
42
+ updatedAt: ts(parsed.updatedAt),
43
+ };
44
+ }
45
+
46
+ export type CreateProviderRoutesDeps = {
47
+ db: DB["db"];
48
+ requireGrant: RequireGrant;
49
+ };
50
+
51
+ export function createProviderRoutes({
52
+ db,
53
+ requireGrant,
54
+ }: CreateProviderRoutesDeps): Hono<TenantEnv> {
55
+ const app = new Hono<TenantEnv>();
56
+
57
+ app.get(
58
+ "/",
59
+ requireGrant("provider:*", "read"),
60
+ describeRoute({
61
+ tags: ["Providers"],
62
+ summary: "List providers",
63
+ description:
64
+ "Lists provider definitions for the tenant, including those inherited from ancestor tenants.",
65
+ parameters: [
66
+ {
67
+ name: "inherited",
68
+ in: "query",
69
+ schema: { type: "string", enum: ["true", "false"] },
70
+ },
71
+ ...pageParameters,
72
+ ],
73
+ responses: {
74
+ 200: {
75
+ description: "List of providers",
76
+ content: {
77
+ "application/json": {
78
+ schema: resolver(paginatedSchema(ProviderResponse)),
79
+ },
80
+ },
81
+ },
82
+ },
83
+ }),
84
+ async (c) => {
85
+ const tenantCtx = c.get("tenant");
86
+ const inherited = c.req.query("inherited") !== "false";
87
+ const { limit, cursor } = parsePageParams({
88
+ cursor: c.req.query("cursor"),
89
+ limit: c.req.query("limit"),
90
+ });
91
+
92
+ if (inherited) {
93
+ const chain = await getAncestorChain(db, tenantCtx.id);
94
+ const seen = new Set<string>();
95
+ const items: ReturnType<typeof formatProvider>[] = [];
96
+
97
+ for (const tid of chain) {
98
+ const rows = await db.query.provider.findMany({
99
+ where: eq(provider.tenantId, tid),
100
+ });
101
+ for (const row of rows) {
102
+ if (!seen.has(row.name)) {
103
+ seen.add(row.name);
104
+ items.push(formatProvider(row));
105
+ }
106
+ }
107
+ }
108
+
109
+ return c.json({ data: items, nextCursor: null });
110
+ }
111
+
112
+ const conditions = [eq(provider.tenantId, tenantCtx.id)];
113
+ if (cursor) {
114
+ conditions.push(
115
+ cursorCondition(provider.createdAt, provider.id, cursor),
116
+ );
117
+ }
118
+
119
+ const rows = await db.query.provider.findMany({
120
+ where: and(...conditions),
121
+ orderBy: pageOrder(provider.createdAt, provider.id),
122
+ limit,
123
+ });
124
+
125
+ return c.json(paginatedResponse(rows.map(formatProvider), rows, limit));
126
+ },
127
+ );
128
+
129
+ app.post(
130
+ "/",
131
+ requireGrant("provider:*", "create"),
132
+ describeRoute({
133
+ tags: ["Providers"],
134
+ summary: "Create a provider definition",
135
+ description:
136
+ "Defines a new service provider for the tenant. The plugin field determines how Interchange integrates with the service.",
137
+ responses: {
138
+ 201: {
139
+ description: "Provider created",
140
+ content: {
141
+ "application/json": { schema: resolver(ProviderResponse) },
142
+ },
143
+ },
144
+ 400: {
145
+ description: "Validation error",
146
+ content: {
147
+ "application/json": { schema: resolver(ErrorResponse) },
148
+ },
149
+ },
150
+ 409: {
151
+ description: "Provider name already exists in this tenant",
152
+ content: {
153
+ "application/json": { schema: resolver(ErrorResponse) },
154
+ },
155
+ },
156
+ },
157
+ }),
158
+ validator("json", CreateProvider),
159
+ async (c) => {
160
+ const tenantCtx = c.get("tenant");
161
+ const body = c.req.valid("json");
162
+
163
+ const existing = await db.query.provider.findFirst({
164
+ where: and(
165
+ eq(provider.tenantId, tenantCtx.id),
166
+ eq(provider.name, body.name),
167
+ ),
168
+ });
169
+ if (existing) {
170
+ return c.json(
171
+ {
172
+ error: {
173
+ code: "conflict",
174
+ message: "Provider name already exists in this tenant",
175
+ },
176
+ },
177
+ 409,
178
+ );
179
+ }
180
+
181
+ const now = new Date();
182
+ const row = first(
183
+ await db
184
+ .insert(provider)
185
+ .values({
186
+ id: generateId("provider"),
187
+ tenantId: tenantCtx.id,
188
+ name: body.name,
189
+ plugin: body.plugin,
190
+ authorizationUrl: body.authorizationUrl ?? null,
191
+ tokenUrl: body.tokenUrl ?? null,
192
+ userInfoUrl: body.userInfoUrl ?? null,
193
+ scopes: body.scopes ?? null,
194
+ metadata: body.metadata ?? null,
195
+ createdAt: now,
196
+ updatedAt: now,
197
+ })
198
+ .returning(),
199
+ );
200
+
201
+ return c.json(formatProvider(row), 201);
202
+ },
203
+ );
204
+
205
+ app.get(
206
+ "/:providerId",
207
+ requireGrant(idResource("provider", "providerId"), "read"),
208
+ describeRoute({
209
+ tags: ["Providers"],
210
+ summary: "Get provider details",
211
+ responses: {
212
+ 200: {
213
+ description: "Provider details",
214
+ content: {
215
+ "application/json": { schema: resolver(ProviderResponse) },
216
+ },
217
+ },
218
+ 404: {
219
+ description: "Provider not found",
220
+ content: {
221
+ "application/json": { schema: resolver(ErrorResponse) },
222
+ },
223
+ },
224
+ },
225
+ }),
226
+ async (c) => {
227
+ const tenantCtx = c.get("tenant");
228
+ const providerId = c.req.param("providerId");
229
+
230
+ const row = await db.query.provider.findFirst({
231
+ where: eq(provider.id, providerId),
232
+ });
233
+
234
+ if (!row) {
235
+ return c.json(
236
+ { error: { code: "not_found", message: "Provider not found" } },
237
+ 404,
238
+ );
239
+ }
240
+
241
+ const chain = await getAncestorChain(db, tenantCtx.id);
242
+ if (!chain.includes(row.tenantId)) {
243
+ return c.json(
244
+ { error: { code: "not_found", message: "Provider not found" } },
245
+ 404,
246
+ );
247
+ }
248
+
249
+ return c.json(formatProvider(row));
250
+ },
251
+ );
252
+
253
+ app.patch(
254
+ "/:providerId",
255
+ requireGrant(idResource("provider", "providerId"), "manage"),
256
+ describeRoute({
257
+ tags: ["Providers"],
258
+ summary: "Update a provider definition",
259
+ description: "Only providers owned by this tenant can be updated.",
260
+ responses: {
261
+ 200: {
262
+ description: "Provider updated",
263
+ content: {
264
+ "application/json": { schema: resolver(ProviderResponse) },
265
+ },
266
+ },
267
+ 404: {
268
+ description: "Provider not found",
269
+ content: {
270
+ "application/json": { schema: resolver(ErrorResponse) },
271
+ },
272
+ },
273
+ },
274
+ }),
275
+ validator("json", UpdateProvider),
276
+ async (c) => {
277
+ const tenantCtx = c.get("tenant");
278
+ const providerId = c.req.param("providerId");
279
+ const body = c.req.valid("json");
280
+
281
+ const updates: Record<string, unknown> = { updatedAt: new Date() };
282
+ if (body.name !== undefined) updates["name"] = body.name;
283
+ if (body.plugin !== undefined) updates["plugin"] = body.plugin;
284
+ if (body.authorizationUrl !== undefined)
285
+ updates["authorizationUrl"] = body.authorizationUrl;
286
+ if (body.tokenUrl !== undefined) updates["tokenUrl"] = body.tokenUrl;
287
+ if (body.userInfoUrl !== undefined)
288
+ updates["userInfoUrl"] = body.userInfoUrl;
289
+ if (body.scopes !== undefined) updates["scopes"] = body.scopes;
290
+ if (body.metadata !== undefined) updates["metadata"] = body.metadata;
291
+
292
+ const [updated] = await db
293
+ .update(provider)
294
+ .set(updates)
295
+ .where(
296
+ and(eq(provider.id, providerId), eq(provider.tenantId, tenantCtx.id)),
297
+ )
298
+ .returning();
299
+
300
+ if (!updated) {
301
+ return c.json(
302
+ { error: { code: "not_found", message: "Provider not found" } },
303
+ 404,
304
+ );
305
+ }
306
+
307
+ return c.json(formatProvider(updated));
308
+ },
309
+ );
310
+
311
+ app.delete(
312
+ "/:providerId",
313
+ requireGrant(idResource("provider", "providerId"), "manage"),
314
+ describeRoute({
315
+ tags: ["Providers"],
316
+ summary: "Remove a provider definition",
317
+ description: "Only providers owned by this tenant can be removed.",
318
+ responses: {
319
+ 204: { description: "Provider removed" },
320
+ 404: {
321
+ description: "Provider not found",
322
+ content: {
323
+ "application/json": { schema: resolver(ErrorResponse) },
324
+ },
325
+ },
326
+ },
327
+ }),
328
+ async (c) => {
329
+ const tenantCtx = c.get("tenant");
330
+ const providerId = c.req.param("providerId");
331
+
332
+ const deleted = await db
333
+ .delete(provider)
334
+ .where(
335
+ and(eq(provider.id, providerId), eq(provider.tenantId, tenantCtx.id)),
336
+ )
337
+ .returning();
338
+
339
+ if (deleted.length === 0) {
340
+ return c.json(
341
+ { error: { code: "not_found", message: "Provider not found" } },
342
+ 404,
343
+ );
344
+ }
345
+
346
+ return c.body(null, 204);
347
+ },
348
+ );
349
+
350
+ return app;
351
+ }