@vellumai/credential-executor 0.10.7-dev.202607091111.4355283 → 0.10.7-dev.202607091507.bac9bfe
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/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
- package/package.json +1 -1
- package/src/main.ts +11 -9
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
inferMimeType,
|
|
5
|
+
resolveAttachmentFilename,
|
|
6
|
+
} from "../attachment-naming.js";
|
|
7
|
+
|
|
8
|
+
describe("inferMimeType", () => {
|
|
9
|
+
test("maps known extensions case-insensitively", () => {
|
|
10
|
+
expect(inferMimeType("shot.PNG")).toBe("image/png");
|
|
11
|
+
expect(inferMimeType("report.pdf")).toBe("application/pdf");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("falls back to octet-stream for unknown or missing extensions", () => {
|
|
15
|
+
expect(inferMimeType("desktop")).toBe("application/octet-stream");
|
|
16
|
+
expect(inferMimeType("archive.xyz")).toBe("application/octet-stream");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("resolveAttachmentFilename", () => {
|
|
21
|
+
test("uses explicit filenames verbatim", () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveAttachmentFilename("custom.dat", "/tmp/shot.png", "explicit"),
|
|
24
|
+
).toBe("custom.dat");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("honors labels with recognized extensions", () => {
|
|
28
|
+
expect(
|
|
29
|
+
resolveAttachmentFilename("nice-name.png", "/tmp/shot.png", "label"),
|
|
30
|
+
).toBe("nice-name.png");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("keeps bare labels and appends the POSIX path extension", () => {
|
|
34
|
+
expect(resolveAttachmentFilename("desktop", "/tmp/shot.png", "label")).toBe(
|
|
35
|
+
"desktop.png",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("keeps bare labels and appends the Windows path extension", () => {
|
|
40
|
+
expect(
|
|
41
|
+
resolveAttachmentFilename(
|
|
42
|
+
"desktop",
|
|
43
|
+
"C:\\Users\\user1\\Pictures\\shot.png",
|
|
44
|
+
"label",
|
|
45
|
+
),
|
|
46
|
+
).toBe("desktop.png");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("bare labels to files sharing a basename resolve to unique names", () => {
|
|
50
|
+
expect(
|
|
51
|
+
resolveAttachmentFilename("first", "/workspace/a/result.png", "label"),
|
|
52
|
+
).toBe("first.png");
|
|
53
|
+
expect(
|
|
54
|
+
resolveAttachmentFilename("second", "/workspace/b/result.png", "label"),
|
|
55
|
+
).toBe("second.png");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("does not duplicate an unknown extension the label already has", () => {
|
|
59
|
+
expect(
|
|
60
|
+
resolveAttachmentFilename(
|
|
61
|
+
"report.xlsx",
|
|
62
|
+
"/workspace/report.xlsx",
|
|
63
|
+
"label",
|
|
64
|
+
),
|
|
65
|
+
).toBe("report.xlsx");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("matches the existing extension case-insensitively", () => {
|
|
69
|
+
expect(
|
|
70
|
+
resolveAttachmentFilename("data.YAML", "/workspace/data.yaml", "label"),
|
|
71
|
+
).toBe("data.YAML");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("still appends when the label ends differently", () => {
|
|
75
|
+
expect(
|
|
76
|
+
resolveAttachmentFilename(
|
|
77
|
+
"summary.v2",
|
|
78
|
+
"/workspace/data.parquet",
|
|
79
|
+
"label",
|
|
80
|
+
),
|
|
81
|
+
).toBe("summary.v2.parquet");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("falls back to the basename when the path has no extension", () => {
|
|
85
|
+
expect(resolveAttachmentFilename("label", "/tmp/rawfile", "label")).toBe(
|
|
86
|
+
"rawfile",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("uses the basename when no preferred name is given", () => {
|
|
91
|
+
expect(
|
|
92
|
+
resolveAttachmentFilename(undefined, "C:\\temp\\report.pdf", "label"),
|
|
93
|
+
).toBe("report.pdf");
|
|
94
|
+
expect(resolveAttachmentFilename(undefined, "/tmp/report.pdf")).toBe(
|
|
95
|
+
"report.pdf",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("ignores trailing separators", () => {
|
|
100
|
+
expect(resolveAttachmentFilename(undefined, "/tmp/dir/", "label")).toBe(
|
|
101
|
+
"dir",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachment filename contract shared by the daemon (which materializes
|
|
3
|
+
* `vellum://` links and `<vellum-attachment>` directives into stored
|
|
4
|
+
* attachments) and the web client (which resolves clicked links back to
|
|
5
|
+
* those attachments). Both sides must agree on the stored filename, so the
|
|
6
|
+
* naming rule lives here once.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const EXTENSION_MIME_MAP: Record<string, string> = {
|
|
10
|
+
// Images
|
|
11
|
+
png: "image/png",
|
|
12
|
+
jpg: "image/jpeg",
|
|
13
|
+
jpeg: "image/jpeg",
|
|
14
|
+
gif: "image/gif",
|
|
15
|
+
webp: "image/webp",
|
|
16
|
+
svg: "image/svg+xml",
|
|
17
|
+
ico: "image/x-icon",
|
|
18
|
+
bmp: "image/bmp",
|
|
19
|
+
heic: "image/heic",
|
|
20
|
+
heif: "image/heif",
|
|
21
|
+
|
|
22
|
+
// Documents
|
|
23
|
+
pdf: "application/pdf",
|
|
24
|
+
json: "application/json",
|
|
25
|
+
xml: "application/xml",
|
|
26
|
+
csv: "text/csv",
|
|
27
|
+
txt: "text/plain",
|
|
28
|
+
md: "text/markdown",
|
|
29
|
+
html: "text/html",
|
|
30
|
+
css: "text/css",
|
|
31
|
+
js: "text/javascript",
|
|
32
|
+
ts: "text/typescript",
|
|
33
|
+
|
|
34
|
+
// Audio
|
|
35
|
+
mp3: "audio/mpeg",
|
|
36
|
+
wav: "audio/wav",
|
|
37
|
+
ogg: "audio/ogg",
|
|
38
|
+
flac: "audio/flac",
|
|
39
|
+
aac: "audio/aac",
|
|
40
|
+
m4a: "audio/x-m4a",
|
|
41
|
+
opus: "audio/opus",
|
|
42
|
+
|
|
43
|
+
// Video
|
|
44
|
+
mp4: "video/mp4",
|
|
45
|
+
webm: "video/webm",
|
|
46
|
+
mov: "video/quicktime",
|
|
47
|
+
mpeg: "video/mpeg",
|
|
48
|
+
|
|
49
|
+
// Archives
|
|
50
|
+
zip: "application/zip",
|
|
51
|
+
gz: "application/gzip",
|
|
52
|
+
tar: "application/x-tar",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Infer a MIME type from a filename extension.
|
|
57
|
+
* Returns `application/octet-stream` when the extension is unrecognised.
|
|
58
|
+
*/
|
|
59
|
+
export function inferMimeType(filename: string): string {
|
|
60
|
+
const dot = filename.lastIndexOf(".");
|
|
61
|
+
if (dot === -1) {
|
|
62
|
+
return "application/octet-stream";
|
|
63
|
+
}
|
|
64
|
+
const ext = filename.slice(dot + 1).toLowerCase();
|
|
65
|
+
return EXTENSION_MIME_MAP[ext] ?? "application/octet-stream";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pick the stored attachment filename for a materialized file directive.
|
|
70
|
+
*
|
|
71
|
+
* Explicit filenames (`<vellum-attachment filename="..." />` attributes)
|
|
72
|
+
* are an authoring contract and always used verbatim. Link display text
|
|
73
|
+
* (`[label](vellum://...)`) is cosmetic: it is only honored as the filename
|
|
74
|
+
* when it carries a recognized extension. A bare label
|
|
75
|
+
* (e.g. `[desktop](vellum://.../shot.png)`) would otherwise produce an
|
|
76
|
+
* extensionless download with an `application/octet-stream` MIME type,
|
|
77
|
+
* which macOS opens as raw text. In that case the label keeps the path's
|
|
78
|
+
* extension (`desktop.png`), staying unique per link even when several
|
|
79
|
+
* links share a basename; the raw basename is the last resort.
|
|
80
|
+
*
|
|
81
|
+
* The basename fallback splits on both `/` and `\\` so it handles POSIX
|
|
82
|
+
* sandbox paths, `vellum://` URL paths, and Windows host paths alike.
|
|
83
|
+
* Splitting on both separators regardless of the runtime platform also
|
|
84
|
+
* covers Windows host paths resolved by a non-Windows daemon, which
|
|
85
|
+
* platform-selected `node:path` semantics cannot.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveAttachmentFilename(
|
|
88
|
+
preferred: string | undefined,
|
|
89
|
+
resolvedPath: string,
|
|
90
|
+
filenameSource: "explicit" | "label" = "explicit",
|
|
91
|
+
): string {
|
|
92
|
+
const fallback =
|
|
93
|
+
resolvedPath.split(/[\\/]/).filter(Boolean).pop() ?? resolvedPath;
|
|
94
|
+
if (!preferred) {
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
97
|
+
if (filenameSource === "explicit") {
|
|
98
|
+
return preferred;
|
|
99
|
+
}
|
|
100
|
+
if (inferMimeType(preferred) !== "application/octet-stream") {
|
|
101
|
+
return preferred;
|
|
102
|
+
}
|
|
103
|
+
// Bare label: keep the label as a per-link disambiguator (two links to
|
|
104
|
+
// different files sharing a basename must not collide, because the
|
|
105
|
+
// transcript resolves clicks by filename) and append the path's
|
|
106
|
+
// extension so the stored name carries the correct type. Skip the
|
|
107
|
+
// append when the label already ends with that extension (a label like
|
|
108
|
+
// `report.xlsx` for `report.xlsx` must not become `report.xlsx.xlsx`).
|
|
109
|
+
const dot = fallback.lastIndexOf(".");
|
|
110
|
+
if (dot > 0 && dot < fallback.length - 1) {
|
|
111
|
+
const ext = fallback.slice(dot + 1);
|
|
112
|
+
if (preferred.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {
|
|
113
|
+
return preferred;
|
|
114
|
+
}
|
|
115
|
+
return `${preferred}.${ext}`;
|
|
116
|
+
}
|
|
117
|
+
return fallback;
|
|
118
|
+
}
|
package/package.json
CHANGED
package/src/main.ts
CHANGED
|
@@ -610,6 +610,7 @@ function serveStandaloneSocket(opts: {
|
|
|
610
610
|
});
|
|
611
611
|
|
|
612
612
|
netServer.on("connection", (socket: Socket) => {
|
|
613
|
+
connectionCount++;
|
|
613
614
|
const readable = new Readable({ read() {} });
|
|
614
615
|
const writable = new Writable({
|
|
615
616
|
write(chunk, _encoding, callback) {
|
|
@@ -634,7 +635,6 @@ function serveStandaloneSocket(opts: {
|
|
|
634
635
|
logger,
|
|
635
636
|
signal,
|
|
636
637
|
onHandshakeComplete: (sessionId, apiKey, assistantId) => {
|
|
637
|
-
rpcConnected = true;
|
|
638
638
|
onHandshakeComplete?.(sessionId, apiKey, assistantId);
|
|
639
639
|
},
|
|
640
640
|
onApiKeyUpdate: onApiKeyUpdate ?? (() => {}),
|
|
@@ -646,7 +646,7 @@ function serveStandaloneSocket(opts: {
|
|
|
646
646
|
"CES connection ended with a transport error",
|
|
647
647
|
);
|
|
648
648
|
}).then(() => {
|
|
649
|
-
|
|
649
|
+
connectionCount = Math.max(0, connectionCount - 1);
|
|
650
650
|
});
|
|
651
651
|
});
|
|
652
652
|
|
|
@@ -672,7 +672,7 @@ function serveStandaloneSocket(opts: {
|
|
|
672
672
|
// Health server (managed mode only)
|
|
673
673
|
// ---------------------------------------------------------------------------
|
|
674
674
|
|
|
675
|
-
let
|
|
675
|
+
let connectionCount = 0;
|
|
676
676
|
|
|
677
677
|
function startHealthServer(
|
|
678
678
|
port: number,
|
|
@@ -695,7 +695,7 @@ function startHealthServer(
|
|
|
695
695
|
// scheduling during dark-launch. The sidecar can't do useful work
|
|
696
696
|
// without a connection anyway, so readiness is purely about the
|
|
697
697
|
// process being up and able to accept a future connection.
|
|
698
|
-
return new Response(JSON.stringify({ status: "ok", rpcConnected }), {
|
|
698
|
+
return new Response(JSON.stringify({ status: "ok", connections: connectionCount, rpcConnected: connectionCount > 0 }), {
|
|
699
699
|
status: 200,
|
|
700
700
|
headers: { "Content-Type": "application/json" },
|
|
701
701
|
});
|
|
@@ -834,11 +834,13 @@ async function main(): Promise<void> {
|
|
|
834
834
|
log,
|
|
835
835
|
onHandshakeComplete: refs
|
|
836
836
|
? (_hsSessionId, hsApiKey, hsAssistantId) => {
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
837
|
+
// Update the process-scoped credential refs on every handshake.
|
|
838
|
+
// With multi-client CES, multiple connections from the same
|
|
839
|
+
// assistant instance share these refs. All connections belong to
|
|
840
|
+
// the same guardian, so they carry the same API key - last
|
|
841
|
+
// handshake wins is correct. A connection that omits the key
|
|
842
|
+
// fails closed (falling back to the env key, or no key) rather
|
|
843
|
+
// than reusing a previous session's credentials.
|
|
842
844
|
applyManagedCredentialRefs(
|
|
843
845
|
refs.apiKeyRef,
|
|
844
846
|
refs.assistantIdRef,
|