@securecoms/agent-bridge 0.11.0 → 0.11.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.
@@ -18,9 +18,11 @@ export type AttachmentMeta = {
18
18
  sha256: string;
19
19
  };
20
20
  export interface ReceiveDeps {
21
+ /** Warm libsodium before any WASM-backed call. See src/sodium-ready.ts. */
22
+ ready(): Promise<void>;
21
23
  downloadBlob(blobId: string): Promise<Uint8Array>;
22
- decryptFile(enc: Uint8Array, key: Uint8Array, nonce: Uint8Array): Uint8Array;
23
- computeFileDigest(b: Uint8Array): string;
24
+ decryptFile(enc: Uint8Array, key: Uint8Array, nonce: Uint8Array): Promise<Uint8Array>;
25
+ computeFileDigest(b: Uint8Array): Promise<string>;
24
26
  base64ToBytes(s: string): Uint8Array;
25
27
  mkdirp(dir: string): Promise<void>;
26
28
  writeFile(p: string, data: Uint8Array): Promise<void>;
@@ -30,13 +32,15 @@ export interface ReceiveDeps {
30
32
  export declare function sanitizeFilename(name: string, fallback: string): string;
31
33
  export declare function mimeForName(name: string): string;
32
34
  export interface SendDeps {
35
+ /** Warm libsodium before any WASM-backed call. See src/sodium-ready.ts. */
36
+ ready(): Promise<void>;
33
37
  uploadBlob(ciphertext: Uint8Array, channelId: string): Promise<string>;
34
- encryptFile(b: Uint8Array): {
38
+ encryptFile(b: Uint8Array): Promise<{
35
39
  encryptedData: Uint8Array;
36
40
  fileKey: Uint8Array;
37
41
  fileNonce: Uint8Array;
38
42
  digest: string;
39
- };
43
+ }>;
40
44
  bytesToBase64(b: Uint8Array): string;
41
45
  readFile(p: string): Promise<Uint8Array>;
42
46
  }
@@ -0,0 +1,49 @@
1
+ import { basename } from "node:path";
2
+ /** Reduce any name to a safe basename inside the target dir. Strips path
3
+ * separators and `..`; falls back when nothing usable remains. */
4
+ export function sanitizeFilename(name, fallback) {
5
+ const base = basename(name ?? "");
6
+ const cleaned = base.replace(/[/\\]/g, "").replace(/^\.+$/, "");
7
+ return cleaned.length > 0 ? cleaned : fallback;
8
+ }
9
+ const MIME_BY_EXT = {
10
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif",
11
+ webp: "image/webp", svg: "image/svg+xml", pdf: "application/pdf",
12
+ txt: "text/plain", md: "text/markdown", json: "application/json",
13
+ csv: "text/csv", html: "text/html", mp4: "video/mp4", mp3: "audio/mpeg",
14
+ zip: "application/zip", wav: "audio/wav",
15
+ };
16
+ export function mimeForName(name) {
17
+ const dot = name.lastIndexOf(".");
18
+ if (dot < 0 || dot === name.length - 1)
19
+ return "application/octet-stream";
20
+ return MIME_BY_EXT[name.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
21
+ }
22
+ export async function prepareAttachment(filePath, caption, channelId, deps) {
23
+ const bytes = await deps.readFile(filePath);
24
+ const name = basename(filePath);
25
+ const { encryptedData, fileKey, fileNonce, digest } = deps.encryptFile(bytes);
26
+ const blobId = await deps.uploadBlob(encryptedData, channelId);
27
+ const body = {
28
+ v: 1, type: "artifact_share", name, mime: mimeForName(name), size: bytes.length,
29
+ blobId, fileKey: deps.bytesToBase64(fileKey), fileNonce: deps.bytesToBase64(fileNonce), digest,
30
+ };
31
+ if (caption)
32
+ body.text = caption;
33
+ return body;
34
+ }
35
+ export async function materializeAttachment(body, ctx, deps) {
36
+ const ciphertext = await deps.downloadBlob(body.blobId);
37
+ // Verify digest on ciphertext BEFORE decrypting — tampered blobs fail here.
38
+ const sha256 = deps.computeFileDigest(ciphertext);
39
+ if (sha256 !== body.digest) {
40
+ throw new Error(`attachment digest mismatch for blob ${body.blobId}`);
41
+ }
42
+ const plain = deps.decryptFile(ciphertext, deps.base64ToBytes(body.fileKey), deps.base64ToBytes(body.fileNonce));
43
+ const safeName = sanitizeFilename(body.name, ctx.messageId);
44
+ const dir = `${ctx.attachmentsDir}/${ctx.channelId}`;
45
+ const path = `${dir}/${ctx.messageId}-${safeName}`;
46
+ await deps.mkdirp(dir);
47
+ await deps.writeFile(path, plain);
48
+ return { filename: safeName, mime: body.mime, size: plain.length, path, sha256 };
49
+ }
package/dist/auth.js ADDED
@@ -0,0 +1,56 @@
1
+ import { randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { isLoopback } from "./config.js";
4
+ export function resolveToken(opts) {
5
+ if (opts.token)
6
+ return opts.token;
7
+ const f = opts.tokenFile;
8
+ if (f && existsSync(f))
9
+ return readFileSync(f, "utf8").trim();
10
+ const tok = randomBytes(32).toString("base64url");
11
+ if (f)
12
+ writeFileSync(f, tok, { mode: 0o600 });
13
+ return tok;
14
+ }
15
+ export function tokenMatches(provided, expected) {
16
+ if (!provided)
17
+ return false;
18
+ const a = Buffer.from(provided);
19
+ const b = Buffer.from(expected);
20
+ if (a.length !== b.length)
21
+ return false;
22
+ return timingSafeEqual(a, b);
23
+ }
24
+ export function bearerFrom(header) {
25
+ if (!header)
26
+ return undefined;
27
+ const m = /^Bearer (.+)$/i.exec(header.trim());
28
+ return m ? m[1] : undefined;
29
+ }
30
+ function hostnameOf(hostHeader) {
31
+ if (!hostHeader)
32
+ return undefined;
33
+ // Strip an optional :port (but keep [::1] intact).
34
+ if (hostHeader.startsWith("["))
35
+ return hostHeader.slice(0, hostHeader.indexOf("]") + 1);
36
+ const i = hostHeader.lastIndexOf(":");
37
+ return i > 0 ? hostHeader.slice(0, i) : hostHeader;
38
+ }
39
+ export function originHostAllowed(headers, allowOrigins) {
40
+ // Host must be a loopback name (blocks the DNS-rebind target Host: evil.com).
41
+ const host = hostnameOf(headers.host);
42
+ if (host && !isLoopback(host) && !isLoopback(`[${host}]`))
43
+ return false;
44
+ // Origin: absent → allow (non-browser). Present → must be loopback or allow-listed.
45
+ const origin = headers.origin;
46
+ if (!origin)
47
+ return true;
48
+ if (allowOrigins.includes(origin))
49
+ return true;
50
+ try {
51
+ return isLoopback(new URL(origin).hostname);
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }