@hwip/cli 0.0.1 → 0.1.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/LICENSE +190 -0
- package/NOTICE +11 -0
- package/README.md +1983 -0
- package/dist/agent-list.d.ts +76 -0
- package/dist/agent-list.js +102 -0
- package/dist/agent-read.d.ts +105 -0
- package/dist/agent-read.js +309 -0
- package/dist/agent-rules-fetch.d.ts +51 -0
- package/dist/agent-rules-fetch.js +275 -0
- package/dist/agent-rules.default.d.ts +30 -0
- package/dist/agent-rules.default.js +305 -0
- package/dist/agent-session.d.ts +23 -0
- package/dist/agent-session.js +44 -0
- package/dist/agent-state.d.ts +84 -0
- package/dist/agent-state.js +188 -0
- package/dist/agent-target.d.ts +60 -0
- package/dist/agent-target.js +81 -0
- package/dist/agents.d.ts +31 -0
- package/dist/agents.js +75 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +76 -0
- package/dist/args.d.ts +74 -0
- package/dist/args.js +254 -0
- package/dist/ask-context-attach.d.ts +73 -0
- package/dist/ask-context-attach.js +151 -0
- package/dist/ask-context-screen.d.ts +21 -0
- package/dist/ask-context-screen.js +49 -0
- package/dist/ask-context-switch.d.ts +31 -0
- package/dist/ask-context-switch.js +74 -0
- package/dist/ask-context.d.ts +84 -0
- package/dist/ask-context.js +172 -0
- package/dist/ask-pending.d.ts +47 -0
- package/dist/ask-pending.js +221 -0
- package/dist/ask-seal.d.ts +120 -0
- package/dist/ask-seal.js +179 -0
- package/dist/ask.d.ts +107 -0
- package/dist/ask.js +551 -0
- package/dist/blocked-prompt-seal.d.ts +39 -0
- package/dist/blocked-prompt-seal.js +92 -0
- package/dist/blocked-prompt.d.ts +62 -0
- package/dist/blocked-prompt.js +518 -0
- package/dist/channel.d.ts +67 -0
- package/dist/channel.js +136 -0
- package/dist/check-update.d.ts +28 -0
- package/dist/check-update.js +153 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +813 -0
- package/dist/codex-config.d.ts +24 -0
- package/dist/codex-config.js +169 -0
- package/dist/codex-transcript.d.ts +59 -0
- package/dist/codex-transcript.js +373 -0
- package/dist/command-refusal.d.ts +56 -0
- package/dist/command-refusal.js +127 -0
- package/dist/command-scan.d.ts +46 -0
- package/dist/command-scan.js +154 -0
- package/dist/command-seal.d.ts +57 -0
- package/dist/command-seal.js +88 -0
- package/dist/command-seq.d.ts +30 -0
- package/dist/command-seq.js +150 -0
- package/dist/config.d.ts +156 -0
- package/dist/config.js +392 -0
- package/dist/core-sections.d.ts +18 -0
- package/dist/core-sections.js +28 -0
- package/dist/crypto.d.ts +278 -0
- package/dist/crypto.js +698 -0
- package/dist/desktop-notify.d.ts +33 -0
- package/dist/desktop-notify.js +49 -0
- package/dist/device-directory.d.ts +36 -0
- package/dist/device-directory.js +97 -0
- package/dist/device-presence.d.ts +17 -0
- package/dist/device-presence.js +62 -0
- package/dist/downloads.d.ts +29 -0
- package/dist/downloads.js +106 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +28 -0
- package/dist/file-crypto.d.ts +94 -0
- package/dist/file-crypto.js +278 -0
- package/dist/file-view-budget.d.ts +15 -0
- package/dist/file-view-budget.js +34 -0
- package/dist/file-view-switch.d.ts +28 -0
- package/dist/file-view-switch.js +122 -0
- package/dist/file-view.d.ts +108 -0
- package/dist/file-view.js +327 -0
- package/dist/gate.d.ts +141 -0
- package/dist/gate.js +336 -0
- package/dist/gemini-transcript.d.ts +32 -0
- package/dist/gemini-transcript.js +189 -0
- package/dist/http-failure.d.ts +98 -0
- package/dist/http-failure.js +349 -0
- package/dist/hwip-core.generated.d.ts +7 -0
- package/dist/hwip-core.generated.js +14 -0
- package/dist/hwip-hook.d.ts +77 -0
- package/dist/hwip-hook.js +340 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/input-sequencer.d.ts +65 -0
- package/dist/input-sequencer.js +142 -0
- package/dist/insecure-url.d.ts +16 -0
- package/dist/insecure-url.js +76 -0
- package/dist/installer.d.ts +96 -0
- package/dist/installer.js +850 -0
- package/dist/inventory-offload.d.ts +49 -0
- package/dist/inventory-offload.js +209 -0
- package/dist/inventory-worker.d.ts +2 -0
- package/dist/inventory-worker.js +27 -0
- package/dist/jsonc-doc.d.ts +106 -0
- package/dist/jsonc-doc.js +354 -0
- package/dist/keep-awake.d.ts +32 -0
- package/dist/keep-awake.js +46 -0
- package/dist/key-changes.d.ts +48 -0
- package/dist/key-changes.js +140 -0
- package/dist/lan-auth.d.ts +140 -0
- package/dist/lan-auth.js +217 -0
- package/dist/lan-endpoint.d.ts +41 -0
- package/dist/lan-endpoint.js +115 -0
- package/dist/lan-receiver.d.ts +122 -0
- package/dist/lan-receiver.js +510 -0
- package/dist/lan-sender.d.ts +76 -0
- package/dist/lan-sender.js +184 -0
- package/dist/lan-transfer.d.ts +36 -0
- package/dist/lan-transfer.js +54 -0
- package/dist/listener-device-id.d.ts +20 -0
- package/dist/listener-device-id.js +133 -0
- package/dist/listener-log.d.ts +4 -0
- package/dist/listener-log.js +23 -0
- package/dist/listener-process.d.ts +200 -0
- package/dist/listener-process.js +470 -0
- package/dist/listener-service.d.ts +225 -0
- package/dist/listener-service.js +682 -0
- package/dist/listener-worktree-cleanup.d.ts +27 -0
- package/dist/listener-worktree-cleanup.js +62 -0
- package/dist/listener.d.ts +1325 -0
- package/dist/listener.js +5681 -0
- package/dist/login-address.d.ts +25 -0
- package/dist/login-address.js +97 -0
- package/dist/login-page.d.ts +29 -0
- package/dist/login-page.js +93 -0
- package/dist/login.d.ts +98 -0
- package/dist/login.js +388 -0
- package/dist/mcp-command.d.ts +45 -0
- package/dist/mcp-command.js +50 -0
- package/dist/mcp.d.ts +19 -0
- package/dist/mcp.js +29 -0
- package/dist/node-support.d.ts +19 -0
- package/dist/node-support.js +38 -0
- package/dist/notify-failure.d.ts +57 -0
- package/dist/notify-failure.js +150 -0
- package/dist/opencode-transcript.d.ts +139 -0
- package/dist/opencode-transcript.js +390 -0
- package/dist/optional-features.d.ts +6 -0
- package/dist/optional-features.js +19 -0
- package/dist/pane-fit.d.ts +40 -0
- package/dist/pane-fit.js +119 -0
- package/dist/peer-key.d.ts +33 -0
- package/dist/peer-key.js +34 -0
- package/dist/pi-transcript.d.ts +26 -0
- package/dist/pi-transcript.js +255 -0
- package/dist/presence.d.ts +14 -0
- package/dist/presence.js +124 -0
- package/dist/private-files.d.ts +51 -0
- package/dist/private-files.js +218 -0
- package/dist/project-switch.d.ts +19 -0
- package/dist/project-switch.js +60 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.js +21 -0
- package/dist/remote-agents.d.ts +410 -0
- package/dist/remote-agents.js +1218 -0
- package/dist/remote-hook.d.ts +12 -0
- package/dist/remote-hook.js +196 -0
- package/dist/replace-file.d.ts +50 -0
- package/dist/replace-file.js +328 -0
- package/dist/schedule-command.d.ts +35 -0
- package/dist/schedule-command.js +260 -0
- package/dist/schedule-launch.d.ts +15 -0
- package/dist/schedule-launch.js +24 -0
- package/dist/schedule-ready.d.ts +44 -0
- package/dist/schedule-ready.js +71 -0
- package/dist/schedule-remote.d.ts +77 -0
- package/dist/schedule-remote.js +167 -0
- package/dist/schedule-runner.d.ts +89 -0
- package/dist/schedule-runner.js +452 -0
- package/dist/schedule.d.ts +149 -0
- package/dist/schedule.js +428 -0
- package/dist/secret-mask.d.ts +29 -0
- package/dist/secret-mask.js +245 -0
- package/dist/self-register.d.ts +45 -0
- package/dist/self-register.js +67 -0
- package/dist/send.d.ts +56 -0
- package/dist/send.js +104 -0
- package/dist/session-diff.d.ts +161 -0
- package/dist/session-diff.js +445 -0
- package/dist/session-registry.d.ts +143 -0
- package/dist/session-registry.js +216 -0
- package/dist/sessions-report.d.ts +59 -0
- package/dist/sessions-report.js +99 -0
- package/dist/shell-audit.d.ts +34 -0
- package/dist/shell-audit.js +47 -0
- package/dist/subagent-transcripts.d.ts +167 -0
- package/dist/subagent-transcripts.js +437 -0
- package/dist/subscriber-trust.d.ts +128 -0
- package/dist/subscriber-trust.js +397 -0
- package/dist/templates.d.ts +105 -0
- package/dist/templates.js +871 -0
- package/dist/tmux-install.d.ts +44 -0
- package/dist/tmux-install.js +65 -0
- package/dist/tmux-liveness.d.ts +45 -0
- package/dist/tmux-liveness.js +71 -0
- package/dist/tmux-pane.d.ts +55 -0
- package/dist/tmux-pane.js +126 -0
- package/dist/tmux-socket.d.ts +20 -0
- package/dist/tmux-socket.js +292 -0
- package/dist/todo-fold.d.ts +31 -0
- package/dist/todo-fold.js +102 -0
- package/dist/transcript-tail.d.ts +312 -0
- package/dist/transcript-tail.js +718 -0
- package/dist/trust-command.d.ts +49 -0
- package/dist/trust-command.js +200 -0
- package/dist/trust-lock.d.ts +26 -0
- package/dist/trust-lock.js +94 -0
- package/dist/trust-pins.d.ts +37 -0
- package/dist/trust-pins.js +82 -0
- package/dist/turn-recorder-wire.d.ts +70 -0
- package/dist/turn-recorder-wire.js +165 -0
- package/dist/turn-recorder.d.ts +190 -0
- package/dist/turn-recorder.js +695 -0
- package/dist/turn-ring.d.ts +93 -0
- package/dist/turn-ring.js +418 -0
- package/dist/turn-watch.d.ts +177 -0
- package/dist/turn-watch.js +630 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.js +2 -0
- package/dist/uninstall.d.ts +55 -0
- package/dist/uninstall.js +507 -0
- package/dist/usage.d.ts +17 -0
- package/dist/usage.js +421 -0
- package/dist/verify.d.ts +47 -0
- package/dist/verify.js +396 -0
- package/dist/worktree.d.ts +144 -0
- package/dist/worktree.js +310 -0
- package/dist/wrapper.d.ts +254 -0
- package/dist/wrapper.js +704 -0
- package/dist/ws-url.d.ts +109 -0
- package/dist/ws-url.js +202 -0
- package/package.json +78 -10
- package/bin/hwip.js +0 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop banner for a file that just landed (ADR-0013): macOS through
|
|
3
|
+
* `osascript`, Linux through `notify-send` when it is on PATH, otherwise
|
|
4
|
+
* nothing — the listener log line is the fallback everywhere.
|
|
5
|
+
*
|
|
6
|
+
* The banner names the file and the folder it is in, because "a file
|
|
7
|
+
* arrived" is only useful to someone who can then go and find it. On macOS
|
|
8
|
+
* a click reveals the file in Finder when `terminal-notifier` is on PATH
|
|
9
|
+
* (Homebrew; the service's PATH includes /opt/homebrew/bin). `osascript`
|
|
10
|
+
* cannot attach a click action, so without `terminal-notifier` the text is
|
|
11
|
+
* the whole UI.
|
|
12
|
+
*
|
|
13
|
+
* Best-effort by design. Never throws, never rejects; a failed banner
|
|
14
|
+
* must not turn a delivered file into an error.
|
|
15
|
+
*/
|
|
16
|
+
export interface DesktopNotification {
|
|
17
|
+
title: string;
|
|
18
|
+
body: string;
|
|
19
|
+
/** Absolute path a click should reveal in the file manager, where the notifier supports it. */
|
|
20
|
+
reveal?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DesktopNotifyDeps {
|
|
23
|
+
platform?: NodeJS.Platform;
|
|
24
|
+
/** `execFile`-shaped: argv, not a shell string, so nothing is interpolated. */
|
|
25
|
+
run?: (cmd: string, args: string[]) => Promise<void>;
|
|
26
|
+
onPath?: (cmd: string) => boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Show the banner. Resolves true when a command ran and exited cleanly,
|
|
30
|
+
* false when there is no notifier here or it failed.
|
|
31
|
+
*/
|
|
32
|
+
export declare const notifyDesktop: (note: DesktopNotification, deps?: DesktopNotifyDeps) => Promise<boolean>;
|
|
33
|
+
//# sourceMappingURL=desktop-notify.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.notifyDesktop = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const agents_js_1 = require("./agents.js");
|
|
6
|
+
const runCommand = (cmd, args) => new Promise((resolve, reject) => {
|
|
7
|
+
(0, node_child_process_1.execFile)(cmd, args, { timeout: 5_000 }, (err) => (err ? reject(err) : resolve()));
|
|
8
|
+
});
|
|
9
|
+
const commandOnPath = (cmd) => (0, agents_js_1.findOnPath)(cmd) !== null;
|
|
10
|
+
/** POSIX shell single-quoted literal: nothing is special inside it but `'` itself. */
|
|
11
|
+
const shellQuote = (s) => `'${s.split("'").join(`'\\''`)}'`;
|
|
12
|
+
/** AppleScript string literal: only `\` and `"` need escaping inside double quotes. */
|
|
13
|
+
const appleScriptString = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
14
|
+
/**
|
|
15
|
+
* Show the banner. Resolves true when a command ran and exited cleanly,
|
|
16
|
+
* false when there is no notifier here or it failed.
|
|
17
|
+
*/
|
|
18
|
+
const notifyDesktop = async (note, deps = {}) => {
|
|
19
|
+
const platform = deps.platform ?? process.platform;
|
|
20
|
+
const run = deps.run ?? runCommand;
|
|
21
|
+
const onPath = deps.onPath ?? commandOnPath;
|
|
22
|
+
try {
|
|
23
|
+
if (platform === 'darwin') {
|
|
24
|
+
if (note.reveal && onPath('terminal-notifier')) {
|
|
25
|
+
// `-execute` goes through a shell, so the path is quoted, and
|
|
26
|
+
// `--` keeps a name starting with '-' from reading as a flag.
|
|
27
|
+
await run('terminal-notifier', [
|
|
28
|
+
'-title', note.title,
|
|
29
|
+
'-message', note.body,
|
|
30
|
+
'-execute', `open -R -- ${shellQuote(note.reveal)}`,
|
|
31
|
+
]);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
const script = `display notification ${appleScriptString(note.body)} with title ${appleScriptString(note.title)}`;
|
|
35
|
+
await run('osascript', ['-e', script]);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (platform === 'linux' && onPath('notify-send')) {
|
|
39
|
+
// `--`: a title or body starting with '-' is text, not a flag.
|
|
40
|
+
await run('notify-send', ['--', note.title, note.body]);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
exports.notifyDesktop = notifyDesktop;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local transfer (ADR-0013) — who is allowed to send to this machine.
|
|
3
|
+
*
|
|
4
|
+
* A LAN upload is authenticated against the sender's *registered* public
|
|
5
|
+
* key, and the registry is the account's device list (`GET /devices`).
|
|
6
|
+
* This caches it so a burst of uploads does not become a burst of API
|
|
7
|
+
* calls, and refreshes on a miss — once per `refreshMinMs` — so a device
|
|
8
|
+
* registered a moment ago is accepted without waiting a whole TTL. An
|
|
9
|
+
* unknown id after a fresh fetch is simply not a device of this account.
|
|
10
|
+
*/
|
|
11
|
+
export interface DeviceDirectoryDeps {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
log: (msg: string) => void;
|
|
15
|
+
fetchFn?: typeof fetch;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
/** Age after which the next lookup refetches even on a hit. */
|
|
18
|
+
ttlMs?: number;
|
|
19
|
+
/** Shortest interval between two miss-triggered refetches. */
|
|
20
|
+
refreshMinMs?: number;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** What the account's device list says about a device besides its key. */
|
|
24
|
+
export interface DeviceInfo {
|
|
25
|
+
deviceId: string;
|
|
26
|
+
type?: string;
|
|
27
|
+
nickname?: string;
|
|
28
|
+
publicKey?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface DeviceDirectory {
|
|
31
|
+
publicKeyOf: (deviceId: string) => Promise<string | null>;
|
|
32
|
+
/** The devices the account lists, refetched when the cache is stale; the last list fetched (empty before any) when the refetch fails. */
|
|
33
|
+
devices: () => Promise<DeviceInfo[]>;
|
|
34
|
+
}
|
|
35
|
+
export declare const createDeviceDirectory: (deps: DeviceDirectoryDeps) => DeviceDirectory;
|
|
36
|
+
//# sourceMappingURL=device-directory.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local transfer (ADR-0013) — who is allowed to send to this machine.
|
|
4
|
+
*
|
|
5
|
+
* A LAN upload is authenticated against the sender's *registered* public
|
|
6
|
+
* key, and the registry is the account's device list (`GET /devices`).
|
|
7
|
+
* This caches it so a burst of uploads does not become a burst of API
|
|
8
|
+
* calls, and refreshes on a miss — once per `refreshMinMs` — so a device
|
|
9
|
+
* registered a moment ago is accepted without waiting a whole TTL. An
|
|
10
|
+
* unknown id after a fresh fetch is simply not a device of this account.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.createDeviceDirectory = void 0;
|
|
14
|
+
const api_js_1 = require("./api.js");
|
|
15
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
16
|
+
const DEFAULT_REFRESH_MIN_MS = 10_000;
|
|
17
|
+
const createDeviceDirectory = (deps) => {
|
|
18
|
+
const fetchFn = deps.fetchFn ?? fetch;
|
|
19
|
+
const now = deps.now ?? (() => Date.now());
|
|
20
|
+
const ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS;
|
|
21
|
+
const refreshMinMs = deps.refreshMinMs ?? DEFAULT_REFRESH_MIN_MS;
|
|
22
|
+
const url = (0, api_js_1.apiUrl)(deps.baseUrl, '/devices');
|
|
23
|
+
let keys = new Map();
|
|
24
|
+
let infos = [];
|
|
25
|
+
let fetchedAt = -Infinity;
|
|
26
|
+
/** Last attempt, successful or not — the miss brake counts failures too,
|
|
27
|
+
* or an unreachable API is hit on every unknown id. */
|
|
28
|
+
let triedAt = -Infinity;
|
|
29
|
+
let inFlight = null;
|
|
30
|
+
const refresh = () => {
|
|
31
|
+
if (inFlight)
|
|
32
|
+
return inFlight;
|
|
33
|
+
triedAt = now();
|
|
34
|
+
inFlight = (async () => {
|
|
35
|
+
const res = await (0, api_js_1.apiFetch)(fetchFn, url, deps.apiKey, deps.timeoutMs ?? 5_000);
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
throw new Error(`devices ${res.status}`);
|
|
38
|
+
const json = (await res.json());
|
|
39
|
+
const next = new Map();
|
|
40
|
+
const listed = [];
|
|
41
|
+
for (const d of json.data ?? []) {
|
|
42
|
+
if (typeof d.deviceId !== 'string')
|
|
43
|
+
continue;
|
|
44
|
+
const publicKey = typeof d.publicKey === 'string' && d.publicKey.length > 0 ? d.publicKey : undefined;
|
|
45
|
+
if (publicKey)
|
|
46
|
+
next.set(d.deviceId, publicKey);
|
|
47
|
+
listed.push({
|
|
48
|
+
deviceId: d.deviceId,
|
|
49
|
+
...(typeof d.type === 'string' ? { type: d.type } : {}),
|
|
50
|
+
...(typeof d.nickname === 'string' ? { nickname: d.nickname } : {}),
|
|
51
|
+
...(publicKey ? { publicKey } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
keys = next;
|
|
55
|
+
infos = listed;
|
|
56
|
+
fetchedAt = now();
|
|
57
|
+
})().finally(() => { inFlight = null; });
|
|
58
|
+
return inFlight;
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
publicKeyOf: async (deviceId) => {
|
|
62
|
+
const stale = now() - fetchedAt > ttlMs;
|
|
63
|
+
const hit = keys.get(deviceId);
|
|
64
|
+
if (hit && !stale)
|
|
65
|
+
return hit;
|
|
66
|
+
// Miss or stale: refetch, but a miss alone only once per refreshMinMs —
|
|
67
|
+
// an attacker probing unknown ids must not drive the API.
|
|
68
|
+
if (inFlight) {
|
|
69
|
+
await inFlight.catch(() => undefined); // logged by the caller that started it
|
|
70
|
+
}
|
|
71
|
+
else if ((stale || now() - fetchedAt > refreshMinMs) && now() - triedAt > refreshMinMs) {
|
|
72
|
+
try {
|
|
73
|
+
await refresh();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
deps.log(`local transfer: device list unavailable — ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return keys.get(deviceId) ?? null;
|
|
80
|
+
},
|
|
81
|
+
devices: async () => {
|
|
82
|
+
if (inFlight) {
|
|
83
|
+
await inFlight.catch(() => undefined);
|
|
84
|
+
}
|
|
85
|
+
else if (now() - fetchedAt > ttlMs && now() - triedAt > refreshMinMs) {
|
|
86
|
+
try {
|
|
87
|
+
await refresh();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
deps.log(`local transfer: device list unavailable — ${err instanceof Error ? err.message : String(err)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return infos;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
exports.createDeviceDirectory = createDeviceDirectory;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The one message both halves of presence ride; the server answers
|
|
2
|
+
* `listener.presence.ack` or `listener.presence.error`. */
|
|
3
|
+
export declare const LISTENER_PRESENCE = "listener.presence";
|
|
4
|
+
export interface DeviceKeyRegistrationDeps {
|
|
5
|
+
log: (msg: string) => void;
|
|
6
|
+
/** Send one frame on the live socket; false when there is none. */
|
|
7
|
+
send: (msg: object) => boolean;
|
|
8
|
+
/** Load-or-create the keypair; resolves the Base64 SPKI public key. */
|
|
9
|
+
publicKey: () => Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
export interface DeviceKeyRegistration {
|
|
12
|
+
/** Call on every WebSocket open. Never throws; resolves true once the
|
|
13
|
+
* frame went out. A failure waits for the next open. */
|
|
14
|
+
onOpen: () => Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
export declare const createDeviceKeyRegistration: (deps: DeviceKeyRegistrationDeps) => DeviceKeyRegistration;
|
|
17
|
+
//# sourceMappingURL=device-presence.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDeviceKeyRegistration = exports.LISTENER_PRESENCE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Per-device E2E (ADR-0007) — put this machine's public key on its device
|
|
6
|
+
* record, so other devices can wrap pushes and files for it.
|
|
7
|
+
*
|
|
8
|
+
* Senders only encrypt for devices whose record carries a `publicKey`, and
|
|
9
|
+
* a local transfer (ADR-0013 decision 3) is only attempted to such a device.
|
|
10
|
+
* The listener held a keypair from the start but never registered it, so
|
|
11
|
+
* the Mac was neither an E2E recipient nor a local-transfer target.
|
|
12
|
+
*
|
|
13
|
+
* It goes over the WebSocket, not an HTTP write: the device-record route
|
|
14
|
+
* (`PATCH /v1/devices/{deviceId}`) is behind the JWT authorizer and this
|
|
15
|
+
* daemon holds an API key, so the socket — which already proves which
|
|
16
|
+
* device is speaking, and which created the record at `$connect` — is the
|
|
17
|
+
* write path. `lan-endpoint.ts` publishes the transfer endpoint on the same
|
|
18
|
+
* message.
|
|
19
|
+
*
|
|
20
|
+
* Sent on every WebSocket open: `$connect` is what makes the record exist,
|
|
21
|
+
* and a record that was recreated has lost its key. The server broadcasts
|
|
22
|
+
* `device.publicKey.changed` only when the key actually differs, so an
|
|
23
|
+
* unchanged re-registration is one quiet write. Senders seal for every listed
|
|
24
|
+
* device that has a key, so a missing key means nothing can be read there. The
|
|
25
|
+
* private half never leaves `~/.hwip/device-keys.json`.
|
|
26
|
+
*
|
|
27
|
+
* The same frame says what this build opens (`capabilities`): a sender seals
|
|
28
|
+
* a file in chunks only for a device that lists `file-chunk-v1`, and one over
|
|
29
|
+
* the relay's ceiling over LAN only to one that lists `lan-large-v1`. In one frame
|
|
30
|
+
* with the key, because a server from before `capabilities` answers a frame
|
|
31
|
+
* that carries nothing else it knows with an error.
|
|
32
|
+
*/
|
|
33
|
+
const file_crypto_js_1 = require("./file-crypto.js");
|
|
34
|
+
const lan_receiver_js_1 = require("./lan-receiver.js");
|
|
35
|
+
/** The one message both halves of presence ride; the server answers
|
|
36
|
+
* `listener.presence.ack` or `listener.presence.error`. */
|
|
37
|
+
exports.LISTENER_PRESENCE = 'listener.presence';
|
|
38
|
+
const createDeviceKeyRegistration = (deps) => {
|
|
39
|
+
// Two opens in quick succession (a flapping socket) share one load. A
|
|
40
|
+
// record recreated inside that window waits for the open after — accepted.
|
|
41
|
+
let inFlight = null;
|
|
42
|
+
const register = async () => {
|
|
43
|
+
try {
|
|
44
|
+
const publicKey = await deps.publicKey();
|
|
45
|
+
const sent = deps.send({ type: exports.LISTENER_PRESENCE, data: { publicKey, capabilities: [file_crypto_js_1.FILE_CHUNK_CAPABILITY, lan_receiver_js_1.LAN_LARGE_CAPABILITY] } });
|
|
46
|
+
if (!sent)
|
|
47
|
+
deps.log('device key: not registered — no connection right now, retried at the next open');
|
|
48
|
+
return sent;
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
deps.log(`device key: registration failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
onOpen: () => {
|
|
57
|
+
inFlight ??= register().finally(() => { inFlight = null; });
|
|
58
|
+
return inFlight;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
exports.createDeviceKeyRegistration = createDeviceKeyRegistration;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a file push addressed to this machine lands (ADR-0013): a flat
|
|
3
|
+
* `~/Downloads/Hwip/`, never garbage-collected — it is the user's folder,
|
|
4
|
+
* not the daemon's cache. `~/.hwip/attachments/` stays what it is: the
|
|
5
|
+
* `agent.command` scratch space with the hourly sweep.
|
|
6
|
+
*/
|
|
7
|
+
export declare const downloadsDir: () => string;
|
|
8
|
+
/**
|
|
9
|
+
* Remove the `.part` files a daemon that died mid-download left in the
|
|
10
|
+
* downloads folder — up to 1 GiB each, hidden, and nothing else ever
|
|
11
|
+
* removes them. Only our own temp names, only plain files, only stale ones;
|
|
12
|
+
* the user's files in the folder are never looked at. Returns how many went.
|
|
13
|
+
*/
|
|
14
|
+
export declare const sweepStaleParts: (dir?: string, now?: number) => number;
|
|
15
|
+
/**
|
|
16
|
+
* Reduce whatever the sender called the file to one safe path segment:
|
|
17
|
+
* basename (so `../../x` and absolute paths lose their directories),
|
|
18
|
+
* no control characters or separators, no leading dots (a hidden file
|
|
19
|
+
* is not what the user asked to receive), `file` when nothing is left.
|
|
20
|
+
*/
|
|
21
|
+
export declare const safeFileName: (raw: string) => string;
|
|
22
|
+
/**
|
|
23
|
+
* Absolute path to write `fileName` under `dir`, never overwriting: an
|
|
24
|
+
* existing `a.png` yields `a (2).png`, then `a (3).png`. The counter starts
|
|
25
|
+
* at 2 because the first copy has no suffix — that is the convention macOS
|
|
26
|
+
* and the browsers use, and the one users already read.
|
|
27
|
+
*/
|
|
28
|
+
export declare const resolveDownloadPath: (fileName: string, dir?: string, exists?: (path: string) => boolean) => string;
|
|
29
|
+
//# sourceMappingURL=downloads.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveDownloadPath = exports.safeFileName = exports.sweepStaleParts = exports.downloadsDir = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_os_1 = require("node:os");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
/**
|
|
8
|
+
* Where a file push addressed to this machine lands (ADR-0013): a flat
|
|
9
|
+
* `~/Downloads/Hwip/`, never garbage-collected — it is the user's folder,
|
|
10
|
+
* not the daemon's cache. `~/.hwip/attachments/` stays what it is: the
|
|
11
|
+
* `agent.command` scratch space with the hourly sweep.
|
|
12
|
+
*/
|
|
13
|
+
const downloadsDir = () => (0, node_path_1.join)((0, node_os_1.homedir)(), 'Downloads', 'Hwip');
|
|
14
|
+
exports.downloadsDir = downloadsDir;
|
|
15
|
+
/** The temp name `saveStream` (file-crypto.ts) streams a download into. */
|
|
16
|
+
const PART_FILE = /^\.hwip-[0-9a-f]{12}\.part$/;
|
|
17
|
+
/**
|
|
18
|
+
* A `.part` untouched this long has no download behind it: a live one is
|
|
19
|
+
* written to at least every minute (DOWNLOAD_IDLE_MS) or aborted.
|
|
20
|
+
*/
|
|
21
|
+
const STALE_PART_MS = 60 * 60 * 1000;
|
|
22
|
+
/**
|
|
23
|
+
* Remove the `.part` files a daemon that died mid-download left in the
|
|
24
|
+
* downloads folder — up to 1 GiB each, hidden, and nothing else ever
|
|
25
|
+
* removes them. Only our own temp names, only plain files, only stale ones;
|
|
26
|
+
* the user's files in the folder are never looked at. Returns how many went.
|
|
27
|
+
*/
|
|
28
|
+
const sweepStaleParts = (dir = (0, exports.downloadsDir)(), now = Date.now()) => {
|
|
29
|
+
let entries;
|
|
30
|
+
try {
|
|
31
|
+
entries = (0, node_fs_1.readdirSync)(dir);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return 0; // no downloads yet
|
|
35
|
+
}
|
|
36
|
+
let removed = 0;
|
|
37
|
+
for (const name of entries) {
|
|
38
|
+
if (!PART_FILE.test(name))
|
|
39
|
+
continue;
|
|
40
|
+
const path = (0, node_path_1.join)(dir, name);
|
|
41
|
+
try {
|
|
42
|
+
const st = (0, node_fs_1.lstatSync)(path);
|
|
43
|
+
if (!st.isFile() || now - st.mtimeMs < STALE_PART_MS)
|
|
44
|
+
continue;
|
|
45
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
46
|
+
removed++;
|
|
47
|
+
}
|
|
48
|
+
catch { /* gone already, or not ours to remove */ }
|
|
49
|
+
}
|
|
50
|
+
return removed;
|
|
51
|
+
};
|
|
52
|
+
exports.sweepStaleParts = sweepStaleParts;
|
|
53
|
+
/** Longest name most filesystems take; the suffix has to fit inside it too. */
|
|
54
|
+
const MAX_NAME_BYTES = 255;
|
|
55
|
+
/** `.tar.gz`-class extensions are ~8 bytes; nothing real is near this. */
|
|
56
|
+
const MAX_EXT_BYTES = 32;
|
|
57
|
+
const byteLength = (s) => Buffer.byteLength(s, 'utf8');
|
|
58
|
+
/** Trim `stem` until `stem + tail` fits MAX_NAME_BYTES, never splitting a code point. */
|
|
59
|
+
const fitStem = (stem, tail) => {
|
|
60
|
+
let out = stem;
|
|
61
|
+
while (out.length > 0 && byteLength(out) + byteLength(tail) > MAX_NAME_BYTES) {
|
|
62
|
+
out = Array.from(out).slice(0, -1).join('');
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Reduce whatever the sender called the file to one safe path segment:
|
|
68
|
+
* basename (so `../../x` and absolute paths lose their directories),
|
|
69
|
+
* no control characters or separators, no leading dots (a hidden file
|
|
70
|
+
* is not what the user asked to receive), `file` when nothing is left.
|
|
71
|
+
*/
|
|
72
|
+
const safeFileName = (raw) => {
|
|
73
|
+
const cleaned = (0, node_path_1.basename)(raw)
|
|
74
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
75
|
+
.replace(/[/\\]/g, '_')
|
|
76
|
+
.replace(/^\.+/, '')
|
|
77
|
+
.trim();
|
|
78
|
+
return cleaned || 'file';
|
|
79
|
+
};
|
|
80
|
+
exports.safeFileName = safeFileName;
|
|
81
|
+
/**
|
|
82
|
+
* Absolute path to write `fileName` under `dir`, never overwriting: an
|
|
83
|
+
* existing `a.png` yields `a (2).png`, then `a (3).png`. The counter starts
|
|
84
|
+
* at 2 because the first copy has no suffix — that is the convention macOS
|
|
85
|
+
* and the browsers use, and the one users already read.
|
|
86
|
+
*/
|
|
87
|
+
const resolveDownloadPath = (fileName, dir = (0, exports.downloadsDir)(), exists = node_fs_1.existsSync) => {
|
|
88
|
+
const safe = (0, exports.safeFileName)(fileName);
|
|
89
|
+
// An "extension" longer than any real one is a sender playing with the
|
|
90
|
+
// trimming rule: treat it as part of the stem so it gets cut like the rest.
|
|
91
|
+
const rawExt = (0, node_path_1.extname)(safe);
|
|
92
|
+
const ext = byteLength(rawExt) <= MAX_EXT_BYTES ? rawExt : '';
|
|
93
|
+
const stem = ext ? safe.slice(0, -ext.length) : safe;
|
|
94
|
+
// The trim can empty the stem and leave `.ext` — a hidden file — so the
|
|
95
|
+
// name rules run once more on what is actually written.
|
|
96
|
+
const nameFor = (tail) => (0, exports.safeFileName)(fitStem(stem, tail) + tail);
|
|
97
|
+
const first = (0, node_path_1.join)(dir, nameFor(ext));
|
|
98
|
+
if (!exists(first))
|
|
99
|
+
return first;
|
|
100
|
+
for (let n = 2;; n++) {
|
|
101
|
+
const candidate = (0, node_path_1.join)(dir, nameFor(` (${n})${ext}`));
|
|
102
|
+
if (!exists(candidate))
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
exports.resolveDownloadPath = resolveDownloadPath;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class HwipError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
status: number;
|
|
4
|
+
constructor(message: string, code: string, status: number);
|
|
5
|
+
}
|
|
6
|
+
export declare class AuthenticationError extends HwipError {
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class QuotaExceededError extends HwipError {
|
|
10
|
+
constructor(message?: string);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuotaExceededError = exports.AuthenticationError = exports.HwipError = void 0;
|
|
4
|
+
class HwipError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
constructor(message, code, status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'HwipError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.HwipError = HwipError;
|
|
15
|
+
class AuthenticationError extends HwipError {
|
|
16
|
+
constructor(message = 'Authentication failed') {
|
|
17
|
+
super(message, 'AUTHENTICATION_ERROR', 401);
|
|
18
|
+
this.name = 'AuthenticationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.AuthenticationError = AuthenticationError;
|
|
22
|
+
class QuotaExceededError extends HwipError {
|
|
23
|
+
constructor(message = 'Quota exceeded') {
|
|
24
|
+
super(message, 'QUOTA_EXCEEDED', 403);
|
|
25
|
+
this.name = 'QuotaExceededError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.QuotaExceededError = QuotaExceededError;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Transform, type Readable } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* Streaming decrypt of a file the web / MCP side produced with
|
|
4
|
+
* `libs/crypto` `encryptFile` (WebCrypto AES-256-GCM, 12-byte IV). That
|
|
5
|
+
* API is one-shot; a 1 GiB attachment would sit in memory here, so this
|
|
6
|
+
* is the same wire format on `node:crypto`'s incremental cipher:
|
|
7
|
+
* WebCrypto appends the 16-byte GCM tag to the ciphertext, and the stream
|
|
8
|
+
* holds back the last 16 bytes of whatever it is fed as that tag.
|
|
9
|
+
*
|
|
10
|
+
* The key is the raw per-file AES key, already unwrapped from the
|
|
11
|
+
* attachment's `deviceKeyMap` slot (see `unwrapDeviceKey` in crypto.ts).
|
|
12
|
+
*/
|
|
13
|
+
export declare const GCM_TAG_BYTES = 16;
|
|
14
|
+
export declare const GCM_IV_BYTES = 12;
|
|
15
|
+
export declare const createFileDecryptStream: (rawKey: Buffer, iv: Buffer) => Transform;
|
|
16
|
+
/**
|
|
17
|
+
* A file sealed in chunks (`file-chunk-v1`), the format a phone seals a file
|
|
18
|
+
* in that is too large to hold in memory. The padded plaintext is cut into
|
|
19
|
+
* `FILE_CHUNK_BYTES` pieces — the last one shorter or equal, never empty —
|
|
20
|
+
* and each is sealed alone with AES-256-GCM under the file key, no AAD:
|
|
21
|
+
*
|
|
22
|
+
* nonce = prefix[7] ‖ piece index (u32 big-endian) ‖ 0x01 on the last piece, else 0x00
|
|
23
|
+
* file = every `ciphertext ‖ tag[16]`, in order
|
|
24
|
+
*
|
|
25
|
+
* The index in the nonce refuses a piece moved or repeated; the last-piece
|
|
26
|
+
* flag refuses a file cut at a piece's end, and bytes added after it. The
|
|
27
|
+
* prefix travels where a whole-sealed file's IV does (`iv`), and the sealed
|
|
28
|
+
* message says `chunk` next to it — see `chunkOf`. Vectors:
|
|
29
|
+
* `file-chunk.vectors.json` (scripts/gen-file-chunk-vectors.py).
|
|
30
|
+
*/
|
|
31
|
+
export declare const FILE_CHUNK_BYTES = 1048576;
|
|
32
|
+
export declare const FILE_CHUNK_PREFIX_BYTES = 7;
|
|
33
|
+
/** What a listener tells senders it opens (`capabilities` on its device record). */
|
|
34
|
+
export declare const FILE_CHUNK_CAPABILITY = "file-chunk-v1";
|
|
35
|
+
/**
|
|
36
|
+
* The `chunk` a sender wrote beside a file's `iv`: absent for a file sealed
|
|
37
|
+
* whole, `FILE_CHUNK_BYTES` for one sealed in chunks. Any other value is a
|
|
38
|
+
* format this build does not open — `ok: false`, and the file is left alone.
|
|
39
|
+
*/
|
|
40
|
+
export declare const chunkOf: (said: unknown) => {
|
|
41
|
+
ok: true;
|
|
42
|
+
chunk?: number;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Streaming decrypt of a file sealed in chunks. A piece is opened only once
|
|
48
|
+
* a byte past it has arrived, because whether it is the last one is in its
|
|
49
|
+
* nonce: what is left when the stream ends is the last piece, and it has to
|
|
50
|
+
* open as one. Pieces already opened have reached the `.part` file by then;
|
|
51
|
+
* `saveStream` removes it on any failure, so nothing a later piece disowns
|
|
52
|
+
* is ever linked into place.
|
|
53
|
+
*/
|
|
54
|
+
export declare const createChunkedFileDecryptStream: (rawKey: Buffer, prefix: Buffer, chunk?: number) => Transform;
|
|
55
|
+
/**
|
|
56
|
+
* Abort `controller` when `source` goes quiet for `idleMs`. A stalled S3
|
|
57
|
+
* connection would otherwise park a download — and its part file — for
|
|
58
|
+
* the life of the daemon with nothing in the log. Bytes still flowing,
|
|
59
|
+
* however slowly, never trip it: a 1 GiB file on a bad link is not a stall.
|
|
60
|
+
* More than `maxBytes` fails the stream: a body that never ends is not a
|
|
61
|
+
* stall either, and would fill the disk.
|
|
62
|
+
*/
|
|
63
|
+
export declare const withIdleTimeout: (source: Readable, idleMs: number, controller: AbortController, maxBytes?: number) => Readable;
|
|
64
|
+
/**
|
|
65
|
+
* The most bytes one download may carry: the largest file any tier may send
|
|
66
|
+
* (1 GiB, `fileMaxBytes`), plus the Padmé padding (at most about 12%) and
|
|
67
|
+
* the GCM tag a sealed file adds. The record's `fileSize` is the relay's
|
|
68
|
+
* word, so it cannot be the limit; without one, a URL that streams forever
|
|
69
|
+
* fills the disk.
|
|
70
|
+
*/
|
|
71
|
+
export declare const DOWNLOAD_MAX_BYTES: number;
|
|
72
|
+
export interface SavedFile {
|
|
73
|
+
path: string;
|
|
74
|
+
bytes: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Stream `source` to the path `nextDest()` names, optionally decrypting
|
|
78
|
+
* on the way — a file sealed whole, or with `chunk` one sealed in chunks
|
|
79
|
+
* (`iv` is then its 7-byte prefix). The bytes go to a private `.part` file first (a per-call
|
|
80
|
+
* random name, so two saves of the same file cannot touch each other's
|
|
81
|
+
* temp) and are fsynced before being linked into place — a reader never
|
|
82
|
+
* sees a half file, and an authentication failure (which GCM only reports
|
|
83
|
+
* at the very end) leaves nothing behind. `link` refuses an existing dest
|
|
84
|
+
* where `rename` would replace it; on that collision `nextDest()` is asked
|
|
85
|
+
* again, so a name taken between the caller's check and here costs a
|
|
86
|
+
* ` (n)` suffix, never the download. Resolves with the path written and
|
|
87
|
+
* the plaintext byte count.
|
|
88
|
+
*/
|
|
89
|
+
export declare const saveStream: (source: Readable, nextDest: () => string, decrypt?: {
|
|
90
|
+
rawKey: Buffer;
|
|
91
|
+
iv: Buffer;
|
|
92
|
+
chunk?: number;
|
|
93
|
+
}) => Promise<SavedFile>;
|
|
94
|
+
//# sourceMappingURL=file-crypto.d.ts.map
|