@totemsdk/omnia-host 0.10.0 → 0.11.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.
@@ -38,7 +38,7 @@ function createTotemNodeAdapter(options) {
38
38
  getTokensByCreator: (address) => provider.getTokensByCreator(address),
39
39
  broadcastTxPoW: (txpowHex) => provider.broadcastTxPoW(txpowHex),
40
40
  getMinTxPowWork: () => provider.getMinTxPowWork ? provider.getMinTxPowWork() : Promise.resolve(null),
41
- ...(provider.submitTransaction
41
+ ...(provider.submitTransaction && process.env.OMNIA_CHAIN_NO_TXNMINE !== '1'
42
42
  ? { submitTransaction: provider.submitTransaction.bind(provider) }
43
43
  : {}),
44
44
  async getMode() {
package/dist/cli.js CHANGED
@@ -1,10 +1,74 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_path_1 = __importDefault(require("node:path"));
4
8
  const index_js_1 = require("./index.js");
9
+ const pairing_js_1 = require("./pairing.js");
5
10
  const signing_js_1 = require("./signing.js");
6
11
  const identity_js_1 = require("./identity.js");
12
+ const signer_port_js_1 = require("./signer-port.js");
13
+ async function runPair() {
14
+ const config = (0, index_js_1.loadConfigFromEnv)();
15
+ const port = Number(process.env.OMNIA_PAIR_PORT ?? 7789);
16
+ const host = process.env.OMNIA_PAIR_HOST ?? '127.0.0.1';
17
+ const origins = (process.env.OMNIA_PAIR_ORIGINS ?? '')
18
+ .split(',')
19
+ .map((value) => value.trim())
20
+ .filter(Boolean);
21
+ const capabilities = {
22
+ chain: { rpcUrl: config.chainRpcUrl, network: 'minima' },
23
+ lease: { modes: ['local', 'hybrid'] },
24
+ ...(config.p2pHubPort
25
+ ? {
26
+ p2p: {
27
+ hubUrl: `ws://${config.p2pHubHost ?? '127.0.0.1'}:${config.p2pHubPort}`,
28
+ scopes: ['p2p.pubsub'],
29
+ ...(config.p2pHubTopics ? { topicPrefixes: config.p2pHubTopics } : {}),
30
+ },
31
+ }
32
+ : {}),
33
+ };
34
+ const service = (0, pairing_js_1.createPairingService)({
35
+ storePath: node_path_1.default.join(node_path_1.default.dirname(config.dbPath), 'pairing.json'),
36
+ endpoint: `http://${host}:${port}`,
37
+ network: 'minima',
38
+ capabilities,
39
+ ...(origins.length ? { allowedOrigins: origins } : {}),
40
+ });
41
+ const server = (0, pairing_js_1.createPairingHttpServer)(service, { host, port });
42
+ const address = await server.listen();
43
+ const invitation = service.createInvitation();
44
+ if (process.argv.includes('--json')) {
45
+ console.log(JSON.stringify({ ...invitation, endpoint: `http://${address.host}:${address.port}` }, null, 2));
46
+ }
47
+ else {
48
+ console.log('');
49
+ console.log('Pair this node with your Totem extension');
50
+ console.log('');
51
+ console.log(`Node: ${service.nodeId}`);
52
+ console.log(`Network: ${service.network}`);
53
+ console.log(`Fingerprint: ${service.fingerprint}`);
54
+ console.log(`Endpoint: http://${address.host}:${address.port}`);
55
+ console.log(`Expires: ${new Date(invitation.expiresAt).toISOString()}`);
56
+ console.log('');
57
+ console.log(`Code: ${invitation.code}`);
58
+ console.log('');
59
+ console.log('Open Totem → Settings → Pair Node and paste the endpoint + code.');
60
+ }
61
+ await new Promise((resolve) => {
62
+ process.once('SIGINT', resolve);
63
+ process.once('SIGTERM', resolve);
64
+ });
65
+ await server.close();
66
+ }
7
67
  async function main() {
68
+ if (process.argv[2] === 'pair') {
69
+ await runPair();
70
+ return;
71
+ }
8
72
  const config = (0, index_js_1.loadConfigFromEnv)();
9
73
  const signing = config.readOnly || !(0, signing_js_1.hasSigningMaterial)(config)
10
74
  ? undefined
@@ -16,6 +80,7 @@ async function main() {
16
80
  ? {
17
81
  signer: signing.signer,
18
82
  leaseProvider: signing.leaseProvider,
83
+ signerPort: (0, signer_port_js_1.createLocalSignerPort)(signing),
19
84
  transportKeyPair: signing.transportKeyPair,
20
85
  transportPubkey: signing.transportPubkey,
21
86
  identity: identityAndManifest?.identity
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { CustodyMode } from '@totemsdk/omnia';
1
2
  export interface OmniaHostConfig {
2
3
  port: number;
3
4
  host: string;
@@ -5,6 +6,14 @@ export interface OmniaHostConfig {
5
6
  chainRpcUrl: string;
6
7
  chainRpcPassword?: string;
7
8
  analyticsDbPath?: string;
9
+ /** Durable observation journal DB (default: alongside dbPath). */
10
+ observeDbPath?: string;
11
+ /** Observation network id (multi-network deployments). */
12
+ networkId?: string;
13
+ /** Stable observation source id (default: localPartyId or "omnia-host"). */
14
+ sourceNodeId?: string;
15
+ /** Disable the observation feed entirely (OMNIA_OBSERVE=0). */
16
+ observeDisabled?: boolean;
8
17
  relay?: string;
9
18
  localPubkey?: string;
10
19
  localPartyId?: string;
@@ -28,6 +37,11 @@ export interface OmniaHostConfig {
28
37
  deviceId?: string;
29
38
  /** "1" forces read-only mode even with keys present. */
30
39
  readOnly: boolean;
40
+ /**
41
+ * Deployment custody mode (RFC-058). Defaults to self-custody; hosted custody
42
+ * is explicit and policy-controlled. This does not change on-chain custody.
43
+ */
44
+ custodyMode?: CustodyMode;
31
45
  /** Identity file containing a delegated identity claim (operator root → service delegate). */
32
46
  identityFile?: string;
33
47
  /** EdgeServiceManifest serviceType (default "omnia-router"). */
@@ -42,6 +56,12 @@ export interface OmniaHostConfig {
42
56
  * Undefined disables token auth.
43
57
  */
44
58
  controlToken?: string;
59
+ /** RFC-062 local P2P hub (optional). One Hyperswarm shared by dapps. */
60
+ p2pHubPort?: number;
61
+ p2pHubHost?: string;
62
+ p2pHubToken?: string;
63
+ p2pHubOrigins?: string[];
64
+ p2pHubTopics?: string[];
45
65
  }
46
66
  /** Load the daemon configuration from the documented OMNIA_* environment. */
47
67
  export declare function loadConfigFromEnv(env?: NodeJS.ProcessEnv): OmniaHostConfig;
package/dist/config.js CHANGED
@@ -35,6 +35,27 @@ function parseOrigins(value) {
35
35
  .filter((part) => part.length > 0);
36
36
  return parts && parts.length > 0 ? parts : undefined;
37
37
  }
38
+ function parseOptionalPort(value, name) {
39
+ const trimmed = value?.trim();
40
+ if (!trimmed)
41
+ return undefined;
42
+ const port = Number(trimmed);
43
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
44
+ throw new Error(`${name} must be an integer between 1 and 65535; received ${value}`);
45
+ }
46
+ return port;
47
+ }
48
+ function parseList(value) {
49
+ const parts = value?.split(',').map((part) => part.trim()).filter(Boolean);
50
+ return parts && parts.length > 0 ? parts : undefined;
51
+ }
52
+ function parseCustodyMode(value) {
53
+ const mode = optional(value) ?? 'self';
54
+ if (mode !== 'self' && mode !== 'trust-minimised' && mode !== 'hosted') {
55
+ throw new Error(`OMNIA_CUSTODY must be self, trust-minimised or hosted; received ${value}`);
56
+ }
57
+ return mode;
58
+ }
38
59
  function parseSeed(value) {
39
60
  const seed = optional(value);
40
61
  if (!seed)
@@ -62,13 +83,18 @@ function loadConfigFromEnv(env = process.env) {
62
83
  if (seed && keyfile) {
63
84
  throw new Error('OMNIA_HOST_SEED and OMNIA_HOST_KEYFILE are mutually exclusive — set exactly one of them');
64
85
  }
86
+ const dbPath = env.OMNIA_HOST_DB?.trim() || node_path_1.default.join(dataRoot, 'omnia.sqlite');
65
87
  return {
66
88
  port: parsePort(env.OMNIA_HOST_PORT),
67
89
  host: env.OMNIA_HOST_BIND?.trim() || '127.0.0.1',
68
- dbPath: env.OMNIA_HOST_DB?.trim() || node_path_1.default.join(dataRoot, 'omnia.sqlite'),
90
+ dbPath,
69
91
  chainRpcUrl: env.OMNIA_CHAIN_RPC?.trim() || 'http://127.0.0.1:9005',
70
92
  chainRpcPassword: optional(env.OMNIA_CHAIN_RPC_PASSWORD),
71
93
  analyticsDbPath: optional(env.OMNIA_ANALYTICS_DB),
94
+ observeDbPath: optional(env.OMNIA_OBSERVE_DB) ?? node_path_1.default.join(node_path_1.default.dirname(dbPath), 'observe.sqlite'),
95
+ networkId: optional(env.OMNIA_NETWORK_ID),
96
+ sourceNodeId: optional(env.OMNIA_SOURCE_NODE_ID),
97
+ observeDisabled: env.OMNIA_OBSERVE?.trim() === '0' || env.OMNIA_OBSERVE?.trim().toLowerCase() === 'false',
72
98
  relay: optional(env.OMNIA_RELAY),
73
99
  localPubkey: optional(env.OMNIA_LOCAL_PUBKEY),
74
100
  localPartyId: optional(env.OMNIA_LOCAL_PARTY_ID),
@@ -82,9 +108,15 @@ function loadConfigFromEnv(env = process.env) {
82
108
  keyfilePassphrase: optional(env.OMNIA_HOST_KEYFILE_PASSPHRASE),
83
109
  deviceId: optional(env.OMNIA_HOST_DEVICE_ID),
84
110
  readOnly: env.OMNIA_HOST_READ_ONLY?.trim() === '1',
111
+ custodyMode: parseCustodyMode(env.OMNIA_CUSTODY),
85
112
  identityFile: optional(env.OMNIA_HOST_IDENTITY_FILE),
86
113
  serviceType: optional(env.OMNIA_HOST_SERVICE_TYPE) ?? 'omnia-router',
87
114
  allowedOrigins: parseOrigins(env.OMNIA_ALLOWED_ORIGINS),
115
+ p2pHubPort: parseOptionalPort(env.OMNIA_P2P_HUB_PORT, 'OMNIA_P2P_HUB_PORT'),
116
+ p2pHubHost: optional(env.OMNIA_P2P_HUB_HOST),
117
+ p2pHubToken: optional(env.OMNIA_P2P_HUB_TOKEN),
118
+ p2pHubOrigins: parseOrigins(env.OMNIA_P2P_HUB_ORIGINS),
119
+ p2pHubTopics: parseList(env.OMNIA_P2P_HUB_TOPICS),
88
120
  controlToken: optional(env.OMNIA_CONTROL_TOKEN),
89
121
  };
90
122
  }
package/dist/index.d.ts CHANGED
@@ -19,3 +19,6 @@ export type { HostApiContext } from './api/methods.js';
19
19
  export type { OperationStoreLike } from './api/methods.js';
20
20
  export { DisabledAnalyticsStore, DuckDbAnalyticsStore, initializeDuckDb } from './stores/analytics.js';
21
21
  export type { AnalyticsEvent, AnalyticsStore, DuckDbConnection } from './stores/analytics.js';
22
+ export { createLocalSignerPort, createPolicySignerPort } from './signer-port.js';
23
+ export type { SignerPolicy } from './signer-port.js';
24
+ export * from './observe/index.js';
package/dist/index.js CHANGED
@@ -1,6 +1,20 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initializeDuckDb = exports.DuckDbAnalyticsStore = exports.DisabledAnalyticsStore = exports.createHostMethods = exports.GoRoutingProvider = exports.InProcessRoutingProvider = exports.createTotemNodeAdapter = exports.OperationStore = exports.SqliteChannelStore = exports.loadHostIdentity = exports.createHostManifest = exports.createHostIdentityAndManifest = exports.encryptSeedForKeyfile = exports.leaseStorageDir = exports.JsonFileStorageAdapter = exports.hasSigningMaterial = exports.createHostSigning = exports.createOmniaHost = exports.loadConfigFromEnv = void 0;
17
+ exports.createPolicySignerPort = exports.createLocalSignerPort = exports.initializeDuckDb = exports.DuckDbAnalyticsStore = exports.DisabledAnalyticsStore = exports.createHostMethods = exports.GoRoutingProvider = exports.InProcessRoutingProvider = exports.createTotemNodeAdapter = exports.OperationStore = exports.SqliteChannelStore = exports.loadHostIdentity = exports.createHostManifest = exports.createHostIdentityAndManifest = exports.encryptSeedForKeyfile = exports.leaseStorageDir = exports.JsonFileStorageAdapter = exports.hasSigningMaterial = exports.createHostSigning = exports.createOmniaHost = exports.loadConfigFromEnv = void 0;
4
18
  var config_js_1 = require("./config.js");
5
19
  Object.defineProperty(exports, "loadConfigFromEnv", { enumerable: true, get: function () { return config_js_1.loadConfigFromEnv; } });
6
20
  var lifecycle_js_1 = require("./lifecycle.js");
@@ -31,3 +45,8 @@ var analytics_js_1 = require("./stores/analytics.js");
31
45
  Object.defineProperty(exports, "DisabledAnalyticsStore", { enumerable: true, get: function () { return analytics_js_1.DisabledAnalyticsStore; } });
32
46
  Object.defineProperty(exports, "DuckDbAnalyticsStore", { enumerable: true, get: function () { return analytics_js_1.DuckDbAnalyticsStore; } });
33
47
  Object.defineProperty(exports, "initializeDuckDb", { enumerable: true, get: function () { return analytics_js_1.initializeDuckDb; } });
48
+ var signer_port_js_1 = require("./signer-port.js");
49
+ Object.defineProperty(exports, "createLocalSignerPort", { enumerable: true, get: function () { return signer_port_js_1.createLocalSignerPort; } });
50
+ Object.defineProperty(exports, "createPolicySignerPort", { enumerable: true, get: function () { return signer_port_js_1.createPolicySignerPort; } });
51
+ // Durable, replayable observation feed (RFC-0011 Amendment A).
52
+ __exportStar(require("./observe/index.js"), exports);
@@ -1,4 +1,6 @@
1
1
  import type { OmniaHostConfig } from './config.js';
2
+ import { ObservationBus } from './observe/bus.js';
3
+ import type { ObservationStore } from './observe/types.js';
2
4
  import type { OmniaSwarm, ChannelSigner, WotsLeaseProviderLike, MinimalChainProvider } from '@totemsdk/omnia';
3
5
  import { createOmniaSwarm } from '@totemsdk/omnia';
4
6
  import { OperationStore } from './stores/operations.js';
@@ -7,6 +9,7 @@ import { type ControlServer } from './api/jsonrpc.js';
7
9
  import { type TotemNodeAdapter } from './chain/totem-node-adapter.js';
8
10
  import { type RoutingProvider } from './router/routing-provider.js';
9
11
  import type { JsonRpcHandler } from './api/jsonrpc.js';
12
+ import { type HubNetwork } from '@totemsdk/p2p-hub';
10
13
  import { type OperationStoreLike } from './api/methods.js';
11
14
  import type { ChannelFactory, WotsLeaseBundle } from '@totemsdk/omnia-factory';
12
15
  import type { SpliceAcceptance, SpliceLeaseProvider, SpliceProposal } from '@totemsdk/omnia-splice';
@@ -17,6 +20,8 @@ export interface OmniaHostDependencies {
17
20
  operationStore?: OperationStore | Pick<OperationStore, 'close'>;
18
21
  signer?: ChannelSigner;
19
22
  leaseProvider?: WotsLeaseProviderLike;
23
+ /** RFC-058 signer port; defaults to a local signer in CLI deployments. */
24
+ signerPort?: import('@totemsdk/omnia').SignerPort;
20
25
  chainProvider?: MinimalChainProvider;
21
26
  controlServer?: ControlServer;
22
27
  chainAdapter?: TotemNodeAdapter;
@@ -45,6 +50,14 @@ export interface OmniaHostDependencies {
45
50
  };
46
51
  /** Boot-time signed EdgeServiceManifest (getManifest). */
47
52
  manifest?: unknown;
53
+ /** Optional P2P hub network (tests/embedding). */
54
+ p2pHubNetwork?: HubNetwork;
55
+ /** Observation feed: inject a store/bus (tests/embedding) or disable it. */
56
+ observationStore?: ObservationStore;
57
+ observationBus?: ObservationBus;
58
+ observe?: boolean;
59
+ networkId?: string;
60
+ sourceNodeId?: string;
48
61
  }
49
62
  export interface OmniaHost {
50
63
  readonly config: OmniaHostConfig;
@@ -53,6 +66,8 @@ export interface OmniaHost {
53
66
  isStarted(): boolean;
54
67
  readonly swarm?: OmniaSwarm;
55
68
  readonly channels?: Map<string, OmniaChannel>;
69
+ /** Loopback WebSocket endpoint for local dapps, when enabled (RFC-062). */
70
+ readonly p2pHubUrl?: string;
56
71
  }
57
72
  /**
58
73
  * Phase-1 lifecycle shell. Subsystems will be attached in later phases while
package/dist/lifecycle.js CHANGED
@@ -1,12 +1,21 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.createOmniaHost = createOmniaHost;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const bus_js_1 = require("./observe/bus.js");
9
+ const emitter_js_1 = require("./observe/emitter.js");
10
+ const store_js_1 = require("./observe/store.js");
11
+ const snapshot_js_1 = require("./observe/snapshot.js");
4
12
  const omnia_1 = require("@totemsdk/omnia");
5
13
  const sqlite_store_js_1 = require("./stores/sqlite-store.js");
6
14
  const operations_js_1 = require("./stores/operations.js");
7
15
  const jsonrpc_js_1 = require("./api/jsonrpc.js");
8
16
  const totem_node_adapter_js_1 = require("./chain/totem-node-adapter.js");
9
17
  const routing_provider_js_1 = require("./router/routing-provider.js");
18
+ const p2p_hub_1 = require("@totemsdk/p2p-hub");
10
19
  const methods_js_1 = require("./api/methods.js");
11
20
  const reply_cache_js_1 = require("./reply-cache.js");
12
21
  const omnia_factory_1 = require("@totemsdk/omnia-factory");
@@ -26,9 +35,15 @@ function createOmniaHost(config, dependencies = {}) {
26
35
  : undefined;
27
36
  const injectedOperationStore = dependencies.operationStore;
28
37
  let ownedOperationStore;
38
+ let observationStore = dependencies.observationStore;
39
+ let ownedObservationStore;
40
+ const observationBus = dependencies.observationBus ?? new bus_js_1.ObservationBus();
41
+ let observation;
29
42
  let controlServer = dependencies.controlServer;
30
43
  let chainAdapter = dependencies.chainAdapter;
31
44
  let routingProvider = dependencies.routingProvider ?? new routing_provider_js_1.InProcessRoutingProvider();
45
+ let p2pHubServer;
46
+ let p2pHubUrl;
32
47
  return {
33
48
  config,
34
49
  get swarm() {
@@ -37,6 +52,9 @@ function createOmniaHost(config, dependencies = {}) {
37
52
  get channels() {
38
53
  return channelStore;
39
54
  },
55
+ get p2pHubUrl() {
56
+ return p2pHubUrl;
57
+ },
40
58
  async start() {
41
59
  if (started)
42
60
  return;
@@ -53,6 +71,25 @@ function createOmniaHost(config, dependencies = {}) {
53
71
  ownedOperationStore = new operations_js_1.OperationStore(config.dbPath);
54
72
  operationStore = ownedOperationStore;
55
73
  }
74
+ const observeEnabled = dependencies.observe === true || (dependencies.observe !== false && config.observeDisabled !== true && config.observeDbPath !== undefined);
75
+ if (observeEnabled && !observationStore) {
76
+ const observePath = config.observeDbPath ?? node_path_1.default.join(node_path_1.default.dirname(config.dbPath), 'observe.sqlite');
77
+ try {
78
+ ownedObservationStore = await (0, store_js_1.createSqliteObservationStore)(observePath);
79
+ observationStore = ownedObservationStore;
80
+ }
81
+ catch (err) {
82
+ console.error('[omnia-host] observation store disabled:', err instanceof Error ? err.message : String(err));
83
+ }
84
+ }
85
+ if (observationStore) {
86
+ observation = new emitter_js_1.ObservationEmitter({
87
+ store: observationStore,
88
+ bus: observationBus,
89
+ networkId: config.networkId ?? dependencies.networkId ?? 'omnia',
90
+ sourceNodeId: config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host',
91
+ });
92
+ }
56
93
  if (!swarm) {
57
94
  const relay = config.relay
58
95
  ? { mode: 'self-hosted', relayUrl: config.relay }
@@ -85,6 +122,21 @@ function createOmniaHost(config, dependencies = {}) {
85
122
  ssl: rpc.protocol === 'https:',
86
123
  });
87
124
  }
125
+ if (config.p2pHubPort !== undefined || dependencies.p2pHubNetwork) {
126
+ const native = swarm.nativeSwarm?.();
127
+ const network = dependencies.p2pHubNetwork ?? await (0, p2p_hub_1.createHyperswarmHubNetwork)(native ? { swarm: native } : {});
128
+ p2pHubServer = await (0, p2p_hub_1.createWebSocketHubServer)({
129
+ network,
130
+ host: config.p2pHubHost ?? '127.0.0.1',
131
+ port: config.p2pHubPort ?? 0,
132
+ ...(config.p2pHubToken ? { token: config.p2pHubToken } : {}),
133
+ ...(config.p2pHubOrigins ? { allowedOrigins: config.p2pHubOrigins } : {}),
134
+ ...(config.p2pHubTopics ? { allowedTopicPrefixes: config.p2pHubTopics } : {}),
135
+ });
136
+ const address = await p2pHubServer.listen();
137
+ const host = address.host.includes(':') ? `[${address.host}]` : address.host;
138
+ p2pHubUrl = `ws://${host}:${address.port}`;
139
+ }
88
140
  const localParticipant = dependencies.localParticipant ?? (config.localPartyId && config.localPubkey && config.localAddressIndex !== undefined
89
141
  ? {
90
142
  partyId: config.localPartyId,
@@ -158,6 +210,12 @@ function createOmniaHost(config, dependencies = {}) {
158
210
  if (msg.type === 'SPLICE_FINALIZED' && payload.channel) {
159
211
  const channel = payload.channel;
160
212
  channelStore.set(channel.channelId, channel);
213
+ void observation?.emit({
214
+ domain: 'splice', kind: 'splice.finalized', phase: 'confirmed',
215
+ entity: { type: 'channel', id: channel.channelId },
216
+ amounts: { capacity: channel.totalValue?.toString(), balances: Object.fromEntries(Object.entries(channel.balances ?? {}).map(([k, v]) => [k, v.toString()])) },
217
+ data: { spliceId: payload.spliceId, txId: payload.txId },
218
+ });
161
219
  const acceptance = spliceAcceptances.get(payload.spliceId);
162
220
  if (acceptance && typeof payload.txId === 'string') {
163
221
  await spliceLeaseProvider.wotsLease
@@ -217,6 +275,11 @@ function createOmniaHost(config, dependencies = {}) {
217
275
  if (msg.type === 'FACTORY_PROPOSAL' && payload.factory) {
218
276
  const accepted = await (0, omnia_factory_1.acceptFactory)(payload.factory, localBundle);
219
277
  factories.set(accepted.factoryId, accepted);
278
+ void observation?.emit({
279
+ domain: 'factory', kind: 'factory.accepted', phase: 'accepted',
280
+ entity: { type: 'factory', id: accepted.factoryId },
281
+ data: { participants: accepted.participants },
282
+ });
220
283
  return {
221
284
  type: 'FACTORY_ACCEPT',
222
285
  channelId: accepted.factoryId,
@@ -261,6 +324,47 @@ function createOmniaHost(config, dependencies = {}) {
261
324
  if (reply)
262
325
  await peer.sendMessage(reply);
263
326
  };
327
+ const channelPeers = new Map();
328
+ const onPeerMessage = (peer, msg) => {
329
+ if (msg?.channelId)
330
+ channelPeers.set(msg.channelId, peer);
331
+ void observation?.emit({
332
+ domain: 'transport', kind: 'transport.message', phase: 'observed',
333
+ entity: { type: 'node', id: config.sourceNodeId ?? config.localPartyId ?? 'omnia-host' },
334
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
335
+ data: { type: msg?.type, channelId: msg?.channelId, nonce: msg?.nonce },
336
+ });
337
+ };
338
+ const onChannelAccepted = (channel, peer) => {
339
+ void observation?.emit({
340
+ domain: 'channel', kind: 'channel.proposal.accepted', phase: 'accepted',
341
+ entity: { type: 'channel', id: channel.channelId },
342
+ parties: { local: localParticipant?.partyId, remote: channel.parties?.find((p) => p.partyId !== localParticipant?.partyId)?.partyId },
343
+ token: { tokenId: channel.tokenId, scale: channel.tokenScale ?? 0 },
344
+ amounts: { capacity: channel.totalValue?.toString(), balances: Object.fromEntries(Object.entries(channel.balances ?? {}).map(([k, v]) => [k, v.toString()])) },
345
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(channel),
346
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
347
+ });
348
+ };
349
+ const onStateUpdated = (channel, peer) => {
350
+ void observation?.emit({
351
+ domain: 'channel', kind: 'channel.updated', phase: 'committed',
352
+ entity: { type: 'channel', id: channel.channelId },
353
+ parties: { local: localParticipant?.partyId, remote: channel.parties?.find((p) => p.partyId !== localParticipant?.partyId)?.partyId },
354
+ token: { tokenId: channel.tokenId, scale: channel.tokenScale ?? 0 },
355
+ after: { sequence: channel.currentSequence, balances: Object.fromEntries(Object.entries(channel.balances ?? {}).map(([k, v]) => [k, v.toString()])) },
356
+ transitionHash: (0, emitter_js_1.channelTransitionHash)(channel),
357
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
358
+ });
359
+ };
360
+ const onSettlementProposed = (payload, peer) => {
361
+ void observation?.emit({
362
+ domain: 'settlement', kind: 'settlement.proposed', phase: 'proposed',
363
+ entity: { type: 'channel', id: payload?.channelId ?? 'unknown' },
364
+ evidence: { peerPublicKeyDigest: peer?.publicKeyDigest },
365
+ data: { payload },
366
+ });
367
+ };
264
368
  const onVcMessage = async (peer, msg) => {
265
369
  if (!factories || !localBundle)
266
370
  return;
@@ -270,9 +374,19 @@ function createOmniaHost(config, dependencies = {}) {
270
374
  factories.set(payload.factory.factoryId, payload.factory);
271
375
  if (payload.kind === 'vc-open' && payload.channel) {
272
376
  channelStore?.set(payload.channel.channelId, payload.channel);
377
+ void observation?.emit({
378
+ domain: 'factory', kind: 'factory.vc_opened', phase: 'accepted',
379
+ entity: { type: 'channel', id: payload.channel.channelId },
380
+ data: { factoryId: payload.factory?.factoryId },
381
+ });
273
382
  }
274
383
  else if (payload.kind === 'vc-close' && payload.channelId) {
275
384
  channelStore?.delete(payload.channelId);
385
+ void observation?.emit({
386
+ domain: 'factory', kind: 'factory.vc_closed', phase: 'closed',
387
+ entity: { type: 'channel', id: payload.channelId },
388
+ data: { factoryId: payload.factory?.factoryId },
389
+ });
276
390
  }
277
391
  return;
278
392
  }
@@ -325,7 +439,22 @@ function createOmniaHost(config, dependencies = {}) {
325
439
  onFactoryMessage,
326
440
  onVcMessage,
327
441
  onSpliceMessage,
442
+ onPeerMessage,
443
+ onChannelAccepted,
444
+ onStateUpdated,
445
+ onSettlementProposed,
328
446
  });
447
+ const preparedPay = new Map();
448
+ const preparedSettle = new Map();
449
+ const preparedOpen = new Map();
450
+ const preparedSplice = new Map();
451
+ const preparedFactory = new Map();
452
+ const vtxoPools = new Map();
453
+ const vtxos = new Map();
454
+ const vtxoReceipts = new Map();
455
+ const preparedVtxoReceipts = new Map();
456
+ const pools = new Map();
457
+ const poolPositions = new Map();
329
458
  if (!controlServer) {
330
459
  const readOnly = !dependencies.signer || !dependencies.leaseProvider;
331
460
  if (readOnly) {
@@ -338,14 +467,32 @@ function createOmniaHost(config, dependencies = {}) {
338
467
  isReady: () => started,
339
468
  allowedOrigins: config.allowedOrigins,
340
469
  authToken: config.controlToken,
470
+ observe: observationStore ? {
471
+ store: observationStore,
472
+ bus: observationBus,
473
+ snapshot: async () => (0, snapshot_js_1.createObservationSnapshot)({
474
+ networkId: config.networkId ?? dependencies.networkId ?? 'omnia',
475
+ sourceNodeId: config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host',
476
+ streamId: `${config.sourceNodeId ?? dependencies.sourceNodeId ?? config.localPartyId ?? 'omnia-host'}:${config.networkId ?? dependencies.networkId ?? 'omnia'}`,
477
+ cursor: await observationStore.head(),
478
+ channels: channelStore?.values(),
479
+ factories: factories?.values(),
480
+ vtxoPools: vtxoPools.values(),
481
+ vtxos: vtxos.values(),
482
+ pools: pools.values(),
483
+ operations: (dependencies.operationStoreForApi ?? operationStore),
484
+ }),
485
+ } : undefined,
341
486
  methods: dependencies.methods ?? (0, methods_js_1.createHostMethods)({
342
487
  channels: channelStore,
343
488
  routing: routingProvider,
344
489
  operations: dependencies.operationStoreForApi ?? operationStore,
490
+ observe: observation,
345
491
  signer: dependencies.signer,
346
492
  leaseProvider: dependencies.leaseProvider,
347
493
  chainProvider: dependencies.chainProvider ?? chainAdapter,
348
494
  swarm,
495
+ channelPeers,
349
496
  localParticipant,
350
497
  transportPubkey: dependencies.transportPubkey,
351
498
  factories,
@@ -353,6 +500,20 @@ function createOmniaHost(config, dependencies = {}) {
353
500
  spliceProposals,
354
501
  spliceAcceptances,
355
502
  spliceLeaseProvider,
503
+ custodyMode: config.custodyMode ?? 'self',
504
+ signerPort: dependencies.signerPort,
505
+ preparedPay,
506
+ preparedSettle,
507
+ preparedOpen,
508
+ preparedSplice,
509
+ preparedFactory,
510
+ vtxoPools,
511
+ vtxos,
512
+ vtxoReceipts,
513
+ preparedVtxoReceipts,
514
+ pools,
515
+ poolPositions,
516
+ coordinator: !dependencies.signer && !config.readOnly,
356
517
  fundingSigner: config.fundingPubkey
357
518
  ? async (digest) => {
358
519
  const sig = await chainAdapter.signData(config.fundingPubkey, '0x' + Buffer.from(digest).toString('hex'));
@@ -362,6 +523,7 @@ function createOmniaHost(config, dependencies = {}) {
362
523
  readOnly,
363
524
  identity: dependencies.identity,
364
525
  manifest: dependencies.manifest,
526
+ p2pHubUrl,
365
527
  refreshRouting: () => routingProvider.rebuild((0, routing_provider_js_1.channelGraphEdges)(channelStore.values())),
366
528
  }),
367
529
  });
@@ -373,6 +535,9 @@ function createOmniaHost(config, dependencies = {}) {
373
535
  catch (error) {
374
536
  unsubscribe?.();
375
537
  unsubscribe = undefined;
538
+ await p2pHubServer?.close();
539
+ p2pHubServer = undefined;
540
+ p2pHubUrl = undefined;
376
541
  chainAdapter?.close();
377
542
  await swarm?.close();
378
543
  if (ownedOperationStore)
@@ -388,6 +553,9 @@ function createOmniaHost(config, dependencies = {}) {
388
553
  return;
389
554
  unsubscribe?.();
390
555
  unsubscribe = undefined;
556
+ await p2pHubServer?.close();
557
+ p2pHubServer = undefined;
558
+ p2pHubUrl = undefined;
391
559
  await controlServer?.close();
392
560
  chainAdapter?.close();
393
561
  await swarm?.close();
@@ -396,6 +564,9 @@ function createOmniaHost(config, dependencies = {}) {
396
564
  else if (injectedOperationStore)
397
565
  await injectedOperationStore.close();
398
566
  ownedOperationStore = undefined;
567
+ if (ownedObservationStore)
568
+ await ownedObservationStore.close();
569
+ ownedObservationStore = undefined;
399
570
  ownedStore?.close();
400
571
  ownedStore = undefined;
401
572
  started = false;
@@ -0,0 +1,13 @@
1
+ import type { OmniaObservation } from "./types.js";
2
+ export type ObservationListener = (observation: OmniaObservation) => void;
3
+ /**
4
+ * In-process live fan-out for SSE consumers. The durable journal remains the
5
+ * source of truth: a subscriber that misses a live event still replays it from
6
+ * `ObservationStore.readSince(cursor)`.
7
+ */
8
+ export declare class ObservationBus {
9
+ private readonly listeners;
10
+ subscribe(listener: ObservationListener): () => void;
11
+ publish(observation: OmniaObservation): void;
12
+ get size(): number;
13
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ObservationBus = void 0;
4
+ /**
5
+ * In-process live fan-out for SSE consumers. The durable journal remains the
6
+ * source of truth: a subscriber that misses a live event still replays it from
7
+ * `ObservationStore.readSince(cursor)`.
8
+ */
9
+ class ObservationBus {
10
+ listeners = new Set();
11
+ subscribe(listener) {
12
+ this.listeners.add(listener);
13
+ return () => this.listeners.delete(listener);
14
+ }
15
+ publish(observation) {
16
+ for (const listener of this.listeners) {
17
+ try {
18
+ listener(observation);
19
+ }
20
+ catch {
21
+ // A broken consumer must never break the mutation path.
22
+ }
23
+ }
24
+ }
25
+ get size() {
26
+ return this.listeners.size;
27
+ }
28
+ }
29
+ exports.ObservationBus = ObservationBus;