@nicknisi/pi-relay 0.2.0 → 0.3.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/README.md CHANGED
@@ -9,6 +9,20 @@ Architecture follows [shift-labs/pi-peer](https://github.com/shift-labs-ai/pi-pe
9
9
  - **`relay` tool** — actions: `list`, `list-cwd`, `send`, `ask`, `reply`, `pending`, `cancel`, `status`, `claim`, `watch`
10
10
  - **`/relay` command** — prints the session listing; `/relay log [N]` prints the last N audit entries (default 50)
11
11
 
12
+ ## Pi-free core API
13
+
14
+ Node applications can use the registry, mailbox, and transport-policy primitives without loading the pi extension or its TUI dependencies:
15
+
16
+ ```ts
17
+ import { OutboundPolicy, claimInbox, deposit, deriveAddr, type Letter } from '@nicknisi/pi-relay/core';
18
+ ```
19
+
20
+ The `@nicknisi/pi-relay/core` subpath exports a narrow record and alias registry, mailbox and ask/audit operations, presence and sweep utilities, policy guards and constants, and their public types. Filesystem-facing operations validate canonical relay addresses, aliases, claim tokens, and ask/message IDs before accessing the relay root; path constructors and raw persistence helpers remain internal. Core uses Node built-ins plus Koffi's prebuilt native bridge for descriptor-relative Unix syscalls; Pi and TUI remain optional peers, so core-only installations do not fetch them. The package root remains the pi extension entry point; import `/core` from plain Node services and CLIs.
21
+
22
+ ### Durable core inbox claims
23
+
24
+ A non-Pi consumer can atomically detach the current inbox with `claimInbox(root, addr)`. It receives only a stable `claimToken` and stable `fileTokens`, never filesystem paths. New deposits immediately land in a fresh inbox. `readClaimedLetter` reopens and validates a claimed letter without following links; after the consumer durably writes and `fsync`s its own journal, `ackClaimedLetter` deletes that exact file or `requeueClaimedLetter` atomically returns it to the current inbox. `recoverInboxClaims` enumerates the same tokens after a process crash. All claim, read, ack, and requeue work is relative to pinned root/inbox/claim descriptors, and empty completed claims are removed automatically.
25
+
12
26
  ## Audit log
13
27
 
14
28
  Every deposit and every delivery appends one line-delimited JSON record to `<PI_RELAY_DIR>/audit.log` — written from the transport choke points so draining a letter as a receipt no longer destroys the evidence that it existed. Each line records the timestamp, the event (`deposit`/`deliver`), the letter kind, the from/to addresses, and the message id. **The full body is never logged** — only a short (≤80 char) whitespace-collapsed preview. The file is `0600`, append-only, and survives corrupt lines (skipped on parse). Read it with `/relay log [N]`.
@@ -37,7 +51,7 @@ main moved; rebase before you push.
37
51
 
38
52
  - **A mailbox outlives the process.** The address is a hash of the working directory and pi's session id, so a session resumed with `pi -c` answers to the same address. Mail sent to a closed session waits on disk and is read when it resumes — the common case when you're opening and closing terminals all day.
39
53
  - **The queue is inspectable.** Diagnosing delivery is `ls`, not instrumenting a transport.
40
- - **Consumption is the receipt.** The receiver deletes the letter as it reads it, so the sender learns _delivered_ vs _queued_ — "the letter vanished" means the agent has it, which no socket ack can tell you.
54
+ - **Delivery is the receipt.** The receiver detaches its inbox into a durable claim and deletes each exact full-message-id letter only after the session has accepted it — a failed delivery is requeued for retry, and a crash mid-delivery leaves the letter recoverable on the next start (redeliveries are deduped by message id, seeded from the transcript). The sender therefore learns _delivered_ vs _queued_ honestly: a claimed-but-unacknowledged letter still reads _queued_, never a false receipt.
41
55
 
42
56
  ## Semantics
43
57
 
@@ -45,7 +59,7 @@ main moved; rebase before you push.
45
59
  - **Authority boundary on every delivery.** Each message arrives with a repeated statement that it came from a peer and carries no authority — it cannot approve anything, cannot change configuration, slash commands in it are inert text. The sending side's tool guidelines carry the reciprocal rule: never ask a peer to do something your own permissions would refuse.
46
60
  - **Loops break structurally**, independent of what either model decides: identical text from one sender inside 10s is dropped; >8 messages per 30s per sender is refused; an unread backlog of 50 refuses new mail until the peer drains.
47
61
  - **Plain text only, ≤32KB.** Send a summary and a path, not a payload.
48
- - **Sweeping is narrow:** a running session is never touched; a mailbox holding undelivered mail is kept 30 days; an offline-but-resumable session keeps its record (its address — new mail must remain deliverable while it's down); only an empty mailbox of a session that can no longer be resumed is discarded promptly. Listing never has side effects.
62
+ - **Sweeping is narrow:** a running session is never touched; an inbox or durable claim holding undelivered mail is kept 30 days; an offline-but-resumable session keeps its record (its address — new mail must remain deliverable while it's down); only an empty mailbox of a session that can no longer be resumed is discarded promptly. Listing never has side effects.
49
63
 
50
64
  ### ask / reply / pending / cancel
51
65
 
@@ -86,7 +100,7 @@ The directory is created `0700` and every file `0600` — other users on the mac
86
100
 
87
101
  ## Caveats
88
102
 
89
- - **Mailbox semantics are unix-only.** Delivery relies on atomic `renameSync`-over-existing, `fs.watch`, and `0600`/`0700` permission bits; Windows is untested and unsupported.
103
+ - **Mailbox semantics support Linux and macOS only.** Relay rejects user-controlled symlinks in configured-root components, permits protected root-owned system aliases such as macOS `/var`, pins root, mailbox, and durable-claim descriptors for each operation, and uses descriptor-relative `openat`/`renameat`/`unlinkat` calls, atomic renames, descriptor polling for watches, and `0600`/`0700` permission bits. Windows is unsupported.
90
104
  - **One machine.** Delivery is a file landing in a directory; two sessions reach each other exactly when they share a filesystem. A container and its host cannot.
91
105
  - **Presence is heartbeat-accurate**, not instantaneous (within ~45s).
92
106
  - Delivery injects with `deliverAs: "steer"` (lands between tool calls) and `triggerTurn: true` (wakes an idle session).
package/core.ts ADDED
@@ -0,0 +1,314 @@
1
+ /** Pi-free relay registry, mailbox, and policy API for non-extension consumers. */
2
+ import {
3
+ ackClaimedLetter as ackClaimedLetterInternal,
4
+ awaitReceipt as awaitReceiptInternal,
5
+ claimInbox as claimInboxInternal,
6
+ clearAsk as clearAskInternal,
7
+ deposit as depositInternal,
8
+ drain as drainInternal,
9
+ pendingAsks as pendingAsksInternal,
10
+ readAudit as readAuditInternal,
11
+ readClaimedLetter as readClaimedLetterInternal,
12
+ readIncomingAsk as readIncomingAskInternal,
13
+ readOutgoingAsk as readOutgoingAskInternal,
14
+ recoverInboxClaims as recoverInboxClaimsInternal,
15
+ requeueClaimedLetter as requeueClaimedLetterInternal,
16
+ resolveAskByRef as resolveAskByRefInternal,
17
+ trackIncomingAsk as trackIncomingAskInternal,
18
+ trackOutgoingAsk as trackOutgoingAskInternal,
19
+ unreadCount as unreadCountInternal,
20
+ watchInbox as watchInboxInternal,
21
+ type AuditRecord,
22
+ type InboxClaim,
23
+ type Letter,
24
+ type OutAsk,
25
+ } from './mailbox.js';
26
+ import {
27
+ claimAlias as claimAliasInternal,
28
+ clearAlias as clearAliasInternal,
29
+ isValidAliasName as isValidAliasNameInternal,
30
+ listAliases as listAliasesInternal,
31
+ listRecords as listRecordsInternal,
32
+ presenceOf as presenceOfInternal,
33
+ readAlias as readAliasInternal,
34
+ readRecord as readRecordInternal,
35
+ sweep as sweepInternal,
36
+ type AliasRecord,
37
+ type Presence,
38
+ type SessionRecord,
39
+ } from './registry.js';
40
+
41
+ export {
42
+ MAX_BODY_CHARS,
43
+ previewBody,
44
+ type AuditRecord,
45
+ type InboxClaim,
46
+ type Letter,
47
+ type LetterKind,
48
+ type OutAsk,
49
+ } from './mailbox.js';
50
+ export {
51
+ BACKLOG_CAP,
52
+ DEDUPE_WINDOW_MS,
53
+ OutboundPolicy,
54
+ RATE_LIMIT_MAX,
55
+ RATE_LIMIT_WINDOW_MS,
56
+ inboundAccepts,
57
+ type OutboundVerdict,
58
+ } from './policy.js';
59
+ export {
60
+ HEARTBEAT_STALE_MS,
61
+ SWEEP_MAIL_KEEP_MS,
62
+ deriveAddr,
63
+ isValidAliasName,
64
+ type AliasRecord,
65
+ type Presence,
66
+ type SessionRecord,
67
+ } from './registry.js';
68
+
69
+ const ADDRESS_PATTERN = /^[a-f0-9]{12}$/;
70
+ const ASK_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
71
+ const INBOX_CLAIM_TOKEN_PATTERN = /^[a-f0-9]{32}$/;
72
+ const INBOX_FILE_TOKEN_PATTERN = /^(0|[1-9][0-9]*)-[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}\.json$/;
73
+
74
+ function assertAddress(addr: string): void {
75
+ if (!ADDRESS_PATTERN.test(addr)) throw new TypeError(`Invalid relay address: ${addr}`);
76
+ }
77
+
78
+ function assertAliasName(name: string): void {
79
+ if (!isValidAliasNameInternal(name)) throw new TypeError(`Invalid relay alias: ${name}`);
80
+ }
81
+
82
+ function assertAskId(id: string): void {
83
+ if (!ASK_ID_PATTERN.test(id)) throw new TypeError(`Invalid relay ask/message id: ${id}`);
84
+ }
85
+
86
+ function assertInboxClaimToken(token: string): void {
87
+ if (!INBOX_CLAIM_TOKEN_PATTERN.test(token)) throw new TypeError(`Invalid relay inbox claim token: ${token}`);
88
+ }
89
+
90
+ function assertInboxFileToken(token: string): void {
91
+ if (!INBOX_FILE_TOKEN_PATTERN.test(token)) throw new TypeError(`Invalid relay inbox file token: ${token}`);
92
+ }
93
+
94
+ function assertLetter(letter: Letter): void {
95
+ if (typeof letter !== 'object' || letter === null) throw new TypeError('Invalid relay letter');
96
+ assertAskId(letter.id);
97
+ if (typeof letter.from !== 'object' || letter.from === null) throw new TypeError('Invalid relay letter sender');
98
+ assertAddress(letter.from.addr);
99
+ if (typeof letter.from.name !== 'string' || typeof letter.from.cwd !== 'string') {
100
+ throw new TypeError('Invalid relay letter sender');
101
+ }
102
+ if (!['message', 'ask', 'reply', 'cancel'].includes(letter.kind) || typeof letter.body !== 'string') {
103
+ throw new TypeError('Invalid relay letter payload');
104
+ }
105
+ if (letter.replyTo !== undefined) assertAskId(letter.replyTo);
106
+ if (!Number.isSafeInteger(letter.ts) || letter.ts < 0) {
107
+ throw new TypeError(`Invalid relay letter timestamp: ${letter.ts}`);
108
+ }
109
+ }
110
+
111
+ function isSafeLetter(letter: Letter): boolean {
112
+ try {
113
+ assertLetter(letter);
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ function isSafeRecord(record: SessionRecord): boolean {
121
+ try {
122
+ assertAddress(record.addr);
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ function isSafeAlias(alias: AliasRecord): boolean {
130
+ try {
131
+ assertAliasName(alias.name);
132
+ assertAddress(alias.addr);
133
+ return true;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ function isSafeOutAsk(out: OutAsk): boolean {
140
+ try {
141
+ assertAskId(out.askId);
142
+ assertAddress(out.toAddr);
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ function isSafeAuditRecord(record: AuditRecord): boolean {
150
+ try {
151
+ assertAddress(record.from);
152
+ assertAddress(record.to);
153
+ assertAskId(record.messageId);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ export function readRecord(root: string, addr: string): SessionRecord | null {
161
+ assertAddress(addr);
162
+ const record = readRecordInternal(root, addr);
163
+ return record && isSafeRecord(record) ? record : null;
164
+ }
165
+
166
+ export function listRecords(root: string): SessionRecord[] {
167
+ return listRecordsInternal(root).filter(isSafeRecord);
168
+ }
169
+
170
+ export function presenceOf(record: SessionRecord, now: number = Date.now()): Presence {
171
+ assertAddress(record.addr);
172
+ return presenceOfInternal(record, now);
173
+ }
174
+
175
+ export function readAlias(root: string, name: string): AliasRecord | null {
176
+ assertAliasName(name);
177
+ const alias = readAliasInternal(root, name);
178
+ return alias && isSafeAlias(alias) ? alias : null;
179
+ }
180
+
181
+ export function listAliases(root: string): AliasRecord[] {
182
+ return listAliasesInternal(root).filter(isSafeAlias);
183
+ }
184
+
185
+ export function claimAlias(root: string, name: string, addr: string, sessionId: string): AliasRecord {
186
+ assertAliasName(name);
187
+ assertAddress(addr);
188
+ return claimAliasInternal(root, name, addr, sessionId);
189
+ }
190
+
191
+ export function clearAlias(root: string, name: string): void {
192
+ assertAliasName(name);
193
+ clearAliasInternal(root, name);
194
+ }
195
+
196
+ export function sweep(root: string, now: number = Date.now(), sessionExists?: (sessionId: string) => boolean): void {
197
+ if (!listRecordsInternal(root).every(isSafeRecord) || !listAliasesInternal(root).every(isSafeAlias)) {
198
+ throw new TypeError('Relay root contains an invalid address or alias');
199
+ }
200
+ sweepInternal(root, now, sessionExists);
201
+ }
202
+
203
+ export function deposit(root: string, toAddr: string, letter: Letter): void {
204
+ assertAddress(toAddr);
205
+ assertLetter(letter);
206
+ depositInternal(root, toAddr, letter);
207
+ }
208
+
209
+ export function drain(root: string, addr: string): Letter[] {
210
+ assertAddress(addr);
211
+ return drainInternal(root, addr).filter(isSafeLetter);
212
+ }
213
+
214
+ export function unreadCount(root: string, addr: string): number {
215
+ assertAddress(addr);
216
+ return unreadCountInternal(root, addr);
217
+ }
218
+
219
+ export function claimInbox(root: string, addr: string): InboxClaim | null {
220
+ assertAddress(addr);
221
+ return claimInboxInternal(root, addr);
222
+ }
223
+
224
+ export function recoverInboxClaims(root: string, addr: string): InboxClaim[] {
225
+ assertAddress(addr);
226
+ return recoverInboxClaimsInternal(root, addr);
227
+ }
228
+
229
+ export function readClaimedLetter(root: string, addr: string, claimToken: string, fileToken: string): Letter | null {
230
+ assertAddress(addr);
231
+ assertInboxClaimToken(claimToken);
232
+ assertInboxFileToken(fileToken);
233
+ const letter = readClaimedLetterInternal(root, addr, claimToken, fileToken);
234
+ return letter && isSafeLetter(letter) ? letter : null;
235
+ }
236
+
237
+ export function ackClaimedLetter(root: string, addr: string, claimToken: string, fileToken: string): boolean {
238
+ assertAddress(addr);
239
+ assertInboxClaimToken(claimToken);
240
+ assertInboxFileToken(fileToken);
241
+ return ackClaimedLetterInternal(root, addr, claimToken, fileToken);
242
+ }
243
+
244
+ export function requeueClaimedLetter(root: string, addr: string, claimToken: string, fileToken: string): boolean {
245
+ assertAddress(addr);
246
+ assertInboxClaimToken(claimToken);
247
+ assertInboxFileToken(fileToken);
248
+ return requeueClaimedLetterInternal(root, addr, claimToken, fileToken);
249
+ }
250
+
251
+ export function watchInbox(root: string, addr: string, onMail: () => void): () => void {
252
+ assertAddress(addr);
253
+ return watchInboxInternal(root, addr, onMail);
254
+ }
255
+
256
+ export async function awaitReceipt(
257
+ root: string,
258
+ toAddr: string,
259
+ letter: Letter,
260
+ timeoutMs = 1500,
261
+ ): Promise<'delivered' | 'queued'> {
262
+ assertAddress(toAddr);
263
+ assertLetter(letter);
264
+ return awaitReceiptInternal(root, toAddr, letter, timeoutMs);
265
+ }
266
+
267
+ export function trackIncomingAsk(root: string, addr: string, letter: Letter): void {
268
+ assertAddress(addr);
269
+ assertLetter(letter);
270
+ trackIncomingAskInternal(root, addr, letter);
271
+ }
272
+
273
+ export function trackOutgoingAsk(root: string, addr: string, out: OutAsk): void {
274
+ assertAddress(addr);
275
+ assertAskId(out.askId);
276
+ assertAddress(out.toAddr);
277
+ trackOutgoingAskInternal(root, addr, out);
278
+ }
279
+
280
+ export function readIncomingAsk(root: string, addr: string, askId: string): Letter | null {
281
+ assertAddress(addr);
282
+ assertAskId(askId);
283
+ const letter = readIncomingAskInternal(root, addr, askId);
284
+ return letter && isSafeLetter(letter) ? letter : null;
285
+ }
286
+
287
+ export function readOutgoingAsk(root: string, addr: string, askId: string): OutAsk | null {
288
+ assertAddress(addr);
289
+ assertAskId(askId);
290
+ const out = readOutgoingAskInternal(root, addr, askId);
291
+ return out && isSafeOutAsk(out) ? out : null;
292
+ }
293
+
294
+ export function clearAsk(root: string, addr: string, askId: string): void {
295
+ assertAddress(addr);
296
+ assertAskId(askId);
297
+ clearAskInternal(root, addr, askId);
298
+ }
299
+
300
+ export function resolveAskByRef(root: string, addr: string, replyTo: string): Letter | null {
301
+ assertAddress(addr);
302
+ assertAskId(replyTo);
303
+ const letter = resolveAskByRefInternal(root, addr, replyTo);
304
+ return letter && isSafeLetter(letter) ? letter : null;
305
+ }
306
+
307
+ export function pendingAsks(root: string, addr: string): Letter[] {
308
+ assertAddress(addr);
309
+ return pendingAsksInternal(root, addr).filter(isSafeLetter);
310
+ }
311
+
312
+ export function readAudit(root: string, limit = 50): AuditRecord[] {
313
+ return readAuditInternal(root, limit).filter(isSafeAuditRecord);
314
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /** Pi-free relay registry, mailbox, and policy API for non-extension consumers. */
2
+ import { type AuditRecord, type InboxClaim, type Letter, type OutAsk } from './mailbox.js';
3
+ import { type AliasRecord, type Presence, type SessionRecord } from './registry.js';
4
+ export { MAX_BODY_CHARS, previewBody, type AuditRecord, type InboxClaim, type Letter, type LetterKind, type OutAsk, } from './mailbox.js';
5
+ export { BACKLOG_CAP, DEDUPE_WINDOW_MS, OutboundPolicy, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_MS, inboundAccepts, type OutboundVerdict, } from './policy.js';
6
+ export { HEARTBEAT_STALE_MS, SWEEP_MAIL_KEEP_MS, deriveAddr, isValidAliasName, type AliasRecord, type Presence, type SessionRecord, } from './registry.js';
7
+ export declare function readRecord(root: string, addr: string): SessionRecord | null;
8
+ export declare function listRecords(root: string): SessionRecord[];
9
+ export declare function presenceOf(record: SessionRecord, now?: number): Presence;
10
+ export declare function readAlias(root: string, name: string): AliasRecord | null;
11
+ export declare function listAliases(root: string): AliasRecord[];
12
+ export declare function claimAlias(root: string, name: string, addr: string, sessionId: string): AliasRecord;
13
+ export declare function clearAlias(root: string, name: string): void;
14
+ export declare function sweep(root: string, now?: number, sessionExists?: (sessionId: string) => boolean): void;
15
+ export declare function deposit(root: string, toAddr: string, letter: Letter): void;
16
+ export declare function drain(root: string, addr: string): Letter[];
17
+ export declare function unreadCount(root: string, addr: string): number;
18
+ export declare function claimInbox(root: string, addr: string): InboxClaim | null;
19
+ export declare function recoverInboxClaims(root: string, addr: string): InboxClaim[];
20
+ export declare function readClaimedLetter(root: string, addr: string, claimToken: string, fileToken: string): Letter | null;
21
+ export declare function ackClaimedLetter(root: string, addr: string, claimToken: string, fileToken: string): boolean;
22
+ export declare function requeueClaimedLetter(root: string, addr: string, claimToken: string, fileToken: string): boolean;
23
+ export declare function watchInbox(root: string, addr: string, onMail: () => void): () => void;
24
+ export declare function awaitReceipt(root: string, toAddr: string, letter: Letter, timeoutMs?: number): Promise<'delivered' | 'queued'>;
25
+ export declare function trackIncomingAsk(root: string, addr: string, letter: Letter): void;
26
+ export declare function trackOutgoingAsk(root: string, addr: string, out: OutAsk): void;
27
+ export declare function readIncomingAsk(root: string, addr: string, askId: string): Letter | null;
28
+ export declare function readOutgoingAsk(root: string, addr: string, askId: string): OutAsk | null;
29
+ export declare function clearAsk(root: string, addr: string, askId: string): void;
30
+ export declare function resolveAskByRef(root: string, addr: string, replyTo: string): Letter | null;
31
+ export declare function pendingAsks(root: string, addr: string): Letter[];
32
+ export declare function readAudit(root: string, limit?: number): AuditRecord[];
package/dist/core.js ADDED
@@ -0,0 +1,223 @@
1
+ /** Pi-free relay registry, mailbox, and policy API for non-extension consumers. */
2
+ import { ackClaimedLetter as ackClaimedLetterInternal, awaitReceipt as awaitReceiptInternal, claimInbox as claimInboxInternal, clearAsk as clearAskInternal, deposit as depositInternal, drain as drainInternal, pendingAsks as pendingAsksInternal, readAudit as readAuditInternal, readClaimedLetter as readClaimedLetterInternal, readIncomingAsk as readIncomingAskInternal, readOutgoingAsk as readOutgoingAskInternal, recoverInboxClaims as recoverInboxClaimsInternal, requeueClaimedLetter as requeueClaimedLetterInternal, resolveAskByRef as resolveAskByRefInternal, trackIncomingAsk as trackIncomingAskInternal, trackOutgoingAsk as trackOutgoingAskInternal, unreadCount as unreadCountInternal, watchInbox as watchInboxInternal, } from './mailbox.js';
3
+ import { claimAlias as claimAliasInternal, clearAlias as clearAliasInternal, isValidAliasName as isValidAliasNameInternal, listAliases as listAliasesInternal, listRecords as listRecordsInternal, presenceOf as presenceOfInternal, readAlias as readAliasInternal, readRecord as readRecordInternal, sweep as sweepInternal, } from './registry.js';
4
+ export { MAX_BODY_CHARS, previewBody, } from './mailbox.js';
5
+ export { BACKLOG_CAP, DEDUPE_WINDOW_MS, OutboundPolicy, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW_MS, inboundAccepts, } from './policy.js';
6
+ export { HEARTBEAT_STALE_MS, SWEEP_MAIL_KEEP_MS, deriveAddr, isValidAliasName, } from './registry.js';
7
+ const ADDRESS_PATTERN = /^[a-f0-9]{12}$/;
8
+ const ASK_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/;
9
+ const INBOX_CLAIM_TOKEN_PATTERN = /^[a-f0-9]{32}$/;
10
+ const INBOX_FILE_TOKEN_PATTERN = /^(0|[1-9][0-9]*)-[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}\.json$/;
11
+ function assertAddress(addr) {
12
+ if (!ADDRESS_PATTERN.test(addr))
13
+ throw new TypeError(`Invalid relay address: ${addr}`);
14
+ }
15
+ function assertAliasName(name) {
16
+ if (!isValidAliasNameInternal(name))
17
+ throw new TypeError(`Invalid relay alias: ${name}`);
18
+ }
19
+ function assertAskId(id) {
20
+ if (!ASK_ID_PATTERN.test(id))
21
+ throw new TypeError(`Invalid relay ask/message id: ${id}`);
22
+ }
23
+ function assertInboxClaimToken(token) {
24
+ if (!INBOX_CLAIM_TOKEN_PATTERN.test(token))
25
+ throw new TypeError(`Invalid relay inbox claim token: ${token}`);
26
+ }
27
+ function assertInboxFileToken(token) {
28
+ if (!INBOX_FILE_TOKEN_PATTERN.test(token))
29
+ throw new TypeError(`Invalid relay inbox file token: ${token}`);
30
+ }
31
+ function assertLetter(letter) {
32
+ if (typeof letter !== 'object' || letter === null)
33
+ throw new TypeError('Invalid relay letter');
34
+ assertAskId(letter.id);
35
+ if (typeof letter.from !== 'object' || letter.from === null)
36
+ throw new TypeError('Invalid relay letter sender');
37
+ assertAddress(letter.from.addr);
38
+ if (typeof letter.from.name !== 'string' || typeof letter.from.cwd !== 'string') {
39
+ throw new TypeError('Invalid relay letter sender');
40
+ }
41
+ if (!['message', 'ask', 'reply', 'cancel'].includes(letter.kind) || typeof letter.body !== 'string') {
42
+ throw new TypeError('Invalid relay letter payload');
43
+ }
44
+ if (letter.replyTo !== undefined)
45
+ assertAskId(letter.replyTo);
46
+ if (!Number.isSafeInteger(letter.ts) || letter.ts < 0) {
47
+ throw new TypeError(`Invalid relay letter timestamp: ${letter.ts}`);
48
+ }
49
+ }
50
+ function isSafeLetter(letter) {
51
+ try {
52
+ assertLetter(letter);
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ function isSafeRecord(record) {
60
+ try {
61
+ assertAddress(record.addr);
62
+ return true;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ function isSafeAlias(alias) {
69
+ try {
70
+ assertAliasName(alias.name);
71
+ assertAddress(alias.addr);
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ function isSafeOutAsk(out) {
79
+ try {
80
+ assertAskId(out.askId);
81
+ assertAddress(out.toAddr);
82
+ return true;
83
+ }
84
+ catch {
85
+ return false;
86
+ }
87
+ }
88
+ function isSafeAuditRecord(record) {
89
+ try {
90
+ assertAddress(record.from);
91
+ assertAddress(record.to);
92
+ assertAskId(record.messageId);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ export function readRecord(root, addr) {
100
+ assertAddress(addr);
101
+ const record = readRecordInternal(root, addr);
102
+ return record && isSafeRecord(record) ? record : null;
103
+ }
104
+ export function listRecords(root) {
105
+ return listRecordsInternal(root).filter(isSafeRecord);
106
+ }
107
+ export function presenceOf(record, now = Date.now()) {
108
+ assertAddress(record.addr);
109
+ return presenceOfInternal(record, now);
110
+ }
111
+ export function readAlias(root, name) {
112
+ assertAliasName(name);
113
+ const alias = readAliasInternal(root, name);
114
+ return alias && isSafeAlias(alias) ? alias : null;
115
+ }
116
+ export function listAliases(root) {
117
+ return listAliasesInternal(root).filter(isSafeAlias);
118
+ }
119
+ export function claimAlias(root, name, addr, sessionId) {
120
+ assertAliasName(name);
121
+ assertAddress(addr);
122
+ return claimAliasInternal(root, name, addr, sessionId);
123
+ }
124
+ export function clearAlias(root, name) {
125
+ assertAliasName(name);
126
+ clearAliasInternal(root, name);
127
+ }
128
+ export function sweep(root, now = Date.now(), sessionExists) {
129
+ if (!listRecordsInternal(root).every(isSafeRecord) || !listAliasesInternal(root).every(isSafeAlias)) {
130
+ throw new TypeError('Relay root contains an invalid address or alias');
131
+ }
132
+ sweepInternal(root, now, sessionExists);
133
+ }
134
+ export function deposit(root, toAddr, letter) {
135
+ assertAddress(toAddr);
136
+ assertLetter(letter);
137
+ depositInternal(root, toAddr, letter);
138
+ }
139
+ export function drain(root, addr) {
140
+ assertAddress(addr);
141
+ return drainInternal(root, addr).filter(isSafeLetter);
142
+ }
143
+ export function unreadCount(root, addr) {
144
+ assertAddress(addr);
145
+ return unreadCountInternal(root, addr);
146
+ }
147
+ export function claimInbox(root, addr) {
148
+ assertAddress(addr);
149
+ return claimInboxInternal(root, addr);
150
+ }
151
+ export function recoverInboxClaims(root, addr) {
152
+ assertAddress(addr);
153
+ return recoverInboxClaimsInternal(root, addr);
154
+ }
155
+ export function readClaimedLetter(root, addr, claimToken, fileToken) {
156
+ assertAddress(addr);
157
+ assertInboxClaimToken(claimToken);
158
+ assertInboxFileToken(fileToken);
159
+ const letter = readClaimedLetterInternal(root, addr, claimToken, fileToken);
160
+ return letter && isSafeLetter(letter) ? letter : null;
161
+ }
162
+ export function ackClaimedLetter(root, addr, claimToken, fileToken) {
163
+ assertAddress(addr);
164
+ assertInboxClaimToken(claimToken);
165
+ assertInboxFileToken(fileToken);
166
+ return ackClaimedLetterInternal(root, addr, claimToken, fileToken);
167
+ }
168
+ export function requeueClaimedLetter(root, addr, claimToken, fileToken) {
169
+ assertAddress(addr);
170
+ assertInboxClaimToken(claimToken);
171
+ assertInboxFileToken(fileToken);
172
+ return requeueClaimedLetterInternal(root, addr, claimToken, fileToken);
173
+ }
174
+ export function watchInbox(root, addr, onMail) {
175
+ assertAddress(addr);
176
+ return watchInboxInternal(root, addr, onMail);
177
+ }
178
+ export async function awaitReceipt(root, toAddr, letter, timeoutMs = 1500) {
179
+ assertAddress(toAddr);
180
+ assertLetter(letter);
181
+ return awaitReceiptInternal(root, toAddr, letter, timeoutMs);
182
+ }
183
+ export function trackIncomingAsk(root, addr, letter) {
184
+ assertAddress(addr);
185
+ assertLetter(letter);
186
+ trackIncomingAskInternal(root, addr, letter);
187
+ }
188
+ export function trackOutgoingAsk(root, addr, out) {
189
+ assertAddress(addr);
190
+ assertAskId(out.askId);
191
+ assertAddress(out.toAddr);
192
+ trackOutgoingAskInternal(root, addr, out);
193
+ }
194
+ export function readIncomingAsk(root, addr, askId) {
195
+ assertAddress(addr);
196
+ assertAskId(askId);
197
+ const letter = readIncomingAskInternal(root, addr, askId);
198
+ return letter && isSafeLetter(letter) ? letter : null;
199
+ }
200
+ export function readOutgoingAsk(root, addr, askId) {
201
+ assertAddress(addr);
202
+ assertAskId(askId);
203
+ const out = readOutgoingAskInternal(root, addr, askId);
204
+ return out && isSafeOutAsk(out) ? out : null;
205
+ }
206
+ export function clearAsk(root, addr, askId) {
207
+ assertAddress(addr);
208
+ assertAskId(askId);
209
+ clearAskInternal(root, addr, askId);
210
+ }
211
+ export function resolveAskByRef(root, addr, replyTo) {
212
+ assertAddress(addr);
213
+ assertAskId(replyTo);
214
+ const letter = resolveAskByRefInternal(root, addr, replyTo);
215
+ return letter && isSafeLetter(letter) ? letter : null;
216
+ }
217
+ export function pendingAsks(root, addr) {
218
+ assertAddress(addr);
219
+ return pendingAsksInternal(root, addr).filter(isSafeLetter);
220
+ }
221
+ export function readAudit(root, limit = 50) {
222
+ return readAuditInternal(root, limit).filter(isSafeAuditRecord);
223
+ }
@@ -0,0 +1 @@
1
+ export {};