@treeseed/agent 0.8.5

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 (138) hide show
  1. package/Dockerfile +7 -0
  2. package/README.md +198 -0
  3. package/dist/agent-runtime.d.ts +17 -0
  4. package/dist/agent-runtime.js +117 -0
  5. package/dist/agents/adapters/execution.d.ts +41 -0
  6. package/dist/agents/adapters/execution.js +73 -0
  7. package/dist/agents/adapters/mutations.d.ts +22 -0
  8. package/dist/agents/adapters/mutations.js +30 -0
  9. package/dist/agents/adapters/notification.d.ts +26 -0
  10. package/dist/agents/adapters/notification.js +46 -0
  11. package/dist/agents/adapters/repository.d.ts +28 -0
  12. package/dist/agents/adapters/repository.js +61 -0
  13. package/dist/agents/adapters/research.d.ts +26 -0
  14. package/dist/agents/adapters/research.js +59 -0
  15. package/dist/agents/adapters/verification.d.ts +36 -0
  16. package/dist/agents/adapters/verification.js +62 -0
  17. package/dist/agents/cli-tools.d.ts +1 -0
  18. package/dist/agents/cli-tools.js +5 -0
  19. package/dist/agents/cli.d.ts +15 -0
  20. package/dist/agents/cli.js +109 -0
  21. package/dist/agents/contracts/messages.d.ts +88 -0
  22. package/dist/agents/contracts/messages.js +138 -0
  23. package/dist/agents/contracts/run.d.ts +21 -0
  24. package/dist/agents/contracts/run.js +0 -0
  25. package/dist/agents/index.d.ts +1 -0
  26. package/dist/agents/index.js +5 -0
  27. package/dist/agents/kernel/agent-kernel.d.ts +63 -0
  28. package/dist/agents/kernel/agent-kernel.js +291 -0
  29. package/dist/agents/kernel/trigger-resolver.d.ts +19 -0
  30. package/dist/agents/kernel/trigger-resolver.js +157 -0
  31. package/dist/agents/registry-helper.d.ts +4 -0
  32. package/dist/agents/registry-helper.js +14 -0
  33. package/dist/agents/registry.d.ts +6 -0
  34. package/dist/agents/registry.js +98 -0
  35. package/dist/agents/runtime-types.d.ts +118 -0
  36. package/dist/agents/runtime-types.js +0 -0
  37. package/dist/agents/spec-loader.d.ts +18 -0
  38. package/dist/agents/spec-loader.js +54 -0
  39. package/dist/agents/spec-normalizer.d.ts +2 -0
  40. package/dist/agents/spec-normalizer.js +327 -0
  41. package/dist/agents/spec-types.d.ts +64 -0
  42. package/dist/agents/spec-types.js +0 -0
  43. package/dist/agents/testing/agents-smoke.d.ts +1 -0
  44. package/dist/agents/testing/agents-smoke.js +32 -0
  45. package/dist/agents/testing/e2e-harness.d.ts +44 -0
  46. package/dist/agents/testing/e2e-harness.js +503 -0
  47. package/dist/api/agent-routes.d.ts +13 -0
  48. package/dist/api/agent-routes.js +327 -0
  49. package/dist/api/app.d.ts +8 -0
  50. package/dist/api/app.js +444 -0
  51. package/dist/api/auth/d1-database.d.ts +3 -0
  52. package/dist/api/auth/d1-database.js +20 -0
  53. package/dist/api/auth/d1-provider.d.ts +79 -0
  54. package/dist/api/auth/d1-provider.js +92 -0
  55. package/dist/api/auth/d1-store.d.ts +114 -0
  56. package/dist/api/auth/d1-store.js +895 -0
  57. package/dist/api/auth/memory-provider.d.ts +77 -0
  58. package/dist/api/auth/memory-provider.js +249 -0
  59. package/dist/api/auth/rbac.d.ts +22 -0
  60. package/dist/api/auth/rbac.js +162 -0
  61. package/dist/api/auth/tokens.d.ts +18 -0
  62. package/dist/api/auth/tokens.js +56 -0
  63. package/dist/api/capabilities.d.ts +9 -0
  64. package/dist/api/capabilities.js +33 -0
  65. package/dist/api/config.d.ts +2 -0
  66. package/dist/api/config.js +77 -0
  67. package/dist/api/http.d.ts +28 -0
  68. package/dist/api/http.js +51 -0
  69. package/dist/api/index.d.ts +9 -0
  70. package/dist/api/index.js +20 -0
  71. package/dist/api/operations-routes.d.ts +11 -0
  72. package/dist/api/operations-routes.js +87 -0
  73. package/dist/api/operations.d.ts +3 -0
  74. package/dist/api/operations.js +26 -0
  75. package/dist/api/project-routes.d.ts +8 -0
  76. package/dist/api/project-routes.js +585 -0
  77. package/dist/api/providers.d.ts +2 -0
  78. package/dist/api/providers.js +62 -0
  79. package/dist/api/railway.d.ts +51 -0
  80. package/dist/api/railway.js +71 -0
  81. package/dist/api/sdk-dispatch.d.ts +5 -0
  82. package/dist/api/sdk-dispatch.js +13 -0
  83. package/dist/api/sdk-routes.d.ts +11 -0
  84. package/dist/api/sdk-routes.js +29 -0
  85. package/dist/api/server.d.ts +2 -0
  86. package/dist/api/server.js +10 -0
  87. package/dist/api/templates.d.ts +3 -0
  88. package/dist/api/templates.js +31 -0
  89. package/dist/api/types.d.ts +237 -0
  90. package/dist/api/types.js +0 -0
  91. package/dist/env.yaml +957 -0
  92. package/dist/index.d.ts +14 -0
  93. package/dist/index.js +41 -0
  94. package/dist/scripts/assert-release-tag-version.d.ts +1 -0
  95. package/dist/scripts/assert-release-tag-version.js +20 -0
  96. package/dist/scripts/build-dist.d.ts +1 -0
  97. package/dist/scripts/build-dist.js +106 -0
  98. package/dist/scripts/package-tools.d.ts +1 -0
  99. package/dist/scripts/package-tools.js +7 -0
  100. package/dist/scripts/publish-package.d.ts +1 -0
  101. package/dist/scripts/publish-package.js +24 -0
  102. package/dist/scripts/release-verify.d.ts +1 -0
  103. package/dist/scripts/release-verify.js +152 -0
  104. package/dist/scripts/test-smoke.d.ts +1 -0
  105. package/dist/scripts/test-smoke.js +23 -0
  106. package/dist/scripts/treeseed-agent-api.d.ts +2 -0
  107. package/dist/scripts/treeseed-agent-api.js +25 -0
  108. package/dist/scripts/treeseed-agent-service.d.ts +2 -0
  109. package/dist/scripts/treeseed-agent-service.js +36 -0
  110. package/dist/scripts/treeseed-agents.d.ts +2 -0
  111. package/dist/scripts/treeseed-agents.js +13 -0
  112. package/dist/services/agents.d.ts +17 -0
  113. package/dist/services/agents.js +48 -0
  114. package/dist/services/common.d.ts +66 -0
  115. package/dist/services/common.js +212 -0
  116. package/dist/services/index.d.ts +6 -0
  117. package/dist/services/index.js +19 -0
  118. package/dist/services/manager.d.ts +333 -0
  119. package/dist/services/manager.js +1368 -0
  120. package/dist/services/remote-runner.d.ts +30 -0
  121. package/dist/services/remote-runner.js +230 -0
  122. package/dist/services/workday-content.d.ts +53 -0
  123. package/dist/services/workday-content.js +190 -0
  124. package/dist/services/workday-manager.d.ts +391 -0
  125. package/dist/services/workday-manager.js +163 -0
  126. package/dist/services/workday-report.d.ts +238 -0
  127. package/dist/services/workday-report.js +17 -0
  128. package/dist/services/workday-start.d.ts +238 -0
  129. package/dist/services/workday-start.js +17 -0
  130. package/dist/services/worker-capacity.d.ts +58 -0
  131. package/dist/services/worker-capacity.js +208 -0
  132. package/dist/services/worker-pool-scaler.d.ts +27 -0
  133. package/dist/services/worker-pool-scaler.js +127 -0
  134. package/dist/services/worker.d.ts +19 -0
  135. package/dist/services/worker.js +436 -0
  136. package/dist/templates/github/deploy-processing.workflow.yml +119 -0
  137. package/package.json +136 -0
  138. package/templates/github/deploy-processing.workflow.yml +119 -0
@@ -0,0 +1,444 @@
1
+ import crypto from "node:crypto";
2
+ import { AgentSdk, TREESEED_REMOTE_CONTRACT_HEADER, TREESEED_REMOTE_CONTRACT_VERSION } from "@treeseed/sdk";
3
+ import { Hono } from "hono";
4
+ import { registerAgentRoutes } from "./agent-routes.js";
5
+ import { resolveApiConfig } from "./config.js";
6
+ import { bearerTokenFromRequest, jsonError, requirePermission, requireScope } from "./http.js";
7
+ import { registerOperationRoutes } from "./operations-routes.js";
8
+ import { resolveApiRuntimeProviders } from "./providers.js";
9
+ import { registerProjectRoutes } from "./project-routes.js";
10
+ import { registerSdkRoutes } from "./sdk-routes.js";
11
+ import { loadTemplateCatalog } from "./templates.js";
12
+ function mergeApiOptions(options) {
13
+ const baseConfig = resolveApiConfig();
14
+ return {
15
+ config: {
16
+ ...baseConfig,
17
+ ...options.config ?? {},
18
+ providers: {
19
+ ...baseConfig.providers,
20
+ ...options.config?.providers ?? {},
21
+ agents: {
22
+ ...baseConfig.providers.agents,
23
+ ...options.config?.providers?.agents ?? {}
24
+ }
25
+ }
26
+ },
27
+ surfaces: {
28
+ auth: true,
29
+ templates: true,
30
+ sdk: true,
31
+ agent: true,
32
+ operations: true,
33
+ project: true,
34
+ ...options.surfaces ?? {}
35
+ },
36
+ scopes: {
37
+ authMe: "auth:me",
38
+ sdk: "sdk",
39
+ agent: "agent",
40
+ operations: "operations",
41
+ ...options.scopes ?? {}
42
+ }
43
+ };
44
+ }
45
+ function normalizePrefix(prefix) {
46
+ if (!prefix?.trim()) return "";
47
+ const normalized = prefix.trim().replace(/\/+$/u, "");
48
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
49
+ }
50
+ function principalScopes(permissions) {
51
+ const scopes = /* @__PURE__ */ new Set(["auth:me"]);
52
+ if (permissions.includes("*:*:*") || permissions.includes("sdk:execute:global")) scopes.add("sdk");
53
+ if (permissions.includes("*:*:*") || permissions.includes("agent:execute:global")) scopes.add("agent");
54
+ if (permissions.includes("*:*:*") || permissions.includes("operations:execute:global")) scopes.add("operations");
55
+ return [...scopes];
56
+ }
57
+ function buildProjectApiPrincipal(config) {
58
+ return {
59
+ id: `project:${config.projectId}`,
60
+ displayName: config.projectApiLabel,
61
+ roles: ["project_api"],
62
+ permissions: [...config.projectApiPermissions],
63
+ scopes: principalScopes(config.projectApiPermissions),
64
+ metadata: {
65
+ projectId: config.projectId
66
+ }
67
+ };
68
+ }
69
+ function matchesProjectApiKey(token, projectApiKey) {
70
+ if (!projectApiKey) return false;
71
+ const left = Buffer.from(token);
72
+ const right = Buffer.from(projectApiKey);
73
+ return left.length === right.length && crypto.timingSafeEqual(left, right);
74
+ }
75
+ function createTreeseedApiRouter(options = {}) {
76
+ const resolved = mergeApiOptions(options);
77
+ const runtimeProviders = resolveApiRuntimeProviders(resolved.config, options.runtimeProviders);
78
+ const sharedSdk = options.sdk ?? new AgentSdk({ repoRoot: resolved.config.repoRoot });
79
+ const app = new Hono();
80
+ const internalPrefix = normalizePrefix(options.internalPrefix);
81
+ const runtime = {
82
+ resolved,
83
+ runtimeProviders,
84
+ sharedSdk,
85
+ internalPrefix
86
+ };
87
+ const extensionMounts = [];
88
+ app.use("*", async (c, next) => {
89
+ c.set("requestId", crypto.randomUUID());
90
+ c.set("config", resolved.config);
91
+ c.set("principal", null);
92
+ c.set("actingUser", null);
93
+ c.set("credential", null);
94
+ c.set("actorType", "anonymous");
95
+ c.set("permissionGrants", []);
96
+ c.header(TREESEED_REMOTE_CONTRACT_HEADER, String(TREESEED_REMOTE_CONTRACT_VERSION));
97
+ await next();
98
+ });
99
+ app.use("*", async (c, next) => {
100
+ const serviceId = c.req.header("x-treeseed-service-id");
101
+ const serviceSecret = c.req.header("x-treeseed-service-secret");
102
+ if (serviceId && serviceSecret) {
103
+ const result = await runtimeProviders.auth.authenticateServiceCredential(serviceId, serviceSecret);
104
+ if (!result) {
105
+ return jsonError(c, 401, "Invalid internal service credential.");
106
+ }
107
+ c.set("principal", result.principal);
108
+ c.set("credential", result.credential);
109
+ c.set("actorType", "service");
110
+ c.set("permissionGrants", result.principal.permissions);
111
+ }
112
+ await next();
113
+ });
114
+ app.use("*", async (c, next) => {
115
+ const token = bearerTokenFromRequest(c.req.raw);
116
+ if (token) {
117
+ if (matchesProjectApiKey(token, resolved.config.projectApiKey)) {
118
+ const principal = buildProjectApiPrincipal(resolved.config);
119
+ c.set("principal", principal);
120
+ c.set("credential", {
121
+ type: "project_api_key",
122
+ id: resolved.config.projectId,
123
+ label: resolved.config.projectApiLabel
124
+ });
125
+ c.set("actorType", "project");
126
+ c.set("permissionGrants", principal.permissions);
127
+ await next();
128
+ return;
129
+ }
130
+ const result = await runtimeProviders.auth.authenticateBearerToken(token);
131
+ if (result) {
132
+ c.set("principal", result.principal);
133
+ c.set("credential", result.credential);
134
+ c.set("actorType", result.credential.type === "service_token" ? "service" : "user");
135
+ c.set("permissionGrants", result.principal.permissions);
136
+ }
137
+ }
138
+ await next();
139
+ });
140
+ app.use("*", async (c, next) => {
141
+ const assertion = c.req.header("x-treeseed-user-assertion");
142
+ if (c.get("actorType") === "service" && assertion) {
143
+ const claims = runtimeProviders.auth.verifyTrustedUserAssertion(assertion);
144
+ if (!claims) {
145
+ return jsonError(c, 401, "Invalid trusted user assertion.");
146
+ }
147
+ const exchange = await runtimeProviders.auth.exchangeTrustedUserAssertion(claims);
148
+ c.set("actingUser", exchange.principal);
149
+ c.set("principal", exchange.principal);
150
+ c.set("actorType", "user");
151
+ c.set("permissionGrants", exchange.principal.permissions);
152
+ }
153
+ await next();
154
+ });
155
+ app.get("/healthz", (c) => c.json({
156
+ ok: true,
157
+ service: resolved.config.name,
158
+ status: "ok",
159
+ requestId: c.get("requestId")
160
+ }));
161
+ app.get("/readyz", (c) => c.json({
162
+ ok: true,
163
+ ready: true,
164
+ providers: runtimeProviders.selections,
165
+ surfaces: resolved.surfaces
166
+ }));
167
+ app.get("/internal/capabilities", (c) => c.json({
168
+ ok: true,
169
+ payload: {
170
+ service: resolved.config.name,
171
+ capabilities: [
172
+ "graph.refresh",
173
+ "workday.manage",
174
+ "task.execute",
175
+ "report.publish",
176
+ "capacity-provider.heartbeat"
177
+ ]
178
+ }
179
+ }));
180
+ app.get("/internal/capacity-provider/health", (c) => c.json({
181
+ ok: true,
182
+ payload: {
183
+ ok: true,
184
+ status: process.env.TREESEED_PROCESSING_DRAIN === "1" ? "degraded" : "active",
185
+ capabilities: [
186
+ "graph.refresh",
187
+ "workday.manage",
188
+ "task.execute",
189
+ "report.publish"
190
+ ],
191
+ queueDepth: 0,
192
+ activeWorkers: Number(process.env.TREESEED_ACTIVE_WORKERS ?? 0),
193
+ draining: process.env.TREESEED_PROCESSING_DRAIN === "1",
194
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
195
+ }
196
+ }));
197
+ app.post("/internal/capacity-provider/register", async (c) => {
198
+ const body = await c.req.json().catch(() => ({}));
199
+ return c.json({
200
+ ok: true,
201
+ payload: {
202
+ id: String(body.id ?? process.env.TREESEED_CAPACITY_PROVIDER_ID ?? resolved.config.projectId),
203
+ teamId: String(body.teamId ?? process.env.TREESEED_CAPACITY_PROVIDER_TEAM_ID ?? resolved.config.projectId),
204
+ providerKind: "processing-host",
205
+ serviceBaseUrl: String(body.serviceBaseUrl ?? resolved.config.baseUrl),
206
+ environments: Array.isArray(body.environments) ? body.environments : ["local"],
207
+ capabilities: Array.isArray(body.capabilities) ? body.capabilities : ["graph.refresh", "workday.manage", "task.execute", "report.publish"],
208
+ status: "active",
209
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
210
+ limits: {
211
+ maxWorkers: Number(process.env.TREESEED_AGENT_POOL_MAX_WORKERS ?? 1),
212
+ dailyTaskCreditBudget: Number(process.env.TREESEED_WORKDAY_TASK_CREDIT_BUDGET ?? 100),
213
+ maxQueuedTasks: Number(process.env.TREESEED_MANAGER_MAX_QUEUED_TASKS ?? 100)
214
+ }
215
+ }
216
+ });
217
+ });
218
+ app.post("/internal/capacity-provider/heartbeat", async (c) => {
219
+ const body = await c.req.json().catch(() => ({}));
220
+ return c.json({
221
+ ok: true,
222
+ payload: {
223
+ providerId: String(body.providerId ?? process.env.TREESEED_CAPACITY_PROVIDER_ID ?? resolved.config.projectId),
224
+ status: process.env.TREESEED_PROCESSING_DRAIN === "1" ? "degraded" : "active",
225
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
226
+ queueDepth: Number(body.queueDepth ?? 0),
227
+ activeWorkers: Number(body.activeWorkers ?? process.env.TREESEED_ACTIVE_WORKERS ?? 0),
228
+ draining: process.env.TREESEED_PROCESSING_DRAIN === "1"
229
+ }
230
+ });
231
+ });
232
+ if (resolved.surfaces.templates) {
233
+ app.get("/templates", (c) => c.json(loadTemplateCatalog(resolved.config)));
234
+ app.get("/search/templates", (c) => c.json(loadTemplateCatalog(resolved.config)));
235
+ app.get("/templates/:id", (c) => {
236
+ const catalog = loadTemplateCatalog(resolved.config);
237
+ const item = catalog.items.find((entry) => entry.id === c.req.param("id"));
238
+ return item ? c.json({ ok: true, payload: item }) : jsonError(c, 404, `Unknown template "${c.req.param("id")}".`);
239
+ });
240
+ }
241
+ if (resolved.surfaces.auth) {
242
+ app.post("/auth/device/start", async (c) => {
243
+ const body = await c.req.json().catch(() => ({}));
244
+ return c.json(await runtimeProviders.auth.startDeviceFlow(body));
245
+ });
246
+ app.post("/auth/device/poll", async (c) => {
247
+ const body = await c.req.json().catch(() => ({}));
248
+ const response = await runtimeProviders.auth.pollDeviceFlow(body);
249
+ return c.json(response, { status: response.ok ? 200 : response.status === "expired" ? 410 : 400 });
250
+ });
251
+ app.post("/auth/device/approve", async (c) => {
252
+ const body = await c.req.json().catch(() => ({}));
253
+ try {
254
+ return c.json(await runtimeProviders.auth.approveDeviceFlow(body));
255
+ } catch (error) {
256
+ return jsonError(c, 400, error instanceof Error ? error.message : String(error));
257
+ }
258
+ });
259
+ app.post("/auth/token/refresh", async (c) => {
260
+ const body = await c.req.json().catch(() => ({}));
261
+ try {
262
+ return c.json(await runtimeProviders.auth.refreshAccessToken(body));
263
+ } catch (error) {
264
+ return jsonError(c, 401, error instanceof Error ? error.message : String(error));
265
+ }
266
+ });
267
+ app.get("/auth/me", (c) => {
268
+ const unauthorized = requireScope(c, resolved.scopes.authMe);
269
+ if (unauthorized) return unauthorized;
270
+ return c.json({
271
+ ok: true,
272
+ payload: c.get("principal")
273
+ });
274
+ });
275
+ app.post("/auth/pat", async (c) => {
276
+ const unauthorized = requirePermission(c, "api_tokens:create:self");
277
+ if (unauthorized) return unauthorized;
278
+ const principal = c.get("principal");
279
+ const body = await c.req.json().catch(() => ({}));
280
+ if (!body.name?.trim() || !principal) {
281
+ return jsonError(c, 400, "Token name is required.");
282
+ }
283
+ return c.json({
284
+ ok: true,
285
+ payload: await runtimeProviders.auth.createPersonalAccessToken(principal.id, {
286
+ name: body.name.trim(),
287
+ scopes: body.scopes,
288
+ expiresAt: body.expiresAt ?? null
289
+ })
290
+ });
291
+ });
292
+ app.get("/auth/pat", async (c) => {
293
+ const unauthorized = requirePermission(c, "api_tokens:read:self");
294
+ if (unauthorized) return unauthorized;
295
+ const principal = c.get("principal");
296
+ if (!principal) return jsonError(c, 401, "Authentication required.");
297
+ return c.json({
298
+ ok: true,
299
+ payload: await runtimeProviders.auth.listPersonalAccessTokens(principal.id)
300
+ });
301
+ });
302
+ app.delete("/auth/pat/:id", async (c) => {
303
+ const unauthorized = requirePermission(c, "api_tokens:delete:self");
304
+ if (unauthorized) return unauthorized;
305
+ const principal = c.get("principal");
306
+ if (!principal) return jsonError(c, 401, "Authentication required.");
307
+ await runtimeProviders.auth.revokePersonalAccessToken(principal.id, c.req.param("id"));
308
+ return c.json({ ok: true });
309
+ });
310
+ app.post("/auth/admin/users", async (c) => {
311
+ const unauthorized = requirePermission(c, "users:manage:global");
312
+ if (unauthorized) return unauthorized;
313
+ if (!runtimeProviders.auth.createUser) {
314
+ return jsonError(c, 501, "User management is unavailable for this auth provider.");
315
+ }
316
+ const body = await c.req.json().catch(() => ({}));
317
+ return c.json({
318
+ ok: true,
319
+ payload: await runtimeProviders.auth.createUser({
320
+ email: body.email ?? null,
321
+ displayName: body.displayName ?? null,
322
+ metadata: typeof body.metadata === "object" && body.metadata ? body.metadata : {}
323
+ })
324
+ });
325
+ });
326
+ app.post("/auth/admin/users/:userId/roles", async (c) => {
327
+ const unauthorized = requirePermission(c, "roles:manage:global");
328
+ if (unauthorized) return unauthorized;
329
+ if (!runtimeProviders.auth.setUserRoles) {
330
+ return jsonError(c, 501, "Role management is unavailable for this auth provider.");
331
+ }
332
+ const body = await c.req.json().catch(() => ({}));
333
+ const roles = Array.isArray(body.roles) ? body.roles.map(String) : [];
334
+ return c.json({
335
+ ok: true,
336
+ payload: await runtimeProviders.auth.setUserRoles(c.req.param("userId"), roles)
337
+ });
338
+ });
339
+ }
340
+ app.post("/internal/auth/web/sync-user", async (c) => {
341
+ if (c.get("actorType") !== "service") {
342
+ return jsonError(c, 401, "Trusted service authentication required.");
343
+ }
344
+ const unauthorized = requirePermission(c, "services:impersonate:global");
345
+ if (unauthorized) return unauthorized;
346
+ const body = await c.req.json().catch(() => ({}));
347
+ return c.json({
348
+ ok: true,
349
+ payload: await runtimeProviders.auth.syncUserIdentity(body)
350
+ });
351
+ });
352
+ app.post("/internal/auth/web/exchange", async (c) => {
353
+ if (c.get("actorType") !== "service") {
354
+ return jsonError(c, 401, "Trusted service authentication required.");
355
+ }
356
+ const unauthorized = requirePermission(c, "services:impersonate:global");
357
+ if (unauthorized) return unauthorized;
358
+ const body = await c.req.json().catch(() => ({}));
359
+ return c.json(await runtimeProviders.auth.exchangeTrustedUserAssertion(body));
360
+ });
361
+ app.post("/internal/auth/service/token", async (c) => {
362
+ const unauthorized = requirePermission(c, "services:manage:global");
363
+ if (unauthorized) return unauthorized;
364
+ const body = await c.req.json().catch(() => ({}));
365
+ if (!body.serviceId?.trim() || !body.name?.trim()) {
366
+ return jsonError(c, 400, "serviceId and name are required.");
367
+ }
368
+ return c.json({
369
+ ok: true,
370
+ payload: await runtimeProviders.auth.createServiceToken({
371
+ serviceId: body.serviceId.trim(),
372
+ name: body.name.trim(),
373
+ roles: body.roles,
374
+ permissions: body.permissions
375
+ })
376
+ });
377
+ });
378
+ app.post("/internal/auth/service/rotate", async (c) => {
379
+ const unauthorized = requirePermission(c, "services:manage:global");
380
+ if (unauthorized) return unauthorized;
381
+ const body = await c.req.json().catch(() => ({}));
382
+ if (!body.serviceId?.trim()) {
383
+ return jsonError(c, 400, "serviceId is required.");
384
+ }
385
+ return c.json({
386
+ ok: true,
387
+ payload: await runtimeProviders.auth.rotateServiceToken(body.serviceId.trim())
388
+ });
389
+ });
390
+ if (resolved.surfaces.sdk) {
391
+ registerSdkRoutes(app, {
392
+ config: resolved.config,
393
+ sharedSdk,
394
+ scope: resolved.scopes.sdk,
395
+ prefix: internalPrefix
396
+ });
397
+ }
398
+ if (resolved.surfaces.agent) {
399
+ registerAgentRoutes(app, {
400
+ sdk: sharedSdk,
401
+ prefix: `${internalPrefix}/agent`,
402
+ scope: resolved.scopes.agent,
403
+ projectId: resolved.config.projectId,
404
+ defaultActor: "api"
405
+ });
406
+ }
407
+ if (resolved.surfaces.operations) {
408
+ registerOperationRoutes(app, {
409
+ config: resolved.config,
410
+ scope: resolved.scopes.operations,
411
+ prefix: internalPrefix,
412
+ sdk: sharedSdk,
413
+ executeOperation: options.workflowExecutor
414
+ });
415
+ }
416
+ if (resolved.surfaces.project) {
417
+ registerProjectRoutes(app, {
418
+ config: resolved.config,
419
+ sharedSdk
420
+ });
421
+ }
422
+ for (const extension of options.extensions ?? []) {
423
+ const mounted = extension.mount(app, runtime);
424
+ if (mounted && typeof mounted.then === "function") {
425
+ extensionMounts.push(mounted);
426
+ }
427
+ }
428
+ options.extendApp?.(app, runtime);
429
+ if (extensionMounts.length > 0) {
430
+ app.use("*", async (_c, next) => {
431
+ await Promise.all(extensionMounts);
432
+ await next();
433
+ });
434
+ }
435
+ app.notFound((c) => jsonError(c, 404, "Not found."));
436
+ return app;
437
+ }
438
+ function createTreeseedApiApp(options = {}) {
439
+ return createTreeseedApiRouter(options);
440
+ }
441
+ export {
442
+ createTreeseedApiApp,
443
+ createTreeseedApiRouter
444
+ };
@@ -0,0 +1,3 @@
1
+ import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare';
2
+ import type { ApiConfig } from '../types.ts';
3
+ export declare function resolveApiD1Database(config: ApiConfig): D1DatabaseLike;
@@ -0,0 +1,20 @@
1
+ import { CloudflareHttpD1Database } from "@treeseed/sdk";
2
+ import { NodeSqliteD1Database } from "@treeseed/sdk/db/node-sqlite";
3
+ function resolveApiD1Database(config) {
4
+ if (config.cloudflareAccountId && config.cloudflareApiToken && config.d1DatabaseId) {
5
+ return new CloudflareHttpD1Database({
6
+ accountId: config.cloudflareAccountId,
7
+ apiToken: config.cloudflareApiToken,
8
+ databaseId: config.d1DatabaseId
9
+ });
10
+ }
11
+ if (config.d1LocalPersistTo || config.d1DatabaseName) {
12
+ return new NodeSqliteD1Database(config.d1LocalPersistTo);
13
+ }
14
+ throw new Error(
15
+ "Treeseed API auth requires either CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_API_TOKEN + TREESEED_API_D1_DATABASE_ID for remote D1 access, or TREESEED_API_D1_LOCAL_PERSIST_TO for local SQLite-backed D1-compatible access."
16
+ );
17
+ }
18
+ export {
19
+ resolveApiD1Database
20
+ };
@@ -0,0 +1,79 @@
1
+ import type { D1DatabaseLike } from '@treeseed/sdk/types/cloudflare';
2
+ import type { ApiAuthProvider, ApiConfig, ApiCredential, ApiPrincipal, DeviceCodeApproveRequest, DeviceCodePollRequest, DeviceCodePollResponse, DeviceCodeStartRequest, DeviceCodeStartResponse, TokenRefreshRequest, TokenRefreshResponse, TrustedUserAssertionClaims, UserIdentityProfileInput } from '../types.ts';
3
+ export declare class D1AuthProvider implements ApiAuthProvider {
4
+ private readonly config;
5
+ readonly id = "d1";
6
+ private readonly store;
7
+ constructor(config: ApiConfig, options?: {
8
+ db?: D1DatabaseLike;
9
+ });
10
+ startDeviceFlow(request: DeviceCodeStartRequest): Promise<DeviceCodeStartResponse>;
11
+ pollDeviceFlow(request: DeviceCodePollRequest): Promise<DeviceCodePollResponse>;
12
+ refreshAccessToken(request: TokenRefreshRequest): Promise<TokenRefreshResponse>;
13
+ approveDeviceFlow(request: DeviceCodeApproveRequest): Promise<{
14
+ ok: true;
15
+ }>;
16
+ authenticateBearerToken(token: string): Promise<{
17
+ principal: ApiPrincipal;
18
+ credential: ApiCredential;
19
+ } | null>;
20
+ authenticateServiceCredential(serviceId: string, secret: string): Promise<{
21
+ principal: ApiPrincipal;
22
+ credential: ApiCredential;
23
+ } | null>;
24
+ createPersonalAccessToken(userId: string, input: {
25
+ name: string;
26
+ scopes?: string[];
27
+ expiresAt?: string | null;
28
+ }): Promise<{
29
+ id: `${string}-${string}-${string}-${string}-${string}`;
30
+ token: string;
31
+ prefix: string;
32
+ name: string;
33
+ expiresAt: string | null;
34
+ }>;
35
+ listPersonalAccessTokens(userId: string): Promise<{
36
+ id: string;
37
+ name: string;
38
+ token_prefix: string;
39
+ expires_at: string | null;
40
+ last_used_at: string | null;
41
+ revoked_at: string | null;
42
+ created_at: string;
43
+ }[]>;
44
+ revokePersonalAccessToken(userId: string, tokenId: string): Promise<void>;
45
+ syncUserIdentity(identity: UserIdentityProfileInput): Promise<{
46
+ identityId: string | null;
47
+ principal: ApiPrincipal;
48
+ userId: string;
49
+ }>;
50
+ createUser(input: {
51
+ email?: string | null;
52
+ displayName?: string | null;
53
+ metadata?: Record<string, unknown>;
54
+ }): Promise<{
55
+ principal: ApiPrincipal;
56
+ userId: string;
57
+ }>;
58
+ setUserRoles(userId: string, roles: string[]): Promise<{
59
+ principal: ApiPrincipal;
60
+ userId: string;
61
+ }>;
62
+ createServiceToken(input: {
63
+ serviceId: string;
64
+ name: string;
65
+ roles?: string[];
66
+ permissions?: string[];
67
+ }): Promise<import("./d1-store.ts").ServiceCredentialResult>;
68
+ rotateServiceToken(serviceId: string): Promise<import("./d1-store.ts").ServiceCredentialResult>;
69
+ createTrustedUserAssertion(claims: TrustedUserAssertionClaims): string;
70
+ verifyTrustedUserAssertion(assertion: string): TrustedUserAssertionClaims | null;
71
+ exchangeTrustedUserAssertion(claims: TrustedUserAssertionClaims): Promise<{
72
+ ok: true;
73
+ accessToken: string;
74
+ tokenType: "Bearer";
75
+ expiresAt: string;
76
+ expiresInSeconds: number;
77
+ principal: ApiPrincipal;
78
+ }>;
79
+ }
@@ -0,0 +1,92 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { D1AuthStore } from "./d1-store.js";
3
+ function encodePayload(payload) {
4
+ return Buffer.from(JSON.stringify(payload)).toString("base64url");
5
+ }
6
+ function decodePayload(value) {
7
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
8
+ }
9
+ function signPayload(payload, secret) {
10
+ return createHmac("sha256", secret).update(payload).digest("base64url");
11
+ }
12
+ function safeEqual(left, right) {
13
+ const leftBuffer = Buffer.from(left);
14
+ const rightBuffer = Buffer.from(right);
15
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
16
+ }
17
+ class D1AuthProvider {
18
+ constructor(config, options = {}) {
19
+ this.config = config;
20
+ if (!options.db) {
21
+ throw new Error("D1AuthProvider requires an explicit database binding or adapter.");
22
+ }
23
+ this.store = new D1AuthStore(config, options.db);
24
+ }
25
+ config;
26
+ id = "d1";
27
+ store;
28
+ startDeviceFlow(request) {
29
+ return this.store.startDeviceFlow(request);
30
+ }
31
+ pollDeviceFlow(request) {
32
+ return this.store.pollDeviceFlow(request);
33
+ }
34
+ refreshAccessToken(request) {
35
+ return this.store.refreshAccessToken(request);
36
+ }
37
+ approveDeviceFlow(request) {
38
+ return this.store.approveDeviceFlow(request);
39
+ }
40
+ authenticateBearerToken(token) {
41
+ return this.store.authenticateBearerToken(token);
42
+ }
43
+ authenticateServiceCredential(serviceId, secret) {
44
+ return this.store.authenticateService(serviceId, secret);
45
+ }
46
+ createPersonalAccessToken(userId, input) {
47
+ return this.store.createPersonalAccessToken(userId, input);
48
+ }
49
+ listPersonalAccessTokens(userId) {
50
+ return this.store.listPersonalAccessTokens(userId);
51
+ }
52
+ revokePersonalAccessToken(userId, tokenId) {
53
+ return this.store.revokePersonalAccessToken(userId, tokenId);
54
+ }
55
+ syncUserIdentity(identity) {
56
+ return this.store.syncUser(identity);
57
+ }
58
+ createUser(input) {
59
+ return this.store.createUser(input);
60
+ }
61
+ setUserRoles(userId, roles) {
62
+ return this.store.setUserRoles(userId, roles);
63
+ }
64
+ createServiceToken(input) {
65
+ return this.store.createServiceCredential(input);
66
+ }
67
+ rotateServiceToken(serviceId) {
68
+ return this.store.rotateServiceCredential(serviceId);
69
+ }
70
+ createTrustedUserAssertion(claims) {
71
+ const payload = encodePayload(claims);
72
+ const signature = signPayload(payload, this.config.webAssertionSecret);
73
+ return `${payload}.${signature}`;
74
+ }
75
+ verifyTrustedUserAssertion(assertion) {
76
+ const [payload, signature] = assertion.split(".");
77
+ if (!payload || !signature) return null;
78
+ const expectedSignature = signPayload(payload, this.config.webAssertionSecret);
79
+ if (!safeEqual(signature, expectedSignature)) return null;
80
+ const claims = decodePayload(payload);
81
+ if (!claims.expiresAt || new Date(claims.expiresAt).getTime() <= Date.now()) {
82
+ return null;
83
+ }
84
+ return claims;
85
+ }
86
+ exchangeTrustedUserAssertion(claims) {
87
+ return this.store.exchangeTrustedUserAssertion(claims);
88
+ }
89
+ }
90
+ export {
91
+ D1AuthProvider
92
+ };