@stacknet/stacks 0.2.0 → 0.2.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 (63) hide show
  1. package/README.md +136 -0
  2. package/dist/{billing-eQZIWeNW.d.cts → billing-cj0eSVrp.d.cts} +59 -1
  3. package/dist/{billing-eQZIWeNW.d.ts → billing-cj0eSVrp.d.ts} +59 -1
  4. package/dist/clients/index.cjs +4 -4
  5. package/dist/clients/index.d.cts +24 -1
  6. package/dist/clients/index.d.ts +24 -1
  7. package/dist/clients/index.js +4 -4
  8. package/dist/index.cjs +7 -7
  9. package/dist/index.d.cts +3 -3
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +7 -7
  12. package/dist/proxy/index.cjs +2 -2
  13. package/dist/proxy/index.js +2 -2
  14. package/dist/streaming/index.cjs +5 -5
  15. package/dist/streaming/index.js +5 -5
  16. package/dist/types/index.d.cts +1 -1
  17. package/dist/types/index.d.ts +1 -1
  18. package/package.json +15 -13
  19. package/src/clients/agents.ts +0 -250
  20. package/src/clients/billing.ts +0 -197
  21. package/src/clients/coder.ts +0 -655
  22. package/src/clients/files.ts +0 -86
  23. package/src/clients/index.ts +0 -93
  24. package/src/clients/magma.ts +0 -299
  25. package/src/clients/mcp.ts +0 -208
  26. package/src/clients/network.ts +0 -118
  27. package/src/clients/points.ts +0 -403
  28. package/src/clients/skills.ts +0 -236
  29. package/src/clients/social.ts +0 -286
  30. package/src/clients/stack-management.ts +0 -279
  31. package/src/clients/task-network.ts +0 -303
  32. package/src/clients/user.ts +0 -84
  33. package/src/clients/widgets.ts +0 -171
  34. package/src/index.ts +0 -387
  35. package/src/managers/index.ts +0 -10
  36. package/src/managers/task-manager.ts +0 -332
  37. package/src/proxy/forwarder.ts +0 -235
  38. package/src/proxy/index.ts +0 -32
  39. package/src/proxy/route-handlers.ts +0 -1107
  40. package/src/streaming/component-stream.ts +0 -319
  41. package/src/streaming/index.ts +0 -21
  42. package/src/streaming/sse.ts +0 -266
  43. package/src/types/agent.ts +0 -108
  44. package/src/types/billing.ts +0 -121
  45. package/src/types/chat.ts +0 -58
  46. package/src/types/coder.ts +0 -345
  47. package/src/types/credential.ts +0 -111
  48. package/src/types/file.ts +0 -15
  49. package/src/types/imagination.ts +0 -50
  50. package/src/types/index.ts +0 -20
  51. package/src/types/mcp.ts +0 -35
  52. package/src/types/network.ts +0 -97
  53. package/src/types/points.ts +0 -250
  54. package/src/types/skill.ts +0 -107
  55. package/src/types/social.ts +0 -109
  56. package/src/types/stack.ts +0 -269
  57. package/src/types/task.ts +0 -41
  58. package/src/types/user.ts +0 -29
  59. package/src/types/widget.ts +0 -57
  60. package/src/utils/constants.ts +0 -26
  61. package/src/utils/errors.ts +0 -169
  62. package/src/utils/helpers.ts +0 -85
  63. package/src/utils/index.ts +0 -7
@@ -1,332 +0,0 @@
1
- /**
2
- * TaskManager - Redis-backed task state management
3
- *
4
- * Manages task lifecycle for long-running operations with real-time updates
5
- */
6
-
7
- import type { TaskType, TaskStatus, TaskState } from '../types';
8
- import { TASK_PREFIX, CHAT_TASKS_PREFIX, TASK_TTL } from '../utils/constants';
9
- import { generateUUID } from '../utils/helpers';
10
-
11
- // Redis client type - we use a generic interface to avoid hard dependency
12
- export interface RedisClientLike {
13
- hSet(key: string, value: Record<string, string>): Promise<number>;
14
- hGetAll(key: string): Promise<Record<string, string>>;
15
- expire(key: string, seconds: number): Promise<boolean>;
16
- sAdd(key: string, ...members: string[]): Promise<number>;
17
- sMembers(key: string): Promise<string[]>;
18
- sRem(key: string, ...members: string[]): Promise<number>;
19
- del(key: string): Promise<number>;
20
- publish(channel: string, message: string): Promise<number>;
21
- subscribe(channel: string, callback: (message: string) => void): Promise<void>;
22
- unsubscribe(channel: string): Promise<void>;
23
- }
24
-
25
- export interface TaskManagerConfig {
26
- redis?: RedisClientLike;
27
- getRedisClient?: () => Promise<RedisClientLike>;
28
- taskTTL?: number;
29
- }
30
-
31
- export class TaskManager {
32
- private redis?: RedisClientLike;
33
- private getRedisClient?: () => Promise<RedisClientLike>;
34
- private taskTTL: number;
35
-
36
- constructor(config: TaskManagerConfig = {}) {
37
- this.redis = config.redis;
38
- this.getRedisClient = config.getRedisClient;
39
- this.taskTTL = config.taskTTL || TASK_TTL;
40
- }
41
-
42
- /**
43
- * Get Redis client (lazily initialized)
44
- */
45
- private async getClient(): Promise<RedisClientLike> {
46
- if (this.redis) return this.redis;
47
- if (this.getRedisClient) {
48
- this.redis = await this.getRedisClient();
49
- return this.redis;
50
- }
51
- throw new Error('Redis client not configured. Provide redis or getRedisClient in config.');
52
- }
53
-
54
- /**
55
- * Create a new task and return its ID
56
- */
57
- async createTask(
58
- chatId: string,
59
- messageId: string,
60
- type: TaskType,
61
- initialMessage: string = 'Starting...'
62
- ): Promise<string> {
63
- const redis = await this.getClient();
64
- const taskId = generateUUID();
65
- const now = Date.now();
66
-
67
- const task: TaskState = {
68
- id: taskId,
69
- chatId,
70
- messageId,
71
- type,
72
- status: 'pending',
73
- progress: 0,
74
- message: initialMessage,
75
- createdAt: now,
76
- updatedAt: now,
77
- };
78
-
79
- await redis.hSet(`${TASK_PREFIX}${taskId}`, this.taskToRedis(task));
80
- await redis.expire(`${TASK_PREFIX}${taskId}`, this.taskTTL);
81
-
82
- await redis.sAdd(`${CHAT_TASKS_PREFIX}${chatId}`, taskId);
83
- await redis.expire(`${CHAT_TASKS_PREFIX}${chatId}`, this.taskTTL);
84
-
85
- console.log(`[TaskManager] Created task ${taskId} for chat ${chatId} (${type})`);
86
-
87
- return taskId;
88
- }
89
-
90
- /**
91
- * Update task progress
92
- */
93
- async updateProgress(
94
- taskId: string,
95
- progress: number,
96
- message: string
97
- ): Promise<void> {
98
- const redis = await this.getClient();
99
-
100
- await redis.hSet(`${TASK_PREFIX}${taskId}`, {
101
- status: 'generating',
102
- progress: progress.toString(),
103
- message,
104
- updatedAt: Date.now().toString(),
105
- });
106
-
107
- await redis.publish(
108
- `task:progress:${taskId}`,
109
- JSON.stringify({
110
- taskId,
111
- progress,
112
- message,
113
- status: 'generating',
114
- })
115
- );
116
-
117
- console.log(`[TaskManager] Task ${taskId} progress: ${progress}% - ${message}`);
118
- }
119
-
120
- /**
121
- * Mark task as complete with result
122
- */
123
- async complete(taskId: string, result: unknown): Promise<void> {
124
- const redis = await this.getClient();
125
-
126
- await redis.hSet(`${TASK_PREFIX}${taskId}`, {
127
- status: 'complete',
128
- progress: '100',
129
- message: 'Complete',
130
- result: JSON.stringify(result),
131
- updatedAt: Date.now().toString(),
132
- });
133
-
134
- await redis.publish(
135
- `task:progress:${taskId}`,
136
- JSON.stringify({
137
- taskId,
138
- progress: 100,
139
- message: 'Complete',
140
- status: 'complete',
141
- result,
142
- })
143
- );
144
-
145
- console.log(`[TaskManager] Task ${taskId} completed`);
146
- }
147
-
148
- /**
149
- * Mark task as failed
150
- */
151
- async fail(taskId: string, error: string): Promise<void> {
152
- const redis = await this.getClient();
153
-
154
- await redis.hSet(`${TASK_PREFIX}${taskId}`, {
155
- status: 'failed',
156
- message: error,
157
- error,
158
- updatedAt: Date.now().toString(),
159
- });
160
-
161
- await redis.publish(
162
- `task:progress:${taskId}`,
163
- JSON.stringify({
164
- taskId,
165
- status: 'failed',
166
- error,
167
- })
168
- );
169
-
170
- console.log(`[TaskManager] Task ${taskId} failed: ${error}`);
171
- }
172
-
173
- /**
174
- * Get task by ID
175
- */
176
- async getTask(taskId: string): Promise<TaskState | null> {
177
- const redis = await this.getClient();
178
- const data = await redis.hGetAll(`${TASK_PREFIX}${taskId}`);
179
-
180
- if (!data || Object.keys(data).length === 0) {
181
- return null;
182
- }
183
-
184
- return this.redisToTask(data);
185
- }
186
-
187
- /**
188
- * Get all tasks for a chat
189
- */
190
- async getTasksForChat(chatId: string): Promise<TaskState[]> {
191
- const redis = await this.getClient();
192
- const taskIds = await redis.sMembers(`${CHAT_TASKS_PREFIX}${chatId}`);
193
-
194
- if (!taskIds || taskIds.length === 0) {
195
- return [];
196
- }
197
-
198
- const tasks: TaskState[] = [];
199
- for (const taskId of taskIds) {
200
- const task = await this.getTask(taskId);
201
- if (task) {
202
- tasks.push(task);
203
- }
204
- }
205
-
206
- return tasks.sort((a, b) => b.createdAt - a.createdAt);
207
- }
208
-
209
- /**
210
- * Get pending/generating tasks for a chat
211
- */
212
- async getPendingTasksForChat(chatId: string): Promise<TaskState[]> {
213
- const tasks = await this.getTasksForChat(chatId);
214
- return tasks.filter((t) => t.status === 'pending' || t.status === 'generating');
215
- }
216
-
217
- /**
218
- * Delete a task
219
- */
220
- async deleteTask(taskId: string): Promise<void> {
221
- const redis = await this.getClient();
222
- const task = await this.getTask(taskId);
223
-
224
- if (task) {
225
- await redis.del(`${TASK_PREFIX}${taskId}`);
226
- await redis.sRem(`${CHAT_TASKS_PREFIX}${task.chatId}`, taskId);
227
- console.log(`[TaskManager] Deleted task ${taskId}`);
228
- }
229
- }
230
-
231
- /**
232
- * Clean up old completed/failed tasks for a chat
233
- */
234
- async cleanupOldTasks(chatId: string, maxAge: number = 86400000): Promise<void> {
235
- const tasks = await this.getTasksForChat(chatId);
236
- const now = Date.now();
237
-
238
- for (const task of tasks) {
239
- if (
240
- (task.status === 'complete' || task.status === 'failed') &&
241
- now - task.updatedAt > maxAge
242
- ) {
243
- await this.deleteTask(task.id);
244
- }
245
- }
246
- }
247
-
248
- /**
249
- * Subscribe to task progress updates
250
- */
251
- async subscribeToTask(
252
- taskId: string,
253
- callback: (update: { taskId: string; status: TaskStatus; progress?: number; message?: string; result?: unknown; error?: string }) => void
254
- ): Promise<() => Promise<void>> {
255
- const redis = await this.getClient();
256
- const channel = `task:progress:${taskId}`;
257
-
258
- await redis.subscribe(channel, (message) => {
259
- let data: unknown;
260
- try {
261
- data = JSON.parse(message);
262
- } catch {
263
- return; // not JSON — ignore
264
- }
265
- // Shape-check: Redis pub/sub is a trust boundary. If anything else
266
- // (a compromised publisher, a misconfigured tenant, or an operator
267
- // pushing test data) posts to this channel, the message reaches
268
- // this callback. Drop anything that doesn't match our own
269
- // publisher's shape before handing it to consumer code — otherwise
270
- // an attacker with publish access could inject arbitrary objects
271
- // (prototype keys, unexpected fields) into the UI layer.
272
- if (!data || typeof data !== 'object' || Array.isArray(data)) return;
273
- const d = data as Record<string, unknown>;
274
- if (typeof d.taskId !== 'string' || d.taskId !== taskId) return;
275
- if (typeof d.status !== 'string') return;
276
- if (d.progress !== undefined && typeof d.progress !== 'number') return;
277
- if (d.message !== undefined && typeof d.message !== 'string') return;
278
- if (d.error !== undefined && typeof d.error !== 'string') return;
279
- callback({
280
- taskId: d.taskId,
281
- status: d.status as TaskStatus,
282
- progress: d.progress as number | undefined,
283
- message: d.message as string | undefined,
284
- result: d.result,
285
- error: d.error as string | undefined,
286
- });
287
- });
288
-
289
- return async () => {
290
- await redis.unsubscribe(channel);
291
- };
292
- }
293
-
294
- private taskToRedis(task: TaskState): Record<string, string> {
295
- return {
296
- id: task.id,
297
- chatId: task.chatId,
298
- messageId: task.messageId,
299
- type: task.type,
300
- status: task.status,
301
- progress: task.progress.toString(),
302
- message: task.message,
303
- result: task.result || '',
304
- error: task.error || '',
305
- createdAt: task.createdAt.toString(),
306
- updatedAt: task.updatedAt.toString(),
307
- };
308
- }
309
-
310
- private redisToTask(data: Record<string, string>): TaskState {
311
- return {
312
- id: data.id,
313
- chatId: data.chatId,
314
- messageId: data.messageId,
315
- type: data.type as TaskType,
316
- status: data.status as TaskStatus,
317
- progress: parseInt(data.progress, 10),
318
- message: data.message,
319
- result: data.result || undefined,
320
- error: data.error || undefined,
321
- createdAt: parseInt(data.createdAt, 10),
322
- updatedAt: parseInt(data.updatedAt, 10),
323
- };
324
- }
325
- }
326
-
327
- /**
328
- * Factory function to create a TaskManager
329
- */
330
- export function createTaskManager(config?: TaskManagerConfig): TaskManager {
331
- return new TaskManager(config);
332
- }
@@ -1,235 +0,0 @@
1
- /**
2
- * Generic request forwarding utilities
3
- */
4
-
5
- import { DEFAULT_TASK_NETWORK_URL } from '../utils/constants';
6
-
7
- export interface ForwarderConfig {
8
- baseUrl?: string;
9
- defaultHeaders?: Record<string, string>;
10
- /** Request timeout in milliseconds. Applied via AbortSignal to every
11
- * outbound fetch. Default: 30_000 (30s). Previously declared but not
12
- * wired — an upstream hang would leak file descriptors indefinitely. */
13
- timeout?: number;
14
- /** Hard cap on inbound JSON body size (bytes) read by `createProxyHandler`.
15
- * Requests larger than this are rejected with 413. Default: 1 MiB. */
16
- maxBodyBytes?: number;
17
- }
18
-
19
- export interface RequestOptions {
20
- method?: string;
21
- headers?: Record<string, string>;
22
- body?: unknown;
23
- searchParams?: Record<string, string | undefined>;
24
- stream?: boolean;
25
- }
26
-
27
- const DEFAULT_TIMEOUT_MS = 30_000;
28
- const DEFAULT_MAX_BODY_BYTES = 1024 * 1024; // 1 MiB
29
-
30
- /** Headers that are safe to forward from an inbound request to the upstream.
31
- * `createProxyHandler` previously copied every inbound header — including
32
- * `cookie`, attacker-set `x-forwarded-for`, and the consumer app's session
33
- * cookies — to an arbitrary upstream. Only the headers that carry request
34
- * semantics (auth + content negotiation + a request id for tracing) are
35
- * preserved now. */
36
- const FORWARDABLE_REQUEST_HEADERS = new Set([
37
- 'authorization',
38
- 'content-type',
39
- 'accept',
40
- 'accept-language',
41
- 'x-request-id',
42
- 'x-correlation-id',
43
- 'idempotency-key',
44
- ]);
45
-
46
- /** Build the outbound URL by resolving `path` relative to `baseUrl`. Using
47
- * the URL constructor here (instead of string concatenation) prevents a
48
- * path like ".evil.com/x" from producing an unintended host.
49
- * Throws if the resolved URL escapes the baseUrl origin. */
50
- function resolveUrl(baseUrl: string, path: string): string {
51
- // Ensure baseUrl ends with '/' so URL() treats it as a directory root.
52
- const baseForResolve = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
53
- // Drop leading '/' on path so it doesn't replace the baseUrl pathname.
54
- const rel = path.startsWith('/') ? path.slice(1) : path;
55
- const resolved = new URL(rel, baseForResolve);
56
- const base = new URL(baseForResolve);
57
- if (resolved.origin !== base.origin) {
58
- throw new Error(`forwarder: resolved URL ${resolved.origin} escapes baseUrl origin ${base.origin}`);
59
- }
60
- return resolved.toString();
61
- }
62
-
63
- /**
64
- * Forward a request to a backend URL
65
- */
66
- export async function forwardRequest(
67
- path: string,
68
- options: RequestOptions = {},
69
- config: ForwarderConfig = {}
70
- ): Promise<Response> {
71
- const baseUrl = config.baseUrl || DEFAULT_TASK_NETWORK_URL;
72
- const { method = 'GET', headers = {}, body, searchParams, stream } = options;
73
-
74
- // Build URL safely, then attach search params.
75
- const urlObj = new URL(resolveUrl(baseUrl, path));
76
- if (searchParams) {
77
- Object.entries(searchParams).forEach(([key, value]) => {
78
- if (value !== undefined) {
79
- urlObj.searchParams.set(key, value);
80
- }
81
- });
82
- }
83
-
84
- const fetchOptions: RequestInit = {
85
- method,
86
- headers: {
87
- ...config.defaultHeaders,
88
- ...headers,
89
- },
90
- signal: AbortSignal.timeout(config.timeout ?? DEFAULT_TIMEOUT_MS),
91
- };
92
-
93
- if (body && method !== 'GET') {
94
- fetchOptions.headers = {
95
- ...fetchOptions.headers,
96
- 'Content-Type': 'application/json',
97
- };
98
- fetchOptions.body = JSON.stringify(body);
99
- }
100
-
101
- const response = await fetch(urlObj.toString(), fetchOptions);
102
-
103
- // For streaming responses, return directly
104
- if (stream && response.body) {
105
- return response;
106
- }
107
-
108
- return response;
109
- }
110
-
111
- /**
112
- * Forward and return JSON response
113
- */
114
- export async function forwardJSON<T = unknown>(
115
- path: string,
116
- options: RequestOptions = {},
117
- config: ForwarderConfig = {}
118
- ): Promise<{ data: T; status: number }> {
119
- const response = await forwardRequest(path, options, config);
120
- const data = await response.json();
121
- return { data, status: response.status };
122
- }
123
-
124
- /** Read a request body with a hard byte cap. Returns `null` if the body
125
- * exceeds the cap, so the caller can return 413 without first buffering
126
- * an attacker-sized payload into memory. */
127
- async function readBodyCapped(request: Request, maxBytes: number): Promise<unknown | { _tooLarge: true }> {
128
- const contentLength = request.headers.get('content-length');
129
- if (contentLength) {
130
- const n = parseInt(contentLength, 10);
131
- if (Number.isFinite(n) && n > maxBytes) return { _tooLarge: true };
132
- }
133
- if (!request.body) return undefined;
134
- const reader = request.body.getReader();
135
- let received = 0;
136
- const chunks: Uint8Array[] = [];
137
- // Streaming read with byte cap — aborts as soon as the cap is exceeded.
138
- // eslint-disable-next-line no-constant-condition
139
- while (true) {
140
- const { done, value } = await reader.read();
141
- if (done) break;
142
- received += value.byteLength;
143
- if (received > maxBytes) {
144
- try { await reader.cancel(); } catch { /* ignore */ }
145
- return { _tooLarge: true };
146
- }
147
- chunks.push(value);
148
- }
149
- if (received === 0) return undefined;
150
- const buf = new Uint8Array(received);
151
- let offset = 0;
152
- for (const c of chunks) { buf.set(c, offset); offset += c.byteLength; }
153
- const text = new TextDecoder().decode(buf);
154
- if (text.trim() === '') return undefined;
155
- try {
156
- return JSON.parse(text);
157
- } catch {
158
- return undefined;
159
- }
160
- }
161
-
162
- /**
163
- * Create a proxy handler that forwards requests.
164
- *
165
- * SECURITY: only a small whitelist of inbound headers is forwarded (see
166
- * FORWARDABLE_REQUEST_HEADERS). Everything else — including `cookie`,
167
- * `x-forwarded-for`, `host` — is stripped so a caller cannot poison the
168
- * upstream's view of the request or smuggle the consumer app's cookies to
169
- * a third-party StackNet endpoint.
170
- */
171
- export function createProxyHandler(
172
- path: string | ((req: Request) => string),
173
- config: ForwarderConfig = {}
174
- ) {
175
- const maxBodyBytes = config.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
176
-
177
- return async (request: Request): Promise<Response> => {
178
- const url = new URL(request.url);
179
- const targetPath = typeof path === 'function' ? path(request) : path;
180
-
181
- // Forward search params
182
- const searchParams: Record<string, string> = {};
183
- url.searchParams.forEach((value, key) => {
184
- searchParams[key] = value;
185
- });
186
-
187
- let body: unknown = undefined;
188
- if (request.method !== 'GET' && request.method !== 'HEAD') {
189
- const read = await readBodyCapped(request, maxBodyBytes);
190
- if (read && typeof read === 'object' && (read as { _tooLarge?: boolean })._tooLarge) {
191
- return Response.json(
192
- { error: `Request body exceeds ${maxBodyBytes} bytes` },
193
- { status: 413 },
194
- );
195
- }
196
- body = read;
197
- }
198
-
199
- // Forward ONLY the whitelisted headers.
200
- const requestHeaders: Record<string, string> = {};
201
- request.headers.forEach((value, key) => {
202
- if (FORWARDABLE_REQUEST_HEADERS.has(key.toLowerCase())) {
203
- requestHeaders[key] = value;
204
- }
205
- });
206
-
207
- const response = await forwardRequest(
208
- targetPath,
209
- {
210
- method: request.method,
211
- headers: requestHeaders,
212
- body,
213
- searchParams,
214
- },
215
- config
216
- );
217
-
218
- // Check if streaming response
219
- const contentType = response.headers.get('content-type') || '';
220
- if (contentType.includes('text/event-stream')) {
221
- return new Response(response.body, {
222
- status: response.status,
223
- headers: {
224
- 'Content-Type': 'text/event-stream',
225
- 'Cache-Control': 'no-cache',
226
- 'Connection': 'keep-alive',
227
- },
228
- });
229
- }
230
-
231
- // Return JSON response
232
- const data = await response.json();
233
- return Response.json(data, { status: response.status });
234
- };
235
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * @stacknet/stacks - Proxy utilities
3
- */
4
-
5
- export {
6
- forwardRequest,
7
- forwardJSON,
8
- createProxyHandler,
9
- type ForwarderConfig,
10
- type RequestOptions,
11
- } from './forwarder';
12
-
13
- export {
14
- createAgentRoutes,
15
- createAgentDetailRoutes,
16
- createAgentExecuteRoute,
17
- createAgentToggleRoutes,
18
- createSkillRoutes,
19
- createSkillDetailRoutes,
20
- createWidgetRoutes,
21
- createWidgetDetailRoutes,
22
- createImaginationRoutes,
23
- // Stack management routes
24
- createStackRoutes,
25
- createStackDetailRoutes,
26
- createStackKeysRoutes,
27
- createStackMembersRoutes,
28
- type RouteHandlerConfig,
29
- type StackRouteHandlerConfig,
30
- type RouteHandler,
31
- type CRUDRouteHandlers,
32
- } from './route-handlers';