@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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hwip trust` — the person's side of subscriber-trust.ts. Advanced: nothing needs it to work.
|
|
3
|
+
*
|
|
4
|
+
* Every device the account lists is pinned the first time hwip sees it (a send, or the listener), and a key that
|
|
5
|
+
* changed replaces its pin with one warning. Whether a new key is the phone after a reinstall or a server putting
|
|
6
|
+
* its own in the device list is not something hwip can tell; a person can, by reading a fingerprint off the phone's
|
|
7
|
+
* screen. This command shows the fingerprints and takes that decision.
|
|
8
|
+
*
|
|
9
|
+
* hwip trust this computer's fingerprint, and the pinned devices
|
|
10
|
+
* hwip trust <deviceId> pin the key the account now registers for a device
|
|
11
|
+
* hwip trust --reset forget every pin; the listener then answers a plaintext screen request again
|
|
12
|
+
*/
|
|
13
|
+
import type { DeviceInfo } from './device-directory.js';
|
|
14
|
+
export interface TrustCommandDeps {
|
|
15
|
+
/** The listener's state file (`~/.hwip/trusted-devices.json`). */
|
|
16
|
+
path: string;
|
|
17
|
+
/** This machine's device public key (load-or-create). */
|
|
18
|
+
devicePublicKey: () => Promise<string>;
|
|
19
|
+
/** The key the account's device list holds for a device; null when it lists none. */
|
|
20
|
+
registeredKey: (deviceId: string) => Promise<string | null>;
|
|
21
|
+
/** The account's device list with each device's kind; empty (or absent) when it cannot be read. */
|
|
22
|
+
devices?: () => Promise<DeviceInfo[]>;
|
|
23
|
+
/** Ask a yes/no question; false when there is nobody to ask. */
|
|
24
|
+
confirm: (question: string) => Promise<boolean>;
|
|
25
|
+
print: (line: string) => void;
|
|
26
|
+
printError: (line: string) => void;
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
}
|
|
29
|
+
export interface TrustCommandArgs {
|
|
30
|
+
deviceId?: string;
|
|
31
|
+
reset: boolean;
|
|
32
|
+
/** Skip the question (`--yes`): the caller has compared the fingerprints already. */
|
|
33
|
+
yes: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** A flag's value when it was given one; a bare `--flag` or an empty string is none. */
|
|
36
|
+
export declare const flagText: (value: string | boolean | undefined) => string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* `hwip trust`'s arguments from the cli's parsed flags, or why they make no
|
|
39
|
+
* command. The parser hands a flag the next word as its value, so
|
|
40
|
+
* `--yes <deviceId>` arrives as `yes: '<deviceId>'` with no positional: that
|
|
41
|
+
* word is the device. `--reset` takes no device at all — with one it would
|
|
42
|
+
* read as "reset this device" and do something much larger.
|
|
43
|
+
*/
|
|
44
|
+
export declare const parseTrustArgs: (args: Record<string, string | boolean>) => TrustCommandArgs | {
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
47
|
+
/** Run `hwip trust`; the return value is the exit code. */
|
|
48
|
+
export declare const runTrust: (args: TrustCommandArgs, deps: TrustCommandDeps) => Promise<number>;
|
|
49
|
+
//# sourceMappingURL=trust-command.d.ts.map
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `hwip trust` — the person's side of subscriber-trust.ts. Advanced: nothing needs it to work.
|
|
4
|
+
*
|
|
5
|
+
* Every device the account lists is pinned the first time hwip sees it (a send, or the listener), and a key that
|
|
6
|
+
* changed replaces its pin with one warning. Whether a new key is the phone after a reinstall or a server putting
|
|
7
|
+
* its own in the device list is not something hwip can tell; a person can, by reading a fingerprint off the phone's
|
|
8
|
+
* screen. This command shows the fingerprints and takes that decision.
|
|
9
|
+
*
|
|
10
|
+
* hwip trust this computer's fingerprint, and the pinned devices
|
|
11
|
+
* hwip trust <deviceId> pin the key the account now registers for a device
|
|
12
|
+
* hwip trust --reset forget every pin; the listener then answers a plaintext screen request again
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.runTrust = exports.parseTrustArgs = exports.flagText = void 0;
|
|
16
|
+
const crypto_js_1 = require("./crypto.js");
|
|
17
|
+
const subscriber_trust_js_1 = require("./subscriber-trust.js");
|
|
18
|
+
const trust_lock_js_1 = require("./trust-lock.js");
|
|
19
|
+
/** A flag's value when it was given one; a bare `--flag` or an empty string is none. */
|
|
20
|
+
const flagText = (value) => (typeof value === 'string' && value !== '' ? value : undefined);
|
|
21
|
+
exports.flagText = flagText;
|
|
22
|
+
/**
|
|
23
|
+
* `hwip trust`'s arguments from the cli's parsed flags, or why they make no
|
|
24
|
+
* command. The parser hands a flag the next word as its value, so
|
|
25
|
+
* `--yes <deviceId>` arrives as `yes: '<deviceId>'` with no positional: that
|
|
26
|
+
* word is the device. `--reset` takes no device at all — with one it would
|
|
27
|
+
* read as "reset this device" and do something much larger.
|
|
28
|
+
*/
|
|
29
|
+
const parseTrustArgs = (args) => {
|
|
30
|
+
const positional = (0, exports.flagText)(args._arg1);
|
|
31
|
+
const afterYes = (0, exports.flagText)(args.yes);
|
|
32
|
+
const reset = args.reset !== undefined && args.reset !== false;
|
|
33
|
+
const yes = args.yes !== undefined && args.yes !== false;
|
|
34
|
+
if (positional && afterYes)
|
|
35
|
+
return { error: 'hwip trust: one device at a time' };
|
|
36
|
+
const deviceId = positional ?? afterYes;
|
|
37
|
+
if (reset && (deviceId || (0, exports.flagText)(args.reset))) {
|
|
38
|
+
return { error: 'hwip trust: --reset forgets every device and takes none — use `hwip trust <deviceId>` to re-pin one' };
|
|
39
|
+
}
|
|
40
|
+
return { ...(deviceId ? { deviceId } : {}), reset, yes };
|
|
41
|
+
};
|
|
42
|
+
exports.parseTrustArgs = parseTrustArgs;
|
|
43
|
+
const fingerprintOf = (publicKey) => (0, crypto_js_1.keyFingerprint)(publicKey) ?? '(not a device key)';
|
|
44
|
+
const NICKNAME_MAX = 40;
|
|
45
|
+
/**
|
|
46
|
+
* A nickname as the screen shows it. The account lists it, so the relay writes it: it may carry control characters that
|
|
47
|
+
* rewrite the terminal, direction or zero-width characters that move or hide text, and a run of spaces that wraps the line so
|
|
48
|
+
* a made-up row (an id and a fingerprint) starts the next one. So: those characters go, whitespace runs become one space,
|
|
49
|
+
* and it is cut at 40 characters.
|
|
50
|
+
*/
|
|
51
|
+
const nicknameOf = (text) => {
|
|
52
|
+
const plain = text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]|\p{Cf}/gu, '').replace(/\s+/g, ' ').trim();
|
|
53
|
+
const chars = Array.from(plain);
|
|
54
|
+
return chars.length > NICKNAME_MAX ? `${chars.slice(0, NICKNAME_MAX - 1).join('')}…` : plain;
|
|
55
|
+
};
|
|
56
|
+
/** The kinds the server lists a device as (`DeviceType` in the server's device types). */
|
|
57
|
+
const DEVICE_KINDS = new Set(['android', 'ios', 'macos', 'chrome', 'firefox', 'safari', 'chrome_extension', 'firefox_extension', 'cli', 'hook']);
|
|
58
|
+
/** The kind the account lists, if it is one of those: a relay's text never reaches the screen, least of all above the fingerprint. */
|
|
59
|
+
const kindOf = (type) => (type === undefined ? undefined : DEVICE_KINDS.has(type) ? type : 'unknown');
|
|
60
|
+
/** A device's kind as the account lists it, for the line it is on. */
|
|
61
|
+
const kindTag = (listed, deviceId) => {
|
|
62
|
+
const kind = kindOf(listed.get(deviceId)?.type);
|
|
63
|
+
return kind ? ` [${kind}]` : '';
|
|
64
|
+
};
|
|
65
|
+
const showState = async (state, deps) => {
|
|
66
|
+
const own = await deps.devicePublicKey();
|
|
67
|
+
deps.print(`This computer ${fingerprintOf(own)}`);
|
|
68
|
+
deps.print('');
|
|
69
|
+
const listed = new Map((await deps.devices?.().catch(() => []) ?? []).map((d) => [d.deviceId, d]));
|
|
70
|
+
if (state.devices.size === 0) {
|
|
71
|
+
deps.print('No device is pinned yet: each one is pinned the first time hwip sees it in the account\'s device list.');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
deps.print('Pinned devices — the listener seals a screen only for these keys:');
|
|
75
|
+
for (const [deviceId, pin] of state.devices) {
|
|
76
|
+
deps.print(` ${deviceId.padEnd(24)} ${fingerprintOf(pin.publicKey)} since ${pin.pinnedAt.slice(0, 10)}${kindTag(listed, deviceId)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// A computer is pinned the first time a send seals for it; this is the chance to compare its fingerprint before that.
|
|
80
|
+
const unpinned = [...listed.values()].filter((d) => d.type === 'cli' && subscriber_trust_js_1.DEVICE_ID.test(d.deviceId) && d.publicKey !== undefined && !state.devices.has(d.deviceId) && d.publicKey !== own);
|
|
81
|
+
if (unpinned.length > 0) {
|
|
82
|
+
deps.print('');
|
|
83
|
+
deps.print('Computers on the account not pinned here yet — the first send seals for each and pins it; to compare a fingerprint before that, run `hwip trust <deviceId>`:');
|
|
84
|
+
for (const d of unpinned)
|
|
85
|
+
deps.print(` ${d.deviceId.padEnd(24)} ${fingerprintOf(d.publicKey ?? '')} ${nicknameOf(d.nickname ?? '')}`.trimEnd());
|
|
86
|
+
}
|
|
87
|
+
deps.print('');
|
|
88
|
+
deps.print(state.devices.size > 0
|
|
89
|
+
? 'This listener only seals: a request for a screen in plaintext is refused.'
|
|
90
|
+
: 'No device is pinned: this listener still answers a plaintext request.');
|
|
91
|
+
deps.print('Each fingerprint has to read the same in the app, on that device\'s page.');
|
|
92
|
+
return 0;
|
|
93
|
+
};
|
|
94
|
+
const trustDevice = async (deviceId, state, args, deps) => {
|
|
95
|
+
if (!subscriber_trust_js_1.DEVICE_ID.test(deviceId)) {
|
|
96
|
+
deps.printError('hwip trust: that is not a device id (see `hwip trust` for the pinned ones)');
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
const registered = await deps.registeredKey(deviceId);
|
|
100
|
+
if (registered === null || (0, crypto_js_1.keyFingerprint)(registered) === null) {
|
|
101
|
+
deps.printError(`hwip trust: the account lists no key for ${deviceId} (or its device list could not be read)`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
const pin = state.devices.get(deviceId);
|
|
105
|
+
if (pin?.publicKey === registered) {
|
|
106
|
+
deps.print(`${deviceId} is already pinned to ${fingerprintOf(registered)}.`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
const kind = kindOf((await deps.devices?.().catch(() => []) ?? []).find((d) => d.deviceId === deviceId)?.type);
|
|
110
|
+
if (kind)
|
|
111
|
+
deps.print(`Device kind ${kind}`);
|
|
112
|
+
deps.print(`Pinned now ${pin ? fingerprintOf(pin.publicKey) : '(nothing)'}`);
|
|
113
|
+
deps.print(`Account lists ${fingerprintOf(registered)}`);
|
|
114
|
+
// Another computer shows its own fingerprint as `This computer` in its `hwip trust`; the app shows a device's on its page.
|
|
115
|
+
deps.print(kind === 'cli'
|
|
116
|
+
? 'Run `hwip trust` on that computer and read its "This computer" line.'
|
|
117
|
+
: 'Open the app on that device and read its own fingerprint (its page under Devices).');
|
|
118
|
+
const agreed = args.yes || await deps.confirm(`Does the ${kind === 'cli' ? 'computer' : 'device'} show ${fingerprintOf(registered)}?`);
|
|
119
|
+
if (!agreed) {
|
|
120
|
+
deps.print('Left as it was.');
|
|
121
|
+
return 1;
|
|
122
|
+
}
|
|
123
|
+
// Read again, under the lock: the lookup and the question took time, and a running
|
|
124
|
+
// listener may have pinned another device meanwhile. Writing the state
|
|
125
|
+
// from before would quietly unpin it.
|
|
126
|
+
const at = (deps.now?.() ?? new Date()).toISOString();
|
|
127
|
+
const done = rewrite(deps, (latest) => {
|
|
128
|
+
if ('broken' in latest)
|
|
129
|
+
return { refused: unusable(latest.broken, deps) };
|
|
130
|
+
const devices = new Map(latest.state.devices);
|
|
131
|
+
devices.set(deviceId, { publicKey: registered, pinnedAt: at });
|
|
132
|
+
// The key being replaced keeps the numbers taken with it: they are written beside the key before the pin goes.
|
|
133
|
+
return { state: { ...latest.state, devices, commandKeys: (0, subscriber_trust_js_1.foldCommandNumbers)(latest.state, [deviceId]) } };
|
|
134
|
+
});
|
|
135
|
+
if (done !== 0)
|
|
136
|
+
return done;
|
|
137
|
+
deps.print(`${deviceId} is pinned to ${fingerprintOf(registered)}.`);
|
|
138
|
+
return 0;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Rewrite the state file under its lock, from the file as it is once the lock is held. `make` returns the state to
|
|
142
|
+
* write, or the exit code of a refusal it has already printed. 0 is done; anything else says why nothing changed.
|
|
143
|
+
*/
|
|
144
|
+
const rewrite = (deps, make) => {
|
|
145
|
+
try {
|
|
146
|
+
const done = (0, trust_lock_js_1.withTrustLock)(deps.path, () => {
|
|
147
|
+
const made = make((0, subscriber_trust_js_1.readTrustState)(deps.path));
|
|
148
|
+
if ('refused' in made)
|
|
149
|
+
return made.refused;
|
|
150
|
+
(0, subscriber_trust_js_1.writeTrustState)(deps.path, made.state);
|
|
151
|
+
return 0;
|
|
152
|
+
});
|
|
153
|
+
if (done.locked)
|
|
154
|
+
return done.value;
|
|
155
|
+
deps.printError(`hwip trust: ${deps.path} is being written by another process — nothing changed, try again.`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
deps.printError(`hwip trust: could not write ${deps.path} (${err instanceof Error ? err.message : String(err)}) — nothing changed.`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const unusable = (why, deps) => {
|
|
164
|
+
deps.printError(`hwip trust: ${deps.path} is unusable (${why}).`);
|
|
165
|
+
deps.printError('The listener refuses every screen until it is fixed; `hwip trust --reset` starts over.');
|
|
166
|
+
return 1;
|
|
167
|
+
};
|
|
168
|
+
const reset = async (args, deps) => {
|
|
169
|
+
const agreed = args.yes || await deps.confirm('Forget every pinned device? Each is pinned again the next time hwip sees it in the account\'s device list; until then this listener answers a request for a screen in plaintext.');
|
|
170
|
+
if (!agreed) {
|
|
171
|
+
deps.print('Left as it was.');
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
// Rewritten, not removed: a running listener treats a state file that
|
|
175
|
+
// vanished as a reason to refuse, and an empty one as a fresh start. The
|
|
176
|
+
// sealed command numbers stay, with the keys they were taken with: without them an old command could be typed
|
|
177
|
+
// again (a file too broken to read has none to keep).
|
|
178
|
+
const done = rewrite(deps, (before) => {
|
|
179
|
+
const commands = 'state' in before ? before.state.commands : new Map();
|
|
180
|
+
// A file from before `commandKeys` holds the numbers by id only, and the pins that tell which key each id had are
|
|
181
|
+
// going: they are written beside their keys first.
|
|
182
|
+
const commandKeys = 'state' in before ? (0, subscriber_trust_js_1.foldCommandNumbers)(before.state, before.state.devices.keys()) : new Map();
|
|
183
|
+
return { state: { devices: new Map(), commands, commandKeys, ...('state' in before && before.state.extra ? { extra: before.state.extra } : {}) } };
|
|
184
|
+
});
|
|
185
|
+
if (done !== 0)
|
|
186
|
+
return done;
|
|
187
|
+
deps.print('Reset. Each device is pinned again the next time hwip sees it.');
|
|
188
|
+
return 0;
|
|
189
|
+
};
|
|
190
|
+
/** Run `hwip trust`; the return value is the exit code. */
|
|
191
|
+
const runTrust = async (args, deps) => {
|
|
192
|
+
// A reset is the way out of an unusable file, so it does not read it.
|
|
193
|
+
if (args.reset)
|
|
194
|
+
return reset(args, deps);
|
|
195
|
+
const loaded = (0, subscriber_trust_js_1.readTrustState)(deps.path);
|
|
196
|
+
if ('broken' in loaded)
|
|
197
|
+
return unusable(loaded.broken, deps);
|
|
198
|
+
return args.deviceId ? trustDevice(args.deviceId, loaded.state, args, deps) : showState(loaded.state, deps);
|
|
199
|
+
};
|
|
200
|
+
exports.runTrust = runTrust;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lock every writer of `~/.hwip/trusted-devices.json` holds while it re-reads and rewrites the file.
|
|
3
|
+
*
|
|
4
|
+
* The listener, `hwip trust`, `hwip notify`, `hwip ask`, `hwip agent send` and the MCP server all pin
|
|
5
|
+
* devices now, and the listener keeps the sealed-command numbers (`commands`, `commandKeys`) in the
|
|
6
|
+
* same file: a writer that read the file before another one saved would put the old numbers back and
|
|
7
|
+
* let a command be typed again. The lock is `trusted-devices.lock` beside the file, created with
|
|
8
|
+
* `wx`; the MCP server follows the same protocol (cli and mcp-server agree on it).
|
|
9
|
+
*/
|
|
10
|
+
export declare const trustLockPath: (trustPath: string) => string;
|
|
11
|
+
export declare const sleepSync: (ms: number) => void;
|
|
12
|
+
export type Locked<T> = {
|
|
13
|
+
locked: true;
|
|
14
|
+
value: T;
|
|
15
|
+
} | {
|
|
16
|
+
locked: false;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Run `fn` with the lock held. `locked: false` means the lock could not be had — held by someone for
|
|
20
|
+
* longer than `waitMs`, or the folder cannot hold one — and `fn` did not run. The caller decides what
|
|
21
|
+
* that costs: a pin is skipped and the send goes on, a command number is not taken.
|
|
22
|
+
*/
|
|
23
|
+
export declare const withTrustLock: <T>(trustPath: string, fn: () => T, opts?: {
|
|
24
|
+
waitMs?: number;
|
|
25
|
+
}) => Locked<T>;
|
|
26
|
+
//# sourceMappingURL=trust-lock.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The lock every writer of `~/.hwip/trusted-devices.json` holds while it re-reads and rewrites the file.
|
|
4
|
+
*
|
|
5
|
+
* The listener, `hwip trust`, `hwip notify`, `hwip ask`, `hwip agent send` and the MCP server all pin
|
|
6
|
+
* devices now, and the listener keeps the sealed-command numbers (`commands`, `commandKeys`) in the
|
|
7
|
+
* same file: a writer that read the file before another one saved would put the old numbers back and
|
|
8
|
+
* let a command be typed again. The lock is `trusted-devices.lock` beside the file, created with
|
|
9
|
+
* `wx`; the MCP server follows the same protocol (cli and mcp-server agree on it).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.withTrustLock = exports.sleepSync = exports.trustLockPath = void 0;
|
|
13
|
+
const node_fs_1 = require("node:fs");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
const private_files_js_1 = require("./private-files.js");
|
|
16
|
+
/** A lock older than this was left by a process that died. */
|
|
17
|
+
const STALE_MS = 10_000;
|
|
18
|
+
/** How long a writer waits for a lock that is held. */
|
|
19
|
+
const DEFAULT_WAIT_MS = 1_000;
|
|
20
|
+
const POLL_MS = 20;
|
|
21
|
+
const trustLockPath = (trustPath) => `${trustPath.replace(/\.json$/, '')}.lock`;
|
|
22
|
+
exports.trustLockPath = trustLockPath;
|
|
23
|
+
const sleepSync = (ms) => {
|
|
24
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
25
|
+
};
|
|
26
|
+
exports.sleepSync = sleepSync;
|
|
27
|
+
const ageOf = (lock) => {
|
|
28
|
+
try {
|
|
29
|
+
return Date.now() - (0, node_fs_1.statSync)(lock).mtimeMs;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Run `fn` with the lock held. `locked: false` means the lock could not be had — held by someone for
|
|
37
|
+
* longer than `waitMs`, or the folder cannot hold one — and `fn` did not run. The caller decides what
|
|
38
|
+
* that costs: a pin is skipped and the send goes on, a command number is not taken.
|
|
39
|
+
*/
|
|
40
|
+
const withTrustLock = (trustPath, fn, opts = {}) => {
|
|
41
|
+
const lock = (0, exports.trustLockPath)(trustPath);
|
|
42
|
+
const deadline = Date.now() + (opts.waitMs ?? DEFAULT_WAIT_MS);
|
|
43
|
+
let brokeStale = false;
|
|
44
|
+
try {
|
|
45
|
+
(0, private_files_js_1.mkdirHandedBack)((0, node_path_1.dirname)(lock), 0o700);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { locked: false };
|
|
49
|
+
}
|
|
50
|
+
for (;;) {
|
|
51
|
+
try {
|
|
52
|
+
const fd = (0, node_fs_1.openSync)(lock, 'wx', 0o600);
|
|
53
|
+
try {
|
|
54
|
+
(0, node_fs_1.writeSync)(fd, `${process.pid} ${Date.now()}\n`);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// An empty lock nobody owns would hold every writer off until it goes stale.
|
|
58
|
+
(0, node_fs_1.rmSync)(lock, { force: true });
|
|
59
|
+
return { locked: false };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
(0, node_fs_1.closeSync)(fd);
|
|
63
|
+
}
|
|
64
|
+
(0, private_files_js_1.handBackToSudoUser)(lock);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// `err.code` is how node names a lock that is already there: the cast reads that and nothing else
|
|
69
|
+
if (err.code !== 'EEXIST')
|
|
70
|
+
return { locked: false };
|
|
71
|
+
const age = ageOf(lock);
|
|
72
|
+
// Looked at again right before the unlink: narrows, does not close, the window in which another waiter broke this
|
|
73
|
+
// one and took a fresh lock (the critical sections are short sync file I/O, so it is rare).
|
|
74
|
+
if (!brokeStale && age !== null && age > STALE_MS && (ageOf(lock) ?? 0) > STALE_MS) {
|
|
75
|
+
brokeStale = true;
|
|
76
|
+
try {
|
|
77
|
+
(0, node_fs_1.unlinkSync)(lock);
|
|
78
|
+
}
|
|
79
|
+
catch { /* someone else broke it first */ }
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() >= deadline)
|
|
83
|
+
return { locked: false };
|
|
84
|
+
(0, exports.sleepSync)(POLL_MS);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return { locked: true, value: fn() };
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
(0, node_fs_1.rmSync)(lock, { force: true });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
exports.withTrustLock = withTrustLock;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinning the account's devices as a sender sees them.
|
|
3
|
+
*
|
|
4
|
+
* `hwip notify`, `hwip ask`, `hwip agent send`, the listener's recorders and the MCP server all read `GET /devices`
|
|
5
|
+
* before they seal. Every device that lists a key is pinned the first time it is seen, and a pin whose device now
|
|
6
|
+
* lists another key is replaced — with one warning, because a key that changed is also what a relay swapping the
|
|
7
|
+
* list looks like, and a record (`key-changes.ts`) a person is shown later, since the warning often reaches nobody. Nothing here decides who is sealed for: the list does, the pins are what the listener holds a
|
|
8
|
+
* request against (`subscriber-trust.ts`) and what a person compares with `hwip trust`.
|
|
9
|
+
*/
|
|
10
|
+
export interface ObservedDevice {
|
|
11
|
+
deviceId: string;
|
|
12
|
+
publicKey?: string;
|
|
13
|
+
/** The name the account lists it under, for the key-change record. */
|
|
14
|
+
nickname?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ObserveOptions {
|
|
17
|
+
/** This computer's own device id: it never pins itself. */
|
|
18
|
+
selfId?: string;
|
|
19
|
+
now?: () => Date;
|
|
20
|
+
/** One line for a person; the caller adds its own prefix. */
|
|
21
|
+
warn: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export interface Observed {
|
|
24
|
+
/** Devices pinned for the first time. */
|
|
25
|
+
pinned: string[];
|
|
26
|
+
/** Devices whose pin was replaced by the key they list now — or would have been, when the status is not `ok`. */
|
|
27
|
+
replaced: string[];
|
|
28
|
+
/** Devices not pinned: their key is pinned for another id, or the cap is reached. */
|
|
29
|
+
skipped: string[];
|
|
30
|
+
/**
|
|
31
|
+
* `unreadable`: the trust file cannot be read, nothing was checked or written. `unlocked`: another process held
|
|
32
|
+
* the file, or it could not be written; nothing was written. A send goes on in both cases.
|
|
33
|
+
*/
|
|
34
|
+
status: 'ok' | 'unreadable' | 'unlocked';
|
|
35
|
+
}
|
|
36
|
+
export declare const observeDevices: (path: string, devices: ObservedDevice[], opts: ObserveOptions) => Observed;
|
|
37
|
+
//# sourceMappingURL=trust-pins.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pinning the account's devices as a sender sees them.
|
|
4
|
+
*
|
|
5
|
+
* `hwip notify`, `hwip ask`, `hwip agent send`, the listener's recorders and the MCP server all read `GET /devices`
|
|
6
|
+
* before they seal. Every device that lists a key is pinned the first time it is seen, and a pin whose device now
|
|
7
|
+
* lists another key is replaced — with one warning, because a key that changed is also what a relay swapping the
|
|
8
|
+
* list looks like, and a record (`key-changes.ts`) a person is shown later, since the warning often reaches nobody. Nothing here decides who is sealed for: the list does, the pins are what the listener holds a
|
|
9
|
+
* request against (`subscriber-trust.ts`) and what a person compares with `hwip trust`.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.observeDevices = void 0;
|
|
13
|
+
const subscriber_trust_js_1 = require("./subscriber-trust.js");
|
|
14
|
+
const key_changes_js_1 = require("./key-changes.js");
|
|
15
|
+
const trust_lock_js_1 = require("./trust-lock.js");
|
|
16
|
+
const apply = (from, devices, selfId, at) => {
|
|
17
|
+
const changes = { state: from, pinned: [], replaced: [], skipped: [] };
|
|
18
|
+
for (const { deviceId, publicKey } of devices) {
|
|
19
|
+
if (!publicKey || deviceId === selfId || !subscriber_trust_js_1.DEVICE_ID.test(deviceId))
|
|
20
|
+
continue;
|
|
21
|
+
const plan = (0, subscriber_trust_js_1.planPin)(changes.state, deviceId, publicKey);
|
|
22
|
+
if (plan === 'same')
|
|
23
|
+
continue;
|
|
24
|
+
if (plan === 'in-use' || plan === 'cap') {
|
|
25
|
+
changes.skipped.push(deviceId);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
changes.state = (0, subscriber_trust_js_1.withPin)(changes.state, deviceId, publicKey, at);
|
|
29
|
+
(plan === 'new' ? changes.pinned : changes.replaced).push(deviceId);
|
|
30
|
+
}
|
|
31
|
+
return changes;
|
|
32
|
+
};
|
|
33
|
+
const unchanged = (c) => c.pinned.length === 0 && c.replaced.length === 0;
|
|
34
|
+
const observeDevices = (path, devices, opts) => {
|
|
35
|
+
const now = opts.now ?? (() => new Date());
|
|
36
|
+
const quick = (0, subscriber_trust_js_1.readTrustState)(path);
|
|
37
|
+
if ('broken' in quick) {
|
|
38
|
+
opts.warn('trust: the trust file is unreadable — pins not checked');
|
|
39
|
+
return { pinned: [], replaced: [], skipped: [], status: 'unreadable' };
|
|
40
|
+
}
|
|
41
|
+
const first = apply(quick.state, devices, opts.selfId, now());
|
|
42
|
+
// The usual send changes nothing: no lock, no write.
|
|
43
|
+
if (unchanged(first))
|
|
44
|
+
return { pinned: [], replaced: [], skipped: first.skipped, status: 'ok' };
|
|
45
|
+
// Not written: the send goes on with the key the list holds, and a changed key is still said here, since no record will.
|
|
46
|
+
const notWritten = (status) => {
|
|
47
|
+
for (const deviceId of first.replaced)
|
|
48
|
+
opts.warn(`trust: ${deviceId} changed its key — could not record the new pin`);
|
|
49
|
+
return { pinned: [], replaced: first.replaced, skipped: [], status };
|
|
50
|
+
};
|
|
51
|
+
let recorded = true;
|
|
52
|
+
try {
|
|
53
|
+
// Read again under the lock: the listener or another send may have pinned meanwhile.
|
|
54
|
+
const done = (0, trust_lock_js_1.withTrustLock)(path, () => {
|
|
55
|
+
const latest = (0, subscriber_trust_js_1.readTrustState)(path);
|
|
56
|
+
if ('broken' in latest)
|
|
57
|
+
return null;
|
|
58
|
+
const at = now();
|
|
59
|
+
const changes = apply(latest.state, devices, opts.selfId, at);
|
|
60
|
+
if (unchanged(changes))
|
|
61
|
+
return changes;
|
|
62
|
+
(0, subscriber_trust_js_1.writeTrustState)(path, changes.state);
|
|
63
|
+
// In the same section as the pin: a process that sees the new key as `same` next has nothing left to say it.
|
|
64
|
+
const names = new Map(devices.map((d) => [d.deviceId, d.nickname]));
|
|
65
|
+
recorded = (0, key_changes_js_1.recordKeyChanges)(path, changes.replaced.map((deviceId) => ({ deviceId, at: at.toISOString(), name: names.get(deviceId) })));
|
|
66
|
+
return changes;
|
|
67
|
+
});
|
|
68
|
+
if (!done.locked || done.value === null)
|
|
69
|
+
return notWritten(done.locked ? 'unreadable' : 'unlocked');
|
|
70
|
+
for (const deviceId of done.value.replaced)
|
|
71
|
+
opts.warn(`trust: ${deviceId} changed its key — now pinned to the new one`);
|
|
72
|
+
if (!recorded)
|
|
73
|
+
opts.warn('trust: could not save that key change for later (key-changes.json) — this line is the only notice');
|
|
74
|
+
const { pinned, replaced, skipped } = done.value;
|
|
75
|
+
return { pinned, replaced, skipped, status: 'ok' };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A full disk, a folder that is not ours: the send does not depend on the pin.
|
|
79
|
+
return notWritten('unlocked');
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
exports.observeDevices = observeDevices;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The world the recorder (`turn-recorder.ts`) talks to: the account API that
|
|
3
|
+
* stores chunks, the push-v1 envelope that seals them, and the list of devices
|
|
4
|
+
* the account has signed in. Kept apart so the recorder's tests need none of it.
|
|
5
|
+
*/
|
|
6
|
+
import { type OutboxChunk, type RecorderRecipient, type SealedChunk, type UploadOutcome } from './turn-recorder.js';
|
|
7
|
+
/** The turns route has its own per-minute allowance on the server (240); chunks keep to 30, well inside it. */
|
|
8
|
+
export declare const RECORDER_UPLOADS_PER_MINUTE = 30;
|
|
9
|
+
/** Seal one plaintext body (`{sessionKey, seq, events}`) as a push-v1 envelope for the recipients. */
|
|
10
|
+
export declare const sealChunk: (plaintext: string, recipients: RecorderRecipient[]) => Promise<SealedChunk>;
|
|
11
|
+
export interface AccountRecipientsDeps {
|
|
12
|
+
/** The listener's state file (`~/.hwip/trusted-devices.json`). */
|
|
13
|
+
trustFile: string;
|
|
14
|
+
deviceId: string;
|
|
15
|
+
/** The account's device list; rejects when it cannot be had. */
|
|
16
|
+
devices: () => Promise<{
|
|
17
|
+
deviceId: string;
|
|
18
|
+
publicKey?: string;
|
|
19
|
+
}[]>;
|
|
20
|
+
log: (message: string) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* This machine plus the devices the account lists with a key, the newest pins first and
|
|
24
|
+
* within the server's slot limit. The list decides who a chunk is sealed for: a device
|
|
25
|
+
* that signed in reads its history once the directory refreshes its list (a minute at most). Each listed device is pinned on
|
|
26
|
+
* the way (`observeDevices`) — the pin is what `hwip trust` shows and what the listener
|
|
27
|
+
* holds a request against, and its date orders who is cut when the list is longer than
|
|
28
|
+
* the slots. The directory keeps the last list it fetched, so a blip does not stop the
|
|
29
|
+
* history; a list never had means no
|
|
30
|
+
* recipients, and pins from an earlier run do not stand in for it. A machine with no other
|
|
31
|
+
* device has no one who could read a chunk.
|
|
32
|
+
*/
|
|
33
|
+
export declare const accountRecipients: (deps: AccountRecipientsDeps) => (() => Promise<RecorderRecipient[]>);
|
|
34
|
+
/**
|
|
35
|
+
* `POST /sessions/{key}/turns` for one stored chunk. The same bytes go out on
|
|
36
|
+
* every try, so a chunk the server already holds comes back 200 (stored) rather
|
|
37
|
+
* than 409 (a different body under the same `seq`).
|
|
38
|
+
*
|
|
39
|
+
* 201 / 200 stored.
|
|
40
|
+
* 429 RATE_LIMIT a minute-long limit: try again after what the server says.
|
|
41
|
+
* 429 TURN_DAILY the day's cap: leave the session alone until its reset.
|
|
42
|
+
* 408, 5xx, network try again with the recorder's backoff.
|
|
43
|
+
* redirect refused the base URL sends the request elsewhere on every try (a permanent
|
|
44
|
+
* answer, like a wrong key): pause the session for an hour and say why.
|
|
45
|
+
* The chunk is kept, so a corrected base URL (a restart) sends it.
|
|
46
|
+
* 400, 409 the server will never take these bytes (a bad body, a `seq`
|
|
47
|
+
* that holds another): drop the chunk, or it blocks the session;
|
|
48
|
+
* after a 409 the next `seq` moves past what the server holds.
|
|
49
|
+
* 403 the key may only just have been registered: pause the session 5 min;
|
|
50
|
+
* the fifth in a row on one chunk drops it.
|
|
51
|
+
* any other 4xx wrong key, device or session (401, 404): pause the
|
|
52
|
+
* session for an hour and say why in the log.
|
|
53
|
+
*/
|
|
54
|
+
export declare const createChunkUploader: (opts: {
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
apiKey: string;
|
|
57
|
+
fetchFn?: typeof fetch;
|
|
58
|
+
now?: () => number;
|
|
59
|
+
perMinute?: number;
|
|
60
|
+
}) => ((sessionKey: string, chunk: OutboxChunk) => Promise<UploadOutcome>);
|
|
61
|
+
/**
|
|
62
|
+
* The newest `seq` the server holds for a session (`GET .../turns?limit=1`, newest
|
|
63
|
+
* first), or null when it cannot say. Used only after a 409, to number past it.
|
|
64
|
+
*/
|
|
65
|
+
export declare const createLatestSeqReader: (opts: {
|
|
66
|
+
baseUrl: string;
|
|
67
|
+
apiKey: string;
|
|
68
|
+
fetchFn?: typeof fetch;
|
|
69
|
+
}) => ((sessionKey: string) => Promise<number | null>);
|
|
70
|
+
//# sourceMappingURL=turn-recorder-wire.d.ts.map
|