@msgboard/history 0.0.30
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 +85 -0
- package/dist/archive.d.ts +61 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +90 -0
- package/dist/archive.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +79 -0
- package/dist/server.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @msgboard/history
|
|
2
|
+
|
|
3
|
+
A durable historical archive for the msgboard board. The board itself is **ephemeral** — it retains
|
|
4
|
+
only roughly the last 120 blocks of messages — so anything that needs durable history has to record
|
|
5
|
+
messages as they flow by. This package is the storage core for that: a Postgres-backed archive plus a
|
|
6
|
+
read-only HTTP query API.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm i @msgboard/history
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`pg` is an optional peer dependency — bring your own `Pool` (or any `{ query(text, params) }`).
|
|
13
|
+
|
|
14
|
+
## What's in the box
|
|
15
|
+
|
|
16
|
+
- **`createArchive({ pool, retention })`** — the storage core: `migrate()`, `record(message, chainId)`,
|
|
17
|
+
`prune()`, and `query(filter)` over a `message_archive` table.
|
|
18
|
+
- **`archiveServer({ archive, port, host, token })`** — a read-only HTTP API over `query`.
|
|
19
|
+
|
|
20
|
+
The same `createArchive` backs `@msgboard/relayer`'s `postgresArchiveSink`, which adapts it to the
|
|
21
|
+
relayer's sink interface so an **archivist** relayer can populate the archive from live board traffic.
|
|
22
|
+
|
|
23
|
+
## Recording history
|
|
24
|
+
|
|
25
|
+
Run an archivist relayer (see `@msgboard/relayer` and `packages/examples/src/archivist.ts`) to write
|
|
26
|
+
every message into the archive, or record directly:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import pg from 'pg'
|
|
30
|
+
import { createArchive } from '@msgboard/history'
|
|
31
|
+
|
|
32
|
+
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
|
|
33
|
+
const archive = createArchive({ pool, retention: { days: 365 } })
|
|
34
|
+
await archive.migrate() // once at startup
|
|
35
|
+
|
|
36
|
+
await archive.record(message, 943) // message: RPCMessage, 943: chainId
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`record` is idempotent on `(hash, chain_id)`. The archive grows forever until `prune()` deletes rows
|
|
40
|
+
older than the retention window.
|
|
41
|
+
|
|
42
|
+
## Querying history
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const recent = await archive.query({
|
|
46
|
+
chainId: 943,
|
|
47
|
+
category: 'lorem', // bytes32 hex or its decoded text
|
|
48
|
+
contains: 'hello', // substring match on decoded content
|
|
49
|
+
since: new Date('2026-01-01'),
|
|
50
|
+
limit: 20,
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`limit` is clamped to at most 1000; results come back newest-first. `since`/`until` filter on
|
|
55
|
+
`first_seen_at`.
|
|
56
|
+
|
|
57
|
+
## Serving history over HTTP
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createArchive, archiveServer } from '@msgboard/history'
|
|
61
|
+
|
|
62
|
+
const archive = createArchive({ pool, retention: { days: 365 } })
|
|
63
|
+
await archive.migrate()
|
|
64
|
+
const server = archiveServer({ archive, port: 4040 }) // loopback by default
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- `GET /health` → `{ ok: true }`
|
|
68
|
+
- `GET /messages?chainId=943&category=lorem&contains=hello&since=2026-01-01&limit=20&offset=0` →
|
|
69
|
+
`{ messages: [...] }`
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
curl 'http://localhost:4040/messages?chainId=943&category=lorem&limit=20'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Security
|
|
76
|
+
|
|
77
|
+
The server binds to `127.0.0.1` by default. To expose it on a public interface, set `host` **and** a
|
|
78
|
+
`token` — a non-loopback bind without a token throws at startup. When `token` is set, `/messages`
|
|
79
|
+
requires `Authorization: Bearer <token>` (`/health` stays open). Slow connections are bounded by 10 s
|
|
80
|
+
header/request timeouts. `limit`/`offset` are coerced to bounded integers before they reach SQL.
|
|
81
|
+
|
|
82
|
+
## A full example
|
|
83
|
+
|
|
84
|
+
`packages/examples/src/history-server.ts` wires the whole flow end to end: an archivist relayer writes
|
|
85
|
+
live board traffic into Postgres while `archiveServer` serves queries over it.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RPCMessage } from '@msgboard/sdk';
|
|
2
|
+
/** The minimal `pg`-compatible surface this package needs — a `Pool` or `Client` satisfies it. */
|
|
3
|
+
export type Queryable = {
|
|
4
|
+
query(text: string, params?: unknown[]): Promise<{
|
|
5
|
+
rows: unknown[];
|
|
6
|
+
}>;
|
|
7
|
+
};
|
|
8
|
+
export type ArchiveRetention = {
|
|
9
|
+
/** Rows older than this many days are pruned. */
|
|
10
|
+
days: number;
|
|
11
|
+
};
|
|
12
|
+
export type ArchiveOptions = {
|
|
13
|
+
pool: Queryable;
|
|
14
|
+
retention: ArchiveRetention;
|
|
15
|
+
};
|
|
16
|
+
/** Filters for querying the historical archive. */
|
|
17
|
+
export type ArchiveQuery = {
|
|
18
|
+
chainId?: number;
|
|
19
|
+
/** A bytes32 hex category or its decoded text. */
|
|
20
|
+
category?: string;
|
|
21
|
+
since?: Date;
|
|
22
|
+
until?: Date;
|
|
23
|
+
/** Substring match on decoded content. */
|
|
24
|
+
contains?: string;
|
|
25
|
+
limit?: number;
|
|
26
|
+
offset?: number;
|
|
27
|
+
};
|
|
28
|
+
/** A row of the historical archive. */
|
|
29
|
+
export type ArchivedMessage = {
|
|
30
|
+
hash: string;
|
|
31
|
+
chain_id: number;
|
|
32
|
+
category: string | null;
|
|
33
|
+
category_text: string | null;
|
|
34
|
+
data: string | null;
|
|
35
|
+
content: string | null;
|
|
36
|
+
block_number: string | null;
|
|
37
|
+
block_hash: string | null;
|
|
38
|
+
first_seen_at: string;
|
|
39
|
+
};
|
|
40
|
+
/** The archive's read/write surface. */
|
|
41
|
+
export type Archive = {
|
|
42
|
+
/** Creates the table and indexes. Call once at startup. */
|
|
43
|
+
migrate(): Promise<void>;
|
|
44
|
+
/** Records a message seen on `chainId`. Idempotent on `(hash, chain_id)`. */
|
|
45
|
+
record(message: RPCMessage, chainId: number): Promise<void>;
|
|
46
|
+
/** Deletes rows older than the retention window. */
|
|
47
|
+
prune(): Promise<void>;
|
|
48
|
+
/** Returns matching rows, newest first, bounded to at most 1000. */
|
|
49
|
+
query(filter: ArchiveQuery): Promise<ArchivedMessage[]>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The historical index of every message seen flowing through the board. An
|
|
53
|
+
* ever-growing table, pruned to a retention window (default one year). `record`
|
|
54
|
+
* is idempotent on `(hash, chain_id)`. Call `migrate()` once at startup.
|
|
55
|
+
*
|
|
56
|
+
* This is the storage core behind `@msgboard/relayer`'s `postgresArchiveSink`
|
|
57
|
+
* (which adapts it to the relayer's sink interface) and `archiveServer` (which
|
|
58
|
+
* exposes `query` over HTTP).
|
|
59
|
+
*/
|
|
60
|
+
export declare const createArchive: (options: ArchiveOptions) => Archive;
|
|
61
|
+
//# sourceMappingURL=archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C,kGAAkG;AAClG,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,EAAE,CAAA;KAAE,CAAC,CAAA;CACtE,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,SAAS,CAAA;IACf,SAAS,EAAE,gBAAgB,CAAA;CAC5B,CAAA;AAED,mDAAmD;AACnD,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,uCAAuC;AACvC,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,aAAa,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,wCAAwC;AACxC,MAAM,MAAM,OAAO,GAAG;IACpB,2DAA2D;IAC3D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACxB,6EAA6E;IAC7E,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,oDAAoD;IACpD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,oEAAoE;IACpE,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;CACxD,CAAA;AAeD;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,cAAc,KAAG,OAyEvD,CAAA"}
|
package/dist/archive.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { hexToString } from 'viem';
|
|
2
|
+
/** Decodes a hex blob to text, stripping null padding and returning null if not printable. */
|
|
3
|
+
const tryDecodeText = (hex) => {
|
|
4
|
+
try {
|
|
5
|
+
const text = hexToString(hex).replace(/\0+$/g, '').trim();
|
|
6
|
+
if (text.length === 0)
|
|
7
|
+
return null;
|
|
8
|
+
// Reject blobs with non-printable control characters
|
|
9
|
+
if (/[\x00-\x1f\x7f]/u.test(text))
|
|
10
|
+
return null;
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The historical index of every message seen flowing through the board. An
|
|
19
|
+
* ever-growing table, pruned to a retention window (default one year). `record`
|
|
20
|
+
* is idempotent on `(hash, chain_id)`. Call `migrate()` once at startup.
|
|
21
|
+
*
|
|
22
|
+
* This is the storage core behind `@msgboard/relayer`'s `postgresArchiveSink`
|
|
23
|
+
* (which adapts it to the relayer's sink interface) and `archiveServer` (which
|
|
24
|
+
* exposes `query` over HTTP).
|
|
25
|
+
*/
|
|
26
|
+
export const createArchive = (options) => {
|
|
27
|
+
const { pool } = options;
|
|
28
|
+
const retentionDays = options.retention.days;
|
|
29
|
+
const migrate = async () => {
|
|
30
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS message_archive (
|
|
31
|
+
hash TEXT NOT NULL,
|
|
32
|
+
chain_id INTEGER NOT NULL,
|
|
33
|
+
category TEXT,
|
|
34
|
+
category_text TEXT,
|
|
35
|
+
data TEXT,
|
|
36
|
+
content TEXT,
|
|
37
|
+
block_number BIGINT,
|
|
38
|
+
block_hash TEXT,
|
|
39
|
+
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
40
|
+
PRIMARY KEY (hash, chain_id)
|
|
41
|
+
)`);
|
|
42
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS message_archive_seen_idx ON message_archive (first_seen_at)`);
|
|
43
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS message_archive_chain_seen ON message_archive (chain_id, first_seen_at)`);
|
|
44
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS message_archive_category_idx ON message_archive (category)`);
|
|
45
|
+
};
|
|
46
|
+
const record = async (message, chainId) => {
|
|
47
|
+
await pool.query(`INSERT INTO message_archive
|
|
48
|
+
(hash, chain_id, category, category_text, data, content, block_number, block_hash)
|
|
49
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
50
|
+
ON CONFLICT (hash, chain_id) DO NOTHING`, [
|
|
51
|
+
message.hash,
|
|
52
|
+
chainId,
|
|
53
|
+
message.category,
|
|
54
|
+
tryDecodeText(message.category),
|
|
55
|
+
message.data,
|
|
56
|
+
tryDecodeText(message.data),
|
|
57
|
+
BigInt(message.blockNumber).toString(),
|
|
58
|
+
message.blockHash,
|
|
59
|
+
]);
|
|
60
|
+
};
|
|
61
|
+
const prune = async () => {
|
|
62
|
+
await pool.query(`DELETE FROM message_archive WHERE first_seen_at < now() - INTERVAL '${retentionDays} days'`);
|
|
63
|
+
};
|
|
64
|
+
const query = async (filter) => {
|
|
65
|
+
const clauses = [];
|
|
66
|
+
const params = [];
|
|
67
|
+
const add = (clause, value) => {
|
|
68
|
+
params.push(value);
|
|
69
|
+
clauses.push(clause.replace(/\$\?/g, `$${params.length}`));
|
|
70
|
+
};
|
|
71
|
+
if (filter.chainId !== undefined)
|
|
72
|
+
add('chain_id = $?', filter.chainId);
|
|
73
|
+
if (filter.category !== undefined)
|
|
74
|
+
add('(category = $? OR category_text = $?)', filter.category);
|
|
75
|
+
if (filter.since)
|
|
76
|
+
add('first_seen_at >= $?', filter.since.toISOString());
|
|
77
|
+
if (filter.until)
|
|
78
|
+
add('first_seen_at <= $?', filter.until.toISOString());
|
|
79
|
+
if (filter.contains)
|
|
80
|
+
add('content ILIKE $?', `%${filter.contains}%`);
|
|
81
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : '';
|
|
82
|
+
// limit/offset are interpolated, so coerce to bounded integers (never user SQL).
|
|
83
|
+
const limit = Math.min(Math.max(Number.parseInt(String(filter.limit ?? 100), 10) || 100, 1), 1000);
|
|
84
|
+
const offset = Math.max(Number.parseInt(String(filter.offset ?? 0), 10) || 0, 0);
|
|
85
|
+
const { rows } = await pool.query(`SELECT hash, chain_id, category, category_text, data, content, block_number, block_hash, first_seen_at FROM message_archive ${where} ORDER BY first_seen_at DESC LIMIT ${limit} OFFSET ${offset}`, params);
|
|
86
|
+
return rows;
|
|
87
|
+
};
|
|
88
|
+
return { migrate, record, prune, query };
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createArchive } from './archive.js';
|
|
2
|
+
export type { Archive, ArchiveOptions, ArchiveQuery, ArchiveRetention, ArchivedMessage, Queryable, } from './archive.js';
|
|
3
|
+
export { archiveServer } from './server.js';
|
|
4
|
+
export type { ArchiveServer, ArchiveServerOptions } from './server.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAC5C,YAAY,EACV,OAAO,EACP,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,SAAS,GACV,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,YAAY,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAU5C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Archive } from './archive.js';
|
|
2
|
+
export type ArchiveServerOptions = {
|
|
3
|
+
/** The archive to serve. */
|
|
4
|
+
archive: Archive;
|
|
5
|
+
/** Port to listen on. Defaults to 4040. */
|
|
6
|
+
port?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Host / interface to bind. Defaults to `'127.0.0.1'` (loopback only).
|
|
9
|
+
* Set to `'0.0.0.0'` for LAN/Internet exposure — requires `token`, or the
|
|
10
|
+
* server refuses to start (the archive is read-only, but still yours to gate).
|
|
11
|
+
*/
|
|
12
|
+
host?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional bearer token. When set, `/messages` requests without
|
|
15
|
+
* `Authorization: Bearer <token>` receive a 401.
|
|
16
|
+
*/
|
|
17
|
+
token?: string;
|
|
18
|
+
};
|
|
19
|
+
export type ArchiveServer = {
|
|
20
|
+
/** Closes the HTTP server. */
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Serves an {@link Archive} over HTTP, read-only:
|
|
25
|
+
*
|
|
26
|
+
* - `GET /health` → `{ ok: true }`
|
|
27
|
+
* - `GET /messages` → `{ messages: ArchivedMessage[] }`, filtered by query params:
|
|
28
|
+
* `chainId`, `category` (hex or decoded text), `since`/`until` (ISO dates),
|
|
29
|
+
* `contains` (substring on decoded content), `limit` (≤ 1000), `offset`.
|
|
30
|
+
*
|
|
31
|
+
* Security defaults mirror the relayer's push source: binds to `127.0.0.1`
|
|
32
|
+
* unless `host` is set, and a non-loopback bind requires `token`. Slow
|
|
33
|
+
* connections are bounded by 10 s header/request timeouts.
|
|
34
|
+
*/
|
|
35
|
+
export declare const archiveServer: (options: ArchiveServerOptions) => ArchiveServer;
|
|
36
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAA;AAEzD,MAAM,MAAM,oBAAoB,GAAG;IACjC,4BAA4B;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,8BAA8B;IAC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAoCD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,GAAI,SAAS,oBAAoB,KAAG,aA2C7D,CAAA"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
const respond = (res, status, body) => {
|
|
3
|
+
const json = JSON.stringify(body);
|
|
4
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(json) });
|
|
5
|
+
res.end(json);
|
|
6
|
+
};
|
|
7
|
+
/** Parses the `/messages` query string into an ArchiveQuery, ignoring unparseable values. */
|
|
8
|
+
const parseQuery = (params) => {
|
|
9
|
+
const query = {};
|
|
10
|
+
const chainId = params.get('chainId');
|
|
11
|
+
if (chainId !== null && Number.isFinite(Number(chainId)))
|
|
12
|
+
query.chainId = Number(chainId);
|
|
13
|
+
const category = params.get('category');
|
|
14
|
+
if (category)
|
|
15
|
+
query.category = category;
|
|
16
|
+
const contains = params.get('contains');
|
|
17
|
+
if (contains)
|
|
18
|
+
query.contains = contains;
|
|
19
|
+
const since = params.get('since');
|
|
20
|
+
if (since && !Number.isNaN(Date.parse(since)))
|
|
21
|
+
query.since = new Date(since);
|
|
22
|
+
const until = params.get('until');
|
|
23
|
+
if (until && !Number.isNaN(Date.parse(until)))
|
|
24
|
+
query.until = new Date(until);
|
|
25
|
+
const limit = params.get('limit');
|
|
26
|
+
if (limit !== null && Number.isFinite(Number(limit)))
|
|
27
|
+
query.limit = Number(limit);
|
|
28
|
+
const offset = params.get('offset');
|
|
29
|
+
if (offset !== null && Number.isFinite(Number(offset)))
|
|
30
|
+
query.offset = Number(offset);
|
|
31
|
+
return query;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Serves an {@link Archive} over HTTP, read-only:
|
|
35
|
+
*
|
|
36
|
+
* - `GET /health` → `{ ok: true }`
|
|
37
|
+
* - `GET /messages` → `{ messages: ArchivedMessage[] }`, filtered by query params:
|
|
38
|
+
* `chainId`, `category` (hex or decoded text), `since`/`until` (ISO dates),
|
|
39
|
+
* `contains` (substring on decoded content), `limit` (≤ 1000), `offset`.
|
|
40
|
+
*
|
|
41
|
+
* Security defaults mirror the relayer's push source: binds to `127.0.0.1`
|
|
42
|
+
* unless `host` is set, and a non-loopback bind requires `token`. Slow
|
|
43
|
+
* connections are bounded by 10 s header/request timeouts.
|
|
44
|
+
*/
|
|
45
|
+
export const archiveServer = (options) => {
|
|
46
|
+
const host = options.host ?? '127.0.0.1';
|
|
47
|
+
const port = options.port ?? 4040;
|
|
48
|
+
const isNonLoopback = host !== '127.0.0.1' && host !== 'localhost' && host !== '::1';
|
|
49
|
+
if (isNonLoopback && !options.token) {
|
|
50
|
+
throw new Error(`archiveServer: non-loopback bind (${host}) requires a token to be set — pass token or set host to '127.0.0.1'`);
|
|
51
|
+
}
|
|
52
|
+
const authorized = (req) => !options.token || req.headers['authorization'] === `Bearer ${options.token}`;
|
|
53
|
+
const server = createServer(async (req, res) => {
|
|
54
|
+
const url = new URL(req.url ?? '/', `http://${host}:${port}`);
|
|
55
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
56
|
+
return respond(res, 200, { ok: true });
|
|
57
|
+
}
|
|
58
|
+
if (req.method === 'GET' && url.pathname === '/messages') {
|
|
59
|
+
if (!authorized(req))
|
|
60
|
+
return respond(res, 401, { ok: false, error: 'unauthorized' });
|
|
61
|
+
try {
|
|
62
|
+
const messages = await options.archive.query(parseQuery(url.searchParams));
|
|
63
|
+
return respond(res, 200, { messages });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return respond(res, 500, { ok: false, error: error instanceof Error ? error.message : 'query failed' });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return respond(res, 404, { ok: false, error: 'not found' });
|
|
70
|
+
});
|
|
71
|
+
// Bound slow connections.
|
|
72
|
+
server.headersTimeout = 10_000;
|
|
73
|
+
server.requestTimeout = 10_000;
|
|
74
|
+
server.listen(port, host);
|
|
75
|
+
return {
|
|
76
|
+
close: () => new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve()))),
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@msgboard/history",
|
|
3
|
+
"version": "0.0.30",
|
|
4
|
+
"description": "Historical archive for the msgboard board: durable storage and an HTTP query API",
|
|
5
|
+
"repository": "github:valve-tech/msgboard",
|
|
6
|
+
"author": "MsgBoard",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"require": "./dist/index.js",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["msgboard", "history", "archive", "indexer", "pulsechain"],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prebuild": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"watch": "tsc -w",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"lint": "prettier --check ."
|
|
31
|
+
},
|
|
32
|
+
"files": ["dist/"],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@msgboard/sdk": "^0.0.30",
|
|
35
|
+
"viem": "^2.25.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"pg": "^8.14.1"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"pg": { "optional": true }
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/pg": "^8.11.11",
|
|
45
|
+
"typescript": "^5.8.2",
|
|
46
|
+
"vitest": "^3.1.1"
|
|
47
|
+
}
|
|
48
|
+
}
|