@seedprotocol/feed-hyper 0.5.2

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 ADDED
@@ -0,0 +1,107 @@
1
+ # @seedprotocol/feed-hyper
2
+
3
+ Publish Seed Protocol RSS/Atom/JSON feeds into a **Hyperdrive**, announce them on **Hyperswarm**, and serve them to ordinary RSS readers over localhost HTTP.
4
+
5
+ `seed feed serve` accepts both drive paths (`/posts/rss.xml`) and historical HTTP paths (`/posts/rss`).
6
+
7
+ Generation stays in [`@seedprotocol/feed`](../feed). This package is transport only (Node 20+). It pulls in Holepunch native modules (`sodium-native`, etc.).
8
+
9
+ ## Trust model
10
+
11
+ - A feed’s identity is its **Hyperdrive public key** (z32), not a hostname.
12
+ - Bytes are signed by that key. Peers and HTTP gateways are interchangeable mirrors.
13
+ - `https://feed.seedprotocol.io` (or any `seed feed serve` process) is a **convenience gateway**, not the source of truth.
14
+ - Official Seed feeds will use `DEFAULT_SEED_FEED_HYPER_KEY` once ops publish and back up the first keypair (empty string until then).
15
+
16
+ ## Drive layout
17
+
18
+ ```
19
+ /registry.json
20
+ /{collection}/rss.xml
21
+ /{collection}/atom.xml
22
+ /{collection}/feed.json
23
+ /{collection}/archive/{year}/{month}/rss.xml # when includeArchives
24
+ ```
25
+
26
+ `registry.json` lists schemas, formats, the drive key, and last update metadata.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ bun add @seedprotocol/feed-hyper
32
+ ```
33
+
34
+ Requires a toolchain that can build Holepunch native addons.
35
+
36
+ ## Library API
37
+
38
+ ```ts
39
+ import {
40
+ publishFeed,
41
+ openFeed,
42
+ seedFeed,
43
+ serveFeed,
44
+ localFeedUrl,
45
+ DEFAULT_SEED_FEED_HYPER_KEY,
46
+ } from '@seedprotocol/feed-hyper'
47
+
48
+ const session = await publishFeed({
49
+ schemas: ['post'],
50
+ formats: ['rss', 'atom', 'json'],
51
+ storePath: '.seed/feed-store',
52
+ announce: true, // keep swarm joined until session.close()
53
+ })
54
+
55
+ console.log(session.key, session.hyperUrl)
56
+
57
+ const opened = await openFeed({
58
+ key: session.key,
59
+ storePath: '.seed/feed-reader',
60
+ })
61
+ const rss = await opened.get('/posts/rss.xml')
62
+
63
+ const gateway = await serveFeed({
64
+ key: session.key,
65
+ storePath: '.seed/feed-reader',
66
+ port: 8080,
67
+ })
68
+ console.log(localFeedUrl(gateway.baseUrl, 'post', 'rss'))
69
+ ```
70
+
71
+ ### Fixtures / tests
72
+
73
+ Pass `fixtureContents: { '/posts/rss.xml': '...' }` to `publishFeed` to skip EAS generation.
74
+
75
+ ## CLI (via `@seedprotocol/cli`)
76
+
77
+ ```bash
78
+ seed feed publish --schema post --format rss,atom,json --store .seed/feed-store
79
+ seed feed seed <key> --store .seed/feed-store
80
+ seed feed serve <key> --port 8080 --host 127.0.0.1
81
+ ```
82
+
83
+ Use `--no-announce` on publish/serve to stay offline (local store only).
84
+
85
+ ## Operator notes
86
+
87
+ 1. Back up the Corestore directory for any key you care about (especially the official key).
88
+ 2. Run at least one always-on `seed feed seed <key>` (or leave `publish` announcing) so cold feeds resolve.
89
+ 3. Point RSS apps at `http://127.0.0.1:8080/posts/rss.xml` or `http://127.0.0.1:8080/posts/rss` after `seed feed serve`.
90
+ 4. Unpublish/revocation: regenerating omits revoked items from the **current** drive version; Hypercore history retains prior versions.
91
+
92
+ See [docs/FEED_HYPER.md](../../docs/FEED_HYPER.md) for the full runbook and manual validation checklist.
93
+
94
+ ## Tests
95
+
96
+ Path tests always run. Native Corestore/Hyperdrive/HTTP tests run when `FEED_HYPER_TESTS=1` or when not in CI (`FEED_HYPER_TESTS=0` to skip).
97
+
98
+ ```bash
99
+ cd packages/feed-hyper && bunx vitest run
100
+ FEED_HYPER_TESTS=1 bunx vitest run
101
+ ```
102
+
103
+ **Runtime note:** Holepunch natives are intended for **Node.js**. Running them under Bun may crash (`uv_get_osfhandle`). Prefer `node` / `tsx` for CLI and Vitest (which uses Node) for tests.
104
+
105
+ ## Manual validation checklist
106
+
107
+ See [docs/FEED_HYPER.md](../../docs/FEED_HYPER.md).
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_SEED_FEED_HYPER_KEY } from '@seedprotocol/arweave';
2
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,16 @@
1
+ export { feedDrivePath, feedHttpPath, resolveHttpToDrivePath, hyperFeedUrl, REGISTRY_PATH, } from './paths';
2
+ export type { ArchivePathOptions } from './paths';
3
+ export { openFeedStore, closeFeedStore, encodeKey, replicateStores, keyToBuffer, } from './store';
4
+ export type { FeedStoreHandles, OpenStoreOptions } from './store';
5
+ export { publishFeed } from './publishFeed';
6
+ export type { PublishFeedSession } from './publishFeed';
7
+ export { openFeed } from './openFeed';
8
+ export type { OpenedFeed } from './openFeed';
9
+ export { seedFeed } from './seedFeed';
10
+ export type { SeedFeedSession } from './seedFeed';
11
+ export { serveFeed, localFeedUrl } from './serveFeed';
12
+ export { watchFeed } from './watchFeed';
13
+ export type { WatchFeedOptions } from './watchFeed';
14
+ export type { PublishFeedOptions, PublishFeedResult, FeedRegistry, FeedRegistryEntry, OpenFeedOptions, SeedFeedOptions, ServeFeedOptions, ServeFeedResult, } from './types';
15
+ export { DEFAULT_SEED_FEED_HYPER_KEY } from './constants';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAA;AAChB,YAAY,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,OAAO,EACL,aAAa,EACb,cAAc,EACd,SAAS,EACT,eAAe,EACf,WAAW,GACZ,MAAM,SAAS,CAAA;AAChB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAEjE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAEvD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AACrC,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAErD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,eAAe,GAChB,MAAM,SAAS,CAAA;AAEhB,OAAO,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,272 @@
1
+ import e from "pluralize";
2
+ import t from "corestore";
3
+ import n from "hyperdrive";
4
+ import r from "hyperswarm";
5
+ import i from "hypercore-id-encoding";
6
+ import { createFeed as a, getFeedItemsBySchemaName as o } from "@seedprotocol/feed";
7
+ import s from "node:http";
8
+ import c from "serve-drive";
9
+ import { DEFAULT_SEED_FEED_HYPER_KEY as l } from "@seedprotocol/arweave";
10
+ //#region src/paths.ts
11
+ function u(t, n, r) {
12
+ let i = e(t.toLowerCase()), a = n === "json" ? "feed.json" : n === "atom" ? "atom.xml" : "rss.xml";
13
+ return r ? `/${i}/archive/${r.year}/${r.month}/${a}` : `/${i}/${a}`;
14
+ }
15
+ var d = "/registry.json";
16
+ function f(t, n) {
17
+ return `/${e(t.toLowerCase())}/${n}`;
18
+ }
19
+ function p(e) {
20
+ let t = e.startsWith("/") ? e : `/${e}`, n = t.length > 1 && t.endsWith("/") ? t.slice(0, -1) : t;
21
+ if (/\.(xml|json)$/i.test(n)) return n;
22
+ let r = n.match(/^(\/[^/]+(?:\/archive\/\d+\/\d+)?)\/(rss|atom|json)$/i);
23
+ if (!r) return n;
24
+ let i = r[1], a = r[2].toLowerCase();
25
+ return a === "json" ? `${i}/feed.json` : a === "atom" ? `${i}/atom.xml` : `${i}/rss.xml`;
26
+ }
27
+ function m(e) {
28
+ return `hyper://${e}`;
29
+ }
30
+ //#endregion
31
+ //#region src/store.ts
32
+ function h(e) {
33
+ return Buffer.from(i.decode(e));
34
+ }
35
+ function g(e) {
36
+ return i.normalize(e);
37
+ }
38
+ async function _(e) {
39
+ let i = new t(e.storePath);
40
+ await i.ready();
41
+ let a = e.key ? new n(i, h(e.key)) : new n(i, { name: e.driveName ?? "seed-feed" });
42
+ await a.ready();
43
+ let o = null;
44
+ return e.announce !== !1 && (o = new r(), o.on("connection", (e) => {
45
+ i.replicate(e);
46
+ }), o.join(a.discoveryKey, {
47
+ server: !0,
48
+ client: !0
49
+ }), await o.flush()), {
50
+ store: i,
51
+ drive: a,
52
+ swarm: o
53
+ };
54
+ }
55
+ async function v(e) {
56
+ e.swarm && await e.swarm.destroy(), await e.drive.close(), await e.store.close();
57
+ }
58
+ function y(e, t) {
59
+ let n = e.replicate(!0), r = t.replicate(!1);
60
+ return n.pipe(r).pipe(n), () => {
61
+ n.destroy?.(), r.destroy?.();
62
+ };
63
+ }
64
+ function b(e) {
65
+ return typeof e == "string" ? h(e) : Buffer.from(e);
66
+ }
67
+ //#endregion
68
+ //#region src/publishFeed.ts
69
+ var x = "https://seedprotocol.io", S = "Seed Protocol";
70
+ async function C(e) {
71
+ try {
72
+ let t = await import("@seedprotocol/feed"), n = typeof t.getSiteConfig == "function" ? t.getSiteConfig() : null;
73
+ return {
74
+ siteUrl: e?.siteUrl ?? n?.siteUrl ?? x,
75
+ title: e?.title ?? n?.title ?? S
76
+ };
77
+ } catch {
78
+ return {
79
+ siteUrl: e?.siteUrl ?? x,
80
+ title: e?.title ?? S
81
+ };
82
+ }
83
+ }
84
+ async function w(e, t, n) {
85
+ await e.put(t, Buffer.from(n, "utf-8"));
86
+ }
87
+ async function T(e, t, n) {
88
+ let r = m(t), i = n.pageSize ?? 25, s = [], c = [];
89
+ if (n.fixtureContents) {
90
+ for (let [t, r] of Object.entries(n.fixtureContents)) await w(e, t, r), s.push(t);
91
+ c.push({
92
+ schema: n.schemas[0],
93
+ formats: n.formats,
94
+ paths: [...s]
95
+ });
96
+ } else {
97
+ let { siteUrl: t, title: l } = await C({ siteUrl: n.siteUrl });
98
+ for (let d of n.schemas) {
99
+ let p = [], m = await o(d, {
100
+ limit: i,
101
+ skip: 0
102
+ }), h = m.length === i;
103
+ for (let o of n.formats) {
104
+ let n = u(d, o);
105
+ await w(e, n, await a(m, d, o, void 0, {
106
+ page: 1,
107
+ pageSize: i,
108
+ hasNext: h,
109
+ baseUrl: `${r}${f(d, o)}`
110
+ }, void 0, !1, void 0, {
111
+ feedUrl: r,
112
+ siteUrl: t,
113
+ title: l
114
+ })), s.push(n), p.push(n);
115
+ }
116
+ if (n.includeArchives) {
117
+ let i = /* @__PURE__ */ new Date(), o = i.getFullYear(), c = i.getMonth() + 1;
118
+ for (let i of n.formats) {
119
+ let n = u(d, i, {
120
+ year: o,
121
+ month: c
122
+ });
123
+ await w(e, n, await a(m, d, i, void 0, void 0, [{
124
+ rel: "current",
125
+ href: `${r}${f(d, i)}`
126
+ }], !0, void 0, {
127
+ feedUrl: r,
128
+ siteUrl: t
129
+ })), s.push(n), p.push(n);
130
+ }
131
+ }
132
+ c.push({
133
+ schema: d,
134
+ formats: [...n.formats],
135
+ paths: p
136
+ });
137
+ }
138
+ }
139
+ let l = {
140
+ key: t,
141
+ version: e.version,
142
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
143
+ schemas: c
144
+ };
145
+ return await w(e, d, JSON.stringify(l, null, 2)), s.push(d), {
146
+ paths: s,
147
+ hyperUrl: r
148
+ };
149
+ }
150
+ async function E(e) {
151
+ if (!e.schemas.length) throw Error("publishFeed: schemas must be non-empty");
152
+ if (!e.formats.length) throw Error("publishFeed: formats must be non-empty");
153
+ let t = e.announce !== !1, n = await _({
154
+ storePath: e.storePath,
155
+ driveName: e.driveName,
156
+ announce: t
157
+ }), r = g(n.drive.key), { paths: i, hyperUrl: a } = await T(n.drive, r, e), o = n.drive.version, s = !1, c = async () => {
158
+ s || (s = !0, await v(n));
159
+ };
160
+ return t || await c(), {
161
+ key: r,
162
+ version: o,
163
+ paths: i,
164
+ hyperUrl: a,
165
+ close: c
166
+ };
167
+ }
168
+ //#endregion
169
+ //#region src/openFeed.ts
170
+ async function D(e) {
171
+ let t = await _({
172
+ storePath: e.storePath,
173
+ key: e.key,
174
+ announce: e.announce !== !1
175
+ }), n = e.syncTimeoutMs ?? 15e3;
176
+ t.drive.core.length === 0 && n > 0 && await Promise.race([t.drive.core.update({ wait: !0 }), new Promise((e) => setTimeout(e, n))]).catch(() => {});
177
+ let r = async (e) => {
178
+ let n = await t.drive.get(e);
179
+ return n ? Buffer.from(n).toString("utf-8") : null;
180
+ };
181
+ return {
182
+ key: g(t.drive.key),
183
+ version: t.drive.version,
184
+ drive: t.drive,
185
+ get: r,
186
+ getRegistry: async () => {
187
+ let e = await r(d);
188
+ if (!e) return null;
189
+ try {
190
+ return JSON.parse(e);
191
+ } catch {
192
+ return null;
193
+ }
194
+ },
195
+ close: () => v(t)
196
+ };
197
+ }
198
+ //#endregion
199
+ //#region src/seedFeed.ts
200
+ async function O(e) {
201
+ let t = await _({
202
+ storePath: e.storePath,
203
+ key: e.key,
204
+ announce: !0
205
+ });
206
+ try {
207
+ await t.drive.core.update({ wait: !1 });
208
+ } catch {}
209
+ return {
210
+ key: g(t.drive.key),
211
+ close: () => v(t)
212
+ };
213
+ }
214
+ //#endregion
215
+ //#region src/serveFeed.ts
216
+ function k(e) {
217
+ e.on("request", (e) => {
218
+ if (!e.url) return;
219
+ let t = e.url.indexOf("?"), n = t === -1 ? e.url : e.url.slice(0, t), r = t === -1 ? "" : e.url.slice(t), i = p(n);
220
+ i !== n && (e.url = i + r);
221
+ });
222
+ }
223
+ async function A(e) {
224
+ let t = e.host ?? "127.0.0.1", n = e.port ?? 8080, r = await _({
225
+ storePath: e.storePath,
226
+ key: e.key,
227
+ announce: e.announce !== !1
228
+ });
229
+ try {
230
+ await r.drive.core.update({ wait: !1 });
231
+ } catch {}
232
+ let i = g(r.drive.key), a = s.createServer();
233
+ k(a);
234
+ let o = new c({
235
+ server: a,
236
+ port: n,
237
+ host: t,
238
+ anyPort: n === 0,
239
+ token: !1,
240
+ get: async ({ key: e }) => e && !e.equals(r.drive.key) ? null : r.drive
241
+ });
242
+ await o.ready();
243
+ let l = typeof o.address == "function" ? o.address()?.port ?? n : n;
244
+ return {
245
+ key: i,
246
+ port: l,
247
+ host: t,
248
+ baseUrl: `http://${t}:${l}`,
249
+ close: async () => {
250
+ typeof o.close == "function" ? await o.close() : typeof o.suspend == "function" && await o.suspend(), await v(r);
251
+ }
252
+ };
253
+ }
254
+ function j(e, t, n) {
255
+ return `${e.replace(/\/$/, "")}${u(t, n)}`;
256
+ }
257
+ //#endregion
258
+ //#region src/watchFeed.ts
259
+ function M(e, t) {
260
+ let n = e.core, r = !1, i = () => {
261
+ r || Promise.resolve(t.onUpdate(e.version)).catch((e) => {
262
+ console.error("[feed-hyper] watchFeed onUpdate error:", e);
263
+ });
264
+ };
265
+ return n.on("append", i), () => {
266
+ r = !0, n.off("append", i);
267
+ };
268
+ }
269
+ //#endregion
270
+ export { l as DEFAULT_SEED_FEED_HYPER_KEY, d as REGISTRY_PATH, v as closeFeedStore, g as encodeKey, u as feedDrivePath, f as feedHttpPath, m as hyperFeedUrl, b as keyToBuffer, j as localFeedUrl, D as openFeed, _ as openFeedStore, E as publishFeed, y as replicateStores, p as resolveHttpToDrivePath, O as seedFeed, A as serveFeed, M as watchFeed };
271
+
272
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/paths.ts","../src/store.ts","../src/publishFeed.ts","../src/openFeed.ts","../src/seedFeed.ts","../src/serveFeed.ts","../src/watchFeed.ts"],"sourcesContent":["import pluralize from 'pluralize'\nimport type { FeedFormat } from '@seedprotocol/feed'\n\nexport type ArchivePathOptions = {\n year: number\n month: number\n}\n\n/**\n * Map schema + format (+ optional archive) to a Hyperdrive path.\n * Layout mirrors feed HTTP routes with file extensions for Content-Type detection:\n * /posts/rss.xml\n * /posts/atom.xml\n * /posts/feed.json\n * /posts/archive/2024/2/rss.xml\n */\nexport function feedDrivePath(\n schemaName: string,\n format: FeedFormat,\n archive?: ArchivePathOptions,\n): string {\n const collection = pluralize(schemaName.toLowerCase())\n const file =\n format === 'json' ? 'feed.json' : format === 'atom' ? 'atom.xml' : 'rss.xml'\n\n if (archive) {\n return `/${collection}/archive/${archive.year}/${archive.month}/${file}`\n }\n return `/${collection}/${file}`\n}\n\n/** Registry manifest path on every published feed drive. */\nexport const REGISTRY_PATH = '/registry.json'\n\n/**\n * HTTP path segment for createFeed / channel self-links (no file extension),\n * matching historical feed.seedprotocol.io routes: /posts/rss\n */\nexport function feedHttpPath(schemaName: string, format: FeedFormat): string {\n const collection = pluralize(schemaName.toLowerCase())\n return `/${collection}/${format}`\n}\n\n/**\n * Map a gateway request pathname to the on-drive file path.\n * Accepts both historical HTTP shapes (`/posts/rss`) and drive shapes (`/posts/rss.xml`).\n * Unknown paths pass through unchanged.\n */\nexport function resolveHttpToDrivePath(pathname: string): string {\n const path = pathname.startsWith('/') ? pathname : `/${pathname}`\n const trimmed = path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path\n\n // Already a drive file path with a known feed extension\n if (/\\.(xml|json)$/i.test(trimmed)) return trimmed\n\n // /{collection}/rss|atom|json\n // /{collection}/archive/{year}/{month}/rss|atom|json\n const match = trimmed.match(\n /^(\\/[^/]+(?:\\/archive\\/\\d+\\/\\d+)?)\\/(rss|atom|json)$/i,\n )\n if (!match) return trimmed\n\n const base = match[1]!\n const format = match[2]!.toLowerCase()\n if (format === 'json') return `${base}/feed.json`\n if (format === 'atom') return `${base}/atom.xml`\n return `${base}/rss.xml`\n}\n\nexport function hyperFeedUrl(keyZ32: string): string {\n return `hyper://${keyZ32}`\n}\n","import Corestore from 'corestore'\nimport Hyperdrive from 'hyperdrive'\nimport Hyperswarm from 'hyperswarm'\nimport ID from 'hypercore-id-encoding'\n\nexport type FeedStoreHandles = {\n store: Corestore\n drive: Hyperdrive\n swarm: Hyperswarm | null\n}\n\nexport type OpenStoreOptions = {\n storePath: string\n /** Named core for a writable publisher drive (default: seed-feed). Ignored when `key` is set. */\n driveName?: string\n /** z32 or hex public key for a readonly / seeder drive */\n key?: string\n /** Join Hyperswarm and announce/lookup the drive discovery key (default: true) */\n announce?: boolean\n}\n\nfunction decodeKey(key: string): Buffer {\n return Buffer.from(ID.decode(key))\n}\n\nexport function encodeKey(key: Buffer | Uint8Array): string {\n return ID.normalize(key)\n}\n\n/**\n * Open a Corestore + Hyperdrive. With `key`, opens that drive (replicate).\n * Without `key`, opens/creates a named writable drive.\n */\nexport async function openFeedStore(options: OpenStoreOptions): Promise<FeedStoreHandles> {\n const store = new Corestore(options.storePath)\n await store.ready()\n\n const drive = options.key\n ? new Hyperdrive(store, decodeKey(options.key))\n : new Hyperdrive(store, { name: options.driveName ?? 'seed-feed' })\n\n await drive.ready()\n\n let swarm: Hyperswarm | null = null\n if (options.announce !== false) {\n swarm = new Hyperswarm()\n swarm.on('connection', (conn: unknown) => {\n store.replicate(conn as Parameters<Corestore['replicate']>[0])\n })\n swarm.join(drive.discoveryKey, { server: true, client: true })\n await swarm.flush()\n }\n\n return { store, drive, swarm }\n}\n\nexport async function closeFeedStore(handles: FeedStoreHandles): Promise<void> {\n if (handles.swarm) {\n await handles.swarm.destroy()\n }\n await handles.drive.close()\n await handles.store.close()\n}\n\n/** In-process duplex replicate between two corestores (for tests / tooling). */\nexport function replicateStores(a: Corestore, b: Corestore): () => void {\n const s1 = a.replicate(true) as NodeJS.ReadWriteStream & { destroy?: () => void }\n const s2 = b.replicate(false) as NodeJS.ReadWriteStream & { destroy?: () => void }\n s1.pipe(s2).pipe(s1)\n return () => {\n s1.destroy?.()\n s2.destroy?.()\n }\n}\n\nexport function keyToBuffer(key: string | Buffer | Uint8Array): Buffer {\n if (typeof key === 'string') return decodeKey(key)\n return Buffer.from(key)\n}\n\nexport { ID }\n","import {\n createFeed,\n getFeedItemsBySchemaName,\n type GraphQLItem,\n} from '@seedprotocol/feed'\nimport {\n REGISTRY_PATH,\n feedDrivePath,\n feedHttpPath,\n hyperFeedUrl,\n} from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore, type FeedStoreHandles } from './store'\nimport type { FeedRegistry, PublishFeedOptions, PublishFeedResult } from './types'\n\nconst DEFAULT_SITE_URL = 'https://seedprotocol.io'\nconst DEFAULT_FEED_TITLE = 'Seed Protocol'\n\nasync function resolveSiteDefaults(overrides?: {\n siteUrl?: string\n title?: string\n}): Promise<{ siteUrl: string; title: string }> {\n try {\n const feed = await import('@seedprotocol/feed')\n const cfg =\n typeof feed.getSiteConfig === 'function' ? feed.getSiteConfig() : null\n return {\n siteUrl: overrides?.siteUrl ?? cfg?.siteUrl ?? DEFAULT_SITE_URL,\n title: overrides?.title ?? cfg?.title ?? DEFAULT_FEED_TITLE,\n }\n } catch {\n return {\n siteUrl: overrides?.siteUrl ?? DEFAULT_SITE_URL,\n title: overrides?.title ?? DEFAULT_FEED_TITLE,\n }\n }\n}\n\nasync function putUtf8(\n drive: { put: (path: string, buf: Buffer) => Promise<unknown> },\n path: string,\n content: string,\n): Promise<void> {\n await drive.put(path, Buffer.from(content, 'utf-8'))\n}\n\nasync function writeFeedsToDrive(\n drive: FeedStoreHandles['drive'],\n key: string,\n options: PublishFeedOptions,\n): Promise<{ paths: string[]; hyperUrl: string }> {\n const hyperUrl = hyperFeedUrl(key)\n const pageSize = options.pageSize ?? 25\n const paths: string[] = []\n const registrySchemas: FeedRegistry['schemas'] = []\n\n if (options.fixtureContents) {\n for (const [path, content] of Object.entries(options.fixtureContents)) {\n await putUtf8(drive, path, content)\n paths.push(path)\n }\n registrySchemas.push({\n schema: options.schemas[0]!,\n formats: options.formats,\n paths: [...paths],\n })\n } else {\n const { siteUrl, title } = await resolveSiteDefaults({\n siteUrl: options.siteUrl,\n })\n for (const schema of options.schemas) {\n const schemaPaths: string[] = []\n const items = (await getFeedItemsBySchemaName(schema, {\n limit: pageSize,\n skip: 0,\n })) as GraphQLItem[]\n const hasNext = items.length === pageSize\n\n for (const format of options.formats) {\n const drivePath = feedDrivePath(schema, format)\n const baseUrl = `${hyperUrl}${feedHttpPath(schema, format)}`\n const content = await createFeed(\n items,\n schema,\n format,\n undefined,\n {\n page: 1,\n pageSize,\n hasNext,\n baseUrl,\n },\n undefined,\n false,\n undefined,\n {\n feedUrl: hyperUrl,\n siteUrl,\n title,\n },\n )\n await putUtf8(drive, drivePath, content)\n paths.push(drivePath)\n schemaPaths.push(drivePath)\n }\n\n if (options.includeArchives) {\n const now = new Date()\n const year = now.getFullYear()\n const month = now.getMonth() + 1\n for (const format of options.formats) {\n const archivePath = feedDrivePath(schema, format, { year, month })\n const archiveContent = await createFeed(\n items,\n schema,\n format,\n undefined,\n undefined,\n [\n {\n rel: 'current',\n href: `${hyperUrl}${feedHttpPath(schema, format)}`,\n },\n ],\n true,\n undefined,\n { feedUrl: hyperUrl, siteUrl },\n )\n await putUtf8(drive, archivePath, archiveContent)\n paths.push(archivePath)\n schemaPaths.push(archivePath)\n }\n }\n\n registrySchemas.push({\n schema,\n formats: [...options.formats],\n paths: schemaPaths,\n })\n }\n }\n\n const registry: FeedRegistry = {\n key,\n version: drive.version,\n updatedAt: new Date().toISOString(),\n schemas: registrySchemas,\n }\n await putUtf8(drive, REGISTRY_PATH, JSON.stringify(registry, null, 2))\n paths.push(REGISTRY_PATH)\n\n return { paths, hyperUrl }\n}\n\nexport type PublishFeedSession = PublishFeedResult & {\n /** Close Corestore / Hyperdrive / Hyperswarm. No-op if already closed. */\n close: () => Promise<void>\n}\n\n/**\n * Generate feeds via `@seedprotocol/feed` and write them into a Hyperdrive.\n *\n * - `announce: false` — write, close store, return (one-shot).\n * - `announce: true` (default) — write, keep swarm joined until `close()`.\n */\nexport async function publishFeed(\n options: PublishFeedOptions,\n): Promise<PublishFeedSession> {\n if (!options.schemas.length) {\n throw new Error('publishFeed: schemas must be non-empty')\n }\n if (!options.formats.length) {\n throw new Error('publishFeed: formats must be non-empty')\n }\n\n const announce = options.announce !== false\n const handles = await openFeedStore({\n storePath: options.storePath,\n driveName: options.driveName,\n announce,\n })\n\n const key = encodeKey(handles.drive.key)\n const { paths, hyperUrl } = await writeFeedsToDrive(handles.drive, key, options)\n const version = handles.drive.version\n\n let closed = false\n const close = async () => {\n if (closed) return\n closed = true\n await closeFeedStore(handles)\n }\n\n if (!announce) {\n await close()\n }\n\n return {\n key,\n version,\n paths,\n hyperUrl,\n close,\n }\n}\n","import { REGISTRY_PATH } from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore, type FeedStoreHandles } from './store'\nimport type { FeedRegistry, OpenFeedOptions } from './types'\n\nexport type OpenedFeed = {\n key: string\n version: number\n drive: FeedStoreHandles['drive']\n get: (path: string) => Promise<string | null>\n getRegistry: () => Promise<FeedRegistry | null>\n close: () => Promise<void>\n}\n\n/**\n * Open a feed Hyperdrive by public key, join the swarm, and optionally wait for data.\n */\nexport async function openFeed(options: OpenFeedOptions): Promise<OpenedFeed> {\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: options.announce !== false,\n })\n\n const syncTimeoutMs = options.syncTimeoutMs ?? 15_000\n if (handles.drive.core.length === 0 && syncTimeoutMs > 0) {\n await Promise.race([\n handles.drive.core.update({ wait: true }),\n new Promise<void>((resolve) => setTimeout(resolve, syncTimeoutMs)),\n ]).catch(() => {\n /* timeout or update failure — caller may still read if peers arrive later */\n })\n }\n\n const get = async (path: string): Promise<string | null> => {\n const buf = await handles.drive.get(path)\n if (!buf) return null\n return Buffer.from(buf).toString('utf-8')\n }\n\n const getRegistry = async (): Promise<FeedRegistry | null> => {\n const raw = await get(REGISTRY_PATH)\n if (!raw) return null\n try {\n return JSON.parse(raw) as FeedRegistry\n } catch {\n return null\n }\n }\n\n return {\n key: encodeKey(handles.drive.key),\n version: handles.drive.version,\n drive: handles.drive,\n get,\n getRegistry,\n close: () => closeFeedStore(handles),\n }\n}\n","import { closeFeedStore, encodeKey, openFeedStore } from './store'\nimport type { SeedFeedOptions } from './types'\n\nexport type SeedFeedSession = {\n key: string\n close: () => Promise<void>\n}\n\n/**\n * Join the swarm and replicate a feed drive until `close()` (or process exit).\n * Does not generate feed content.\n */\nexport async function seedFeed(options: SeedFeedOptions): Promise<SeedFeedSession> {\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: true,\n })\n\n // Prefer longer cores from peers\n try {\n await handles.drive.core.update({ wait: false })\n } catch {\n /* ignore */\n }\n\n return {\n key: encodeKey(handles.drive.key),\n close: () => closeFeedStore(handles),\n }\n}\n","import http from 'node:http'\nimport ServeDrive from 'serve-drive'\nimport { feedDrivePath, resolveHttpToDrivePath } from './paths'\nimport { closeFeedStore, encodeKey, openFeedStore } from './store'\nimport type { ServeFeedOptions, ServeFeedResult } from './types'\n\n/**\n * Rewrite extensionless feed URLs (`/posts/rss`) to drive paths (`/posts/rss.xml`)\n * so serve-drive looks up the real file and gets the correct Content-Type.\n */\nfunction attachFeedPathRewrite(server: http.Server): void {\n server.on('request', (req) => {\n if (!req.url) return\n const q = req.url.indexOf('?')\n const pathname = q === -1 ? req.url : req.url.slice(0, q)\n const query = q === -1 ? '' : req.url.slice(q)\n const resolved = resolveHttpToDrivePath(pathname)\n if (resolved !== pathname) {\n req.url = resolved + query\n }\n })\n}\n\n/**\n * Seed a feed drive and expose it over localhost HTTP via serve-drive.\n * Serves both drive paths (`/posts/rss.xml`) and historical HTTP paths (`/posts/rss`).\n */\nexport async function serveFeed(options: ServeFeedOptions): Promise<ServeFeedResult> {\n const host = options.host ?? '127.0.0.1'\n const port = options.port ?? 8080\n\n const handles = await openFeedStore({\n storePath: options.storePath,\n key: options.key,\n announce: options.announce !== false,\n })\n\n try {\n await handles.drive.core.update({ wait: false })\n } catch {\n /* ignore */\n }\n\n const key = encodeKey(handles.drive.key)\n\n const server = http.createServer()\n // Must register before ServeDrive attaches its handler so rewrite runs first.\n attachFeedPathRewrite(server)\n\n const serve = new ServeDrive({\n server,\n port,\n host,\n anyPort: port === 0,\n // Local RSS gateways need unauthenticated GETs; enable token for public hosts via env later\n token: false,\n get: async ({ key: requestKey }: { key: Buffer | null; filename: string; version: number }) => {\n if (requestKey && !requestKey.equals(handles.drive.key)) {\n return null\n }\n return handles.drive\n },\n })\n\n await serve.ready()\n\n const boundPort =\n typeof serve.address === 'function'\n ? (serve.address()?.port ?? port)\n : port\n\n const baseUrl = `http://${host}:${boundPort}`\n\n return {\n key,\n port: boundPort,\n host,\n baseUrl,\n close: async () => {\n if (typeof serve.close === 'function') {\n await serve.close()\n } else if (typeof (serve as { suspend?: () => Promise<void> }).suspend === 'function') {\n await (serve as { suspend: () => Promise<void> }).suspend()\n }\n await closeFeedStore(handles)\n },\n }\n}\n\n/** Example local URL for a schema/format after serveFeed (drive path with extension). */\nexport function localFeedUrl(\n baseUrl: string,\n schemaName: string,\n format: 'rss' | 'atom' | 'json',\n): string {\n return `${baseUrl.replace(/\\/$/, '')}${feedDrivePath(schemaName, format)}`\n}\n","import type Hyperdrive from 'hyperdrive'\n\nexport type WatchFeedOptions = {\n /** Called whenever the drive version increases. */\n onUpdate: (version: number) => void | Promise<void>\n}\n\n/**\n * Watch a Hyperdrive for new versions (append-only length growth).\n * Returns an unsubscribe function.\n */\nexport function watchFeed(\n drive: Hyperdrive,\n options: WatchFeedOptions,\n): () => void {\n const core = drive.core\n let stopped = false\n\n const onAppend = () => {\n if (stopped) return\n void Promise.resolve(options.onUpdate(drive.version)).catch((err) => {\n console.error('[feed-hyper] watchFeed onUpdate error:', err)\n })\n }\n\n core.on('append', onAppend)\n\n return () => {\n stopped = true\n core.off('append', onAppend)\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type FeedStoreHandles } from './store';
2
+ import type { FeedRegistry, OpenFeedOptions } from './types';
3
+ export type OpenedFeed = {
4
+ key: string;
5
+ version: number;
6
+ drive: FeedStoreHandles['drive'];
7
+ get: (path: string) => Promise<string | null>;
8
+ getRegistry: () => Promise<FeedRegistry | null>;
9
+ close: () => Promise<void>;
10
+ };
11
+ /**
12
+ * Open a feed Hyperdrive by public key, join the swarm, and optionally wait for data.
13
+ */
14
+ export declare function openFeed(options: OpenFeedOptions): Promise<OpenedFeed>;
15
+ //# sourceMappingURL=openFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openFeed.d.ts","sourceRoot":"","sources":["../src/openFeed.ts"],"names":[],"mappings":"AACA,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,SAAS,CAAA;AACzF,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE5D,MAAM,MAAM,UAAU,GAAG;IACvB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAChC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC7C,WAAW,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;IAC/C,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CAyC5E"}
@@ -0,0 +1,29 @@
1
+ import type { FeedFormat } from '@seedprotocol/feed';
2
+ export type ArchivePathOptions = {
3
+ year: number;
4
+ month: number;
5
+ };
6
+ /**
7
+ * Map schema + format (+ optional archive) to a Hyperdrive path.
8
+ * Layout mirrors feed HTTP routes with file extensions for Content-Type detection:
9
+ * /posts/rss.xml
10
+ * /posts/atom.xml
11
+ * /posts/feed.json
12
+ * /posts/archive/2024/2/rss.xml
13
+ */
14
+ export declare function feedDrivePath(schemaName: string, format: FeedFormat, archive?: ArchivePathOptions): string;
15
+ /** Registry manifest path on every published feed drive. */
16
+ export declare const REGISTRY_PATH = "/registry.json";
17
+ /**
18
+ * HTTP path segment for createFeed / channel self-links (no file extension),
19
+ * matching historical feed.seedprotocol.io routes: /posts/rss
20
+ */
21
+ export declare function feedHttpPath(schemaName: string, format: FeedFormat): string;
22
+ /**
23
+ * Map a gateway request pathname to the on-drive file path.
24
+ * Accepts both historical HTTP shapes (`/posts/rss`) and drive shapes (`/posts/rss.xml`).
25
+ * Unknown paths pass through unchanged.
26
+ */
27
+ export declare function resolveHttpToDrivePath(pathname: string): string;
28
+ export declare function hyperFeedUrl(keyZ32: string): string;
29
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,MAAM,CASR;AAED,4DAA4D;AAC5D,eAAO,MAAM,aAAa,mBAAmB,CAAA;AAE7C;;;GAGG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,MAAM,CAG3E;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAmB/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,13 @@
1
+ import type { PublishFeedOptions, PublishFeedResult } from './types';
2
+ export type PublishFeedSession = PublishFeedResult & {
3
+ /** Close Corestore / Hyperdrive / Hyperswarm. No-op if already closed. */
4
+ close: () => Promise<void>;
5
+ };
6
+ /**
7
+ * Generate feeds via `@seedprotocol/feed` and write them into a Hyperdrive.
8
+ *
9
+ * - `announce: false` — write, close store, return (one-shot).
10
+ * - `announce: true` (default) — write, keep swarm joined until `close()`.
11
+ */
12
+ export declare function publishFeed(options: PublishFeedOptions): Promise<PublishFeedSession>;
13
+ //# sourceMappingURL=publishFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publishFeed.d.ts","sourceRoot":"","sources":["../src/publishFeed.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAgB,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AA6IlF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,0EAA0E;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,kBAAkB,CAAC,CAqC7B"}
@@ -0,0 +1,11 @@
1
+ import type { SeedFeedOptions } from './types';
2
+ export type SeedFeedSession = {
3
+ key: string;
4
+ close: () => Promise<void>;
5
+ };
6
+ /**
7
+ * Join the swarm and replicate a feed drive until `close()` (or process exit).
8
+ * Does not generate feed content.
9
+ */
10
+ export declare function seedFeed(options: SeedFeedOptions): Promise<SeedFeedSession>;
11
+ //# sourceMappingURL=seedFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seedFeed.d.ts","sourceRoot":"","sources":["../src/seedFeed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAkBjF"}
@@ -0,0 +1,9 @@
1
+ import type { ServeFeedOptions, ServeFeedResult } from './types';
2
+ /**
3
+ * Seed a feed drive and expose it over localhost HTTP via serve-drive.
4
+ * Serves both drive paths (`/posts/rss.xml`) and historical HTTP paths (`/posts/rss`).
5
+ */
6
+ export declare function serveFeed(options: ServeFeedOptions): Promise<ServeFeedResult>;
7
+ /** Example local URL for a schema/format after serveFeed (drive path with extension). */
8
+ export declare function localFeedUrl(baseUrl: string, schemaName: string, format: 'rss' | 'atom' | 'json'): string;
9
+ //# sourceMappingURL=serveFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serveFeed.d.ts","sourceRoot":"","sources":["../src/serveFeed.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAmBhE;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA4DnF;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,GAC9B,MAAM,CAER"}
@@ -0,0 +1,30 @@
1
+ import Corestore from 'corestore';
2
+ import Hyperdrive from 'hyperdrive';
3
+ import Hyperswarm from 'hyperswarm';
4
+ import ID from 'hypercore-id-encoding';
5
+ export type FeedStoreHandles = {
6
+ store: Corestore;
7
+ drive: Hyperdrive;
8
+ swarm: Hyperswarm | null;
9
+ };
10
+ export type OpenStoreOptions = {
11
+ storePath: string;
12
+ /** Named core for a writable publisher drive (default: seed-feed). Ignored when `key` is set. */
13
+ driveName?: string;
14
+ /** z32 or hex public key for a readonly / seeder drive */
15
+ key?: string;
16
+ /** Join Hyperswarm and announce/lookup the drive discovery key (default: true) */
17
+ announce?: boolean;
18
+ };
19
+ export declare function encodeKey(key: Buffer | Uint8Array): string;
20
+ /**
21
+ * Open a Corestore + Hyperdrive. With `key`, opens that drive (replicate).
22
+ * Without `key`, opens/creates a named writable drive.
23
+ */
24
+ export declare function openFeedStore(options: OpenStoreOptions): Promise<FeedStoreHandles>;
25
+ export declare function closeFeedStore(handles: FeedStoreHandles): Promise<void>;
26
+ /** In-process duplex replicate between two corestores (for tests / tooling). */
27
+ export declare function replicateStores(a: Corestore, b: Corestore): () => void;
28
+ export declare function keyToBuffer(key: string | Buffer | Uint8Array): Buffer;
29
+ export { ID };
30
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAA;AACjC,OAAO,UAAU,MAAM,YAAY,CAAA;AACnC,OAAO,UAAU,MAAM,YAAY,CAAA;AACnC,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAEtC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,EAAE,UAAU,CAAA;IACjB,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,iGAAiG;IACjG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAMD,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAE1D;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqBxF;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAM7E;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,MAAM,IAAI,CAQtE;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,CAGrE;AAED,OAAO,EAAE,EAAE,EAAE,CAAA"}
@@ -0,0 +1,60 @@
1
+ import type { FeedFormat } from '@seedprotocol/feed';
2
+ export type PublishFeedOptions = {
3
+ schemas: string[];
4
+ formats: FeedFormat[];
5
+ storePath: string;
6
+ driveName?: string;
7
+ pageSize?: number;
8
+ announce?: boolean;
9
+ includeArchives?: boolean;
10
+ /** Override channel site URL (home page). Default: current feed site config. */
11
+ siteUrl?: string;
12
+ /**
13
+ * When set, skip EAS/network generation and write these bodies instead.
14
+ * Keys are drive paths (e.g. /posts/rss.xml). Used in tests.
15
+ */
16
+ fixtureContents?: Record<string, string>;
17
+ };
18
+ export type PublishFeedResult = {
19
+ key: string;
20
+ version: number;
21
+ paths: string[];
22
+ hyperUrl: string;
23
+ };
24
+ export type FeedRegistryEntry = {
25
+ schema: string;
26
+ formats: FeedFormat[];
27
+ paths: string[];
28
+ };
29
+ export type FeedRegistry = {
30
+ key: string;
31
+ version: number;
32
+ updatedAt: string;
33
+ schemas: FeedRegistryEntry[];
34
+ };
35
+ export type OpenFeedOptions = {
36
+ key: string;
37
+ storePath: string;
38
+ announce?: boolean;
39
+ /** Max ms to wait for initial sync when length is 0 (default: 15000). */
40
+ syncTimeoutMs?: number;
41
+ };
42
+ export type SeedFeedOptions = {
43
+ key: string;
44
+ storePath: string;
45
+ };
46
+ export type ServeFeedOptions = {
47
+ key: string;
48
+ storePath: string;
49
+ port?: number;
50
+ host?: string;
51
+ announce?: boolean;
52
+ };
53
+ export type ServeFeedResult = {
54
+ key: string;
55
+ port: number;
56
+ host: string;
57
+ baseUrl: string;
58
+ close: () => Promise<void>;
59
+ };
60
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,UAAU,EAAE,CAAA;IACrB,KAAK,EAAE,MAAM,EAAE,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,iBAAiB,EAAE,CAAA;CAC7B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B,CAAA"}
@@ -0,0 +1,11 @@
1
+ import type Hyperdrive from 'hyperdrive';
2
+ export type WatchFeedOptions = {
3
+ /** Called whenever the drive version increases. */
4
+ onUpdate: (version: number) => void | Promise<void>;
5
+ };
6
+ /**
7
+ * Watch a Hyperdrive for new versions (append-only length growth).
8
+ * Returns an unsubscribe function.
9
+ */
10
+ export declare function watchFeed(drive: Hyperdrive, options: WatchFeedOptions): () => void;
11
+ //# sourceMappingURL=watchFeed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchFeed.d.ts","sourceRoot":"","sources":["../src/watchFeed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,YAAY,CAAA;AAExC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,mDAAmD;IACnD,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpD,CAAA;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,gBAAgB,GACxB,MAAM,IAAI,CAiBZ"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@seedprotocol/feed-hyper",
3
+ "version": "0.5.2",
4
+ "description": "Hyperdrive / Hyperswarm distribution for Seed Protocol feeds",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "./package.json": "./package.json"
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "scripts": {
27
+ "build": "rimraf dist && tsc -p tsconfig.build.json && vite build",
28
+ "test": "vitest run --reporter=default",
29
+ "prepublishOnly": "bun run build && node ../../scripts/sync-versions.js"
30
+ },
31
+ "dependencies": {
32
+ "@seedprotocol/arweave": "0.5.2",
33
+ "@seedprotocol/feed": "0.5.2",
34
+ "corestore": "^7.12.0",
35
+ "hypercore-id-encoding": "^1.3.0",
36
+ "hyperdrive": "^13.0.0",
37
+ "hyperswarm": "^4.14.0",
38
+ "pluralize": "^8.0.0",
39
+ "serve-drive": "^5.2.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "latest",
43
+ "@types/pluralize": "^0.0.33",
44
+ "rimraf": "^6.0.1",
45
+ "typescript": "~5.9.3",
46
+ "vite": "^8.0.0",
47
+ "vitest": "^3.0.0"
48
+ },
49
+ "author": "Keith Axline <keith@journodao.xyz>",
50
+ "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }