@totemsdk/omnia-host 0.2.1 → 0.3.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/README.md +7 -6
- package/dist/api/jsonrpc.d.ts +12 -0
- package/dist/api/jsonrpc.js +85 -4
- package/dist/config.d.ts +10 -0
- package/dist/config.js +9 -0
- package/dist/identity.js +16 -1
- package/dist/lifecycle.js +2 -0
- package/dist/signing.d.ts +19 -14
- package/dist/signing.js +40 -54
- package/package.json +13 -12
package/README.md
CHANGED
|
@@ -38,12 +38,13 @@ The daemon configuration is read from environment variables:
|
|
|
38
38
|
|
|
39
39
|
### Signing model
|
|
40
40
|
|
|
41
|
-
`OMNIA_HOST_SEED` (or the keyfile) is the base seed. The host derives
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
channel participant's `publicKeyDigest` is
|
|
45
|
-
|
|
46
|
-
they survive restarts together
|
|
41
|
+
`OMNIA_HOST_SEED` (or the keyfile) is the base seed. The host derives the unified
|
|
42
|
+
child TreeKey (`createUnifiedChildTreeKey(baseSeed, localAddressIndex)`) and signs
|
|
43
|
+
every channel state (and the boot manifest) with serialized TreeSignatures; the
|
|
44
|
+
channel participant's `publicKeyDigest` is that TreeKey's root public key, and the
|
|
45
|
+
identity address is the matching Minima address. The WOTS lease journal and
|
|
46
|
+
watermark persist in `<OMNIA_HOST_DB>.lease/` so they survive restarts together
|
|
47
|
+
with the channel database.
|
|
47
48
|
|
|
48
49
|
### Read-only mode
|
|
49
50
|
|
package/dist/api/jsonrpc.d.ts
CHANGED
|
@@ -24,6 +24,18 @@ export interface ControlServerOptions {
|
|
|
24
24
|
isReady?: () => boolean;
|
|
25
25
|
methods?: Map<string, JsonRpcHandler>;
|
|
26
26
|
onConnection?: (socket: WebSocket) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Browser Origin allowlist (AUD-009). When set, browser requests whose
|
|
29
|
+
* `Origin` header is not listed are rejected. Requests with no Origin
|
|
30
|
+
* (native clients, curl) are always allowed.
|
|
31
|
+
*/
|
|
32
|
+
allowedOrigins?: string[];
|
|
33
|
+
/**
|
|
34
|
+
* When set, control-plane requests must present this bearer token (AUD-009):
|
|
35
|
+
* `Authorization: Bearer <token>` over HTTP, or a query param / subprotocol
|
|
36
|
+
* over the WebSocket handshake.
|
|
37
|
+
*/
|
|
38
|
+
authToken?: string;
|
|
27
39
|
}
|
|
28
40
|
export interface ControlServer {
|
|
29
41
|
listen(): Promise<void>;
|
package/dist/api/jsonrpc.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createControlServer = createControlServer;
|
|
7
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
7
8
|
const node_http_1 = __importDefault(require("node:http"));
|
|
8
9
|
const ws_1 = require("ws");
|
|
9
10
|
function response(id, result) {
|
|
@@ -44,23 +45,95 @@ function sendJson(res, status, body) {
|
|
|
44
45
|
res.writeHead(status, { 'content-type': 'application/json', 'content-length': Buffer.byteLength(payload) });
|
|
45
46
|
res.end(payload);
|
|
46
47
|
}
|
|
48
|
+
function pathname(url) {
|
|
49
|
+
if (!url)
|
|
50
|
+
return '/';
|
|
51
|
+
const query = url.indexOf('?');
|
|
52
|
+
return query === -1 ? url : url.slice(0, query);
|
|
53
|
+
}
|
|
54
|
+
function isOriginAllowed(req, allowedOrigins) {
|
|
55
|
+
const origin = req.headers.origin;
|
|
56
|
+
// Native clients (no browser Origin header) are always allowed.
|
|
57
|
+
if (!origin)
|
|
58
|
+
return true;
|
|
59
|
+
// RFC-020 H1: browser requests require an explicit allowlist (default deny).
|
|
60
|
+
if (!allowedOrigins || allowedOrigins.length === 0)
|
|
61
|
+
return false;
|
|
62
|
+
return allowedOrigins.includes(origin);
|
|
63
|
+
}
|
|
64
|
+
function bearerToken(req) {
|
|
65
|
+
const header = req.headers.authorization;
|
|
66
|
+
if (typeof header === 'string' && header.startsWith('Bearer '))
|
|
67
|
+
return header.slice('Bearer '.length);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function handshakeToken(req) {
|
|
71
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
72
|
+
const queryToken = url.searchParams.get('token');
|
|
73
|
+
if (queryToken)
|
|
74
|
+
return queryToken;
|
|
75
|
+
const header = req.headers['sec-websocket-protocol'];
|
|
76
|
+
const raw = Array.isArray(header) ? header.join(',') : header;
|
|
77
|
+
if (typeof raw === 'string') {
|
|
78
|
+
for (const part of raw.split(',')) {
|
|
79
|
+
const protocol = part.trim();
|
|
80
|
+
if (protocol.startsWith('bearer.'))
|
|
81
|
+
return protocol.slice('bearer.'.length);
|
|
82
|
+
if (protocol)
|
|
83
|
+
return protocol;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function safeTokenEqual(provided, expected) {
|
|
89
|
+
if (provided === undefined)
|
|
90
|
+
return false;
|
|
91
|
+
const a = Buffer.from(provided);
|
|
92
|
+
const b = Buffer.from(expected);
|
|
93
|
+
if (a.length !== b.length)
|
|
94
|
+
return false;
|
|
95
|
+
return node_crypto_1.default.timingSafeEqual(a, b);
|
|
96
|
+
}
|
|
47
97
|
function createControlServer(options) {
|
|
98
|
+
// RFC-020 H1: a non-loopback bind must be authenticated. Refuse to start a
|
|
99
|
+
// network-exposed control plane without a token.
|
|
100
|
+
const isLoopback = options.host === '127.0.0.1' || options.host === '::1' || options.host === 'localhost';
|
|
101
|
+
if (!isLoopback && (options.authToken === undefined || options.authToken.length === 0)) {
|
|
102
|
+
throw new Error(`Refusing to bind the control plane to ${options.host} without a control token (OMNIA_CONTROL_TOKEN)`);
|
|
103
|
+
}
|
|
48
104
|
const methods = options.methods ?? new Map();
|
|
49
105
|
const wsPath = options.wsPath ?? '/rpc';
|
|
106
|
+
const allowedOrigins = options.allowedOrigins;
|
|
107
|
+
const authToken = options.authToken;
|
|
50
108
|
const server = node_http_1.default.createServer(async (req, res) => {
|
|
51
|
-
if (req.method === 'GET' && req.url === '/healthz') {
|
|
109
|
+
if (req.method === 'GET' && pathname(req.url) === '/healthz') {
|
|
52
110
|
sendJson(res, 200, { ok: true });
|
|
53
111
|
return;
|
|
54
112
|
}
|
|
55
|
-
if (req.method === 'GET' && req.url === '/readyz') {
|
|
113
|
+
if (req.method === 'GET' && pathname(req.url) === '/readyz') {
|
|
56
114
|
const ready = options.isReady?.() ?? false;
|
|
57
115
|
sendJson(res, ready ? 200 : 503, { ok: ready });
|
|
58
116
|
return;
|
|
59
117
|
}
|
|
60
|
-
if (req.method !== 'POST' || req.url !== wsPath) {
|
|
118
|
+
if (req.method !== 'POST' || pathname(req.url) !== wsPath) {
|
|
61
119
|
sendJson(res, 404, { error: 'Not found' });
|
|
62
120
|
return;
|
|
63
121
|
}
|
|
122
|
+
if (!isOriginAllowed(req, allowedOrigins)) {
|
|
123
|
+
sendJson(res, 403, { error: 'Forbidden' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// RFC-020 H1: require application/json — a browser cross-origin `text/plain`
|
|
127
|
+
// POST (a classic CSRF vector that skips CORS preflight) must be rejected.
|
|
128
|
+
const contentType = String(req.headers['content-type'] ?? '').toLowerCase();
|
|
129
|
+
if (!contentType.startsWith('application/json')) {
|
|
130
|
+
sendJson(res, 415, { error: 'Unsupported Media Type: application/json required' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (authToken !== undefined && !safeTokenEqual(bearerToken(req), authToken)) {
|
|
134
|
+
sendJson(res, 403, { error: 'Forbidden' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
64
137
|
try {
|
|
65
138
|
const request = JSON.parse(await readBody(req));
|
|
66
139
|
sendJson(res, 200, await dispatch(request, methods));
|
|
@@ -72,7 +145,15 @@ function createControlServer(options) {
|
|
|
72
145
|
});
|
|
73
146
|
const webSockets = new ws_1.WebSocketServer({ noServer: true });
|
|
74
147
|
server.on('upgrade', (req, socket, head) => {
|
|
75
|
-
if (req.url !== wsPath) {
|
|
148
|
+
if (pathname(req.url) !== wsPath) {
|
|
149
|
+
socket.destroy();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!isOriginAllowed(req, allowedOrigins)) {
|
|
153
|
+
socket.destroy();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (authToken !== undefined && !safeTokenEqual(handshakeToken(req), authToken)) {
|
|
76
157
|
socket.destroy();
|
|
77
158
|
return;
|
|
78
159
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -26,6 +26,16 @@ export interface OmniaHostConfig {
|
|
|
26
26
|
identityFile?: string;
|
|
27
27
|
/** EdgeServiceManifest serviceType (default "omnia-router"). */
|
|
28
28
|
serviceType: string;
|
|
29
|
+
/**
|
|
30
|
+
* Browser Origin allowlist for the control plane (OMNIA_ALLOWED_ORIGINS,
|
|
31
|
+
* comma-separated). Undefined/empty allows any Origin.
|
|
32
|
+
*/
|
|
33
|
+
allowedOrigins?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Bearer token required on control-plane requests (OMNIA_CONTROL_TOKEN).
|
|
36
|
+
* Undefined disables token auth.
|
|
37
|
+
*/
|
|
38
|
+
controlToken?: string;
|
|
29
39
|
}
|
|
30
40
|
/** Load the daemon configuration from the documented OMNIA_* environment. */
|
|
31
41
|
export declare function loadConfigFromEnv(env?: NodeJS.ProcessEnv): OmniaHostConfig;
|
package/dist/config.js
CHANGED
|
@@ -28,6 +28,13 @@ function optionalInteger(value) {
|
|
|
28
28
|
throw new Error(`OMNIA_LOCAL_ADDRESS_INDEX must be a non-negative integer; received ${value}`);
|
|
29
29
|
return parsed;
|
|
30
30
|
}
|
|
31
|
+
function parseOrigins(value) {
|
|
32
|
+
const parts = value
|
|
33
|
+
?.split(',')
|
|
34
|
+
.map((part) => part.trim())
|
|
35
|
+
.filter((part) => part.length > 0);
|
|
36
|
+
return parts && parts.length > 0 ? parts : undefined;
|
|
37
|
+
}
|
|
31
38
|
function parseSeed(value) {
|
|
32
39
|
const seed = optional(value);
|
|
33
40
|
if (!seed)
|
|
@@ -76,5 +83,7 @@ function loadConfigFromEnv(env = process.env) {
|
|
|
76
83
|
readOnly: env.OMNIA_HOST_READ_ONLY?.trim() === '1',
|
|
77
84
|
identityFile: optional(env.OMNIA_HOST_IDENTITY_FILE),
|
|
78
85
|
serviceType: optional(env.OMNIA_HOST_SERVICE_TYPE) ?? 'omnia-router',
|
|
86
|
+
allowedOrigins: parseOrigins(env.OMNIA_ALLOWED_ORIGINS),
|
|
87
|
+
controlToken: optional(env.OMNIA_CONTROL_TOKEN),
|
|
79
88
|
};
|
|
80
89
|
}
|
package/dist/identity.js
CHANGED
|
@@ -92,7 +92,22 @@ async function createHostManifest(config, signing, identity) {
|
|
|
92
92
|
capabilities: ['omnia:open-channel', 'omnia:update-channel', 'omnia:route', 'omnia:settle'],
|
|
93
93
|
tags: ['omnia', 'payment-channel', 'daemon'],
|
|
94
94
|
};
|
|
95
|
-
|
|
95
|
+
// RFC-037: sign the manifest with the unified child TreeKey. Allocate a
|
|
96
|
+
// one-time leaf via the lease so restarts never reuse a WOTS leaf.
|
|
97
|
+
const reservation = await signing.leaseProvider.reserveKeyUse({
|
|
98
|
+
treeId: 'omnia-manifest',
|
|
99
|
+
purpose: 'manifest',
|
|
100
|
+
});
|
|
101
|
+
const useIndex = reservation.indices.l1 * 64 + reservation.indices.l2;
|
|
102
|
+
try {
|
|
103
|
+
const signed = await (0, manifest_1.signManifest)(manifest, signing.treeKey, useIndex);
|
|
104
|
+
await signing.leaseProvider.commitKeyUse(reservation.reservationId, 'manifest');
|
|
105
|
+
return signed;
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
await signing.leaseProvider.burnReservation(reservation.reservationId, 'manifest-sign-failed').catch(() => { });
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
96
111
|
}
|
|
97
112
|
/** Boot-time identity + manifest assembly. */
|
|
98
113
|
async function createHostIdentityAndManifest(config, signing) {
|
package/dist/lifecycle.js
CHANGED
|
@@ -86,6 +86,8 @@ function createOmniaHost(config, dependencies = {}) {
|
|
|
86
86
|
port: config.port,
|
|
87
87
|
wsPath: config.wsPath,
|
|
88
88
|
isReady: () => started,
|
|
89
|
+
allowedOrigins: config.allowedOrigins,
|
|
90
|
+
authToken: config.controlToken,
|
|
89
91
|
methods: dependencies.methods ?? (0, methods_js_1.createHostMethods)({
|
|
90
92
|
channels: channelStore,
|
|
91
93
|
routing: routingProvider,
|
package/dist/signing.d.ts
CHANGED
|
@@ -2,20 +2,21 @@
|
|
|
2
2
|
* Host signing material — derives the channel signer, WOTS lease provider,
|
|
3
3
|
* and Minima address from env-provisioned key material.
|
|
4
4
|
*
|
|
5
|
-
* Key model (
|
|
5
|
+
* Key model (RFC-037 unified WOTS):
|
|
6
6
|
* - `baseSeed` comes from OMNIA_HOST_SEED (BIP39 mnemonic or 32-byte hex)
|
|
7
7
|
* or from an OMNIA_HOST_KEYFILE JSON.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* - The channel key is the unified child TreeKey
|
|
9
|
+
* `createUnifiedChildTreeKey(baseSeed, localAddressIndex)`; the participant's
|
|
10
|
+
* `publicKeyDigest` is its tree-root public key — the same key the on-chain
|
|
11
|
+
* `MULTISIG` eltoo script embeds.
|
|
12
|
+
* - Signatures are serialized hierarchical `TreeSignature`s (`serializeTreeSignature`)
|
|
13
|
+
* over the reserved leaf `(l1*64 + l2)`, verified by `verifyChannelSignature`.
|
|
14
|
+
* - The lease indices {addressIndex, l1, l2} select the one-time leaf.
|
|
15
15
|
*
|
|
16
16
|
* The lease journal/watermark persist in a directory next to the channel DB
|
|
17
17
|
* (`<dbPath>.lease/`) so they survive restarts together with the channel store.
|
|
18
18
|
*/
|
|
19
|
+
import { type TreeKey } from '@totemsdk/core';
|
|
19
20
|
import type { StorageAdapter } from '@totemsdk/core';
|
|
20
21
|
import type { ChannelSigner } from '@totemsdk/omnia';
|
|
21
22
|
import type { WotsLeaseProvider } from '@totemsdk/wots-lease';
|
|
@@ -23,12 +24,14 @@ import type { OmniaHostConfig } from './config.js';
|
|
|
23
24
|
export interface HostSigning {
|
|
24
25
|
signer: ChannelSigner;
|
|
25
26
|
leaseProvider: WotsLeaseProvider;
|
|
27
|
+
/** Minima address for the unified child TreeKey (identity + channels). */
|
|
26
28
|
address: string;
|
|
29
|
+
/** Channel participant key: the unified child TreeKey root pubkey (RFC-037). */
|
|
27
30
|
publicKeyDigest: string;
|
|
28
31
|
/** 32-byte base seed (used by the identity/manifest layer). */
|
|
29
32
|
baseSeed: Uint8Array;
|
|
30
|
-
/**
|
|
31
|
-
|
|
33
|
+
/** Unified child TreeKey used for channel + manifest signing (RFC-037). */
|
|
34
|
+
treeKey: TreeKey;
|
|
32
35
|
addressIndex: number;
|
|
33
36
|
deviceId: string;
|
|
34
37
|
}
|
|
@@ -43,13 +46,15 @@ interface KeyfilePayload {
|
|
|
43
46
|
export declare function encryptSeedForKeyfile(seed: Uint8Array, passphrase: string): KeyfilePayload;
|
|
44
47
|
/**
|
|
45
48
|
* JSON-file-backed StorageAdapter rooted at a directory next to the channel DB.
|
|
46
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* Delegates to the hardened shared `FileStore` from `@totemsdk/storage/fs`
|
|
51
|
+
* (RFC-007): one versioned codec record per key written temp+rename with an
|
|
52
|
+
* fsync; corrupt records surface as `StorageError` (`corrupt`) rather than
|
|
53
|
+
* silently returning JSON `null`. The directory is created on first write.
|
|
47
54
|
*/
|
|
48
55
|
export declare class JsonFileStorageAdapter implements StorageAdapter {
|
|
49
|
-
private readonly
|
|
56
|
+
private readonly store;
|
|
50
57
|
constructor(dir: string);
|
|
51
|
-
private fileFor;
|
|
52
|
-
private ensureDir;
|
|
53
58
|
get<T>(key: string): Promise<T | null>;
|
|
54
59
|
set<T>(key: string, value: T): Promise<void>;
|
|
55
60
|
remove(key: string): Promise<boolean>;
|
package/dist/signing.js
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Host signing material — derives the channel signer, WOTS lease provider,
|
|
4
4
|
* and Minima address from env-provisioned key material.
|
|
5
5
|
*
|
|
6
|
-
* Key model (
|
|
6
|
+
* Key model (RFC-037 unified WOTS):
|
|
7
7
|
* - `baseSeed` comes from OMNIA_HOST_SEED (BIP39 mnemonic or 32-byte hex)
|
|
8
8
|
* or from an OMNIA_HOST_KEYFILE JSON.
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* - The channel key is the unified child TreeKey
|
|
10
|
+
* `createUnifiedChildTreeKey(baseSeed, localAddressIndex)`; the participant's
|
|
11
|
+
* `publicKeyDigest` is its tree-root public key — the same key the on-chain
|
|
12
|
+
* `MULTISIG` eltoo script embeds.
|
|
13
|
+
* - Signatures are serialized hierarchical `TreeSignature`s (`serializeTreeSignature`)
|
|
14
|
+
* over the reserved leaf `(l1*64 + l2)`, verified by `verifyChannelSignature`.
|
|
15
|
+
* - The lease indices {addressIndex, l1, l2} select the one-time leaf.
|
|
16
16
|
*
|
|
17
17
|
* The lease journal/watermark persist in a directory next to the channel DB
|
|
18
18
|
* (`<dbPath>.lease/`) so they survive restarts together with the channel store.
|
|
@@ -30,6 +30,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
30
30
|
const node_path_1 = __importDefault(require("node:path"));
|
|
31
31
|
const node_crypto_1 = require("node:crypto");
|
|
32
32
|
const core_1 = require("@totemsdk/core");
|
|
33
|
+
const fs_1 = require("@totemsdk/storage/fs");
|
|
33
34
|
const wots_lease_1 = require("@totemsdk/wots-lease");
|
|
34
35
|
const wots_lease_2 = require("@totemsdk/wots-lease");
|
|
35
36
|
const KEYFILE_SCRYPT_N = 16384;
|
|
@@ -115,58 +116,34 @@ function resolveBaseSeed(config) {
|
|
|
115
116
|
}
|
|
116
117
|
/**
|
|
117
118
|
* JSON-file-backed StorageAdapter rooted at a directory next to the channel DB.
|
|
118
|
-
*
|
|
119
|
+
*
|
|
120
|
+
* Delegates to the hardened shared `FileStore` from `@totemsdk/storage/fs`
|
|
121
|
+
* (RFC-007): one versioned codec record per key written temp+rename with an
|
|
122
|
+
* fsync; corrupt records surface as `StorageError` (`corrupt`) rather than
|
|
123
|
+
* silently returning JSON `null`. The directory is created on first write.
|
|
119
124
|
*/
|
|
120
125
|
class JsonFileStorageAdapter {
|
|
121
|
-
|
|
126
|
+
store;
|
|
122
127
|
constructor(dir) {
|
|
123
|
-
this.
|
|
124
|
-
}
|
|
125
|
-
fileFor(key) {
|
|
126
|
-
const safe = key.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
127
|
-
return node_path_1.default.join(this.dir, `${safe}.json`);
|
|
128
|
-
}
|
|
129
|
-
ensureDir() {
|
|
130
|
-
node_fs_1.default.mkdirSync(this.dir, { recursive: true });
|
|
128
|
+
this.store = new fs_1.FileStore(dir);
|
|
131
129
|
}
|
|
132
130
|
async get(key) {
|
|
133
|
-
|
|
134
|
-
if (!node_fs_1.default.existsSync(file))
|
|
135
|
-
return null;
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
131
|
+
return this.store.get(key);
|
|
142
132
|
}
|
|
143
133
|
async set(key, value) {
|
|
144
|
-
this.
|
|
145
|
-
node_fs_1.default.writeFileSync(this.fileFor(key), JSON.stringify(value));
|
|
134
|
+
return this.store.set(key, value);
|
|
146
135
|
}
|
|
147
136
|
async remove(key) {
|
|
148
|
-
|
|
149
|
-
if (!node_fs_1.default.existsSync(file))
|
|
150
|
-
return false;
|
|
151
|
-
node_fs_1.default.unlinkSync(file);
|
|
152
|
-
return true;
|
|
137
|
+
return this.store.remove(key);
|
|
153
138
|
}
|
|
154
139
|
async clear() {
|
|
155
|
-
|
|
156
|
-
return;
|
|
157
|
-
for (const entry of node_fs_1.default.readdirSync(this.dir)) {
|
|
158
|
-
node_fs_1.default.unlinkSync(node_path_1.default.join(this.dir, entry));
|
|
159
|
-
}
|
|
140
|
+
return this.store.clear();
|
|
160
141
|
}
|
|
161
142
|
async keys() {
|
|
162
|
-
|
|
163
|
-
return [];
|
|
164
|
-
return node_fs_1.default.readdirSync(this.dir)
|
|
165
|
-
.filter((name) => name.endsWith('.json'))
|
|
166
|
-
.map((name) => name.slice(0, -'.json'.length));
|
|
143
|
+
return this.store.keys();
|
|
167
144
|
}
|
|
168
145
|
async has(key) {
|
|
169
|
-
return
|
|
146
|
+
return this.store.has(key);
|
|
170
147
|
}
|
|
171
148
|
}
|
|
172
149
|
exports.JsonFileStorageAdapter = JsonFileStorageAdapter;
|
|
@@ -196,25 +173,34 @@ function createHostSigning(config) {
|
|
|
196
173
|
throw new Error(`localAddressIndex ${addressIndex} is outside device slot ${deviceSlot} range ` +
|
|
197
174
|
`${range.startAddressIndex}–${range.endAddressIndex}`);
|
|
198
175
|
}
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const publicKeyDigest = `0x${(0, core_1.bytesToHex)(
|
|
202
|
-
const address = (0, core_1.
|
|
176
|
+
const treeKey = (0, core_1.createUnifiedChildTreeKey)(baseSeed, addressIndex);
|
|
177
|
+
const publicKey = treeKey.getPublicKey();
|
|
178
|
+
const publicKeyDigest = `0x${(0, core_1.bytesToHex)(publicKey)}`;
|
|
179
|
+
const address = (0, core_1.scriptToAddress)((0, core_1.scriptFromWotsPk)(publicKey));
|
|
180
|
+
const storage = new JsonFileStorageAdapter(leaseStorageDir(config.dbPath));
|
|
181
|
+
const leaseProvider = new wots_lease_1.LocalLeaseProvider(storage, undefined, deviceId);
|
|
203
182
|
const signer = {
|
|
204
183
|
publicKeyDigest,
|
|
205
|
-
async sign(payload,
|
|
206
|
-
|
|
184
|
+
async sign(payload, indices) {
|
|
185
|
+
// RFC-020 C1: honor the reserved leaf instead of hardcoding index 0. The
|
|
186
|
+
// reservation must belong to this signer's address; the leaf within the
|
|
187
|
+
// per-address tree is (l1*64 + l2). Signatures are serialized TreeSignatures
|
|
188
|
+
// (RFC-037), verified by verifyChannelSignature against the tree-root pubkey.
|
|
189
|
+
if (indices.addressIndex !== addressIndex) {
|
|
190
|
+
throw new Error(`omnia-host signer address ${addressIndex} cannot sign reserved address ${indices.addressIndex}`);
|
|
191
|
+
}
|
|
192
|
+
const leafIndex = indices.l1 * 64 + indices.l2;
|
|
193
|
+
treeKey.setUses(leafIndex);
|
|
194
|
+
return (0, core_1.serializeTreeSignature)(treeKey.sign(payload));
|
|
207
195
|
},
|
|
208
196
|
};
|
|
209
|
-
const storage = new JsonFileStorageAdapter(leaseStorageDir(config.dbPath));
|
|
210
|
-
const leaseProvider = new wots_lease_1.LocalLeaseProvider(storage, undefined, deviceId);
|
|
211
197
|
return {
|
|
212
198
|
signer,
|
|
213
199
|
leaseProvider,
|
|
214
200
|
address,
|
|
215
201
|
publicKeyDigest,
|
|
216
202
|
baseSeed,
|
|
217
|
-
|
|
203
|
+
treeKey,
|
|
218
204
|
addressIndex,
|
|
219
205
|
deviceId,
|
|
220
206
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/omnia-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Durable Omnia node daemon for channel lifecycle, routing, and control APIs",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,17 +22,18 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"better-sqlite3": "^12.2.0",
|
|
24
24
|
"ws": "^8.21.1",
|
|
25
|
-
"@totemsdk/
|
|
26
|
-
"@totemsdk/identity": "^1.0.
|
|
25
|
+
"@totemsdk/chain-provider": "^0.2.1",
|
|
26
|
+
"@totemsdk/identity": "^1.0.1",
|
|
27
|
+
"@totemsdk/core": "^1.2.11",
|
|
28
|
+
"@totemsdk/omnia": "^2.0.0",
|
|
27
29
|
"@totemsdk/manifest": "^1.0.0",
|
|
28
|
-
"@totemsdk/
|
|
29
|
-
"@totemsdk/omnia": "^
|
|
30
|
-
"@totemsdk/omnia-
|
|
31
|
-
"@totemsdk/
|
|
32
|
-
"@totemsdk/omnia-splice": "^0.
|
|
33
|
-
"@totemsdk/
|
|
34
|
-
"@totemsdk/
|
|
35
|
-
"@totemsdk/wots-lease": "^1.0.0"
|
|
30
|
+
"@totemsdk/omnia-factory": "^0.3.0",
|
|
31
|
+
"@totemsdk/omnia-router": "^0.3.0",
|
|
32
|
+
"@totemsdk/omnia-vtxo": "^0.4.0",
|
|
33
|
+
"@totemsdk/minima-rpc": "^0.2.2",
|
|
34
|
+
"@totemsdk/omnia-splice": "^0.3.0",
|
|
35
|
+
"@totemsdk/storage": "^0.1.0",
|
|
36
|
+
"@totemsdk/wots-lease": "^1.0.1"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@types/jest": "^30.0.0",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
"jest": "^30.4.2",
|
|
43
44
|
"ts-jest": "^29.4.11",
|
|
44
45
|
"typescript": "5.9.3",
|
|
45
|
-
"@totemsdk/txpow": "^1.0.
|
|
46
|
+
"@totemsdk/txpow": "^1.0.2"
|
|
46
47
|
},
|
|
47
48
|
"engines": {
|
|
48
49
|
"node": ">=20.0.0"
|