@pellux/goodvibes-daemon-sdk 0.18.3 → 0.30.1

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 (82) hide show
  1. package/README.md +13 -7
  2. package/dist/api-router.d.ts +8 -1
  3. package/dist/api-router.d.ts.map +1 -1
  4. package/dist/api-router.js +12 -3
  5. package/dist/artifact-upload.d.ts +26 -0
  6. package/dist/artifact-upload.d.ts.map +1 -0
  7. package/dist/artifact-upload.js +535 -0
  8. package/dist/automation.d.ts +2 -2
  9. package/dist/automation.d.ts.map +1 -1
  10. package/dist/channel-route-types.d.ts.map +1 -1
  11. package/dist/channel-routes.d.ts +2 -2
  12. package/dist/channel-routes.d.ts.map +1 -1
  13. package/dist/channel-routes.js +28 -3
  14. package/dist/context.d.ts +151 -51
  15. package/dist/context.d.ts.map +1 -1
  16. package/dist/control-routes.d.ts +2 -2
  17. package/dist/control-routes.d.ts.map +1 -1
  18. package/dist/error-response.d.ts.map +1 -1
  19. package/dist/error-response.js +172 -12
  20. package/dist/http-policy.d.ts +1 -1
  21. package/dist/http-policy.d.ts.map +1 -1
  22. package/dist/http-policy.js +0 -1
  23. package/dist/index.d.ts +8 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -1
  26. package/dist/integration-route-types.d.ts +3 -1
  27. package/dist/integration-route-types.d.ts.map +1 -1
  28. package/dist/integration-routes.d.ts +2 -2
  29. package/dist/integration-routes.d.ts.map +1 -1
  30. package/dist/integration-routes.js +3 -0
  31. package/dist/knowledge-refinement-routes.d.ts +4 -0
  32. package/dist/knowledge-refinement-routes.d.ts.map +1 -0
  33. package/dist/knowledge-refinement-routes.js +58 -0
  34. package/dist/knowledge-route-types.d.ts +11 -1
  35. package/dist/knowledge-route-types.d.ts.map +1 -1
  36. package/dist/knowledge-routes.d.ts +2 -2
  37. package/dist/knowledge-routes.d.ts.map +1 -1
  38. package/dist/knowledge-routes.js +156 -13
  39. package/dist/media-route-types.d.ts +10 -0
  40. package/dist/media-route-types.d.ts.map +1 -1
  41. package/dist/media-routes.d.ts +2 -2
  42. package/dist/media-routes.d.ts.map +1 -1
  43. package/dist/media-routes.js +134 -15
  44. package/dist/operator.d.ts +2 -2
  45. package/dist/operator.d.ts.map +1 -1
  46. package/dist/operator.js +49 -15
  47. package/dist/otlp-protobuf.d.ts +3 -0
  48. package/dist/otlp-protobuf.d.ts.map +1 -0
  49. package/dist/otlp-protobuf.js +977 -0
  50. package/dist/remote-routes.d.ts +2 -2
  51. package/dist/remote-routes.d.ts.map +1 -1
  52. package/dist/remote-routes.js +146 -13
  53. package/dist/remote.d.ts +2 -2
  54. package/dist/remote.d.ts.map +1 -1
  55. package/dist/route-helpers.d.ts +8 -0
  56. package/dist/route-helpers.d.ts.map +1 -1
  57. package/dist/route-helpers.js +24 -0
  58. package/dist/runtime-automation-routes.d.ts +2 -2
  59. package/dist/runtime-automation-routes.d.ts.map +1 -1
  60. package/dist/runtime-automation-routes.js +4 -1
  61. package/dist/runtime-route-types.d.ts +46 -3
  62. package/dist/runtime-route-types.d.ts.map +1 -1
  63. package/dist/runtime-routes.d.ts +2 -2
  64. package/dist/runtime-routes.d.ts.map +1 -1
  65. package/dist/runtime-routes.js +1 -0
  66. package/dist/runtime-session-routes.d.ts +13 -3
  67. package/dist/runtime-session-routes.d.ts.map +1 -1
  68. package/dist/runtime-session-routes.js +102 -10
  69. package/dist/sessions.d.ts +2 -2
  70. package/dist/sessions.d.ts.map +1 -1
  71. package/dist/sessions.js +3 -0
  72. package/dist/system-route-types.d.ts +19 -0
  73. package/dist/system-route-types.d.ts.map +1 -1
  74. package/dist/system-routes.d.ts +2 -2
  75. package/dist/system-routes.d.ts.map +1 -1
  76. package/dist/system-routes.js +18 -0
  77. package/dist/tasks.d.ts +2 -2
  78. package/dist/tasks.d.ts.map +1 -1
  79. package/dist/telemetry-routes.d.ts +25 -2
  80. package/dist/telemetry-routes.d.ts.map +1 -1
  81. package/dist/telemetry-routes.js +131 -15
  82. package/package.json +128 -5
package/README.md CHANGED
@@ -1,14 +1,20 @@
1
1
  # @pellux/goodvibes-daemon-sdk
2
2
 
3
- Embeddable daemon and control-plane route contracts, dispatchers, and handler builders for GoodVibes.
3
+ Internal workspace package backing `@pellux/goodvibes-sdk/daemon`.
4
4
 
5
- Install:
5
+ Consumers should install `@pellux/goodvibes-sdk` and import this surface from the umbrella package.
6
6
 
7
- ```bash
8
- npm install @pellux/goodvibes-daemon-sdk
7
+ Consumer import:
8
+
9
+ ```ts
10
+ import {
11
+ createDaemonControlRouteHandlers,
12
+ createDaemonTelemetryRouteHandlers,
13
+ dispatchDaemonApiRoutes,
14
+ } from '@pellux/goodvibes-sdk/daemon';
9
15
  ```
10
16
 
11
- Use this package to:
17
+ This surface is for:
12
18
  - embed daemon routes in another host
13
19
  - dispatch operator, automation, session, task, or remote API calls
14
20
  - reuse shared daemon auth/error helpers
@@ -20,7 +26,7 @@ import {
20
26
  createDaemonControlRouteHandlers,
21
27
  createDaemonTelemetryRouteHandlers,
22
28
  dispatchDaemonApiRoutes,
23
- } from '@pellux/goodvibes-daemon-sdk';
29
+ } from '@pellux/goodvibes-sdk/daemon';
24
30
  ```
25
31
 
26
32
  This package gives you reusable route modules, but your host still owns:
@@ -29,4 +35,4 @@ This package gives you reusable route modules, but your host still owns:
29
35
  - auth/session storage
30
36
  - runtime bootstrapping
31
37
 
32
- Use this package when you are embedding GoodVibes into another TypeScript server. Do not use it for normal client-side integrations.
38
+ Use this surface when you are embedding GoodVibes into another TypeScript server. Do not use it for normal client-side integrations.
@@ -1,3 +1,10 @@
1
1
  import type { DaemonApiRouteHandlers } from './context.js';
2
- export declare function dispatchDaemonApiRoutes(req: Request, handlers: DaemonApiRouteHandlers): Promise<Response | null>;
2
+ /**
3
+ * Optional extension dispatchers injected alongside the standard route set.
4
+ * Each dispatcher is tried in order after the built-in routes; the first
5
+ * non-null result wins. Use this to wire companion-chat, provider, or other
6
+ * feature routes into the standalone daemon without modifying core route files.
7
+ */
8
+ export type DaemonApiRouteExtension = (req: Request) => Promise<Response | null> | Response | null;
9
+ export declare function dispatchDaemonApiRoutes(req: Request, handlers: DaemonApiRouteHandlers, extensions?: readonly DaemonApiRouteExtension[]): Promise<Response | null>;
3
10
  //# sourceMappingURL=api-router.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-router.d.ts","sourceRoot":"","sources":["../src/api-router.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,sBAAsB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAQtH"}
1
+ {"version":3,"file":"api-router.d.ts","sourceRoot":"","sources":["../src/api-router.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEnG,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,sBAAsB,EAChC,UAAU,CAAC,EAAE,SAAS,uBAAuB,EAAE,GAC9C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAgB1B"}
@@ -1,13 +1,22 @@
1
- // Synced from goodvibes-tui/src/control-plane/routes/api-router.ts
2
1
  import { dispatchAutomationRoutes } from './automation.js';
3
2
  import { dispatchOperatorRoutes } from './operator.js';
4
3
  import { dispatchRemoteRoutes } from './remote.js';
5
4
  import { dispatchSessionRoutes } from './sessions.js';
6
5
  import { dispatchTaskRoutes } from './tasks.js';
7
- export async function dispatchDaemonApiRoutes(req, handlers) {
8
- return (await dispatchRemoteRoutes(req, handlers)
6
+ export async function dispatchDaemonApiRoutes(req, handlers, extensions) {
7
+ const coreResult = (await dispatchRemoteRoutes(req, handlers)
9
8
  ?? await dispatchOperatorRoutes(req, handlers)
10
9
  ?? await dispatchAutomationRoutes(req, handlers)
11
10
  ?? await dispatchSessionRoutes(req, handlers)
12
11
  ?? await dispatchTaskRoutes(req, handlers));
12
+ if (coreResult !== null)
13
+ return coreResult;
14
+ if (extensions) {
15
+ for (const extension of extensions) {
16
+ const result = await extension(req);
17
+ if (result !== null)
18
+ return result;
19
+ }
20
+ }
21
+ return null;
13
22
  }
@@ -0,0 +1,26 @@
1
+ export type ArtifactUploadFieldMap = Record<string, unknown>;
2
+ export interface ArtifactStoreUploadLike {
3
+ create(input: Record<string, unknown>): Promise<unknown>;
4
+ getMaxBytes?(): number;
5
+ createFromStream?(input: {
6
+ readonly stream: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | Buffer | string> | Iterable<Uint8Array | Buffer | string>;
7
+ readonly kind?: string;
8
+ readonly mimeType?: string;
9
+ readonly filename?: string;
10
+ readonly sourceUri?: string;
11
+ readonly sizeBytes?: number;
12
+ readonly retentionMs?: number;
13
+ readonly acquisitionMode?: string;
14
+ readonly fetchMode?: string;
15
+ readonly metadata?: Record<string, unknown>;
16
+ }): Promise<unknown>;
17
+ }
18
+ export interface ArtifactUploadResult {
19
+ readonly artifact: unknown;
20
+ readonly artifactId: string;
21
+ readonly fields: ArtifactUploadFieldMap;
22
+ }
23
+ export declare function isJsonContentType(contentType: string | null): boolean;
24
+ export declare function isArtifactUploadRequest(req: Request): boolean;
25
+ export declare function createArtifactFromUploadRequest(artifactStore: ArtifactStoreUploadLike, req: Request): Promise<ArtifactUploadResult | Response>;
26
+ //# sourceMappingURL=artifact-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact-upload.d.ts","sourceRoot":"","sources":["../src/artifact-upload.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAE7D,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzD,WAAW,CAAC,IAAI,MAAM,CAAC;IACvB,gBAAgB,CAAC,CAAC,KAAK,EAAE;QACvB,QAAQ,CAAC,MAAM,EACX,cAAc,CAAC,UAAU,CAAC,GAC1B,aAAa,CAAC,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC,GAC3C,QAAQ,CAAC,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC;QAC3C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAC9B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC7C,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AA+BD,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAGrE;AAED,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED,wBAAsB,+BAA+B,CACnD,aAAa,EAAE,uBAAuB,EACtC,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,oBAAoB,GAAG,QAAQ,CAAC,CAM1C"}
@@ -0,0 +1,535 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdtemp, open, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { GoodVibesSdkError } from '@pellux/goodvibes-errors';
6
+ const JSON_FIELD_NAMES = new Set(['metadata', 'target', 'options']);
7
+ const MAX_MULTIPART_FIELD_BYTES = 1024 * 1024;
8
+ const MAX_MULTIPART_BODY_OVERHEAD_BYTES = MAX_MULTIPART_FIELD_BYTES;
9
+ function artifactUploadError(message, options = {}) {
10
+ return new GoodVibesSdkError(message, {
11
+ category: 'bad_request',
12
+ source: 'runtime',
13
+ operation: 'daemon.artifactUpload',
14
+ status: options.status,
15
+ code: options.code ?? 'ARTIFACT_UPLOAD_INVALID_REQUEST',
16
+ });
17
+ }
18
+ function artifactSizeError(maxBytes) {
19
+ return artifactUploadError(`Artifact exceeds the ${maxBytes}-byte limit.`, {
20
+ status: 413,
21
+ code: 'ARTIFACT_UPLOAD_TOO_LARGE',
22
+ });
23
+ }
24
+ export function isJsonContentType(contentType) {
25
+ const lower = (contentType ?? '').split(';')[0]?.trim().toLowerCase() ?? '';
26
+ return lower === '' || lower === 'application/json' || lower.endsWith('+json');
27
+ }
28
+ export function isArtifactUploadRequest(req) {
29
+ return !isJsonContentType(req.headers.get('content-type'));
30
+ }
31
+ export async function createArtifactFromUploadRequest(artifactStore, req) {
32
+ const contentType = req.headers.get('content-type') ?? '';
33
+ if (contentType.toLowerCase().includes('multipart/form-data')) {
34
+ return createArtifactFromMultipart(artifactStore, req);
35
+ }
36
+ return createArtifactFromRawBody(artifactStore, req);
37
+ }
38
+ function parseUploadField(name, value) {
39
+ const trimmed = value.trim();
40
+ if (JSON_FIELD_NAMES.has(name)) {
41
+ return parseJsonField(trimmed, name);
42
+ }
43
+ if (name === 'tags') {
44
+ if (trimmed.startsWith('[')) {
45
+ const parsed = parseJsonField(trimmed, name);
46
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === 'string') : [];
47
+ }
48
+ return trimmed.split(',').map((entry) => entry.trim()).filter(Boolean);
49
+ }
50
+ if (name === 'allowPrivateHosts')
51
+ return trimmed === 'true' || trimmed === '1' || trimmed === 'yes';
52
+ if (name === 'retentionMs') {
53
+ const parsed = Number(trimmed);
54
+ return Number.isFinite(parsed) ? parsed : undefined;
55
+ }
56
+ return value;
57
+ }
58
+ function parseJsonField(value, fieldName) {
59
+ if (!value)
60
+ return {};
61
+ try {
62
+ return JSON.parse(value);
63
+ }
64
+ catch (error) {
65
+ void error;
66
+ throw artifactUploadError(`Invalid JSON in multipart field ${fieldName}.`);
67
+ }
68
+ }
69
+ function readStringField(fields, key) {
70
+ const value = fields[key];
71
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
72
+ }
73
+ function readNumberField(fields, key) {
74
+ const value = fields[key];
75
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
76
+ }
77
+ function readMetadata(fields) {
78
+ const metadata = fields.metadata;
79
+ return typeof metadata === 'object' && metadata !== null && !Array.isArray(metadata)
80
+ ? metadata
81
+ : {};
82
+ }
83
+ function readArtifactId(artifact) {
84
+ if (typeof artifact === 'object' && artifact !== null) {
85
+ const id = artifact.id;
86
+ if (typeof id === 'string' && id.trim().length > 0)
87
+ return id;
88
+ }
89
+ return Response.json({ error: 'Artifact store returned an artifact without an id.' }, { status: 500 });
90
+ }
91
+ function isFileLike(value) {
92
+ return typeof value === 'object'
93
+ && value !== null
94
+ && typeof value.stream === 'function'
95
+ && typeof value.arrayBuffer === 'function';
96
+ }
97
+ async function createArtifactFromMultipart(artifactStore, req) {
98
+ if (req.body)
99
+ return createArtifactFromStreamingMultipart(artifactStore, req);
100
+ let form;
101
+ try {
102
+ form = await req.formData();
103
+ }
104
+ catch (error) {
105
+ void error;
106
+ return Response.json({ error: 'Invalid multipart upload.' }, { status: 400 });
107
+ }
108
+ const fields = {};
109
+ let file = null;
110
+ for (const [name, value] of form.entries()) {
111
+ if (typeof value === 'string') {
112
+ try {
113
+ const parsed = parseUploadField(name, value);
114
+ if (parsed !== undefined)
115
+ fields[name] = parsed;
116
+ }
117
+ catch (error) {
118
+ return Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 });
119
+ }
120
+ continue;
121
+ }
122
+ if (!file && isFileLike(value)) {
123
+ file = value;
124
+ }
125
+ }
126
+ if (!file)
127
+ return Response.json({ error: 'Multipart upload requires a file field.' }, { status: 400 });
128
+ const maxBytes = artifactStore.getMaxBytes?.();
129
+ if (typeof maxBytes === 'number' && typeof file.size === 'number' && file.size > maxBytes) {
130
+ return Response.json({ error: `Artifact exceeds the ${maxBytes}-byte limit.` }, { status: 413 });
131
+ }
132
+ const artifact = await createArtifactFromStream(artifactStore, {
133
+ stream: file.stream(),
134
+ fields,
135
+ filename: readStringField(fields, 'filename') ?? file.name,
136
+ mimeType: readStringField(fields, 'mimeType') ?? file.type,
137
+ sizeBytes: typeof file.size === 'number' ? file.size : undefined,
138
+ maxBytes,
139
+ });
140
+ const artifactId = readArtifactId(artifact);
141
+ return artifactId instanceof Response ? artifactId : { artifact, artifactId, fields };
142
+ }
143
+ async function createArtifactFromStreamingMultipart(artifactStore, req) {
144
+ let spooled = null;
145
+ try {
146
+ spooled = await spoolMultipartUpload(req, artifactStore.getMaxBytes?.());
147
+ const artifact = await createArtifactFromStream(artifactStore, {
148
+ stream: readFileChunks(spooled.filePath),
149
+ fields: spooled.fields,
150
+ filename: readStringField(spooled.fields, 'filename') ?? spooled.filename,
151
+ mimeType: readStringField(spooled.fields, 'mimeType') ?? spooled.mimeType,
152
+ sizeBytes: spooled.sizeBytes,
153
+ maxBytes: artifactStore.getMaxBytes?.(),
154
+ });
155
+ const artifactId = readArtifactId(artifact);
156
+ return artifactId instanceof Response ? artifactId : { artifact, artifactId, fields: spooled.fields };
157
+ }
158
+ catch (error) {
159
+ return Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 });
160
+ }
161
+ finally {
162
+ await spooled?.cleanup();
163
+ }
164
+ }
165
+ async function createArtifactFromRawBody(artifactStore, req) {
166
+ if (!req.body)
167
+ return Response.json({ error: 'Raw artifact upload requires a request body.' }, { status: 400 });
168
+ const url = new URL(req.url);
169
+ const fields = readFieldsFromSearchParams(url.searchParams);
170
+ const contentType = req.headers.get('content-type')?.split(';')[0]?.trim();
171
+ const maxBytes = artifactStore.getMaxBytes?.();
172
+ const sizeBytes = readContentLength(req);
173
+ if (typeof maxBytes === 'number' && typeof sizeBytes === 'number' && sizeBytes > maxBytes) {
174
+ return Response.json({ error: `Artifact exceeds the ${maxBytes}-byte limit.` }, { status: 413 });
175
+ }
176
+ const artifact = await createArtifactFromStream(artifactStore, {
177
+ stream: req.body,
178
+ fields,
179
+ filename: readStringField(fields, 'filename')
180
+ ?? req.headers.get('x-goodvibes-filename')?.trim()
181
+ ?? filenameFromContentDisposition(req.headers.get('content-disposition')),
182
+ mimeType: readStringField(fields, 'mimeType') ?? contentType ?? undefined,
183
+ sizeBytes,
184
+ maxBytes,
185
+ });
186
+ const artifactId = readArtifactId(artifact);
187
+ return artifactId instanceof Response ? artifactId : { artifact, artifactId, fields };
188
+ }
189
+ async function* readFileChunks(path) {
190
+ const handle = await open(path, 'r');
191
+ const buffer = Buffer.allocUnsafe(64 * 1024);
192
+ try {
193
+ for (;;) {
194
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
195
+ if (bytesRead === 0)
196
+ break;
197
+ yield buffer.subarray(0, bytesRead);
198
+ }
199
+ }
200
+ finally {
201
+ await handle.close();
202
+ }
203
+ }
204
+ function readFieldsFromSearchParams(params) {
205
+ const fields = {};
206
+ for (const [name, value] of params.entries()) {
207
+ try {
208
+ const parsed = parseUploadField(name, value);
209
+ if (parsed !== undefined)
210
+ fields[name] = parsed;
211
+ }
212
+ catch (error) {
213
+ void error;
214
+ fields[name] = value;
215
+ }
216
+ }
217
+ return fields;
218
+ }
219
+ function readContentLength(req) {
220
+ const parsed = Number(req.headers.get('content-length') ?? NaN);
221
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
222
+ }
223
+ function filenameFromContentDisposition(header) {
224
+ if (!header)
225
+ return undefined;
226
+ const match = header.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i);
227
+ if (!match?.[1])
228
+ return undefined;
229
+ try {
230
+ return decodeURIComponent(match[1].replace(/^"|"$/g, ''));
231
+ }
232
+ catch (error) {
233
+ void error;
234
+ return match[1].replace(/^"|"$/g, '');
235
+ }
236
+ }
237
+ function multipartBoundary(contentType) {
238
+ const match = contentType.match(/(?:^|;)\s*boundary=(?:"([^"]+)"|([^;]+))/i);
239
+ const boundary = match?.[1] ?? match?.[2];
240
+ return boundary && boundary.trim().length > 0 ? boundary.trim() : null;
241
+ }
242
+ function decodeHeaderValue(value) {
243
+ const unquoted = value.trim().replace(/^"|"$/g, '');
244
+ try {
245
+ return decodeURIComponent(unquoted.replace(/^UTF-8''/i, ''));
246
+ }
247
+ catch (error) {
248
+ void error;
249
+ return unquoted;
250
+ }
251
+ }
252
+ function parseMultipartPartHeaders(headerText) {
253
+ const headers = new Map();
254
+ for (const line of headerText.split('\r\n')) {
255
+ const separator = line.indexOf(':');
256
+ if (separator < 0)
257
+ continue;
258
+ headers.set(line.slice(0, separator).trim().toLowerCase(), line.slice(separator + 1).trim());
259
+ }
260
+ const disposition = headers.get('content-disposition') ?? '';
261
+ const output = {};
262
+ for (const segment of disposition.split(';').slice(1)) {
263
+ const [rawKey, ...rawValue] = segment.split('=');
264
+ const key = rawKey?.trim().toLowerCase();
265
+ const value = rawValue.join('=');
266
+ if (!key || !value)
267
+ continue;
268
+ if (key === 'name')
269
+ output.name = decodeHeaderValue(value);
270
+ if (key === 'filename' || key === 'filename*')
271
+ output.filename = decodeHeaderValue(value);
272
+ }
273
+ const contentType = headers.get('content-type');
274
+ if (contentType)
275
+ output.contentType = contentType.split(';')[0]?.trim();
276
+ return output;
277
+ }
278
+ async function spoolMultipartUpload(req, maxFileBytes) {
279
+ const boundary = multipartBoundary(req.headers.get('content-type') ?? '');
280
+ if (!boundary)
281
+ throw artifactUploadError('Multipart upload is missing a boundary.');
282
+ if (!req.body)
283
+ throw artifactUploadError('Multipart upload requires a request body.');
284
+ const contentLength = readContentLength(req);
285
+ if (typeof maxFileBytes === 'number'
286
+ && typeof contentLength === 'number'
287
+ && contentLength > maxFileBytes + MAX_MULTIPART_BODY_OVERHEAD_BYTES) {
288
+ throw artifactSizeError(maxFileBytes);
289
+ }
290
+ const tempDir = await mkdtemp(join(tmpdir(), 'goodvibes-upload-'));
291
+ const filePath = join(tempDir, `${randomUUID()}.upload`);
292
+ const fields = {};
293
+ const firstBoundary = Buffer.from(`--${boundary}`);
294
+ const bodyBoundary = Buffer.from(`\r\n--${boundary}`);
295
+ const headerSeparator = Buffer.from('\r\n\r\n');
296
+ const reader = req.body.getReader();
297
+ let buffer = Buffer.alloc(0);
298
+ let fileSeen = false;
299
+ let filename;
300
+ let mimeType;
301
+ let sizeBytes = 0;
302
+ const readMore = async () => {
303
+ const { done, value } = await reader.read();
304
+ if (done)
305
+ return false;
306
+ buffer = Buffer.concat([buffer, Buffer.from(value)]);
307
+ return true;
308
+ };
309
+ const consumeBoundarySuffix = async () => {
310
+ while (buffer.length < 2) {
311
+ if (!await readMore())
312
+ throw artifactUploadError('Unexpected end of multipart body.');
313
+ }
314
+ if (buffer.subarray(0, 2).toString() === '--') {
315
+ buffer = buffer.subarray(2);
316
+ return 'done';
317
+ }
318
+ if (buffer.subarray(0, 2).toString() === '\r\n') {
319
+ buffer = buffer.subarray(2);
320
+ return 'next';
321
+ }
322
+ throw artifactUploadError('Invalid multipart boundary terminator.');
323
+ };
324
+ const consumeInitialBoundary = async () => {
325
+ for (;;) {
326
+ const index = buffer.indexOf(firstBoundary);
327
+ if (index >= 0) {
328
+ buffer = buffer.subarray(index + firstBoundary.length);
329
+ return consumeBoundarySuffix();
330
+ }
331
+ if (!await readMore())
332
+ throw artifactUploadError('Invalid multipart upload.');
333
+ const readIndex = buffer.indexOf(firstBoundary);
334
+ if (readIndex >= 0) {
335
+ buffer = buffer.subarray(readIndex + firstBoundary.length);
336
+ return consumeBoundarySuffix();
337
+ }
338
+ if (buffer.length > firstBoundary.length * 2) {
339
+ buffer = buffer.subarray(buffer.length - firstBoundary.length * 2);
340
+ }
341
+ }
342
+ };
343
+ const readHeaders = async () => {
344
+ for (;;) {
345
+ const index = buffer.indexOf(headerSeparator);
346
+ if (index >= 0) {
347
+ const headerText = buffer.subarray(0, index).toString('utf8');
348
+ buffer = buffer.subarray(index + headerSeparator.length);
349
+ return parseMultipartPartHeaders(headerText);
350
+ }
351
+ if (!await readMore())
352
+ throw artifactUploadError('Unexpected end of multipart headers.');
353
+ if (buffer.length > MAX_MULTIPART_FIELD_BYTES)
354
+ throw artifactUploadError('Multipart part headers are too large.');
355
+ }
356
+ };
357
+ const readPart = async (onChunk) => {
358
+ const keepBytes = bodyBoundary.length - 1;
359
+ for (;;) {
360
+ const index = buffer.indexOf(bodyBoundary);
361
+ if (index >= 0) {
362
+ if (index > 0)
363
+ await onChunk(buffer.subarray(0, index));
364
+ buffer = buffer.subarray(index + bodyBoundary.length);
365
+ return consumeBoundarySuffix();
366
+ }
367
+ if (buffer.length > keepBytes) {
368
+ await onChunk(buffer.subarray(0, buffer.length - keepBytes));
369
+ buffer = buffer.subarray(buffer.length - keepBytes);
370
+ }
371
+ if (!await readMore())
372
+ throw artifactUploadError('Unexpected end of multipart part.');
373
+ }
374
+ };
375
+ const readFieldPart = async (name) => {
376
+ const chunks = [];
377
+ let fieldBytes = 0;
378
+ const state = await readPart(async (chunk) => {
379
+ fieldBytes += chunk.byteLength;
380
+ if (fieldBytes > MAX_MULTIPART_FIELD_BYTES)
381
+ throw artifactUploadError(`Multipart field ${name} is too large.`);
382
+ chunks.push(chunk);
383
+ });
384
+ const parsed = parseUploadField(name, Buffer.concat(chunks).toString('utf8'));
385
+ if (parsed !== undefined)
386
+ fields[name] = parsed;
387
+ return state;
388
+ };
389
+ const readFilePart = async (headers) => {
390
+ const handle = await open(filePath, 'wx');
391
+ try {
392
+ const state = await readPart(async (chunk) => {
393
+ sizeBytes += chunk.byteLength;
394
+ if (typeof maxFileBytes === 'number' && sizeBytes > maxFileBytes) {
395
+ throw artifactSizeError(maxFileBytes);
396
+ }
397
+ await handle.write(chunk);
398
+ });
399
+ fileSeen = true;
400
+ filename = headers.filename;
401
+ mimeType = headers.contentType;
402
+ return state;
403
+ }
404
+ finally {
405
+ await handle.close();
406
+ }
407
+ };
408
+ try {
409
+ for (let state = await consumeInitialBoundary(); state !== 'done';) {
410
+ const headers = await readHeaders();
411
+ if (headers.filename !== undefined && !fileSeen) {
412
+ state = await readFilePart(headers);
413
+ }
414
+ else if (headers.filename !== undefined) {
415
+ state = await readPart(async () => { });
416
+ }
417
+ else if (headers.name) {
418
+ state = await readFieldPart(headers.name);
419
+ }
420
+ else {
421
+ state = await readPart(async () => { });
422
+ }
423
+ }
424
+ if (!fileSeen)
425
+ throw artifactUploadError('Multipart upload requires a file field.');
426
+ return {
427
+ filePath,
428
+ ...(filename ? { filename } : {}),
429
+ ...(mimeType ? { mimeType } : {}),
430
+ sizeBytes,
431
+ fields,
432
+ cleanup: async () => {
433
+ await cleanupTempDir(tempDir);
434
+ },
435
+ };
436
+ }
437
+ catch (error) {
438
+ await cleanupTempDir(tempDir, error);
439
+ throw error;
440
+ }
441
+ finally {
442
+ releaseReaderLock(reader);
443
+ }
444
+ }
445
+ async function cleanupTempDir(tempDir, originalError) {
446
+ try {
447
+ await rm(tempDir, { recursive: true, force: true });
448
+ }
449
+ catch (cleanupError) {
450
+ if (originalError !== undefined) {
451
+ throw new AggregateError([originalError, cleanupError], 'Multipart upload failed and temporary upload cleanup also failed.');
452
+ }
453
+ throw cleanupError;
454
+ }
455
+ }
456
+ function releaseReaderLock(reader) {
457
+ try {
458
+ reader.releaseLock();
459
+ }
460
+ catch (error) {
461
+ if (error instanceof TypeError)
462
+ return;
463
+ throw error;
464
+ }
465
+ }
466
+ async function createArtifactFromStream(artifactStore, input) {
467
+ if (typeof input.maxBytes === 'number' && typeof input.sizeBytes === 'number' && input.sizeBytes > input.maxBytes) {
468
+ throw artifactSizeError(input.maxBytes);
469
+ }
470
+ const base = {
471
+ ...(readStringField(input.fields, 'kind') ? { kind: readStringField(input.fields, 'kind') } : {}),
472
+ ...(input.mimeType ? { mimeType: input.mimeType } : {}),
473
+ ...(input.filename ? { filename: input.filename } : {}),
474
+ ...(readStringField(input.fields, 'sourceUri') ? { sourceUri: readStringField(input.fields, 'sourceUri') } : {}),
475
+ ...(typeof input.sizeBytes === 'number' ? { sizeBytes: input.sizeBytes } : {}),
476
+ ...(typeof readNumberField(input.fields, 'retentionMs') === 'number' ? { retentionMs: readNumberField(input.fields, 'retentionMs') } : {}),
477
+ acquisitionMode: 'inline-data',
478
+ fetchMode: 'not-applicable',
479
+ metadata: readMetadata(input.fields),
480
+ };
481
+ if (artifactStore.createFromStream) {
482
+ return artifactStore.createFromStream({
483
+ stream: input.stream,
484
+ ...base,
485
+ });
486
+ }
487
+ const buffer = await bufferUploadStream(input.stream, input.maxBytes);
488
+ return artifactStore.create({
489
+ ...base,
490
+ dataBase64: buffer.toString('base64'),
491
+ });
492
+ }
493
+ function isWebReadableStream(stream) {
494
+ return typeof stream.getReader === 'function';
495
+ }
496
+ function isAsyncUploadIterable(stream) {
497
+ return typeof stream[Symbol.asyncIterator] === 'function';
498
+ }
499
+ async function* iterateUploadStream(stream) {
500
+ if (isWebReadableStream(stream)) {
501
+ const reader = stream.getReader();
502
+ try {
503
+ for (;;) {
504
+ const { done, value } = await reader.read();
505
+ if (done)
506
+ break;
507
+ if (value)
508
+ yield value;
509
+ }
510
+ }
511
+ finally {
512
+ releaseReaderLock(reader);
513
+ }
514
+ return;
515
+ }
516
+ if (isAsyncUploadIterable(stream)) {
517
+ yield* stream;
518
+ return;
519
+ }
520
+ for (const chunk of stream)
521
+ yield chunk;
522
+ }
523
+ async function bufferUploadStream(stream, maxBytes) {
524
+ const chunks = [];
525
+ let totalBytes = 0;
526
+ for await (const chunk of iterateUploadStream(stream)) {
527
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk) : Buffer.from(chunk);
528
+ totalBytes += buffer.byteLength;
529
+ if (typeof maxBytes === 'number' && totalBytes > maxBytes) {
530
+ throw artifactSizeError(maxBytes);
531
+ }
532
+ chunks.push(buffer);
533
+ }
534
+ return Buffer.concat(chunks);
535
+ }
@@ -1,3 +1,3 @@
1
- import type { DaemonApiRouteHandlers } from './context.js';
2
- export declare function dispatchAutomationRoutes(req: Request, handlers: Pick<DaemonApiRouteHandlers, 'getReview' | 'getIntegrationSession' | 'getIntegrationAutomation' | 'getAutomationJobs' | 'postAutomationJob' | 'getAutomationRuns' | 'getAutomationRun' | 'automationRunAction' | 'patchAutomationJob' | 'deleteAutomationJob' | 'setAutomationJobEnabled' | 'runAutomationJobNow' | 'getDeliveries' | 'getDelivery' | 'getSchedules' | 'postSchedule' | 'deleteSchedule' | 'setScheduleEnabled' | 'runScheduleNow'>): Promise<Response | null>;
1
+ import type { DaemonAutomationRouteHandlers } from './context.js';
2
+ export declare function dispatchAutomationRoutes(req: Request, handlers: DaemonAutomationRouteHandlers): Promise<Response | null>;
3
3
  //# sourceMappingURL=automation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"automation.d.ts","sourceRoot":"","sources":["../src/automation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAE3D,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,IAAI,CACZ,sBAAsB,EACpB,WAAW,GACX,uBAAuB,GACvB,0BAA0B,GAC1B,mBAAmB,GACnB,mBAAmB,GACnB,mBAAmB,GACnB,kBAAkB,GAClB,qBAAqB,GACrB,oBAAoB,GACpB,qBAAqB,GACrB,yBAAyB,GACzB,qBAAqB,GACrB,eAAe,GACf,aAAa,GACb,cAAc,GACd,cAAc,GACd,gBAAgB,GAChB,oBAAoB,GACpB,gBAAgB,CACnB,GACA,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA+C1B"}
1
+ {"version":3,"file":"automation.d.ts","sourceRoot":"","sources":["../src/automation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,cAAc,CAAC;AAElE,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,6BAA6B,GACtC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA+C1B"}