@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
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @intx/hub-api
2
+
3
+ Hono application factory for the hub. Owns the public HTTP API
4
+ surface: routes, middleware (session, tenant resolution, grant
5
+ enforcement), authentication via better-auth, and the OpenAPI
6
+ spec served from the running app.
7
+
8
+ Consumed by `apps/hub` as the HTTP entry point. The factory takes
9
+ the database client, session service, sidecar router, repo store,
10
+ asset service, and event collector registry from `@intx/db` and
11
+ `@intx/hub-sessions` and returns a configured Hono app.
12
+
13
+ `createApp` returns a configured Hono app. Its `CreateAppOpts`
14
+ parameter wires together the dependencies the API needs: a
15
+ better-auth `authHandler` and matching `getSession`, the database
16
+ client (`db`), the `SidecarRouter` and `SessionService` from
17
+ `@intx/hub-sessions`, and an `EventCollectorRegistry`. The
18
+ `assetService` and `repoStore` fields must be supplied but accept
19
+ `null` for deployments that don't host the git surface; the
20
+ `grantStore` and `sidecarWsHandler` fields are fully optional. See
21
+ `CreateAppOpts` in `src/app.ts` for the exact field list. The
22
+ companion `createAuth` factory builds the better-auth instance
23
+ that supplies `authHandler` and `getSession`.
24
+
25
+ `createRequireGrant` is the grant-enforcement middleware factory
26
+ exposed for callers that mount additional routes against the same
27
+ authorization stack; it composes with the tenant and session
28
+ middleware so every route sees a resolved principal, tenant, and
29
+ grant decision.
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@intx/hub-api",
3
+ "version": "0.1.2",
4
+ "license": "LGPL-2.1-only",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ }
11
+ },
12
+ "dependencies": {
13
+ "@hono/standard-validator": "^0.2.2",
14
+ "@intx/authz": "0.0.0",
15
+ "@intx/crypto-node": "0.0.0",
16
+ "@intx/db": "0.0.0",
17
+ "@intx/hub-common": "0.0.0",
18
+ "@intx/hub-sessions": "0.0.0",
19
+ "@intx/log": "0.0.0",
20
+ "@intx/mime": "0.0.0",
21
+ "@intx/storage-isogit": "0.0.0",
22
+ "@intx/types": "0.0.0",
23
+ "arktype": "^2.1.29",
24
+ "better-auth": "^1.4.18",
25
+ "hono": "^4.11.9",
26
+ "hono-openapi": "^1.2.0"
27
+ }
28
+ }
@@ -0,0 +1,225 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { type } from "arktype";
3
+ import { Hono } from "hono";
4
+ import type { DB } from "@intx/db";
5
+ import { createApp, createHubContextMiddleware, mountHubRoutes } from "./app";
6
+ import type { AppEnv } from "./context";
7
+ import {
8
+ createEventCollectorRegistry,
9
+ createSidecarRouter,
10
+ type SessionService,
11
+ } from "@intx/hub-sessions";
12
+ import type { GetSession } from "./session";
13
+
14
+ const OpenAPISpec = type({
15
+ info: { title: "string", version: "string" },
16
+ paths: "Record<string, Record<string, unknown>>",
17
+ });
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- drizzle PgDatabase type cannot be structurally satisfied in tests
20
+ const mockDb = {} as unknown as DB["db"];
21
+ const sidecarRouter = createSidecarRouter({});
22
+ const sessionService: SessionService = {
23
+ launchSession(_params) {
24
+ throw new Error("mock: sessionService.launchSession not implemented");
25
+ },
26
+ sendUserMessage(_params) {
27
+ throw new Error("mock: sessionService.sendUserMessage not implemented");
28
+ },
29
+ endSession(_addr, _reason) {
30
+ throw new Error("mock: sessionService.endSession not implemented");
31
+ },
32
+ };
33
+ const eventCollectors = createEventCollectorRegistry({ db: mockDb });
34
+
35
+ const app = createApp({
36
+ getSession: async () => null,
37
+ authHandler: () => new Response("", { status: 404 }),
38
+ db: mockDb,
39
+ sidecarRouter,
40
+ sessionService,
41
+ eventCollectors,
42
+ assetService: null,
43
+ repoStore: null,
44
+ });
45
+
46
+ describe("app", () => {
47
+ test("GET /status returns ok", async () => {
48
+ const res = await app.request("/status");
49
+ expect(res.status).toBe(200);
50
+ const body = await res.json();
51
+ expect(body).toEqual({ status: "ok" });
52
+ });
53
+
54
+ test("GET /openapi.json returns a valid spec with expected tags", async () => {
55
+ const res = await app.request("/openapi.json");
56
+ expect(res.status).toBe(200);
57
+
58
+ const spec = OpenAPISpec.assert(await res.json());
59
+
60
+ expect(spec.info.title).toBe("Interchange Hub");
61
+ expect(spec.info.version).toBe("0.0.0");
62
+
63
+ const paths = Object.keys(spec.paths);
64
+ expect(paths.length).toBeGreaterThan(50);
65
+
66
+ const tags = new Set<string>();
67
+ for (const methods of Object.values(spec.paths)) {
68
+ for (const op of Object.values(methods)) {
69
+ if (typeof op !== "object" || op === null) continue;
70
+ if (!("tags" in op)) continue;
71
+ const { tags: opTags } = op;
72
+ if (!Array.isArray(opTags)) continue;
73
+ for (const tag of opTags) {
74
+ if (typeof tag === "string") tags.add(tag);
75
+ }
76
+ }
77
+ }
78
+
79
+ const expectedTags = [
80
+ "User",
81
+ "Tenants",
82
+ "Principals",
83
+ "Roles",
84
+ "Grants",
85
+ "Agents",
86
+ "Instances",
87
+ "Approvals",
88
+ "Wallets",
89
+ "Credentials",
90
+ "Discovery",
91
+ "Observability",
92
+ "Agent Data",
93
+ "Sidecars",
94
+ ];
95
+
96
+ for (const tag of expectedTags) {
97
+ expect(tags.has(tag)).toBe(true);
98
+ }
99
+ });
100
+
101
+ test("federation routes do not double the /federation path segment", async () => {
102
+ const res = await app.request("/openapi.json");
103
+ const spec = OpenAPISpec.assert(await res.json());
104
+ const federationPaths = Object.keys(spec.paths).filter((p) =>
105
+ p.includes("federation"),
106
+ );
107
+
108
+ expect(federationPaths.length).toBeGreaterThan(0);
109
+ for (const p of federationPaths) {
110
+ expect(p).not.toContain("federation/federation");
111
+ }
112
+ });
113
+
114
+ test("POST with invalid body returns 400", async () => {
115
+ const res = await app.request("/api/tenants", {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({}),
119
+ });
120
+ expect(res.status).toBe(400);
121
+ });
122
+
123
+ test("opting out of asset+repo-store hides the git-tokens mint surface", async () => {
124
+ // Token minting is only useful when at least one smart-HTTP route
125
+ // consumes the tokens. With `assetService: null` and
126
+ // `repoStore: null` there is no consumer, so the OpenAPI spec
127
+ // must not advertise the `Git Tokens` tag and the mint paths
128
+ // must not appear. The HTTP-request shape would test the same
129
+ // invariant but is noisier — the auth middleware fronts the
130
+ // `/api/me/*` tree and short-circuits with 401 before the router
131
+ // matches, masking whether the route exists.
132
+ const res = await app.request("/openapi.json");
133
+ const spec = OpenAPISpec.assert(await res.json());
134
+
135
+ const tags = new Set<string>();
136
+ for (const methods of Object.values(spec.paths)) {
137
+ for (const op of Object.values(methods)) {
138
+ if (typeof op !== "object" || op === null) continue;
139
+ if (!("tags" in op)) continue;
140
+ const { tags: opTags } = op;
141
+ if (!Array.isArray(opTags)) continue;
142
+ for (const tag of opTags) {
143
+ if (typeof tag === "string") tags.add(tag);
144
+ }
145
+ }
146
+ }
147
+ expect(tags.has("Git Tokens")).toBe(false);
148
+
149
+ const paths = Object.keys(spec.paths);
150
+ const gitTokenPaths = paths.filter((p) => p.includes("git-tokens"));
151
+ expect(gitTokenPaths).toEqual([]);
152
+ });
153
+ });
154
+
155
+ describe("mountHubRoutes composition", () => {
156
+ const stubUser = {
157
+ id: "usr_compose",
158
+ email: "compose@example.com",
159
+ emailVerified: true,
160
+ name: "Compose Test",
161
+ image: null,
162
+ createdAt: new Date("2025-01-01"),
163
+ updatedAt: new Date("2025-01-01"),
164
+ };
165
+ const stubSession = {
166
+ id: "ses_compose",
167
+ userId: stubUser.id,
168
+ token: "tok_compose",
169
+ expiresAt: new Date("2999-01-01"),
170
+ createdAt: stubUser.createdAt,
171
+ updatedAt: stubUser.updatedAt,
172
+ };
173
+ const getSession: GetSession = async () => ({
174
+ user: stubUser,
175
+ session: stubSession,
176
+ });
177
+
178
+ test("third-party Hono app shares the hub's request context", async () => {
179
+ const thirdParty = new Hono<AppEnv>();
180
+ thirdParty.use(createHubContextMiddleware({ getSession }));
181
+
182
+ // A sibling route owned by the third party reads from the same
183
+ // request context the hub routes use.
184
+ thirdParty.get("/sibling/whoami", (c) => {
185
+ const user = c.get("user");
186
+ return c.json({ userId: user?.id ?? null });
187
+ });
188
+
189
+ mountHubRoutes(thirdParty, {
190
+ db: mockDb,
191
+ sidecarRouter,
192
+ sessionService,
193
+ eventCollectors,
194
+ assetService: null,
195
+ repoStore: null,
196
+ });
197
+
198
+ const siblingRes = await thirdParty.request("/sibling/whoami");
199
+ expect(siblingRes.status).toBe(200);
200
+ expect(await siblingRes.json()).toEqual({ userId: stubUser.id });
201
+
202
+ // Hub routes mounted on the same app respond as expected.
203
+ const statusRes = await thirdParty.request("/status");
204
+ expect(statusRes.status).toBe(200);
205
+ expect(await statusRes.json()).toEqual({ status: "ok" });
206
+ });
207
+
208
+ test("mountHubRoutes does not mount an auth handler at /api/auth/*", async () => {
209
+ const thirdParty = new Hono<AppEnv>();
210
+ thirdParty.use(createHubContextMiddleware({ getSession }));
211
+ mountHubRoutes(thirdParty, {
212
+ db: mockDb,
213
+ sidecarRouter,
214
+ sessionService,
215
+ eventCollectors,
216
+ assetService: null,
217
+ repoStore: null,
218
+ });
219
+
220
+ // Without an auth handler mounted by the caller, /api/auth/* is
221
+ // free for the third party to wire as they choose.
222
+ const res = await thirdParty.request("/api/auth/anything");
223
+ expect(res.status).toBe(404);
224
+ });
225
+ });
package/src/app.ts ADDED
@@ -0,0 +1,382 @@
1
+ import type { Handler, MiddlewareHandler } from "hono";
2
+ import { Hono } from "hono";
3
+ import { openAPIRouteHandler } from "hono-openapi";
4
+
5
+ import { honoLogger, type HonoContext } from "@intx/log/hono";
6
+ import { timeWindowEvaluator } from "@intx/authz";
7
+ import { type DB, createGrantStore } from "@intx/db";
8
+ import type { ConditionRegistry, GrantStore } from "@intx/types/authz";
9
+
10
+ import type { AppEnv } from "./context";
11
+ import { createSessionMiddleware } from "./middleware/session";
12
+ import { createRequireGrant, type RequireGrant } from "./middleware/grant";
13
+ import { createResolveTenant, requireAuth } from "./middleware/tenant";
14
+ import type { GetSession } from "./session";
15
+ import type {
16
+ AssetService,
17
+ EventCollectorRegistry,
18
+ RepoStore,
19
+ SessionService,
20
+ SidecarRouter,
21
+ } from "@intx/hub-sessions";
22
+
23
+ import { createMeRoutes } from "./routes/me";
24
+ import { createTenantRoutes } from "./routes/tenants";
25
+ import { createTenantFederationRoutes } from "./routes/tenant-federation";
26
+ import { createPrincipalRoutes, createInviteRoutes } from "./routes/principals";
27
+ import { createRoleRoutes, createRoleAssignRoutes } from "./routes/roles";
28
+ import { createGrantRoutes, createEvaluateRoutes } from "./routes/grants";
29
+ import { createAgentRoutes } from "./routes/agents";
30
+ import { createInstanceRoutes } from "./routes/instances";
31
+ import { createApprovalRoutes } from "./routes/approvals";
32
+ import { createWalletRoutes } from "./routes/wallets";
33
+ import { createProviderRoutes } from "./routes/providers";
34
+ import { createOAuthClientRoutes } from "./routes/oauth-clients";
35
+ import { createCredentialRoutes } from "./routes/credentials";
36
+ import { createOfferingRoutes, createModelRoutes } from "./routes/offerings";
37
+ import { createObservabilityRoutes } from "./routes/observability";
38
+ import { createAgentDataRoutes } from "./routes/agent-data";
39
+ import { createSidecarRoutes } from "./routes/sidecars";
40
+ import {
41
+ createMeGitTokenRoutes,
42
+ createTenantGitTokenRoutes,
43
+ } from "./routes/git-tokens";
44
+ import {
45
+ ASSET_OPENAPI_EXCLUDE_GLOBS,
46
+ createAssetRoutes,
47
+ } from "./routes/assets";
48
+ import {
49
+ AGENT_STATE_OPENAPI_EXCLUDE_GLOBS,
50
+ createAgentStateDefinitionGitRoutes,
51
+ createAgentStateInstanceGitRoutes,
52
+ createAgentStateReceivePackDeny,
53
+ } from "./routes/agent-state-git";
54
+ import { createGitTokenAuth } from "./middleware/git-token-auth";
55
+
56
+ export type CreateHubContextMiddlewareDeps = {
57
+ getSession: GetSession;
58
+ };
59
+
60
+ /**
61
+ * Builds the per-request context middleware that resolves the
62
+ * authenticated user and session from the incoming request and
63
+ * exposes them via the Hono variable bag.
64
+ */
65
+ export function createHubContextMiddleware({
66
+ getSession,
67
+ }: CreateHubContextMiddlewareDeps): MiddlewareHandler<AppEnv> {
68
+ return createSessionMiddleware(getSession);
69
+ }
70
+
71
+ export type MountHubRoutesDeps = {
72
+ db: DB["db"];
73
+ sidecarRouter: SidecarRouter;
74
+ sessionService: SessionService;
75
+ eventCollectors: EventCollectorRegistry;
76
+ grantStore?: GrantStore;
77
+ conditionRegistry?: ConditionRegistry;
78
+ sidecarWsHandler?: Handler<AppEnv>;
79
+ /**
80
+ * The asset REST endpoint and smart-HTTP route group mount under
81
+ * `/api/tenants/:tenantId/assets` when both are supplied. Tests
82
+ * that have no reason to exercise the asset surface MUST pass
83
+ * `null` for both to opt out explicitly; passing only one is a
84
+ * wiring bug and throws at construction.
85
+ */
86
+ assetService: AssetService | null;
87
+ repoStore: RepoStore | null;
88
+ };
89
+
90
+ /**
91
+ * Mounts every hub route group, middleware, and supporting endpoint
92
+ * onto the provided Hono application. The caller is responsible for
93
+ * having mounted the request logger and the context middleware first,
94
+ * and for wiring their own auth handler at the path of their choice.
95
+ *
96
+ * `grantStore` and `conditionRegistry` default to the hub's standard
97
+ * choices (a database-backed grant store and the time-window condition
98
+ * evaluator) when not supplied.
99
+ */
100
+ export function mountHubRoutes(
101
+ app: Hono<AppEnv>,
102
+ opts: MountHubRoutesDeps,
103
+ ): void {
104
+ const {
105
+ db,
106
+ sidecarRouter,
107
+ sessionService,
108
+ eventCollectors,
109
+ sidecarWsHandler,
110
+ assetService,
111
+ repoStore,
112
+ } = opts;
113
+ if ((assetService === null) !== (repoStore === null)) {
114
+ throw new Error(
115
+ "mountHubRoutes: assetService and repoStore must be provided together or both omitted",
116
+ );
117
+ }
118
+ const grantStore = opts.grantStore ?? createGrantStore(db);
119
+ const conditionRegistry: ConditionRegistry = opts.conditionRegistry ?? {
120
+ time_window: timeWindowEvaluator,
121
+ };
122
+ const requireGrant: RequireGrant = createRequireGrant({
123
+ grantStore,
124
+ conditionRegistry,
125
+ });
126
+ const resolveTenant = createResolveTenant({ db });
127
+
128
+ app.get("/status", (c) => c.json({ status: "ok" }));
129
+
130
+ // User-scoped (cross-tenant) -- requires auth but not tenant membership
131
+ app.use("/api/me/*", requireAuth);
132
+ app.route("/api/me", createMeRoutes({ db }));
133
+
134
+ // The git-tokens mint surface mounts under the same gate as the
135
+ // smart-HTTP route groups: tokens are only useful when at least one
136
+ // smart-HTTP route consumes them. Both deps null = no smart-HTTP
137
+ // anywhere = no token-mint endpoints.
138
+ if (repoStore !== null) {
139
+ app.route("/api/me/git-tokens", createMeGitTokenRoutes({ db }));
140
+ }
141
+
142
+ // Smart-HTTP asset routes use bearer authentication instead of
143
+ // session+tenant resolution. The bearer middleware mounts ahead of
144
+ // resolveTenant so it populates `principal` + `tenant` first; the
145
+ // tenant resolver short-circuits when both are already set, which
146
+ // lets bearer-only requests bypass the session-required path.
147
+ //
148
+ // The gate is `repoStore !== null` rather than the two-dep check;
149
+ // the XOR throw above already guarantees the deps move as a unit,
150
+ // so checking either one is equivalent. Keeping a single shape
151
+ // across every gate site makes the contract obvious to a reader.
152
+ if (repoStore !== null) {
153
+ app.use(
154
+ "/api/tenants/:tenantId/assets/:kind/:nameDotGit/*",
155
+ createGitTokenAuth({ db }),
156
+ );
157
+ }
158
+
159
+ // Agent-state smart-HTTP read routes also use bearer auth. The
160
+ // receive-pack denial middleware mounts BEFORE bearer auth so an
161
+ // unauthenticated `git push -v` parses the pkt-line ERR record
162
+ // rather than a generic 401. The bearer middleware then gates the
163
+ // upload-pack half (advertise + POST) on a valid token.
164
+ if (repoStore !== null) {
165
+ app.use(
166
+ "/api/tenants/:tenantId/agents/instances/:instanceId/state.git/*",
167
+ createAgentStateReceivePackDeny(),
168
+ );
169
+ app.use(
170
+ "/api/tenants/:tenantId/agents/definitions/:agentId/state.git/*",
171
+ createAgentStateReceivePackDeny(),
172
+ );
173
+ app.use(
174
+ "/api/tenants/:tenantId/agents/instances/:instanceId/state.git/*",
175
+ createGitTokenAuth({ db }),
176
+ );
177
+ app.use(
178
+ "/api/tenants/:tenantId/agents/definitions/:agentId/state.git/*",
179
+ createGitTokenAuth({ db }),
180
+ );
181
+ }
182
+
183
+ // Tenant-scoped middleware -- require auth + tenant membership for any
184
+ // path under /api/tenants/:tenantId/*. Must be registered before routes
185
+ // so Hono includes it in the middleware chain.
186
+ app.use("/api/tenants/:tenantId/*", resolveTenant);
187
+
188
+ // Global tenant routes (create needs auth, detail/update handle auth inline)
189
+ app.route("/api/tenants", createTenantRoutes({ db }));
190
+ app.route("/api/models", createModelRoutes());
191
+
192
+ // Tenant-scoped routes
193
+ app.route(
194
+ "/api/tenants/:tenantId/principals",
195
+ createPrincipalRoutes({ db, requireGrant }),
196
+ );
197
+ app.route(
198
+ "/api/tenants/:tenantId/members/invite",
199
+ createInviteRoutes({ db, requireGrant }),
200
+ );
201
+ app.route(
202
+ "/api/tenants/:tenantId/roles",
203
+ createRoleRoutes({ db, requireGrant }),
204
+ );
205
+ app.route(
206
+ "/api/tenants/:tenantId/principals/:principalId/roles",
207
+ createRoleAssignRoutes({ db, requireGrant }),
208
+ );
209
+ app.route(
210
+ "/api/tenants/:tenantId/grants",
211
+ createGrantRoutes({ db, requireGrant }),
212
+ );
213
+ app.route(
214
+ "/api/tenants/:tenantId/principals/:principalId/evaluate",
215
+ createEvaluateRoutes({ db, grantStore, conditionRegistry }),
216
+ );
217
+ app.route(
218
+ "/api/tenants/:tenantId/agents/definitions",
219
+ createAgentRoutes({ db, requireGrant }),
220
+ );
221
+ app.route(
222
+ "/api/tenants/:tenantId/agents/instances",
223
+ createInstanceRoutes({
224
+ db,
225
+ sessionService,
226
+ sidecarRouter,
227
+ eventCollectors,
228
+ grantStore,
229
+ conditionRegistry,
230
+ requireGrant,
231
+ }),
232
+ );
233
+
234
+ app.route("/api/tenants/:tenantId/approvals", createApprovalRoutes());
235
+ app.route(
236
+ "/api/tenants/:tenantId/wallets",
237
+ createWalletRoutes({ db, requireGrant }),
238
+ );
239
+ app.route(
240
+ "/api/tenants/:tenantId/providers",
241
+ createProviderRoutes({ db, requireGrant }),
242
+ );
243
+ app.route(
244
+ "/api/tenants/:tenantId/oauth-clients",
245
+ createOAuthClientRoutes({ db, requireGrant }),
246
+ );
247
+ app.route(
248
+ "/api/tenants/:tenantId/credentials",
249
+ createCredentialRoutes({ db, sidecarRouter, requireGrant }),
250
+ );
251
+ app.route(
252
+ "/api/tenants/:tenantId/offerings",
253
+ createOfferingRoutes({ db, requireGrant }),
254
+ );
255
+ if (repoStore !== null) {
256
+ app.route(
257
+ "/api/tenants/:tenantId/git-tokens",
258
+ createTenantGitTokenRoutes({ db, requireGrant }),
259
+ );
260
+ }
261
+ app.route("/api/tenants/:tenantId", createObservabilityRoutes());
262
+ app.route(
263
+ "/api/tenants/:tenantId/federation",
264
+ createTenantFederationRoutes({ db }),
265
+ );
266
+ app.route("/api/tenants/:tenantId/agents/:agentId", createAgentDataRoutes());
267
+
268
+ if (assetService !== null && repoStore !== null) {
269
+ app.route(
270
+ "/api/tenants/:tenantId/assets",
271
+ createAssetRoutes({
272
+ db,
273
+ assetService,
274
+ repoStore,
275
+ grantStore,
276
+ conditionRegistry,
277
+ requireGrant,
278
+ }),
279
+ );
280
+ }
281
+
282
+ if (repoStore !== null) {
283
+ app.route(
284
+ "/api/tenants/:tenantId/agents/instances",
285
+ createAgentStateInstanceGitRoutes({
286
+ db,
287
+ repoStore,
288
+ grantStore,
289
+ conditionRegistry,
290
+ }),
291
+ );
292
+ app.route(
293
+ "/api/tenants/:tenantId/agents/definitions",
294
+ createAgentStateDefinitionGitRoutes({
295
+ db,
296
+ repoStore,
297
+ grantStore,
298
+ conditionRegistry,
299
+ }),
300
+ );
301
+ }
302
+
303
+ app.route(
304
+ "/api/sidecars",
305
+ createSidecarRoutes(
306
+ sidecarWsHandler ? { db, wsHandler: sidecarWsHandler } : { db },
307
+ ),
308
+ );
309
+ }
310
+
311
+ export type CreateAppOpts = {
312
+ getSession: GetSession;
313
+ authHandler: Handler<AppEnv>;
314
+ db: DB["db"];
315
+ sidecarRouter: SidecarRouter;
316
+ sessionService: SessionService;
317
+ eventCollectors: EventCollectorRegistry;
318
+ grantStore?: GrantStore;
319
+ sidecarWsHandler?: Handler<AppEnv>;
320
+ assetService: AssetService | null;
321
+ repoStore: RepoStore | null;
322
+ };
323
+
324
+ export function createApp({
325
+ getSession,
326
+ authHandler,
327
+ db,
328
+ sidecarRouter,
329
+ sessionService,
330
+ eventCollectors,
331
+ grantStore,
332
+ sidecarWsHandler,
333
+ assetService,
334
+ repoStore,
335
+ }: CreateAppOpts) {
336
+ const app = new Hono<AppEnv>();
337
+
338
+ app.use(
339
+ honoLogger({
340
+ category: ["hub", "requests"],
341
+ skip: (c: HonoContext) => c.req.path === "/status",
342
+ }),
343
+ );
344
+
345
+ app.use(createHubContextMiddleware({ getSession }));
346
+
347
+ app.all("/api/auth/*", authHandler);
348
+
349
+ mountHubRoutes(app, {
350
+ db,
351
+ sidecarRouter,
352
+ sessionService,
353
+ eventCollectors,
354
+ assetService,
355
+ repoStore,
356
+ ...(grantStore ? { grantStore } : {}),
357
+ ...(sidecarWsHandler ? { sidecarWsHandler } : {}),
358
+ });
359
+
360
+ app.get(
361
+ "/openapi.json",
362
+ openAPIRouteHandler(app, {
363
+ documentation: {
364
+ info: {
365
+ title: "Interchange Hub",
366
+ version: "0.0.0",
367
+ },
368
+ },
369
+ exclude: [
370
+ "/openapi.json",
371
+ "/status",
372
+ "/api/auth/**",
373
+ ...ASSET_OPENAPI_EXCLUDE_GLOBS,
374
+ ...AGENT_STATE_OPENAPI_EXCLUDE_GLOBS,
375
+ ],
376
+ }),
377
+ );
378
+
379
+ return app;
380
+ }
381
+
382
+ export type App = ReturnType<typeof createApp>;
package/src/auth.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { DB } from "@intx/db";
2
+ import { betterAuth } from "better-auth";
3
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
4
+
5
+ export function createAuth(db: DB["db"]) {
6
+ return betterAuth({
7
+ baseURL: process.env["BETTER_AUTH_BASE_URL"] ?? "http://localhost:3000",
8
+ database: drizzleAdapter(db, { provider: "pg" }),
9
+ emailAndPassword: {
10
+ enabled: true,
11
+ },
12
+ socialProviders: {
13
+ google: {
14
+ clientId: process.env["GOOGLE_CLIENT_ID"] ?? "",
15
+ clientSecret: process.env["GOOGLE_CLIENT_SECRET"] ?? "",
16
+ },
17
+ },
18
+ });
19
+ }
20
+
21
+ export type Auth = ReturnType<typeof createAuth>;