@renseiai/agentfactory-nextjs 0.8.0

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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/src/__tests__/middleware-edge-safety.test.d.ts +2 -0
  4. package/dist/src/__tests__/middleware-edge-safety.test.d.ts.map +1 -0
  5. package/dist/src/__tests__/middleware-edge-safety.test.js +74 -0
  6. package/dist/src/__tests__/poll-project-filter.test.d.ts +2 -0
  7. package/dist/src/__tests__/poll-project-filter.test.d.ts.map +1 -0
  8. package/dist/src/__tests__/poll-project-filter.test.js +83 -0
  9. package/dist/src/__tests__/subpath-exports.test.d.ts +2 -0
  10. package/dist/src/__tests__/subpath-exports.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/subpath-exports.test.js +35 -0
  12. package/dist/src/__tests__/webhook-project-filter.test.d.ts +2 -0
  13. package/dist/src/__tests__/webhook-project-filter.test.d.ts.map +1 -0
  14. package/dist/src/__tests__/webhook-project-filter.test.js +48 -0
  15. package/dist/src/factory.d.ts +140 -0
  16. package/dist/src/factory.d.ts.map +1 -0
  17. package/dist/src/factory.js +127 -0
  18. package/dist/src/handlers/cleanup.d.ts +44 -0
  19. package/dist/src/handlers/cleanup.d.ts.map +1 -0
  20. package/dist/src/handlers/cleanup.js +34 -0
  21. package/dist/src/handlers/config.d.ts +11 -0
  22. package/dist/src/handlers/config.d.ts.map +1 -0
  23. package/dist/src/handlers/config.js +20 -0
  24. package/dist/src/handlers/issue-tracker-proxy/index.d.ts +34 -0
  25. package/dist/src/handlers/issue-tracker-proxy/index.d.ts.map +1 -0
  26. package/dist/src/handlers/issue-tracker-proxy/index.js +230 -0
  27. package/dist/src/handlers/issue-tracker-proxy/serializer.d.ts +28 -0
  28. package/dist/src/handlers/issue-tracker-proxy/serializer.d.ts.map +1 -0
  29. package/dist/src/handlers/issue-tracker-proxy/serializer.js +95 -0
  30. package/dist/src/handlers/issue-tracker-proxy/types.d.ts +9 -0
  31. package/dist/src/handlers/issue-tracker-proxy/types.d.ts.map +1 -0
  32. package/dist/src/handlers/issue-tracker-proxy/types.js +4 -0
  33. package/dist/src/handlers/oauth/callback.d.ts +36 -0
  34. package/dist/src/handlers/oauth/callback.d.ts.map +1 -0
  35. package/dist/src/handlers/oauth/callback.js +96 -0
  36. package/dist/src/handlers/public/session-detail.d.ts +31 -0
  37. package/dist/src/handlers/public/session-detail.d.ts.map +1 -0
  38. package/dist/src/handlers/public/session-detail.js +91 -0
  39. package/dist/src/handlers/public/sessions-list.d.ts +22 -0
  40. package/dist/src/handlers/public/sessions-list.d.ts.map +1 -0
  41. package/dist/src/handlers/public/sessions-list.js +75 -0
  42. package/dist/src/handlers/public/stats.d.ts +28 -0
  43. package/dist/src/handlers/public/stats.d.ts.map +1 -0
  44. package/dist/src/handlers/public/stats.js +66 -0
  45. package/dist/src/handlers/sessions/activity.d.ts +15 -0
  46. package/dist/src/handlers/sessions/activity.d.ts.map +1 -0
  47. package/dist/src/handlers/sessions/activity.js +93 -0
  48. package/dist/src/handlers/sessions/claim.d.ts +15 -0
  49. package/dist/src/handlers/sessions/claim.d.ts.map +1 -0
  50. package/dist/src/handlers/sessions/claim.js +139 -0
  51. package/dist/src/handlers/sessions/completion.d.ts +16 -0
  52. package/dist/src/handlers/sessions/completion.d.ts.map +1 -0
  53. package/dist/src/handlers/sessions/completion.js +82 -0
  54. package/dist/src/handlers/sessions/external-urls.d.ts +15 -0
  55. package/dist/src/handlers/sessions/external-urls.d.ts.map +1 -0
  56. package/dist/src/handlers/sessions/external-urls.js +70 -0
  57. package/dist/src/handlers/sessions/get.d.ts +19 -0
  58. package/dist/src/handlers/sessions/get.d.ts.map +1 -0
  59. package/dist/src/handlers/sessions/get.js +47 -0
  60. package/dist/src/handlers/sessions/list.d.ts +27 -0
  61. package/dist/src/handlers/sessions/list.d.ts.map +1 -0
  62. package/dist/src/handlers/sessions/list.js +51 -0
  63. package/dist/src/handlers/sessions/lock-refresh.d.ts +14 -0
  64. package/dist/src/handlers/sessions/lock-refresh.d.ts.map +1 -0
  65. package/dist/src/handlers/sessions/lock-refresh.js +38 -0
  66. package/dist/src/handlers/sessions/progress.d.ts +15 -0
  67. package/dist/src/handlers/sessions/progress.d.ts.map +1 -0
  68. package/dist/src/handlers/sessions/progress.js +94 -0
  69. package/dist/src/handlers/sessions/prompts.d.ts +15 -0
  70. package/dist/src/handlers/sessions/prompts.d.ts.map +1 -0
  71. package/dist/src/handlers/sessions/prompts.js +91 -0
  72. package/dist/src/handlers/sessions/status.d.ts +19 -0
  73. package/dist/src/handlers/sessions/status.d.ts.map +1 -0
  74. package/dist/src/handlers/sessions/status.js +187 -0
  75. package/dist/src/handlers/sessions/tool-error.d.ts +15 -0
  76. package/dist/src/handlers/sessions/tool-error.d.ts.map +1 -0
  77. package/dist/src/handlers/sessions/tool-error.js +103 -0
  78. package/dist/src/handlers/sessions/transfer-ownership.d.ts +14 -0
  79. package/dist/src/handlers/sessions/transfer-ownership.d.ts.map +1 -0
  80. package/dist/src/handlers/sessions/transfer-ownership.js +56 -0
  81. package/dist/src/handlers/workers/get-delete.d.ts +15 -0
  82. package/dist/src/handlers/workers/get-delete.d.ts.map +1 -0
  83. package/dist/src/handlers/workers/get-delete.js +58 -0
  84. package/dist/src/handlers/workers/heartbeat.d.ts +14 -0
  85. package/dist/src/handlers/workers/heartbeat.d.ts.map +1 -0
  86. package/dist/src/handlers/workers/heartbeat.js +42 -0
  87. package/dist/src/handlers/workers/list.d.ts +22 -0
  88. package/dist/src/handlers/workers/list.d.ts.map +1 -0
  89. package/dist/src/handlers/workers/list.js +33 -0
  90. package/dist/src/handlers/workers/poll.d.ts +14 -0
  91. package/dist/src/handlers/workers/poll.d.ts.map +1 -0
  92. package/dist/src/handlers/workers/poll.js +96 -0
  93. package/dist/src/handlers/workers/register.d.ts +9 -0
  94. package/dist/src/handlers/workers/register.d.ts.map +1 -0
  95. package/dist/src/handlers/workers/register.js +45 -0
  96. package/dist/src/index.d.ts +52 -0
  97. package/dist/src/index.d.ts.map +1 -0
  98. package/dist/src/index.js +56 -0
  99. package/dist/src/linear-client-resolver.d.ts +59 -0
  100. package/dist/src/linear-client-resolver.d.ts.map +1 -0
  101. package/dist/src/linear-client-resolver.js +104 -0
  102. package/dist/src/middleware/cron-auth.d.ts +21 -0
  103. package/dist/src/middleware/cron-auth.d.ts.map +1 -0
  104. package/dist/src/middleware/cron-auth.js +46 -0
  105. package/dist/src/middleware/factory.d.ts +33 -0
  106. package/dist/src/middleware/factory.d.ts.map +1 -0
  107. package/dist/src/middleware/factory.js +185 -0
  108. package/dist/src/middleware/index.d.ts +16 -0
  109. package/dist/src/middleware/index.d.ts.map +1 -0
  110. package/dist/src/middleware/index.js +14 -0
  111. package/dist/src/middleware/types.d.ts +35 -0
  112. package/dist/src/middleware/types.d.ts.map +1 -0
  113. package/dist/src/middleware/types.js +4 -0
  114. package/dist/src/middleware/worker-auth.d.ts +25 -0
  115. package/dist/src/middleware/worker-auth.d.ts.map +1 -0
  116. package/dist/src/middleware/worker-auth.js +43 -0
  117. package/dist/src/orchestrator/error-formatting.d.ts +8 -0
  118. package/dist/src/orchestrator/error-formatting.d.ts.map +1 -0
  119. package/dist/src/orchestrator/error-formatting.js +35 -0
  120. package/dist/src/orchestrator/index.d.ts +4 -0
  121. package/dist/src/orchestrator/index.d.ts.map +1 -0
  122. package/dist/src/orchestrator/index.js +2 -0
  123. package/dist/src/orchestrator/types.d.ts +53 -0
  124. package/dist/src/orchestrator/types.d.ts.map +1 -0
  125. package/dist/src/orchestrator/types.js +4 -0
  126. package/dist/src/orchestrator/webhook-orchestrator.d.ts +32 -0
  127. package/dist/src/orchestrator/webhook-orchestrator.d.ts.map +1 -0
  128. package/dist/src/orchestrator/webhook-orchestrator.js +373 -0
  129. package/dist/src/types.d.ts +101 -0
  130. package/dist/src/types.d.ts.map +1 -0
  131. package/dist/src/types.js +7 -0
  132. package/dist/src/webhook/governor-bridge.d.ts +23 -0
  133. package/dist/src/webhook/governor-bridge.d.ts.map +1 -0
  134. package/dist/src/webhook/governor-bridge.js +36 -0
  135. package/dist/src/webhook/handlers/issue-updated.d.ts +15 -0
  136. package/dist/src/webhook/handlers/issue-updated.d.ts.map +1 -0
  137. package/dist/src/webhook/handlers/issue-updated.js +771 -0
  138. package/dist/src/webhook/handlers/session-created.d.ts +9 -0
  139. package/dist/src/webhook/handlers/session-created.d.ts.map +1 -0
  140. package/dist/src/webhook/handlers/session-created.js +337 -0
  141. package/dist/src/webhook/handlers/session-prompted.d.ts +9 -0
  142. package/dist/src/webhook/handlers/session-prompted.d.ts.map +1 -0
  143. package/dist/src/webhook/handlers/session-prompted.js +199 -0
  144. package/dist/src/webhook/handlers/session-updated.d.ts +9 -0
  145. package/dist/src/webhook/handlers/session-updated.d.ts.map +1 -0
  146. package/dist/src/webhook/handlers/session-updated.js +29 -0
  147. package/dist/src/webhook/processor.d.ts +22 -0
  148. package/dist/src/webhook/processor.d.ts.map +1 -0
  149. package/dist/src/webhook/processor.js +98 -0
  150. package/dist/src/webhook/signature.d.ts +16 -0
  151. package/dist/src/webhook/signature.d.ts.map +1 -0
  152. package/dist/src/webhook/signature.js +23 -0
  153. package/dist/src/webhook/utils.d.ts +61 -0
  154. package/dist/src/webhook/utils.d.ts.map +1 -0
  155. package/dist/src/webhook/utils.js +166 -0
  156. package/package.json +86 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Factory — @renseiai/agentfactory-nextjs
3
+ *
4
+ * Wires all handlers together from a single configuration object.
5
+ * Consumers call `createAllRoutes(config)` and get back a nested
6
+ * route tree that maps 1:1 onto Next.js App Router exports.
7
+ *
8
+ * When optional config fields are omitted, sensible defaults from
9
+ * @renseiai/agentfactory-linear are used (defaultGeneratePrompt, etc.).
10
+ */
11
+ import { defaultGeneratePrompt, defaultDetectWorkTypeFromPrompt, defaultGetPriority, defaultBuildParentQAContext, defaultBuildParentAcceptanceContext, } from '@renseiai/agentfactory-linear';
12
+ // Worker handlers
13
+ import { createWorkerRegisterHandler } from './handlers/workers/register.js';
14
+ import { createWorkerListHandler } from './handlers/workers/list.js';
15
+ import { createWorkerGetHandler, createWorkerDeleteHandler } from './handlers/workers/get-delete.js';
16
+ import { createWorkerHeartbeatHandler } from './handlers/workers/heartbeat.js';
17
+ import { createWorkerPollHandler } from './handlers/workers/poll.js';
18
+ // Session handlers (no Linear)
19
+ import { createSessionListHandler } from './handlers/sessions/list.js';
20
+ import { createSessionGetHandler } from './handlers/sessions/get.js';
21
+ import { createSessionClaimHandler } from './handlers/sessions/claim.js';
22
+ import { createSessionStatusPostHandler, createSessionStatusGetHandler } from './handlers/sessions/status.js';
23
+ import { createSessionLockRefreshHandler } from './handlers/sessions/lock-refresh.js';
24
+ import { createSessionPromptsGetHandler, createSessionPromptsPostHandler } from './handlers/sessions/prompts.js';
25
+ import { createSessionTransferOwnershipHandler } from './handlers/sessions/transfer-ownership.js';
26
+ // Session handlers (Linear forwarding)
27
+ import { createSessionActivityHandler } from './handlers/sessions/activity.js';
28
+ import { createSessionCompletionHandler } from './handlers/sessions/completion.js';
29
+ import { createSessionExternalUrlsHandler } from './handlers/sessions/external-urls.js';
30
+ import { createSessionProgressHandler } from './handlers/sessions/progress.js';
31
+ import { createSessionToolErrorHandler } from './handlers/sessions/tool-error.js';
32
+ // Public handlers
33
+ import { createPublicStatsHandler } from './handlers/public/stats.js';
34
+ import { createPublicSessionsListHandler } from './handlers/public/sessions-list.js';
35
+ import { createPublicSessionDetailHandler } from './handlers/public/session-detail.js';
36
+ // Cleanup handler
37
+ import { createCleanupHandler } from './handlers/cleanup.js';
38
+ // Config handler
39
+ import { createConfigHandler } from './handlers/config.js';
40
+ // Webhook processor
41
+ import { createWebhookHandler } from './webhook/processor.js';
42
+ // OAuth handler
43
+ import { createOAuthCallbackHandler } from './handlers/oauth/callback.js';
44
+ // Issue tracker proxy handler
45
+ import { createIssueTrackerProxyHandler } from './handlers/issue-tracker-proxy/index.js';
46
+ /**
47
+ * Create all route handlers from a single config object.
48
+ *
49
+ * Optional fields fall back to sensible defaults from @renseiai/agentfactory-linear:
50
+ * - `generatePrompt` → `defaultGeneratePrompt`
51
+ * - `detectWorkTypeFromPrompt` → `defaultDetectWorkTypeFromPrompt`
52
+ * - `getPriority` → `defaultGetPriority`
53
+ * - `buildParentQAContext` → `defaultBuildParentQAContext`
54
+ * - `buildParentAcceptanceContext` → `defaultBuildParentAcceptanceContext`
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { createAllRoutes, createDefaultLinearClientResolver } from '@renseiai/agentfactory-nextjs'
59
+ *
60
+ * // Minimal — everything uses defaults
61
+ * const routes = createAllRoutes({
62
+ * linearClient: createDefaultLinearClientResolver(),
63
+ * })
64
+ *
65
+ * // Custom — override specific callbacks
66
+ * const routes = createAllRoutes({
67
+ * linearClient: createDefaultLinearClientResolver(),
68
+ * generatePrompt: myCustomPromptFn,
69
+ * oauth: { clientId: '...', clientSecret: '...' },
70
+ * })
71
+ * ```
72
+ */
73
+ export function createAllRoutes(config) {
74
+ // Apply defaults for optional webhook config fields
75
+ const webhookConfig = {
76
+ ...config,
77
+ generatePrompt: config.generatePrompt ?? defaultGeneratePrompt,
78
+ detectWorkTypeFromPrompt: config.detectWorkTypeFromPrompt ?? defaultDetectWorkTypeFromPrompt,
79
+ getPriority: config.getPriority ?? defaultGetPriority,
80
+ buildParentQAContext: config.buildParentQAContext ?? defaultBuildParentQAContext,
81
+ buildParentAcceptanceContext: config.buildParentAcceptanceContext ?? defaultBuildParentAcceptanceContext,
82
+ };
83
+ const routeConfig = {
84
+ linearClient: config.linearClient,
85
+ appUrl: config.appUrl,
86
+ };
87
+ const cleanup = createCleanupHandler(webhookConfig);
88
+ const webhook = createWebhookHandler(webhookConfig);
89
+ const oauth = createOAuthCallbackHandler(config.oauth);
90
+ const configHandler = createConfigHandler(config.projects);
91
+ const issueTrackerProxy = createIssueTrackerProxyHandler(routeConfig);
92
+ return {
93
+ workers: {
94
+ register: { POST: createWorkerRegisterHandler() },
95
+ list: { GET: createWorkerListHandler() },
96
+ detail: { GET: createWorkerGetHandler(), DELETE: createWorkerDeleteHandler() },
97
+ heartbeat: { POST: createWorkerHeartbeatHandler() },
98
+ poll: { GET: createWorkerPollHandler() },
99
+ },
100
+ sessions: {
101
+ list: { GET: createSessionListHandler() },
102
+ detail: { GET: createSessionGetHandler() },
103
+ claim: { POST: createSessionClaimHandler() },
104
+ status: { GET: createSessionStatusGetHandler(), POST: createSessionStatusPostHandler(routeConfig) },
105
+ lockRefresh: { POST: createSessionLockRefreshHandler() },
106
+ prompts: { GET: createSessionPromptsGetHandler(), POST: createSessionPromptsPostHandler() },
107
+ transferOwnership: { POST: createSessionTransferOwnershipHandler() },
108
+ activity: { POST: createSessionActivityHandler(routeConfig) },
109
+ completion: { POST: createSessionCompletionHandler(routeConfig) },
110
+ externalUrls: { POST: createSessionExternalUrlsHandler(routeConfig) },
111
+ progress: { POST: createSessionProgressHandler(routeConfig) },
112
+ toolError: { POST: createSessionToolErrorHandler(routeConfig) },
113
+ },
114
+ public: {
115
+ stats: { GET: createPublicStatsHandler() },
116
+ sessions: { GET: createPublicSessionsListHandler() },
117
+ sessionDetail: { GET: createPublicSessionDetailHandler() },
118
+ },
119
+ config: { GET: configHandler.GET },
120
+ cleanup: { POST: cleanup.POST, GET: cleanup.GET },
121
+ webhook: { POST: webhook.POST, GET: webhook.GET },
122
+ oauth: {
123
+ callback: { GET: oauth.GET },
124
+ },
125
+ issueTrackerProxy: { POST: issueTrackerProxy.POST, GET: issueTrackerProxy.GET },
126
+ };
127
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * POST, GET /api/cleanup
3
+ *
4
+ * Trigger orphan session cleanup.
5
+ */
6
+ import { NextRequest, NextResponse } from 'next/server';
7
+ import type { CronConfig } from '../types.js';
8
+ export declare function createCleanupHandler(config?: CronConfig): {
9
+ POST: (request: NextRequest) => Promise<NextResponse<{
10
+ checked: number;
11
+ orphaned: number;
12
+ requeued: number;
13
+ failed: number;
14
+ details: Array<{
15
+ sessionId: string;
16
+ issueIdentifier: string;
17
+ action: "requeued" | "failed";
18
+ reason?: string;
19
+ worktreePath?: string;
20
+ }>;
21
+ worktreePathsToCleanup: string[];
22
+ success: boolean;
23
+ }> | NextResponse<{
24
+ error: string;
25
+ }>>;
26
+ GET: (request: NextRequest) => Promise<NextResponse<{
27
+ checked: number;
28
+ orphaned: number;
29
+ requeued: number;
30
+ failed: number;
31
+ details: Array<{
32
+ sessionId: string;
33
+ issueIdentifier: string;
34
+ action: "requeued" | "failed";
35
+ reason?: string;
36
+ worktreePath?: string;
37
+ }>;
38
+ worktreePathsToCleanup: string[];
39
+ success: boolean;
40
+ }> | NextResponse<{
41
+ error: string;
42
+ }>>;
43
+ };
44
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../../../src/handlers/cleanup.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7C,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,UAAU;oBAChB,WAAW;;;;;;;;;kBAgC4e,CAAC;wBAAkG,CAAC;;;;;;;mBAhC3lB,WAAW;;;;;;;;;kBAgC4e,CAAC;wBAAkG,CAAC;;;;;;;EADloB"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * POST, GET /api/cleanup
3
+ *
4
+ * Trigger orphan session cleanup.
5
+ */
6
+ import { NextResponse } from 'next/server';
7
+ import { cleanupOrphanedSessions, createLogger } from '@renseiai/agentfactory-server';
8
+ import { verifyCronAuth } from '../middleware/cron-auth.js';
9
+ const log = createLogger('api:cleanup');
10
+ export function createCleanupHandler(config) {
11
+ async function handleCleanup(request) {
12
+ const authResult = verifyCronAuth(request, config?.cronSecret);
13
+ if (!authResult.authorized) {
14
+ log.warn('Unauthorized cleanup request', { reason: authResult.reason });
15
+ return NextResponse.json({ error: 'Unauthorized', message: authResult.reason }, { status: 401 });
16
+ }
17
+ try {
18
+ log.info('Running orphan cleanup');
19
+ const result = await cleanupOrphanedSessions();
20
+ return NextResponse.json({
21
+ success: true,
22
+ ...result,
23
+ });
24
+ }
25
+ catch (error) {
26
+ log.error('Cleanup failed', { error });
27
+ return NextResponse.json({ error: 'Cleanup failed' }, { status: 500 });
28
+ }
29
+ }
30
+ return {
31
+ POST: handleCleanup,
32
+ GET: handleCleanup,
33
+ };
34
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Config endpoint — exposes server configuration to workers.
3
+ *
4
+ * Workers query this during registration to auto-inherit project scope
5
+ * when not explicitly configured via --projects flag.
6
+ */
7
+ import type { RouteHandler } from '../types.js';
8
+ export declare function createConfigHandler(projects?: string[]): {
9
+ GET: RouteHandler;
10
+ };
11
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/handlers/config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE/C,wBAAgB,mBAAmB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,YAAY,CAAA;CAAE,CAW9E"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Config endpoint — exposes server configuration to workers.
3
+ *
4
+ * Workers query this during registration to auto-inherit project scope
5
+ * when not explicitly configured via --projects flag.
6
+ */
7
+ import { NextResponse } from 'next/server';
8
+ import { requireWorkerAuth } from '../middleware/worker-auth.js';
9
+ export function createConfigHandler(projects) {
10
+ return {
11
+ GET: async (request) => {
12
+ const authError = requireWorkerAuth(request);
13
+ if (authError)
14
+ return authError;
15
+ return NextResponse.json({
16
+ projects: projects ?? [],
17
+ });
18
+ },
19
+ };
20
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Issue Tracker Proxy Handler
3
+ *
4
+ * Centralizes all issue tracker API calls through the dashboard server.
5
+ * Agents, governors, and CLI tools call this endpoint instead of the
6
+ * issue tracker (e.g., Linear) directly.
7
+ *
8
+ * Benefits:
9
+ * - Single rate limiter and circuit breaker for all consumers
10
+ * - OAuth token management stays server-side
11
+ * - Response caching (future: Redis-based read-through cache)
12
+ * - Platform-agnostic interface: consumers don't need to know Linear exists
13
+ *
14
+ * POST /api/issue-tracker-proxy
15
+ * Body: { method: string, args: unknown[], organizationId?: string }
16
+ * Auth: Bearer <worker-api-key>
17
+ *
18
+ * GET /api/issue-tracker-proxy/health
19
+ * Returns: circuit breaker state, rate limiter tokens, quota remaining
20
+ */
21
+ import { NextResponse } from 'next/server';
22
+ import type { NextRequest } from 'next/server';
23
+ import type { ProxyHandlerConfig } from './types.js';
24
+ /**
25
+ * Create the issue tracker proxy handler.
26
+ *
27
+ * @param config - Route config with Linear client resolver
28
+ * @returns POST handler for proxy requests, GET handler for health check
29
+ */
30
+ export declare function createIssueTrackerProxyHandler(config: ProxyHandlerConfig): {
31
+ POST: (request: NextRequest) => Promise<NextResponse>;
32
+ GET: (_request: NextRequest) => Promise<NextResponse>;
33
+ };
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/handlers/issue-tracker-proxy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAU9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAiDpD;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,kBAAkB;oBAC1C,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC;oBAgJnC,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC;EAwBjE"}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Issue Tracker Proxy Handler
3
+ *
4
+ * Centralizes all issue tracker API calls through the dashboard server.
5
+ * Agents, governors, and CLI tools call this endpoint instead of the
6
+ * issue tracker (e.g., Linear) directly.
7
+ *
8
+ * Benefits:
9
+ * - Single rate limiter and circuit breaker for all consumers
10
+ * - OAuth token management stays server-side
11
+ * - Response caching (future: Redis-based read-through cache)
12
+ * - Platform-agnostic interface: consumers don't need to know Linear exists
13
+ *
14
+ * POST /api/issue-tracker-proxy
15
+ * Body: { method: string, args: unknown[], organizationId?: string }
16
+ * Auth: Bearer <worker-api-key>
17
+ *
18
+ * GET /api/issue-tracker-proxy/health
19
+ * Returns: circuit breaker state, rate limiter tokens, quota remaining
20
+ */
21
+ import { NextResponse } from 'next/server';
22
+ import { createLogger } from '@renseiai/agentfactory-server';
23
+ import { isCircuitOpenError } from '@renseiai/agentfactory-linear';
24
+ import { requireWorkerAuth } from '../../middleware/worker-auth.js';
25
+ import { serializeIssue, serializeComment, serializeViewer, serializeTeam } from './serializer.js';
26
+ const log = createLogger('api:issue-tracker-proxy');
27
+ /** Methods that are allowed through the proxy */
28
+ const ALLOWED_METHODS = new Set([
29
+ 'getIssue',
30
+ 'updateIssue',
31
+ 'createIssue',
32
+ 'createComment',
33
+ 'getIssueComments',
34
+ 'getTeamStatuses',
35
+ 'updateIssueStatus',
36
+ 'createAgentActivity',
37
+ 'updateAgentSession',
38
+ 'createAgentSessionOnIssue',
39
+ 'createIssueRelation',
40
+ 'getIssueRelations',
41
+ 'deleteIssueRelation',
42
+ 'getSubIssues',
43
+ 'getSubIssueStatuses',
44
+ 'getSubIssueGraph',
45
+ 'isParentIssue',
46
+ 'isChildIssue',
47
+ 'listProjectIssues',
48
+ 'getProjectRepositoryUrl',
49
+ 'getViewer',
50
+ 'getTeam',
51
+ 'unassignIssue',
52
+ ]);
53
+ /**
54
+ * Methods that return Issue objects needing serialization.
55
+ */
56
+ const ISSUE_RETURNING_METHODS = new Set([
57
+ 'getIssue',
58
+ 'updateIssue',
59
+ 'createIssue',
60
+ 'updateIssueStatus',
61
+ 'unassignIssue',
62
+ ]);
63
+ /**
64
+ * Methods that return Comment objects needing serialization.
65
+ */
66
+ const COMMENT_RETURNING_METHODS = new Set([
67
+ 'createComment',
68
+ ]);
69
+ /**
70
+ * Create the issue tracker proxy handler.
71
+ *
72
+ * @param config - Route config with Linear client resolver
73
+ * @returns POST handler for proxy requests, GET handler for health check
74
+ */
75
+ export function createIssueTrackerProxyHandler(config) {
76
+ async function POST(request) {
77
+ // Authenticate caller
78
+ const authError = requireWorkerAuth(request);
79
+ if (authError)
80
+ return authError;
81
+ let body;
82
+ try {
83
+ body = (await request.json());
84
+ }
85
+ catch {
86
+ return NextResponse.json({ success: false, error: { code: 'INVALID_JSON', message: 'Invalid JSON body', retryable: false } }, { status: 400 });
87
+ }
88
+ const { method, args, organizationId } = body;
89
+ // Validate method
90
+ if (!method || !ALLOWED_METHODS.has(method)) {
91
+ return NextResponse.json({ success: false, error: { code: 'INVALID_METHOD', message: `Unknown method: ${method}`, retryable: false } }, { status: 400 });
92
+ }
93
+ // Validate args
94
+ if (!Array.isArray(args)) {
95
+ return NextResponse.json({ success: false, error: { code: 'INVALID_ARGS', message: 'args must be an array', retryable: false } }, { status: 400 });
96
+ }
97
+ log.debug('Proxy request', { method, organizationId, argsLength: args.length });
98
+ try {
99
+ // Resolve the Linear client for this workspace
100
+ const client = await config.linearClient.getClient(organizationId);
101
+ // Call the method on the client
102
+ const fn = client[method];
103
+ if (typeof fn !== 'function') {
104
+ return NextResponse.json({ success: false, error: { code: 'METHOD_NOT_FOUND', message: `Method ${method} not available on client`, retryable: false } }, { status: 400 });
105
+ }
106
+ const rawResult = await fn.apply(client, args);
107
+ // Serialize result based on method type.
108
+ // The serializer functions use duck-typing (no @linear/sdk dependency).
109
+ let data;
110
+ if (ISSUE_RETURNING_METHODS.has(method) && rawResult && typeof rawResult === 'object' && 'id' in rawResult) {
111
+ data = await serializeIssue(rawResult);
112
+ }
113
+ else if (COMMENT_RETURNING_METHODS.has(method) && rawResult && typeof rawResult === 'object' && 'body' in rawResult) {
114
+ data = await serializeComment(rawResult);
115
+ }
116
+ else if (method === 'getIssueComments' && Array.isArray(rawResult)) {
117
+ data = await Promise.all(rawResult.map((c) => serializeComment(c)));
118
+ }
119
+ else if (method === 'getSubIssues' && Array.isArray(rawResult)) {
120
+ data = await Promise.all(rawResult.map((i) => serializeIssue(i)));
121
+ }
122
+ else if (method === 'getViewer' && rawResult && typeof rawResult === 'object' && 'email' in rawResult) {
123
+ data = serializeViewer(rawResult);
124
+ }
125
+ else if (method === 'getTeam' && rawResult && typeof rawResult === 'object' && 'key' in rawResult) {
126
+ data = serializeTeam(rawResult);
127
+ }
128
+ else {
129
+ // Plain JSON-serializable results (e.g., boolean, relation results, status maps, sub-issue statuses, graphs)
130
+ data = rawResult;
131
+ }
132
+ const response = { success: true, data };
133
+ return NextResponse.json(response);
134
+ }
135
+ catch (error) {
136
+ // Circuit breaker open — return 503 so callers know to retry later
137
+ if (isCircuitOpenError(error)) {
138
+ log.warn('Proxy request blocked by circuit breaker', { method });
139
+ return NextResponse.json({
140
+ success: false,
141
+ error: {
142
+ code: 'CIRCUIT_OPEN',
143
+ message: error.message,
144
+ retryable: true,
145
+ },
146
+ }, {
147
+ status: 503,
148
+ headers: { 'Retry-After': String(Math.ceil(error.retryAfterMs / 1000)) },
149
+ });
150
+ }
151
+ // Auth errors — return 401/403
152
+ const statusCode = extractStatusCode(error);
153
+ if (statusCode === 401 || statusCode === 403) {
154
+ log.error('Proxy auth error', { method, statusCode });
155
+ return NextResponse.json({
156
+ success: false,
157
+ error: {
158
+ code: 'AUTH_ERROR',
159
+ message: error instanceof Error ? error.message : 'Authentication failed',
160
+ retryable: false,
161
+ },
162
+ }, { status: statusCode });
163
+ }
164
+ // Rate limited
165
+ if (statusCode === 429) {
166
+ log.warn('Proxy rate limited by upstream', { method });
167
+ return NextResponse.json({
168
+ success: false,
169
+ error: {
170
+ code: 'RATE_LIMITED',
171
+ message: 'Rate limited by upstream issue tracker',
172
+ retryable: true,
173
+ },
174
+ }, { status: 429, headers: { 'Retry-After': '60' } });
175
+ }
176
+ // Generic errors
177
+ log.error('Proxy request failed', {
178
+ method,
179
+ error: error instanceof Error ? error.message : String(error),
180
+ });
181
+ return NextResponse.json({
182
+ success: false,
183
+ error: {
184
+ code: 'PROXY_ERROR',
185
+ message: error instanceof Error ? error.message : 'Proxy request failed',
186
+ retryable: statusCode !== undefined && statusCode >= 500,
187
+ },
188
+ }, { status: statusCode ?? 500 });
189
+ }
190
+ }
191
+ async function GET(_request) {
192
+ // Health endpoint — no auth required for monitoring
193
+ try {
194
+ const { getQuota } = await import('@renseiai/agentfactory-server');
195
+ const quota = await getQuota('default');
196
+ return NextResponse.json({
197
+ healthy: true,
198
+ quota: {
199
+ requestsRemaining: quota.requestsRemaining,
200
+ complexityRemaining: quota.complexityRemaining,
201
+ resetAt: quota.requestsReset,
202
+ updatedAt: quota.updatedAt,
203
+ },
204
+ });
205
+ }
206
+ catch {
207
+ return NextResponse.json({
208
+ healthy: true,
209
+ quota: { requestsRemaining: null, complexityRemaining: null, resetAt: null, updatedAt: 0 },
210
+ });
211
+ }
212
+ }
213
+ return { POST, GET };
214
+ }
215
+ // ---------------------------------------------------------------------------
216
+ // Helpers
217
+ // ---------------------------------------------------------------------------
218
+ function extractStatusCode(error) {
219
+ if (typeof error !== 'object' || error === null)
220
+ return undefined;
221
+ const err = error;
222
+ if (typeof err.status === 'number')
223
+ return err.status;
224
+ if (typeof err.statusCode === 'number')
225
+ return err.statusCode;
226
+ const response = err.response;
227
+ if (response && typeof response.status === 'number')
228
+ return response.status;
229
+ return undefined;
230
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Linear SDK Object Serializer
3
+ *
4
+ * The @linear/sdk returns objects with lazy-loaded relations (e.g., issue.state
5
+ * is a Promise). The proxy must resolve these server-side and return plain JSON.
6
+ *
7
+ * Uses duck-typing rather than direct @linear/sdk imports since the nextjs
8
+ * package doesn't depend on @linear/sdk directly.
9
+ */
10
+ import type { SerializedIssue, SerializedComment, SerializedViewer, SerializedTeam } from '@renseiai/agentfactory-linear';
11
+ /**
12
+ * Serialize a Linear Issue object to plain JSON.
13
+ * Resolves state, labels, assignee, team, parent, and project relations.
14
+ */
15
+ export declare function serializeIssue(issue: unknown): Promise<SerializedIssue>;
16
+ /**
17
+ * Serialize a Linear Comment object to plain JSON.
18
+ */
19
+ export declare function serializeComment(comment: unknown): Promise<SerializedComment>;
20
+ /**
21
+ * Serialize a Linear viewer to plain JSON.
22
+ */
23
+ export declare function serializeViewer(viewer: unknown): SerializedViewer;
24
+ /**
25
+ * Serialize a Linear team to plain JSON.
26
+ */
27
+ export declare function serializeTeam(team: unknown): SerializedTeam;
28
+ //# sourceMappingURL=serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../../../src/handlers/issue-tracker-proxy/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACf,MAAM,+BAA+B,CAAA;AA6CtC;;;GAGG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAqC7E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAWnF;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAOjE;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,cAAc,CAO3D"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Linear SDK Object Serializer
3
+ *
4
+ * The @linear/sdk returns objects with lazy-loaded relations (e.g., issue.state
5
+ * is a Promise). The proxy must resolve these server-side and return plain JSON.
6
+ *
7
+ * Uses duck-typing rather than direct @linear/sdk imports since the nextjs
8
+ * package doesn't depend on @linear/sdk directly.
9
+ */
10
+ /**
11
+ * Safe promise resolve — catches errors and returns fallback.
12
+ */
13
+ async function safeResolve(p, fallback) {
14
+ if (!p)
15
+ return fallback;
16
+ try {
17
+ return await p;
18
+ }
19
+ catch {
20
+ return fallback;
21
+ }
22
+ }
23
+ /**
24
+ * Serialize a Linear Issue object to plain JSON.
25
+ * Resolves state, labels, assignee, team, parent, and project relations.
26
+ */
27
+ export async function serializeIssue(issue) {
28
+ const i = issue;
29
+ const [state, labels, assignee, team, parent, project] = await Promise.all([
30
+ safeResolve(i.state, null),
31
+ safeResolve(i.labels?.().then((r) => r.nodes), []),
32
+ safeResolve(i.assignee, null),
33
+ safeResolve(i.team, null),
34
+ safeResolve(i.parent, null),
35
+ safeResolve(i.project, null),
36
+ ]);
37
+ return {
38
+ id: i.id,
39
+ identifier: i.identifier,
40
+ title: i.title,
41
+ description: i.description ?? undefined,
42
+ url: i.url,
43
+ priority: i.priority,
44
+ state: state
45
+ ? { id: state.id, name: state.name, type: state.type }
46
+ : undefined,
47
+ labels: (labels ?? []).map((l) => ({ id: l.id, name: l.name })),
48
+ assignee: assignee
49
+ ? { id: assignee.id, name: assignee.name, email: assignee.email ?? undefined }
50
+ : null,
51
+ team: team ? { id: team.id, name: team.name, key: team.key } : undefined,
52
+ parent: parent
53
+ ? { id: parent.id, identifier: parent.identifier }
54
+ : null,
55
+ project: project ? { id: project.id, name: project.name } : null,
56
+ createdAt: i.createdAt.toISOString(),
57
+ updatedAt: i.updatedAt.toISOString(),
58
+ };
59
+ }
60
+ /**
61
+ * Serialize a Linear Comment object to plain JSON.
62
+ */
63
+ export async function serializeComment(comment) {
64
+ const c = comment;
65
+ const user = await safeResolve(c.user, null);
66
+ return {
67
+ id: c.id,
68
+ body: c.body,
69
+ createdAt: c.createdAt.toISOString(),
70
+ updatedAt: c.updatedAt.toISOString(),
71
+ user: user ? { id: user.id, name: user.name } : null,
72
+ };
73
+ }
74
+ /**
75
+ * Serialize a Linear viewer to plain JSON.
76
+ */
77
+ export function serializeViewer(viewer) {
78
+ const v = viewer;
79
+ return {
80
+ id: v.id,
81
+ name: v.name,
82
+ email: v.email,
83
+ };
84
+ }
85
+ /**
86
+ * Serialize a Linear team to plain JSON.
87
+ */
88
+ export function serializeTeam(team) {
89
+ const t = team;
90
+ return {
91
+ id: t.id,
92
+ name: t.name,
93
+ key: t.key,
94
+ };
95
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Types for the issue tracker proxy handler.
3
+ */
4
+ import type { RouteConfig } from '../../types.js';
5
+ export interface ProxyHandlerConfig extends RouteConfig {
6
+ /** Optional: override worker API key env var (default: WORKER_API_KEY) */
7
+ workerApiKeyEnvVar?: string;
8
+ }
9
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/handlers/issue-tracker-proxy/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAEjD,MAAM,WAAW,kBAAmB,SAAQ,WAAW;IACrD,0EAA0E;IAC1E,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for the issue tracker proxy handler.
3
+ */
4
+ export {};