@activeledger/nano-gateway 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +56 -0
- package/es/allowlist.d.ts +15 -0
- package/es/allowlist.d.ts.map +1 -0
- package/es/allowlist.js +35 -0
- package/es/allowlist.js.map +1 -0
- package/es/auth.d.ts +12 -0
- package/es/auth.d.ts.map +1 -0
- package/es/auth.js +38 -0
- package/es/auth.js.map +1 -0
- package/es/changesWatcher.d.ts +13 -0
- package/es/changesWatcher.d.ts.map +1 -0
- package/es/changesWatcher.js +30 -0
- package/es/changesWatcher.js.map +1 -0
- package/es/default.config.json +18 -0
- package/es/index.d.ts +3 -0
- package/es/index.d.ts.map +1 -0
- package/es/index.js +26 -0
- package/es/index.js.map +1 -0
- package/es/routes/stream.d.ts +5 -0
- package/es/routes/stream.d.ts.map +1 -0
- package/es/routes/stream.js +21 -0
- package/es/routes/stream.js.map +1 -0
- package/es/routes/subscribe.d.ts +18 -0
- package/es/routes/subscribe.d.ts.map +1 -0
- package/es/routes/subscribe.js +33 -0
- package/es/routes/subscribe.js.map +1 -0
- package/es/routes/tx.d.ts +4 -0
- package/es/routes/tx.d.ts.map +1 -0
- package/es/routes/tx.js +10 -0
- package/es/routes/tx.js.map +1 -0
- package/es/server.d.ts +8 -0
- package/es/server.d.ts.map +1 -0
- package/es/server.js +38 -0
- package/es/server.js.map +1 -0
- package/es/sse.d.ts +18 -0
- package/es/sse.d.ts.map +1 -0
- package/es/sse.js +43 -0
- package/es/sse.js.map +1 -0
- package/lib/allowlist.d.ts +15 -0
- package/lib/allowlist.d.ts.map +1 -0
- package/lib/allowlist.js +39 -0
- package/lib/allowlist.js.map +1 -0
- package/lib/auth.d.ts +12 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +41 -0
- package/lib/auth.js.map +1 -0
- package/lib/changesWatcher.d.ts +13 -0
- package/lib/changesWatcher.d.ts.map +1 -0
- package/lib/changesWatcher.js +34 -0
- package/lib/changesWatcher.js.map +1 -0
- package/lib/default.config.json +18 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +28 -0
- package/lib/index.js.map +1 -0
- package/lib/routes/stream.d.ts +5 -0
- package/lib/routes/stream.d.ts.map +1 -0
- package/lib/routes/stream.js +25 -0
- package/lib/routes/stream.js.map +1 -0
- package/lib/routes/subscribe.d.ts +18 -0
- package/lib/routes/subscribe.d.ts.map +1 -0
- package/lib/routes/subscribe.js +36 -0
- package/lib/routes/subscribe.js.map +1 -0
- package/lib/routes/tx.d.ts +4 -0
- package/lib/routes/tx.d.ts.map +1 -0
- package/lib/routes/tx.js +13 -0
- package/lib/routes/tx.js.map +1 -0
- package/lib/server.d.ts +8 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +42 -0
- package/lib/server.js.map +1 -0
- package/lib/sse.d.ts +18 -0
- package/lib/sse.d.ts.map +1 -0
- package/lib/sse.js +47 -0
- package/lib/sse.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License (MIT)
|
|
2
|
+
Copyright (c) 2026 Activeledger
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @activeledger/nano-gateway
|
|
2
|
+
|
|
3
|
+
A lightweight, permissioned SSE + read gateway for [nano](https://github.com/activeledger/nano) light-node clients.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Activecore (`@activeledger/activecore`, `packages/core`) is nano's originally-planned data source, but it isn't running on the devnet this was built against, and its SSE controller (`controllers/sse.ts`) is written against Node's raw `http.IncomingMessage`/`ServerResponse` API - `res.setHeader()`, `res.write()`, `res.flushHeaders()`, `req.socket.setTimeout()`. The `@activeledger/httpd` this repo actually ships today is built on `uWebSockets.js` (confirmed by reading its published `lib/httpd.js`, not assumed) - route handlers get a raw uWS `HttpResponse`, which has none of those methods. Activecore's SSE path would throw immediately if invoked against it. That's very likely a real, standing cause of Activecore's unreliability, independent of anything else.
|
|
8
|
+
|
|
9
|
+
This package is a much smaller, purpose-built replacement for just what nano needs: a permissioned SSE subscribe endpoint, and the handful of reads nano's mini-SPI needs. It's modeled directly on `packages/hybrid` - direct database access via `ActiveDSConnect`, no consensus involvement, runs as a companion process alongside a real node.
|
|
10
|
+
|
|
11
|
+
## What "permissioned" means here
|
|
12
|
+
|
|
13
|
+
Anyone can read (`GET /api/stream/*`, `POST /api/stream`, `GET /api/tx/*`) - ledger data is public by design, unchanged from how Activecore worked. Only the **subscribe/push channel** is gated: a nano must be on this gateway's allowlist, and must prove it holds the private key for its claimed identity, before it gets a live feed. See `src/auth.ts`.
|
|
14
|
+
|
|
15
|
+
The handshake: a nano signs `${identity}:${timestamp}` with its own key (the same identity it already onboarded on this ledger and signs attestation transactions with - no separate key-distribution step) and sends `{identity, timestamp, signature}` in the subscribe request body. The gateway checks, in order: is this identity on the allowlist -> is the timestamp fresh (within `authWindowSeconds`, anti-replay) -> does the signature actually verify against a public key read *live off the identity's own `:stream` meta doc* (`meta.authorities[].public`) - not a key handed to the gateway out of band. Revoking a nano is just removing it from the allowlist file.
|
|
16
|
+
|
|
17
|
+
Auth travels in the POST body, not headers - `@activeledger/httpd`'s `listen()` only forwards a fixed, hardcoded set of headers to route handlers (confirmed by reading its source), and custom `x-nano-*` headers would never arrive.
|
|
18
|
+
|
|
19
|
+
## Config
|
|
20
|
+
|
|
21
|
+
On first run, a `config.json` (from `src/default.config.json`) and an empty `nano-allowlist.json` (`[]`) are created next to wherever you run the binary from. Nothing can connect until you add identities to the allowlist:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
[
|
|
25
|
+
{ "identity": "abc123...", "label": "adam's phone", "addedAt": 1788000000000 }
|
|
26
|
+
]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Key config fields (`config.json`):
|
|
30
|
+
|
|
31
|
+
- `db.url` / `db.database` - **you must fill this in.** Point it at the same datastore your Activeledger node already uses (its self-host port, or wherever its CouchDB-compatible store lives). This package doesn't know how to bootstrap a datastore itself, unlike `activehybrid`'s `selfhost` option - it's meant to run alongside an already-running node.
|
|
32
|
+
- `host` - `0.0.0.0:5270` by default. Change the port if `5270` collides with anything else on the host.
|
|
33
|
+
- `nanoGateway.allowlist` - path to the allowlist file.
|
|
34
|
+
- `nanoGateway.authWindowSeconds` - default `60`.
|
|
35
|
+
- `nanoGateway.heartbeatSeconds` - default `5`. Deliberately short: uWS's default idle timeout is ~10s of no socket traffic, far shorter than Activecore's own 10-*minute* SSE heartbeat (`packages/core/src/heartbeat.ts`) - that mismatch is a second, independent plausible cause of Activecore's SSE being unreliable even where it does run. Don't raise this without checking the uWS idle timeout first.
|
|
36
|
+
|
|
37
|
+
## Routes
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
GET / health check
|
|
41
|
+
POST /api/activity/subscribe SSE, permissioned - body: {identity, timestamp, signature, streamIds}
|
|
42
|
+
POST /api/stream batch read - body: string[] of stream ids -> {streams: [...]}
|
|
43
|
+
GET /api/stream/:id single read -> {stream: {...} | null}
|
|
44
|
+
GET /api/tx/:umid transaction lookup -> {transaction: {...} | null}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`/api/stream` and `/api/tx/:umid` are **not** wire-compatible with Activecore's equivalents - both had real bugs discovered this session while building the nano client against Activecore's actual source (`getStream()` drops `_id` from its response via a comma-expression bug; `findUmid()` names its response field `umid` when the value is the whole transaction, not a umid string). New code doesn't inherit either. nano's own client (`@activenano/core`'s `MiniSpiClient`/`TransactionResolver`) is written against *this* package's contract.
|
|
48
|
+
|
|
49
|
+
## Deploying this alongside a running node
|
|
50
|
+
|
|
51
|
+
1. `npm install -g @activeledger/nano-gateway` (once published) or build from source (`npm run build`, then `node lib/index.js`).
|
|
52
|
+
2. Run it on the same host as your node, or anywhere with network access to the node's real datastore. First run generates `config.json` - edit `db.url`/`db.database` before starting it for real.
|
|
53
|
+
3. Add approved nano identities to `nano-allowlist.json` as they're provisioned.
|
|
54
|
+
4. Expose the port (`5270` by default) the same way the node's own ports are already exposed - a Docker port mapping, and an nginx `location` block proxying to it (SSE needs `proxy_buffering off;` and a long `proxy_read_timeout` - standard nginx SSE config, not anything nano-gateway-specific).
|
|
55
|
+
|
|
56
|
+
Once this is committed and released, it publishes as its own package with its own bin (`nano-gateway`), so it can run as a sibling process in the same container as the node.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IAllowlistEntry {
|
|
2
|
+
identity: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
addedAt?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class Allowlist {
|
|
7
|
+
private filePath;
|
|
8
|
+
constructor(filePath: string);
|
|
9
|
+
isApproved(identity: string): boolean;
|
|
10
|
+
list(): IAllowlistEntry[];
|
|
11
|
+
add(entry: IAllowlistEntry): void;
|
|
12
|
+
remove(identity: string): void;
|
|
13
|
+
private load;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=allowlist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.d.ts","sourceRoot":"","sources":["../src/allowlist.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAUD,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAE7B,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIrC,IAAI,IAAI,eAAe,EAAE;IAIzB,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAOjC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKrC,OAAO,CAAC,IAAI;CAQb"}
|
package/es/allowlist.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
export class Allowlist {
|
|
3
|
+
filePath;
|
|
4
|
+
constructor(filePath) {
|
|
5
|
+
this.filePath = filePath;
|
|
6
|
+
}
|
|
7
|
+
isApproved(identity) {
|
|
8
|
+
return this.load().some((entry) => entry.identity === identity);
|
|
9
|
+
}
|
|
10
|
+
list() {
|
|
11
|
+
return this.load();
|
|
12
|
+
}
|
|
13
|
+
add(entry) {
|
|
14
|
+
const current = this.load();
|
|
15
|
+
if (current.some((e) => e.identity === entry.identity))
|
|
16
|
+
return;
|
|
17
|
+
current.push({ ...entry, addedAt: entry.addedAt ?? Date.now() });
|
|
18
|
+
fs.writeFileSync(this.filePath, JSON.stringify(current, null, 2) + "\n");
|
|
19
|
+
}
|
|
20
|
+
remove(identity) {
|
|
21
|
+
const current = this.load().filter((e) => e.identity !== identity);
|
|
22
|
+
fs.writeFileSync(this.filePath, JSON.stringify(current, null, 2) + "\n");
|
|
23
|
+
}
|
|
24
|
+
load() {
|
|
25
|
+
if (!fs.existsSync(this.filePath))
|
|
26
|
+
return [];
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(this.filePath, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../src/allowlist.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAwBzB,MAAM,OAAO,SAAS;IACA;IAApB,YAAoB,QAAgB;QAAhB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;IAEjC,UAAU,CAAC,QAAgB;QAChC,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;IAClE,CAAC;IAEM,IAAI;QACT,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;IAEM,GAAG,CAAC,KAAsB;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,KAAK,CAAC,QAAQ,CAAC;YAAE,OAAO;QAC/D,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACjE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3E,CAAC;IAEM,MAAM,CAAC,QAAgB;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;QACnE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3E,CAAC;IAEO,IAAI;QACV,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAsB,CAAC;QACjF,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
|
package/es/auth.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ActiveDSConnect } from "@activeledger/activeoptions";
|
|
2
|
+
import { Allowlist } from "./allowlist";
|
|
3
|
+
export interface IAuthResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function verifyHandshake(headers: {
|
|
8
|
+
identity?: string;
|
|
9
|
+
timestamp?: string;
|
|
10
|
+
signature?: string;
|
|
11
|
+
}, allowlist: Allowlist, db: ActiveDSConnect, authWindowSeconds: number): Promise<IAuthResult>;
|
|
12
|
+
//# sourceMappingURL=auth.d.ts.map
|
package/es/auth.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxC,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAgBD,wBAAsB,eAAe,CACnC,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EACtE,SAAS,EAAE,SAAS,EACpB,EAAE,EAAE,eAAe,EACnB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC,WAAW,CAAC,CAwCtB"}
|
package/es/auth.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ActiveCrypto } from "@activeledger/activecrypto";
|
|
2
|
+
export async function verifyHandshake(headers, allowlist, db, authWindowSeconds) {
|
|
3
|
+
const { identity, timestamp, signature } = headers;
|
|
4
|
+
if (!identity || !timestamp || !signature) {
|
|
5
|
+
return { ok: false, reason: "missing x-nano-identity/x-nano-timestamp/x-nano-signature headers" };
|
|
6
|
+
}
|
|
7
|
+
if (!allowlist.isApproved(identity)) {
|
|
8
|
+
return { ok: false, reason: "identity is not on the allowlist" };
|
|
9
|
+
}
|
|
10
|
+
const ts = Number(timestamp);
|
|
11
|
+
if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > authWindowSeconds * 1000) {
|
|
12
|
+
return { ok: false, reason: "timestamp is missing, invalid, or outside the allowed window" };
|
|
13
|
+
}
|
|
14
|
+
let meta;
|
|
15
|
+
try {
|
|
16
|
+
meta = await db.get(`${identity}:stream`);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return { ok: false, reason: "identity has no on-ledger record" };
|
|
20
|
+
}
|
|
21
|
+
const authorities = meta.authorities ?? [];
|
|
22
|
+
if (authorities.length === 0) {
|
|
23
|
+
return { ok: false, reason: "identity has no registered authorities on-ledger" };
|
|
24
|
+
}
|
|
25
|
+
const payload = `${identity}:${timestamp}`;
|
|
26
|
+
for (const authority of authorities) {
|
|
27
|
+
try {
|
|
28
|
+
const key = new ActiveCrypto.KeyPair(authority.type, authority.public);
|
|
29
|
+
if (key.verify(payload, signature)) {
|
|
30
|
+
return { ok: true };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { ok: false, reason: "signature did not verify against any of the identity's registered authorities" };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=auth.js.map
|
package/es/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AA6B1D,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAsE,EACtE,SAAoB,EACpB,EAAmB,EACnB,iBAAyB;IAEzB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IACnD,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QAC1C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,mEAAmE,EAAE,CAAC;IACpG,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC;IACnE,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,iBAAiB,GAAG,IAAI,EAAE,CAAC;QACjF,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,8DAA8D,EAAE,CAAC;IAC/F,CAAC;IAED,IAAI,IAAwC,CAAC;IAC7C,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,QAAQ,SAAS,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kCAAkC,EAAE,CAAC;IACnE,CAAC;IAED,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;IAC3C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kDAAkD,EAAE,CAAC;IACnF,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC;IAC3C,KAAK,MAAM,SAAS,IAAI,WAAW,EAAE,CAAC;QACpC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;YACvE,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;gBACnC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;YACtB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;QAET,CAAC;IACH,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,+EAA+E,EAAE,CAAC;AAChH,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ActiveDSConnect } from "@activeledger/activeoptions";
|
|
2
|
+
export declare class ChangesWatcher {
|
|
3
|
+
private db;
|
|
4
|
+
private streamIds;
|
|
5
|
+
private feed;
|
|
6
|
+
constructor(db: ActiveDSConnect, streamIds: Set<string>);
|
|
7
|
+
start(onChange: (doc: Record<string, unknown> & {
|
|
8
|
+
_id: string;
|
|
9
|
+
_rev: string;
|
|
10
|
+
}) => void, onError: (error: unknown) => void): void;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=changesWatcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesWatcher.d.ts","sourceRoot":"","sources":["../src/changesWatcher.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAsB9D,qBAAa,cAAc;IAGb,OAAO,CAAC,EAAE;IAAmB,OAAO,CAAC,SAAS;IAF1D,OAAO,CAAC,IAAI,CAAmD;gBAE3C,EAAE,EAAE,eAAe,EAAU,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC;IAEhE,KAAK,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAchI,IAAI,IAAI,IAAI;CAIpB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function isStateDocChange(change) {
|
|
2
|
+
return (change.doc._id.indexOf(":") === -1 &&
|
|
3
|
+
(!change.doc.$activeledger || (!change.doc.$activeledger.delete && !change.doc.$activeledger.rewrite)));
|
|
4
|
+
}
|
|
5
|
+
export class ChangesWatcher {
|
|
6
|
+
db;
|
|
7
|
+
streamIds;
|
|
8
|
+
feed = null;
|
|
9
|
+
constructor(db, streamIds) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.streamIds = streamIds;
|
|
12
|
+
}
|
|
13
|
+
start(onChange, onError) {
|
|
14
|
+
const result = this.db.changes({ live: true, since: "now", include_docs: true });
|
|
15
|
+
this.feed = result;
|
|
16
|
+
this.feed.on("change", (change) => {
|
|
17
|
+
if (!isStateDocChange(change))
|
|
18
|
+
return;
|
|
19
|
+
if (!this.streamIds.has(change.doc._id))
|
|
20
|
+
return;
|
|
21
|
+
onChange(change.doc);
|
|
22
|
+
});
|
|
23
|
+
this.feed.on("error", onError);
|
|
24
|
+
}
|
|
25
|
+
stop() {
|
|
26
|
+
this.feed?.cancel();
|
|
27
|
+
this.feed = null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=changesWatcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesWatcher.js","sourceRoot":"","sources":["../src/changesWatcher.ts"],"names":[],"mappings":"AA+BA,SAAS,gBAAgB,CAAC,MAAyF;IACjH,OAAO,CACL,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CACvG,CAAC;AACJ,CAAC;AASD,MAAM,OAAO,cAAc;IAGL;IAA6B;IAFzC,IAAI,GAA8C,IAAI,CAAC;IAE/D,YAAoB,EAAmB,EAAU,SAAsB;QAAnD,OAAE,GAAF,EAAE,CAAiB;QAAU,cAAS,GAAT,SAAS,CAAa;IAAG,CAAC;IAEpE,KAAK,CAAC,QAAgF,EAAE,OAAiC;QAC9H,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QAEjF,IAAI,CAAC,IAAI,GAAG,MAA4C,CAAC;QAEzD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAwE,EAAE,EAAE;YAClG,IAAI,CAAC,gBAAgB,CAAC,MAAa,CAAC;gBAAE,OAAO;YAC7C,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,OAAO;YAChD,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IAEM,IAAI;QACT,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"debug": true,
|
|
3
|
+
"host": "0.0.0.0:5270",
|
|
4
|
+
"db": {
|
|
5
|
+
"url": "http://[user]:[pass]@[host]:[port]",
|
|
6
|
+
"database": "activeledger",
|
|
7
|
+
"event": "activeledgerevents",
|
|
8
|
+
"error": "activeledgererrors"
|
|
9
|
+
},
|
|
10
|
+
"nanoGateway": {
|
|
11
|
+
"allowlist": "./nano-allowlist.json",
|
|
12
|
+
"authWindowSeconds": 60,
|
|
13
|
+
"heartbeatSeconds": 5
|
|
14
|
+
},
|
|
15
|
+
"CORS": [
|
|
16
|
+
"*"
|
|
17
|
+
]
|
|
18
|
+
}
|
package/es/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/es/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { ActiveOptions } from "@activeledger/activeoptions";
|
|
4
|
+
import { ActiveLogger } from "@activeledger/activelogger";
|
|
5
|
+
import { NanoGatewayServer } from "./server";
|
|
6
|
+
ActiveOptions.init();
|
|
7
|
+
const configPath = ActiveOptions.get("config", "./config.json");
|
|
8
|
+
if (!fs.existsSync(configPath)) {
|
|
9
|
+
const defaultConfig = fs.readFileSync(__dirname + "/default.config.json", "utf8");
|
|
10
|
+
fs.writeFileSync(configPath, defaultConfig);
|
|
11
|
+
ActiveLogger.info(`Created nano-gateway config file at ${configPath}`);
|
|
12
|
+
}
|
|
13
|
+
ActiveOptions.parseConfig();
|
|
14
|
+
if (!ActiveOptions.get("db", false)) {
|
|
15
|
+
ActiveLogger.fatal("Configuration file incomplete - no db connection configured");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const gatewayConfig = ActiveOptions.get("nanoGateway", { allowlist: "./nano-allowlist.json" });
|
|
19
|
+
if (!fs.existsSync(gatewayConfig.allowlist)) {
|
|
20
|
+
fs.writeFileSync(gatewayConfig.allowlist, "[]\n");
|
|
21
|
+
ActiveLogger.info(`Created empty allowlist at ${gatewayConfig.allowlist} - no nanos can connect until you add identities to it`);
|
|
22
|
+
}
|
|
23
|
+
ActiveLogger.enableDebug = ActiveOptions.get("debug", false);
|
|
24
|
+
const server = new NanoGatewayServer();
|
|
25
|
+
server.start(ActiveOptions.get("debug", false));
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
package/es/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAkCA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAE7C,aAAa,CAAC,IAAI,EAAE,CAAC;AAErB,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,CAAS,QAAQ,EAAE,eAAe,CAAC,CAAC;AACxE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;IAC/B,MAAM,aAAa,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,GAAG,sBAAsB,EAAE,MAAM,CAAC,CAAC;IAClF,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAC5C,YAAY,CAAC,IAAI,CAAC,uCAAuC,UAAU,EAAE,CAAC,CAAC;AACzE,CAAC;AACD,aAAa,CAAC,WAAW,EAAE,CAAC;AAE5B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;IACpC,YAAY,CAAC,KAAK,CAAC,6DAA6D,CAAC,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,aAAa,GAAG,aAAa,CAAC,GAAG,CAAwB,aAAa,EAAE,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAC,CAAC;AACtH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;IAC5C,EAAE,CAAC,aAAa,CAAC,aAAa,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAClD,YAAY,CAAC,IAAI,CAAC,8BAA8B,aAAa,CAAC,SAAS,wDAAwD,CAAC,CAAC;AACnI,CAAC;AAED,YAAY,CAAC,WAAW,GAAG,aAAa,CAAC,GAAG,CAAU,OAAO,EAAE,KAAK,CAAC,CAAC;AAEtE,MAAM,MAAM,GAAG,IAAI,iBAAiB,EAAE,CAAC;AACvC,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAU,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ActiveDSConnect } from "@activeledger/activeoptions";
|
|
2
|
+
import { IActiveHttpIncoming } from "@activeledger/httpd";
|
|
3
|
+
export declare function getStream(incoming: IActiveHttpIncoming, db: ActiveDSConnect): Promise<unknown>;
|
|
4
|
+
export declare function getStreams(incoming: IActiveHttpIncoming, db: ActiveDSConnect): Promise<unknown>;
|
|
5
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../../src/routes/stream.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAU1D,wBAAsB,SAAS,CAAC,QAAQ,EAAE,mBAAmB,EAAE,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAOpG;AAQD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,mBAAmB,EAAE,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAUrG"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export async function getStream(incoming, db) {
|
|
2
|
+
try {
|
|
3
|
+
const doc = await db.get(incoming.url[2]);
|
|
4
|
+
return { stream: doc };
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return { stream: null };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function getStreams(incoming, db) {
|
|
11
|
+
const ids = Array.isArray(incoming.body) ? incoming.body : [];
|
|
12
|
+
if (ids.length === 0) {
|
|
13
|
+
return { streams: [] };
|
|
14
|
+
}
|
|
15
|
+
const result = await db.allDocs({ keys: ids, include_docs: true });
|
|
16
|
+
const streams = (result?.rows ?? [])
|
|
17
|
+
.map((row) => row.doc)
|
|
18
|
+
.filter((doc) => doc !== undefined && doc !== null);
|
|
19
|
+
return { streams };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/routes/stream.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAA6B,EAAE,EAAmB;IAChF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAA6B,EAAE,EAAmB;IACjF,MAAM,GAAG,GAAa,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACxE,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACzB,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;SACjC,GAAG,CAAC,CAAC,GAAsB,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC;SACxC,MAAM,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC;IAC/D,OAAO,EAAE,OAAO,EAAE,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ActiveDSConnect } from "@activeledger/activeoptions";
|
|
2
|
+
import { IActiveHttpIncoming } from "@activeledger/httpd";
|
|
3
|
+
import { Allowlist } from "../allowlist";
|
|
4
|
+
import { IWritableHttpResponse } from "../sse";
|
|
5
|
+
export interface ISubscribeRequestBody {
|
|
6
|
+
identity: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
signature: string;
|
|
9
|
+
streamIds: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface ISubscribeOptions {
|
|
12
|
+
db: ActiveDSConnect;
|
|
13
|
+
allowlist: Allowlist;
|
|
14
|
+
authWindowSeconds: number;
|
|
15
|
+
heartbeatSeconds: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function subscribe(incoming: IActiveHttpIncoming, _req: unknown, res: IWritableHttpResponse, options: ISubscribeOptions): Promise<"handled">;
|
|
18
|
+
//# sourceMappingURL=subscribe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGzC,OAAO,EAAE,qBAAqB,EAAW,MAAM,QAAQ,CAAC;AAYxD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,eAAe,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,mBAAmB,EAC7B,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,qBAAqB,EAC1B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,CAAC,CA0CpB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { verifyHandshake } from "../auth";
|
|
2
|
+
import { ChangesWatcher } from "../changesWatcher";
|
|
3
|
+
import { NanoSSE } from "../sse";
|
|
4
|
+
export async function subscribe(incoming, _req, res, options) {
|
|
5
|
+
const body = (incoming.body ?? {});
|
|
6
|
+
const auth = await verifyHandshake({
|
|
7
|
+
identity: body.identity,
|
|
8
|
+
timestamp: body.timestamp !== undefined ? String(body.timestamp) : undefined,
|
|
9
|
+
signature: body.signature,
|
|
10
|
+
}, options.allowlist, options.db, options.authWindowSeconds);
|
|
11
|
+
if (!auth.ok) {
|
|
12
|
+
res.cork(() => {
|
|
13
|
+
res.writeStatus("403 Forbidden").writeHeader("Content-Type", "application/json");
|
|
14
|
+
res.end(JSON.stringify({ error: auth.reason }));
|
|
15
|
+
});
|
|
16
|
+
return "handled";
|
|
17
|
+
}
|
|
18
|
+
const streamIds = Array.isArray(body.streamIds) ? body.streamIds : [];
|
|
19
|
+
if (streamIds.length === 0) {
|
|
20
|
+
res.cork(() => {
|
|
21
|
+
res.writeStatus("400 Bad Request").writeHeader("Content-Type", "application/json");
|
|
22
|
+
res.end(JSON.stringify({ error: "streamIds must be a non-empty array" }));
|
|
23
|
+
});
|
|
24
|
+
return "handled";
|
|
25
|
+
}
|
|
26
|
+
const watcher = new ChangesWatcher(options.db, new Set(streamIds));
|
|
27
|
+
const sse = new NanoSSE(res, options.heartbeatSeconds, () => watcher.stop());
|
|
28
|
+
watcher.start((doc) => sse.write({ event: "update", stream: doc, time: Date.now() }), (error) => {
|
|
29
|
+
sse.write({ event: "error", message: String(error), time: Date.now() });
|
|
30
|
+
});
|
|
31
|
+
return "handled";
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=subscribe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscribe.js","sourceRoot":"","sources":["../../src/routes/subscribe.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAyB,OAAO,EAAE,MAAM,QAAQ,CAAC;AA0BxD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAA6B,EAC7B,IAAa,EACb,GAA0B,EAC1B,OAA0B;IAE1B,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAmC,CAAC;IAErE,MAAM,IAAI,GAAG,MAAM,eAAe,CAChC;QACE,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,SAAS,EAAE,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;QAC5E,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,EACD,OAAO,CAAC,SAAS,EACjB,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,iBAAiB,CAC1B,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;YACZ,GAAG,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,WAAW,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YACjF,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QAClD,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;IACtE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;YACZ,GAAG,CAAC,WAAW,CAAC,iBAAiB,CAAC,CAAC,WAAW,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YACnF,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAC,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IACnE,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAE7E,OAAO,CAAC,KAAK,CACX,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,EACtE,CAAC,KAAK,EAAE,EAAE;QACR,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC1E,CAAC,CACF,CAAC;IAEF,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tx.d.ts","sourceRoot":"","sources":["../../src/routes/tx.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAU1D,wBAAsB,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAOzG"}
|
package/es/routes/tx.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tx.js","sourceRoot":"","sources":["../../src/routes/tx.ts"],"names":[],"mappings":"AAkCA,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAA6B,EAAE,EAAmB;IACrF,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACpD,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;IAC/B,CAAC;AACH,CAAC"}
|
package/es/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAgCA,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,EAAE,CAAkB;IAC5B,OAAO,CAAC,SAAS,CAAY;;IAmCtB,KAAK,CAAC,UAAU,UAAQ,GAAG,IAAI;CAKvC"}
|
package/es/server.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ActiveHttpd } from "@activeledger/httpd";
|
|
2
|
+
import { ActiveDSConnect, ActiveOptions } from "@activeledger/activeoptions";
|
|
3
|
+
import { ActiveLogger } from "@activeledger/activelogger";
|
|
4
|
+
import { Allowlist } from "./allowlist";
|
|
5
|
+
import { getStream, getStreams } from "./routes/stream";
|
|
6
|
+
import { getTransaction } from "./routes/tx";
|
|
7
|
+
import { subscribe } from "./routes/subscribe";
|
|
8
|
+
export class NanoGatewayServer {
|
|
9
|
+
httpServer = new ActiveHttpd();
|
|
10
|
+
db;
|
|
11
|
+
allowlist;
|
|
12
|
+
constructor() {
|
|
13
|
+
const db = ActiveOptions.get("db", false);
|
|
14
|
+
this.db = new ActiveDSConnect(`${db.url}/${db.database}`);
|
|
15
|
+
const gatewayConfig = ActiveOptions.get("nanoGateway", { allowlist: "./nano-allowlist.json", authWindowSeconds: 60, heartbeatSeconds: 5 });
|
|
16
|
+
this.allowlist = new Allowlist(gatewayConfig.allowlist);
|
|
17
|
+
this.httpServer.use("/", "GET", () => ({
|
|
18
|
+
status: "ok",
|
|
19
|
+
service: "nano-gateway",
|
|
20
|
+
approvedNanos: this.allowlist.list().length,
|
|
21
|
+
}));
|
|
22
|
+
this.httpServer.use("/api/stream", "POST", (incoming) => getStreams(incoming, this.db));
|
|
23
|
+
this.httpServer.use("/api/stream/*", "GET", (incoming) => getStream(incoming, this.db));
|
|
24
|
+
this.httpServer.use("/api/tx/*", "GET", (incoming) => getTransaction(incoming, this.db));
|
|
25
|
+
this.httpServer.use("/api/activity/subscribe", "POST", (incoming, req, res) => subscribe(incoming, req, res, {
|
|
26
|
+
db: this.db,
|
|
27
|
+
allowlist: this.allowlist,
|
|
28
|
+
authWindowSeconds: gatewayConfig.authWindowSeconds,
|
|
29
|
+
heartbeatSeconds: gatewayConfig.heartbeatSeconds,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
start(enableLogs = false) {
|
|
33
|
+
const [, port] = ActiveOptions.get("host", ":5270").split(":");
|
|
34
|
+
this.httpServer.listen(parseInt(port, 10), enableLogs);
|
|
35
|
+
ActiveLogger.info(`nano-gateway listening on 0.0.0.0:${port} (${this.allowlist.list().length} approved nanos)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=server.js.map
|
package/es/server.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/es/sse.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface IWritableHttpResponse {
|
|
2
|
+
writable: boolean;
|
|
3
|
+
cork(cb: () => void): IWritableHttpResponse;
|
|
4
|
+
writeStatus(status: string): IWritableHttpResponse;
|
|
5
|
+
writeHeader(key: string, value: string): IWritableHttpResponse;
|
|
6
|
+
write(chunk: string): boolean;
|
|
7
|
+
end(body?: string): IWritableHttpResponse;
|
|
8
|
+
onAborted(handler: () => void): IWritableHttpResponse;
|
|
9
|
+
}
|
|
10
|
+
export declare class NanoSSE {
|
|
11
|
+
private res;
|
|
12
|
+
private heartbeat;
|
|
13
|
+
private closed;
|
|
14
|
+
constructor(res: IWritableHttpResponse, heartbeatSeconds: number, onClose?: () => void);
|
|
15
|
+
write(event: unknown): boolean;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=sse.d.ts.map
|
package/es/sse.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAsDA,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,qBAAqB,CAAC;IAC5C,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,CAAC;IACnD,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAAC;IAC/D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,qBAAqB,CAAC;IAC1C,SAAS,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,qBAAqB,CAAC;CACvD;AAED,qBAAa,OAAO;IAIN,OAAO,CAAC,GAAG;IAHvB,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,MAAM,CAAS;gBAEH,GAAG,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,IAAI;IA+BvF,KAAK,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAQ9B,KAAK,IAAI,IAAI;CAKrB"}
|
package/es/sse.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export class NanoSSE {
|
|
2
|
+
res;
|
|
3
|
+
heartbeat = null;
|
|
4
|
+
closed = false;
|
|
5
|
+
constructor(res, heartbeatSeconds, onClose) {
|
|
6
|
+
this.res = res;
|
|
7
|
+
res.cork(() => {
|
|
8
|
+
res
|
|
9
|
+
.writeStatus("200 OK")
|
|
10
|
+
.writeHeader("Content-Type", "text/event-stream")
|
|
11
|
+
.writeHeader("Cache-Control", "no-cache")
|
|
12
|
+
.writeHeader("Access-Control-Allow-Origin", "*");
|
|
13
|
+
});
|
|
14
|
+
res.onAborted(() => {
|
|
15
|
+
res.writable = false;
|
|
16
|
+
this.close();
|
|
17
|
+
onClose?.();
|
|
18
|
+
});
|
|
19
|
+
this.heartbeat = setInterval(() => {
|
|
20
|
+
if (this.closed || !res.writable)
|
|
21
|
+
return;
|
|
22
|
+
res.cork(() => {
|
|
23
|
+
res.write(":\n\n");
|
|
24
|
+
});
|
|
25
|
+
}, heartbeatSeconds * 1000);
|
|
26
|
+
}
|
|
27
|
+
write(event) {
|
|
28
|
+
if (this.closed || !this.res.writable)
|
|
29
|
+
return false;
|
|
30
|
+
this.res.cork(() => {
|
|
31
|
+
this.res.write(`data:${JSON.stringify(event)}\n\n`);
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
if (this.closed)
|
|
37
|
+
return;
|
|
38
|
+
this.closed = true;
|
|
39
|
+
if (this.heartbeat)
|
|
40
|
+
clearInterval(this.heartbeat);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=sse.js.map
|
package/es/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAgEA,MAAM,OAAO,OAAO;IAIE;IAHZ,SAAS,GAA0B,IAAI,CAAC;IACxC,MAAM,GAAG,KAAK,CAAC;IAEvB,YAAoB,GAA0B,EAAE,gBAAwB,EAAE,OAAoB;QAA1E,QAAG,GAAH,GAAG,CAAuB;QAC5C,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;YACZ,GAAG;iBACA,WAAW,CAAC,QAAQ,CAAC;iBACrB,WAAW,CAAC,cAAc,EAAE,mBAAmB,CAAC;iBAChD,WAAW,CAAC,eAAe,EAAE,UAAU,CAAC;iBACxC,WAAW,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;YAKjB,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC;YACrB,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO,EAAE,EAAE,CAAC;QACd,CAAC,CAAC,CAAC;QAMH,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;YAChC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ;gBAAE,OAAO;YACzC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;gBACZ,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YACrB,CAAC,CAAC,CAAC;QACL,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC9B,CAAC;IAEM,KAAK,CAAC,KAAc;QACzB,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QACpD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE;YACjB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACd,CAAC;IAEM,KAAK;QACV,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,IAAI,CAAC,SAAS;YAAE,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACpD,CAAC;CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface IAllowlistEntry {
|
|
2
|
+
identity: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
addedAt?: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class Allowlist {
|
|
7
|
+
private filePath;
|
|
8
|
+
constructor(filePath: string);
|
|
9
|
+
isApproved(identity: string): boolean;
|
|
10
|
+
list(): IAllowlistEntry[];
|
|
11
|
+
add(entry: IAllowlistEntry): void;
|
|
12
|
+
remove(identity: string): void;
|
|
13
|
+
private load;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=allowlist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.d.ts","sourceRoot":"","sources":["../src/allowlist.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAUD,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAE7B,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAIrC,IAAI,IAAI,eAAe,EAAE;IAIzB,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAOjC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAKrC,OAAO,CAAC,IAAI;CAQb"}
|