@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.
- package/README.md +13 -7
- package/dist/api-router.d.ts +8 -1
- package/dist/api-router.d.ts.map +1 -1
- package/dist/api-router.js +12 -3
- package/dist/artifact-upload.d.ts +26 -0
- package/dist/artifact-upload.d.ts.map +1 -0
- package/dist/artifact-upload.js +535 -0
- package/dist/automation.d.ts +2 -2
- package/dist/automation.d.ts.map +1 -1
- package/dist/channel-route-types.d.ts.map +1 -1
- package/dist/channel-routes.d.ts +2 -2
- package/dist/channel-routes.d.ts.map +1 -1
- package/dist/channel-routes.js +28 -3
- package/dist/context.d.ts +151 -51
- package/dist/context.d.ts.map +1 -1
- package/dist/control-routes.d.ts +2 -2
- package/dist/control-routes.d.ts.map +1 -1
- package/dist/error-response.d.ts.map +1 -1
- package/dist/error-response.js +172 -12
- package/dist/http-policy.d.ts +1 -1
- package/dist/http-policy.d.ts.map +1 -1
- package/dist/http-policy.js +0 -1
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/integration-route-types.d.ts +3 -1
- package/dist/integration-route-types.d.ts.map +1 -1
- package/dist/integration-routes.d.ts +2 -2
- package/dist/integration-routes.d.ts.map +1 -1
- package/dist/integration-routes.js +3 -0
- package/dist/knowledge-refinement-routes.d.ts +4 -0
- package/dist/knowledge-refinement-routes.d.ts.map +1 -0
- package/dist/knowledge-refinement-routes.js +58 -0
- package/dist/knowledge-route-types.d.ts +11 -1
- package/dist/knowledge-route-types.d.ts.map +1 -1
- package/dist/knowledge-routes.d.ts +2 -2
- package/dist/knowledge-routes.d.ts.map +1 -1
- package/dist/knowledge-routes.js +156 -13
- package/dist/media-route-types.d.ts +10 -0
- package/dist/media-route-types.d.ts.map +1 -1
- package/dist/media-routes.d.ts +2 -2
- package/dist/media-routes.d.ts.map +1 -1
- package/dist/media-routes.js +134 -15
- package/dist/operator.d.ts +2 -2
- package/dist/operator.d.ts.map +1 -1
- package/dist/operator.js +49 -15
- package/dist/otlp-protobuf.d.ts +3 -0
- package/dist/otlp-protobuf.d.ts.map +1 -0
- package/dist/otlp-protobuf.js +977 -0
- package/dist/remote-routes.d.ts +2 -2
- package/dist/remote-routes.d.ts.map +1 -1
- package/dist/remote-routes.js +146 -13
- package/dist/remote.d.ts +2 -2
- package/dist/remote.d.ts.map +1 -1
- package/dist/route-helpers.d.ts +8 -0
- package/dist/route-helpers.d.ts.map +1 -1
- package/dist/route-helpers.js +24 -0
- package/dist/runtime-automation-routes.d.ts +2 -2
- package/dist/runtime-automation-routes.d.ts.map +1 -1
- package/dist/runtime-automation-routes.js +4 -1
- package/dist/runtime-route-types.d.ts +46 -3
- package/dist/runtime-route-types.d.ts.map +1 -1
- package/dist/runtime-routes.d.ts +2 -2
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +1 -0
- package/dist/runtime-session-routes.d.ts +13 -3
- package/dist/runtime-session-routes.d.ts.map +1 -1
- package/dist/runtime-session-routes.js +102 -10
- package/dist/sessions.d.ts +2 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +3 -0
- package/dist/system-route-types.d.ts +19 -0
- package/dist/system-route-types.d.ts.map +1 -1
- package/dist/system-routes.d.ts +2 -2
- package/dist/system-routes.d.ts.map +1 -1
- package/dist/system-routes.js +18 -0
- package/dist/tasks.d.ts +2 -2
- package/dist/tasks.d.ts.map +1 -1
- package/dist/telemetry-routes.d.ts +25 -2
- package/dist/telemetry-routes.d.ts.map +1 -1
- package/dist/telemetry-routes.js +131 -15
- package/package.json +128 -5
package/README.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
# @pellux/goodvibes-daemon-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Internal workspace package backing `@pellux/goodvibes-sdk/daemon`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Consumers should install `@pellux/goodvibes-sdk` and import this surface from the umbrella package.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
38
|
+
Use this surface when you are embedding GoodVibes into another TypeScript server. Do not use it for normal client-side integrations.
|
package/dist/api-router.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import type { DaemonApiRouteHandlers } from './context.js';
|
|
2
|
-
|
|
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
|
package/dist/api-router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-router.d.ts","sourceRoot":"","sources":["../src/api-router.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/api-router.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/automation.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function dispatchAutomationRoutes(req: Request, handlers:
|
|
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
|
package/dist/automation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automation.d.ts","sourceRoot":"","sources":["../src/automation.ts"],"names":[],"mappings":"
|
|
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"}
|