@tinycloud/courier 0.2.0-beta.41 → 0.2.0-beta.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/dist/intake/flags.d.ts +48 -0
- package/dist/intake/flags.js +25 -0
- package/dist/intake/flags.js.map +1 -0
- package/dist/intake/intake.d.ts +71 -0
- package/dist/intake/intake.js +640 -0
- package/dist/intake/intake.js.map +1 -0
- package/dist/intake/peers.d.ts +23 -0
- package/dist/intake/peers.js +87 -0
- package/dist/intake/peers.js.map +1 -0
- package/dist/intake/queue.d.ts +48 -0
- package/dist/intake/queue.js +267 -0
- package/dist/intake/queue.js.map +1 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -1
- package/dist/text.d.ts +4 -2
- package/dist/text.js +5 -3
- package/dist/text.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -676,6 +676,71 @@ The service runs the same command line you gave (install flags removed, `claude`
|
|
|
676
676
|
profile on the same machine. systemd needs `loginctl enable-linger $USER` to
|
|
677
677
|
keep the watcher running while logged out.
|
|
678
678
|
|
|
679
|
+
## Intake reader
|
|
680
|
+
|
|
681
|
+
`courier intake` reads the mirror of paired peers' messages from the self-space KV
|
|
682
|
+
and writes one JSON file per message into a queue directory for the intake reader.
|
|
683
|
+
It does not send anything, and it does not change the watcher.
|
|
684
|
+
|
|
685
|
+
```bash
|
|
686
|
+
courier intake --once --queue-dir <dir> --state-dir <dir> --peers-file <file>
|
|
687
|
+
courier intake --interval 60 --queue-dir <dir> --state-dir <dir> --peers-file <file>
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Defaults for `--queue-dir`, `--state-dir` and `--peers-file` are under
|
|
691
|
+
`<COURIER_HOME>/intake/`. Only peers with status `paired` are read. `intake.lock` in the state directory allows one
|
|
692
|
+
intake process per state directory: a live holder always refuses a second start, a lock whose pid is gone is broken,
|
|
693
|
+
and an unreadable lock refuses.
|
|
694
|
+
|
|
695
|
+
Queue record (`<queue-dir>/<peerId>/<msgId>.json`): peer queue directories are created
|
|
696
|
+
with mode 2750 (setgid) and only directories created by that call are chmodded. Records
|
|
697
|
+
are mode 0640, written atomically without overwriting.
|
|
698
|
+
|
|
699
|
+
`{v:1, msgId, peerId, threadId, sender:{id, displayName}, receivedAt, text, cheapChecks:{textVerdict, tier, peerRatePerHourRemaining, flags}}`
|
|
700
|
+
|
|
701
|
+
`peerId` is the peer handle and equals the queue directory name.
|
|
702
|
+
|
|
703
|
+
`receivedAt` is the local time the record was written. `text` is the message text,
|
|
704
|
+
cut to 16 KiB UTF-8 at a code-point boundary when longer; the `size` flag marks a cut.
|
|
705
|
+
|
|
706
|
+
Flags, in this order: `too-long`, `link`, `instruction`, `request`, `secret-request`,
|
|
707
|
+
`encoded`, `unicode`, `size`, `rate`. They are cheap heuristics for the reader, not
|
|
708
|
+
a verdict on content.
|
|
709
|
+
|
|
710
|
+
Ordering: each cycle reads at most 200 mirror messages per peer. A cyclic cursor in `intake-state.json` marks the last
|
|
711
|
+
id picked; the next cycle takes the next 200 ids after it in ascending order and wraps to the oldest id after the
|
|
712
|
+
newest. Ids more than 7 days old by message time are dropped before selection: they are not fetched and get no marker,
|
|
713
|
+
and a queue record that old moves to `done/`. A message is skipped when a queue, done or overflow file for its id
|
|
714
|
+
already exists. A message id more than 10 minutes ahead of local time is not recorded and does not block the others:
|
|
715
|
+
the cursor moves past it, so it is retried only when the cursor next wraps around the mirror. A mirror key whose
|
|
716
|
+
message or thread id is not a ULID is logged as `intake-skip` (`bad-id`) and gets no marker. A message id whose
|
|
717
|
+
time cannot be decoded gets a `bad-id` done marker. A fetched row that is malformed, or whose `messageId` or
|
|
718
|
+
`contextId` does not match its key, gets a `mismatch` done marker. A message whose processing throws is logged
|
|
719
|
+
as `intake-error`, and the cursor still moves past it. Existing history is not queued if you run
|
|
720
|
+
`courier intake --init-from-now` first. It marks the current mirror messages of each paired peer as done (outcome
|
|
721
|
+
`init`), and it refuses to run again while a paired peer has done entries unless `--force` is given. Without it, the
|
|
722
|
+
first cycle works through the backlog from the oldest id inside the 7-day window, 200 rows per cycle.
|
|
723
|
+
|
|
724
|
+
Rate: each peer is limited to 100 inbound messages per hour by default, including
|
|
725
|
+
peers absent from `peers.json`. Over the cap, the message is not queued. It is written
|
|
726
|
+
to the overflow directory (`<COURIER_HOME>/intake/overflow/<peerId>/`, directories 0700,
|
|
727
|
+
files 0600), where it is kept until its message is 7 days old by message time. Each peer's overflow is capped at 200
|
|
728
|
+
files and 1 MiB; the oldest messages are dropped first. Each cycle retires at most 500 expired files per directory.
|
|
729
|
+
Over-cap retirement runs once per peer per pass: it removes at most 500 files by count, then, once at most 200 files
|
|
730
|
+
remain, removes the oldest of them until the 1 MiB cap holds, so one pass can retire up to 700 files. A cycle runs the
|
|
731
|
+
pass from the janitor and again after it writes new overflow for that peer, so one cycle can retire more than 500
|
|
732
|
+
over-cap files for one peer. Each retirement writes an `overflow` done marker before the unlink, so the id is not admitted
|
|
733
|
+
again when the cursor laps. The first overflow in each hour also writes one `rate-<unixHour>` record to the queue with
|
|
734
|
+
`peerRatePerHourRemaining: 0` and the `rate` flag. After a start, the hour's count is rebuilt from `receivedAt` in
|
|
735
|
+
queue and done records whose files were modified within the last hour.
|
|
736
|
+
|
|
737
|
+
`peers.json` must not be group- or world-writable and maps a peer id to
|
|
738
|
+
`{"tier": "trusted"|"strict", "ratePerHour": n, "outPerHour": n}`. `tier` defaults to
|
|
739
|
+
`strict` for absent peers. `ratePerHour` and `outPerHour` are integers from 1 up to the
|
|
740
|
+
default of 100. A value outside that range, or any other invalid entry, makes the whole
|
|
741
|
+
file invalid: startup refuses to run, and a running cycle keeps its last good map and
|
|
742
|
+
logs `intake-error`.
|
|
743
|
+
|
|
679
744
|
## Authority
|
|
680
745
|
|
|
681
746
|
Courier accepts only an OpenKey grant whose signed authority exactly equals
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Tier } from './peers.js';
|
|
2
|
+
/**
|
|
3
|
+
* The complete set of flags the reader may attach to a queue record. Nothing else is emitted; the `Flag` type is derived
|
|
4
|
+
* from these keys, so an unlisted flag does not compile. `severity` is `hard`, `soft`, or `tiered`. A `tiered` flag is soft
|
|
5
|
+
* for the trusted tier and hard for every other tier; `severityFor` resolves it. The reader records the flags; the gate
|
|
6
|
+
* decides what each severity means.
|
|
7
|
+
*/
|
|
8
|
+
export declare const FLAG_TABLE: {
|
|
9
|
+
readonly 'too-long': {
|
|
10
|
+
readonly severity: "tiered";
|
|
11
|
+
readonly description: "text is over 8000 UTF-16 units; the text is still queued";
|
|
12
|
+
};
|
|
13
|
+
readonly link: {
|
|
14
|
+
readonly severity: "soft";
|
|
15
|
+
readonly description: "a URL-like string is present";
|
|
16
|
+
};
|
|
17
|
+
readonly instruction: {
|
|
18
|
+
readonly severity: "soft";
|
|
19
|
+
readonly description: "matches the fixed instruction-phrase table";
|
|
20
|
+
};
|
|
21
|
+
readonly request: {
|
|
22
|
+
readonly severity: "soft";
|
|
23
|
+
readonly description: "asks the reader or its principal to take an action (fixed verb-phrase table)";
|
|
24
|
+
};
|
|
25
|
+
readonly 'secret-request': {
|
|
26
|
+
readonly severity: "hard";
|
|
27
|
+
readonly description: "asks for keys, tokens, passwords or credentials";
|
|
28
|
+
};
|
|
29
|
+
readonly encoded: {
|
|
30
|
+
readonly severity: "hard";
|
|
31
|
+
readonly description: "a long base64- or hex-looking run is present";
|
|
32
|
+
};
|
|
33
|
+
readonly unicode: {
|
|
34
|
+
readonly severity: "hard";
|
|
35
|
+
readonly description: "shared text verdict: not-nfc, disallowed-code-point, edge-whitespace, not-string or empty";
|
|
36
|
+
};
|
|
37
|
+
readonly size: {
|
|
38
|
+
readonly severity: "hard";
|
|
39
|
+
readonly description: "original text exceeded the 16 KiB queue cap and was cut at a code-point boundary";
|
|
40
|
+
};
|
|
41
|
+
readonly rate: {
|
|
42
|
+
readonly severity: "hard";
|
|
43
|
+
readonly description: "peer is over its inbound per-hour cap";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export type Flag = keyof typeof FLAG_TABLE;
|
|
47
|
+
/** The severity a flag has for a peer's tier. `tiered` flags are soft for `trusted` and hard otherwise. */
|
|
48
|
+
export declare function severityFor(flag: Flag, tier: Tier): 'hard' | 'soft';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The complete set of flags the reader may attach to a queue record. Nothing else is emitted; the `Flag` type is derived
|
|
3
|
+
* from these keys, so an unlisted flag does not compile. `severity` is `hard`, `soft`, or `tiered`. A `tiered` flag is soft
|
|
4
|
+
* for the trusted tier and hard for every other tier; `severityFor` resolves it. The reader records the flags; the gate
|
|
5
|
+
* decides what each severity means.
|
|
6
|
+
*/
|
|
7
|
+
export const FLAG_TABLE = {
|
|
8
|
+
'too-long': { severity: 'tiered', description: 'text is over 8000 UTF-16 units; the text is still queued' },
|
|
9
|
+
link: { severity: 'soft', description: 'a URL-like string is present' },
|
|
10
|
+
instruction: { severity: 'soft', description: 'matches the fixed instruction-phrase table' },
|
|
11
|
+
request: { severity: 'soft', description: 'asks the reader or its principal to take an action (fixed verb-phrase table)' },
|
|
12
|
+
'secret-request': { severity: 'hard', description: 'asks for keys, tokens, passwords or credentials' },
|
|
13
|
+
encoded: { severity: 'hard', description: 'a long base64- or hex-looking run is present' },
|
|
14
|
+
unicode: { severity: 'hard', description: 'shared text verdict: not-nfc, disallowed-code-point, edge-whitespace, not-string or empty' },
|
|
15
|
+
size: { severity: 'hard', description: 'original text exceeded the 16 KiB queue cap and was cut at a code-point boundary' },
|
|
16
|
+
rate: { severity: 'hard', description: 'peer is over its inbound per-hour cap' },
|
|
17
|
+
};
|
|
18
|
+
/** The severity a flag has for a peer's tier. `tiered` flags are soft for `trusted` and hard otherwise. */
|
|
19
|
+
export function severityFor(flag, tier) {
|
|
20
|
+
const severity = FLAG_TABLE[flag].severity;
|
|
21
|
+
if (severity !== 'tiered')
|
|
22
|
+
return severity;
|
|
23
|
+
return tier === 'trusted' ? 'soft' : 'hard';
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=flags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flags.js","sourceRoot":"","sources":["../../src/intake/flags.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,UAAU,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,0DAA0D,EAAE;IAC3G,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,8BAA8B,EAAE;IACvE,WAAW,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,4CAA4C,EAAE;IAC5F,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,8EAA8E,EAAE;IAC1H,gBAAgB,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,iDAAiD,EAAE;IACtG,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,8CAA8C,EAAE;IAC1F,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,2FAA2F,EAAE;IACvI,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,kFAAkF,EAAE;IAC3H,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,uCAAuC,EAAE;CACgB,CAAC;AAInG,2GAA2G;AAC3G,MAAM,UAAU,WAAW,CAAC,IAAU,EAAE,IAAU;IAChD,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;IAC3C,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC3C,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9C,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ParsedArgs } from '../args.js';
|
|
2
|
+
import type { KV } from '../inbox.js';
|
|
3
|
+
import { type Profile } from '../profile.js';
|
|
4
|
+
import { type TextVerdict } from '../text.js';
|
|
5
|
+
import { type WatchLog } from '../wake.js';
|
|
6
|
+
import { type Flag } from './flags.js';
|
|
7
|
+
import { type Peers } from './peers.js';
|
|
8
|
+
import { type IntakeState } from './queue.js';
|
|
9
|
+
/** Queued text is capped at this many UTF-16 units; longer text is flagged `too-long`. */
|
|
10
|
+
export declare const MAX_QUEUE_CHARS = 8000;
|
|
11
|
+
export interface IntakeRecord {
|
|
12
|
+
v: 1;
|
|
13
|
+
msgId: string;
|
|
14
|
+
peerId: string;
|
|
15
|
+
threadId: string;
|
|
16
|
+
sender: {
|
|
17
|
+
id: string;
|
|
18
|
+
displayName: string;
|
|
19
|
+
};
|
|
20
|
+
receivedAt: string;
|
|
21
|
+
text: string;
|
|
22
|
+
cheapChecks: {
|
|
23
|
+
textVerdict: TextVerdict;
|
|
24
|
+
tier: string;
|
|
25
|
+
peerRatePerHourRemaining: number;
|
|
26
|
+
flags: Flag[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface CycleInput {
|
|
30
|
+
kv: KV;
|
|
31
|
+
profile: Profile;
|
|
32
|
+
peers: Peers;
|
|
33
|
+
queueRoot: string;
|
|
34
|
+
doneRoot: string;
|
|
35
|
+
overflowRoot: string;
|
|
36
|
+
stateDir: string;
|
|
37
|
+
state: IntakeState;
|
|
38
|
+
now: () => number;
|
|
39
|
+
log: WatchLog;
|
|
40
|
+
/** Mirror ids taken per peer per cycle. Defaults to MIRROR_BATCH. */
|
|
41
|
+
batch?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Cuts `text` to at most `maxBytes` of UTF-8 at a code-point boundary. */
|
|
44
|
+
export declare function capUtf8(text: string, maxBytes: number): {
|
|
45
|
+
text: string;
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
};
|
|
48
|
+
/** Maps the verdict and the cheap text detectors to flag codes from FLAG_TABLE, in table order. */
|
|
49
|
+
export declare function flagsFor(input: {
|
|
50
|
+
text: string;
|
|
51
|
+
verdict: TextVerdict;
|
|
52
|
+
overRate: boolean;
|
|
53
|
+
truncated: boolean;
|
|
54
|
+
}): Flag[];
|
|
55
|
+
/** One pass: janitor, then every paired peer's mirror. Returns how many new queue files were written (rate records included). */
|
|
56
|
+
export declare function intakeCycle(input: CycleInput): Promise<number>;
|
|
57
|
+
/** Input for `--init-from-now`: writes done markers only, never queue records. */
|
|
58
|
+
export interface InitInput {
|
|
59
|
+
kv: KV;
|
|
60
|
+
profile: Profile;
|
|
61
|
+
doneRoot: string;
|
|
62
|
+
now: () => number;
|
|
63
|
+
log: WatchLog;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Marks every mirrored message of each paired peer as done (outcome `init`), so the first real intake skips existing
|
|
67
|
+
* history. Refuses before writing anything when a paired peer already has a done/ entry, unless `force`. Existing markers
|
|
68
|
+
* are not rewritten. Returns the number of markers written per peer.
|
|
69
|
+
*/
|
|
70
|
+
export declare function initFromNow(input: InitInput, force: boolean): Promise<Map<string, number>>;
|
|
71
|
+
export declare function cmdIntake(args: ParsedArgs): Promise<void>;
|