@totemsdk/omnia-host 0.1.0 → 0.1.1

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 CHANGED
@@ -1,13 +1,16 @@
1
1
  # @totemsdk/omnia-host
2
2
 
3
- The Omnia host daemon will provide durable channel lifecycle, routing, chain
4
- integration, and control APIs for a Totem node. Phase 1 contains the package
5
- and lifecycle scaffold; subsystem implementations are added incrementally.
3
+ The Omnia host daemon provides durable channel lifecycle, routing, chain
4
+ integration, and control APIs for a Totem node. With signing material
5
+ provisioned it runs fully operational; without it, it boots in read-only mode
6
+ (channel/route/health queries only).
6
7
 
7
8
  ## Run
8
9
 
9
10
  ```bash
11
+ OMNIA_HOST_SEED="your bip39 mnemonic or 0x…64-hex seed" \
10
12
  OMNIA_CHAIN_RPC_PASSWORD=secret pnpm --filter @totemsdk/omnia-host build
13
+ OMNIA_HOST_SEED="your bip39 mnemonic or 0x…64-hex seed" \
11
14
  OMNIA_CHAIN_RPC_PASSWORD=secret pnpm --filter @totemsdk/omnia-host start
12
15
  ```
13
16
 
@@ -25,13 +28,45 @@ The daemon configuration is read from environment variables:
25
28
  | `OMNIA_LOCAL_PARTY_ID` | unset | Local channel party ID required for mutations |
26
29
  | `OMNIA_LOCAL_ADDRESS_INDEX` | unset | Local WOTS address index |
27
30
  | `OMNIA_LOCAL_SETTLEMENT_ADDRESS` | unset | Default cooperative settlement address |
31
+ | `OMNIA_HOST_SEED` | unset | BIP39 mnemonic or 32-byte hex seed for channel signing |
32
+ | `OMNIA_HOST_KEYFILE` | unset | Path to keyfile JSON (resolved against cwd); mutually exclusive with `OMNIA_HOST_SEED` |
33
+ | `OMNIA_HOST_KEYFILE_PASSPHRASE` | unset | Keyfile decryption passphrase (required for encrypted keyfiles) |
34
+ | `OMNIA_HOST_DEVICE_ID` | `device-<slot>` | Stable device id for the lease journal |
35
+ | `OMNIA_HOST_READ_ONLY` | unset | `1` forces read-only mode even with keys present |
36
+ | `OMNIA_HOST_IDENTITY_FILE` | unset | Optional delegated identity claim JSON (operator root → service delegate) |
37
+ | `OMNIA_HOST_SERVICE_TYPE` | `omnia-router` | EdgeServiceManifest serviceType |
38
+
39
+ ### Signing model
40
+
41
+ `OMNIA_HOST_SEED` (or the keyfile) is the base seed. The host derives a
42
+ per-address seed (`derivePerAddressSeed(baseSeed, localAddressIndex)`) and
43
+ signs every channel state with the flat WOTS key at index 0 of that seed; the
44
+ channel participant's `publicKeyDigest` is `derivePKdigest(perAddressSeed, 0)`.
45
+ The WOTS lease journal and watermark persist in `<OMNIA_HOST_DB>.lease/` so
46
+ they survive restarts together with the channel database.
47
+
48
+ ### Read-only mode
49
+
50
+ Without `OMNIA_HOST_SEED`/`OMNIA_HOST_KEYFILE` (or with `OMNIA_HOST_READ_ONLY=1`)
51
+ the daemon logs `[omnia-host] no signing material; starting READ-ONLY` and
52
+ registers only `getChannels`, `getRoute`, `getSwapRate`, `whoami`, and
53
+ `getManifest`. Mutation methods are never registered without a signer.
54
+
55
+ ### Identity + manifest (opt-in)
56
+
57
+ With signing material present, the daemon derives a service identity document
58
+ from the seed and signs a boot-time `EdgeServiceManifest` (serviceType from
59
+ `OMNIA_HOST_SERVICE_TYPE`). `OMNIA_HOST_IDENTITY_FILE` may supply a delegated
60
+ identity claim (operator root → service delegate) validated via
61
+ `verifyIdentityClaim`. Query them over JSON-RPC with `totem_omniaWhoami`
62
+ (`omnia/whoami`) and `totem_omniaGetManifest` (`omnia/getManifest`).
28
63
 
29
64
  The daemon is intended to run beside `totem-node -mode omnia` or `-mode full`.
30
65
  The current implementation includes the swarm lifecycle wiring, SQLite and
31
66
  operation-store abstractions, JSON-RPC/WebSocket control API, route provider,
32
- chain adapter, Edge adapter contract, optional Go-router boundary, and DuckDB
33
- analytics abstraction. Funding/signing mutations remain capability-gated until
34
- a signer and WOTS lease provider are supplied.
67
+ chain adapter, Edge adapter contract, optional Go-router boundary, DuckDB
68
+ analytics abstraction, env-provisioned signer + WOTS lease provider, and
69
+ multi-hop payment execution via `@totemsdk/omnia-router`.
35
70
 
36
71
  The local test environment may skip SQLite integration tests when the
37
72
  `better-sqlite3` native addon has not been built. Install lifecycle scripts in
@@ -22,5 +22,16 @@ export interface HostApiContext {
22
22
  spliceProposals?: Map<string, SpliceProposal>;
23
23
  spliceAcceptances?: Map<string, SpliceAcceptance>;
24
24
  spliceLeaseProvider?: SpliceLeaseProvider;
25
+ /** When true, only read-only methods are registered (no signer material). */
26
+ readOnly?: boolean;
27
+ /** Opt-in host identity (address, publicKeyDigest, optional delegation). */
28
+ identity?: {
29
+ address: string;
30
+ publicKeyDigest: string;
31
+ identityId?: string;
32
+ delegation?: unknown;
33
+ };
34
+ /** Boot-time signed EdgeServiceManifest. */
35
+ manifest?: unknown;
25
36
  }
26
37
  export declare function createHostMethods(context: HostApiContext): Map<string, JsonRpcHandler>;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createHostMethods = createHostMethods;
4
4
  const omnia_1 = require("@totemsdk/omnia");
5
+ const omnia_router_1 = require("@totemsdk/omnia-router");
5
6
  const omnia_factory_1 = require("@totemsdk/omnia-factory");
6
7
  const omnia_splice_1 = require("@totemsdk/omnia-splice");
7
8
  function channelSummary(channel) {
@@ -180,6 +181,30 @@ function createHostMethods(context) {
180
181
  register('totem_omniaGetRoute', getRoute, 'omnia/getRoute');
181
182
  const getSwapRate = () => ({ success: true, announcements: [] });
182
183
  register('totem_omniaGetSwapRate', getSwapRate, 'omnia/getSwapRate');
184
+ const whoami = () => {
185
+ if (!context.identity) {
186
+ return { address: null, publicKeyDigest: null };
187
+ }
188
+ return {
189
+ address: context.identity.address,
190
+ publicKeyDigest: context.identity.publicKeyDigest,
191
+ identityId: context.identity.identityId,
192
+ delegation: context.identity.delegation,
193
+ };
194
+ };
195
+ register('totem_omniaWhoami', whoami, 'omnia/whoami');
196
+ const getManifest = () => {
197
+ if (!context.manifest) {
198
+ return { manifest: null };
199
+ }
200
+ return { manifest: context.manifest };
201
+ };
202
+ register('totem_omniaGetManifest', getManifest, 'omnia/getManifest');
203
+ // Fail-fast rule: without signing material only the read-only surface is
204
+ // registered. Mutation methods must never exist without a signer.
205
+ if (context.readOnly || !context.signer || !context.leaseProvider) {
206
+ return methods;
207
+ }
183
208
  const openChannel = async (raw) => {
184
209
  const params = paramsObject(raw);
185
210
  requireMutationContext(context);
@@ -287,6 +312,15 @@ function createHostMethods(context) {
287
312
  const params = paramsObject(raw);
288
313
  requireMutationContext(context);
289
314
  const channelId = requiredString(params, 'channelId');
315
+ const broadcastProofs = params.broadcastProofs;
316
+ const parsedProofs = broadcastProofs && typeof broadcastProofs === 'object'
317
+ && Array.isArray(broadcastProofs.coinProofs)
318
+ && Array.isArray(broadcastProofs.scriptProofs)
319
+ ? {
320
+ coinProofs: broadcastProofs.coinProofs.map((hex) => Buffer.from(String(hex).replace(/^0x/i, ''), 'hex')),
321
+ scriptProofs: broadcastProofs.scriptProofs.map((hex) => Buffer.from(String(hex).replace(/^0x/i, ''), 'hex')),
322
+ }
323
+ : undefined;
290
324
  return withOperation(context, params, async () => {
291
325
  const channel = context.channels.get(channelId);
292
326
  if (!channel)
@@ -295,6 +329,7 @@ function createHostMethods(context) {
295
329
  const settlement = await (0, omnia_1.proposeSettlement)(closing, context.leaseProvider, {
296
330
  signer: context.signer,
297
331
  chainProvider: context.chainProvider,
332
+ broadcastProofs: parsedProofs,
298
333
  partyAddresses: Object.fromEntries(closing.parties.map((party) => [party.partyId, party.settlementAddress ?? party.publicKeyDigest])),
299
334
  });
300
335
  context.channels.set(channelId, (0, omnia_1.markChannelClosed)(closing));
@@ -404,9 +439,56 @@ function createHostMethods(context) {
404
439
  };
405
440
  register('totem_omniaSpliceIn', splice('in'), 'omnia/spliceIn');
406
441
  register('totem_omniaSpliceOut', splice('out'), 'omnia/spliceOut');
407
- const unsupported = () => {
408
- throw new Error('This operation requires a counterparty proposal/acceptance round');
442
+ const payMultiHop = async (raw) => {
443
+ const params = paramsObject(raw);
444
+ requireMutationContext(context);
445
+ const route = params.route;
446
+ if (!route || typeof route !== 'object' || !Array.isArray(route.hops)) {
447
+ throw new Error('route with hops is required');
448
+ }
449
+ const paymentRequest = params.paymentRequest;
450
+ if (!paymentRequest || typeof paymentRequest !== 'object') {
451
+ throw new Error('paymentRequest is required');
452
+ }
453
+ const request = paymentRequest;
454
+ const parsedRequest = {
455
+ hashlock: requiredString(request, 'hashlock'),
456
+ amount: requiredBigInt(request, 'amount'),
457
+ tokenId: requiredString(request, 'tokenId'),
458
+ expiryBlock: requiredBigInt(request, 'expiryBlock'),
459
+ preimage: typeof request.preimage === 'string' ? request.preimage : undefined,
460
+ description: typeof request.description === 'string' ? request.description : undefined,
461
+ };
462
+ const parsedRoute = route;
463
+ const isCrossToken = 'swapHops' in parsedRoute && Array.isArray(parsedRoute.swapHops) && parsedRoute.swapHops.length > 0;
464
+ const ops = {
465
+ addHTLC: (channel, htlcParams, leaseProvider) => (0, omnia_1.addHTLC)(channel, htlcParams, leaseProvider, context.signer),
466
+ fulfillHTLC: (channel, htlcId, preimage, leaseProvider) => (0, omnia_1.fulfillHTLC)(channel, htlcId, preimage, leaseProvider, context.signer),
467
+ timeoutHTLC: (channel, htlcId, leaseProvider) => (0, omnia_1.timeoutHTLC)(channel, htlcId, leaseProvider, context.chainProvider, context.signer),
468
+ };
469
+ const leaseProviders = new Map();
470
+ for (const hop of parsedRoute.hops) {
471
+ const channelId = 'channelId' in hop ? hop.channelId : undefined;
472
+ if (channelId)
473
+ leaseProviders.set(channelId, context.leaseProvider);
474
+ if ('inboundChannelId' in hop && hop.inboundChannelId)
475
+ leaseProviders.set(hop.inboundChannelId, context.leaseProvider);
476
+ if ('outboundChannelId' in hop && hop.outboundChannelId)
477
+ leaseProviders.set(hop.outboundChannelId, context.leaseProvider);
478
+ }
479
+ return withOperation(context, params, async () => {
480
+ const result = isCrossToken
481
+ ? await (0, omnia_router_1.executeCrossTokenPayment)(ops, context.channels, parsedRoute, parsedRequest, leaseProviders)
482
+ : await (0, omnia_router_1.executeMultiHopPayment)(ops, context.channels, parsedRoute, parsedRequest, leaseProviders);
483
+ if (!result.success)
484
+ throw new Error(result.error ?? 'multi-hop payment failed');
485
+ return {
486
+ success: true,
487
+ preimage: result.preimage,
488
+ settledHops: result.settledHops,
489
+ };
490
+ });
409
491
  };
410
- register('totem_omniaPayMultiHop', unsupported, 'omnia/payMultiHop');
492
+ register('totem_omniaPayMultiHop', payMultiHop, 'omnia/payMultiHop');
411
493
  return methods;
412
494
  }
package/dist/cli.js CHANGED
@@ -1,8 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const index_js_1 = require("./index.js");
4
+ const signing_js_1 = require("./signing.js");
5
+ const identity_js_1 = require("./identity.js");
4
6
  async function main() {
5
- const host = (0, index_js_1.createOmniaHost)((0, index_js_1.loadConfigFromEnv)());
7
+ const config = (0, index_js_1.loadConfigFromEnv)();
8
+ const signing = config.readOnly || !(0, signing_js_1.hasSigningMaterial)(config)
9
+ ? undefined
10
+ : (0, signing_js_1.createHostSigning)(config);
11
+ const identityAndManifest = signing
12
+ ? await (0, identity_js_1.createHostIdentityAndManifest)(config, signing)
13
+ : undefined;
14
+ const host = (0, index_js_1.createOmniaHost)(config, signing
15
+ ? {
16
+ signer: signing.signer,
17
+ leaseProvider: signing.leaseProvider,
18
+ identity: identityAndManifest?.identity
19
+ ? {
20
+ address: signing.address,
21
+ publicKeyDigest: signing.publicKeyDigest,
22
+ identityId: identityAndManifest.identity.document.id,
23
+ delegation: identityAndManifest.identity.delegation,
24
+ }
25
+ : { address: signing.address, publicKeyDigest: signing.publicKeyDigest },
26
+ manifest: identityAndManifest?.manifest,
27
+ }
28
+ : {});
6
29
  let closing = false;
7
30
  let resolveStopped;
8
31
  const stopped = new Promise((resolve) => {
package/dist/config.d.ts CHANGED
@@ -12,6 +12,20 @@ export interface OmniaHostConfig {
12
12
  localSettlementAddress?: string;
13
13
  nodeMode?: string;
14
14
  wsPath: string;
15
+ /** BIP39 mnemonic or hex seed (same formats @totemsdk/core accepts). */
16
+ seed?: string;
17
+ /** Path to a keyfile JSON (resolved against cwd). */
18
+ keyfile?: string;
19
+ /** Keyfile decryption passphrase. */
20
+ keyfilePassphrase?: string;
21
+ /** Stable device id for the lease journal. */
22
+ deviceId?: string;
23
+ /** "1" forces read-only mode even with keys present. */
24
+ readOnly: boolean;
25
+ /** Identity file containing a delegated identity claim (operator root → service delegate). */
26
+ identityFile?: string;
27
+ /** EdgeServiceManifest serviceType (default "omnia-router"). */
28
+ serviceType: string;
15
29
  }
16
30
  /** Load the daemon configuration from the documented OMNIA_* environment. */
17
31
  export declare function loadConfigFromEnv(env?: NodeJS.ProcessEnv): OmniaHostConfig;
package/dist/config.js CHANGED
@@ -28,9 +28,33 @@ 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 parseSeed(value) {
32
+ const seed = optional(value);
33
+ if (!seed)
34
+ return undefined;
35
+ const words = seed.trim().split(/\s+/);
36
+ const looksLikeMnemonic = words.length >= 12 && words.every((word) => /^[a-zA-Z]+$/.test(word));
37
+ const looksLikeHex = /^(0x)?[0-9a-fA-F]+$/.test(seed.trim());
38
+ if (!looksLikeMnemonic && !looksLikeHex) {
39
+ throw new Error('OMNIA_HOST_SEED must be a BIP39 mnemonic (12+ words) or a hex seed (e.g. 0x…64 hex chars); ' +
40
+ 'received a value that is neither');
41
+ }
42
+ if (looksLikeHex) {
43
+ const raw = seed.trim().replace(/^0x/i, '');
44
+ if (raw.length !== 64) {
45
+ throw new Error(`OMNIA_HOST_SEED hex form must be exactly 32 bytes (64 hex chars); received ${raw.length} hex chars`);
46
+ }
47
+ }
48
+ return seed;
49
+ }
31
50
  /** Load the daemon configuration from the documented OMNIA_* environment. */
32
51
  function loadConfigFromEnv(env = process.env) {
33
52
  const dataRoot = node_path_1.default.join(node_os_1.default.homedir(), '.totem', 'omnia');
53
+ const seed = parseSeed(env.OMNIA_HOST_SEED);
54
+ const keyfile = optional(env.OMNIA_HOST_KEYFILE);
55
+ if (seed && keyfile) {
56
+ throw new Error('OMNIA_HOST_SEED and OMNIA_HOST_KEYFILE are mutually exclusive — set exactly one of them');
57
+ }
34
58
  return {
35
59
  port: parsePort(env.OMNIA_HOST_PORT),
36
60
  host: env.OMNIA_HOST_BIND?.trim() || '127.0.0.1',
@@ -45,5 +69,12 @@ function loadConfigFromEnv(env = process.env) {
45
69
  localSettlementAddress: optional(env.OMNIA_LOCAL_SETTLEMENT_ADDRESS),
46
70
  nodeMode: optional(env.OMNIA_NODE_MODE),
47
71
  wsPath: env.OMNIA_WS_PATH?.trim() || '/rpc',
72
+ seed,
73
+ keyfile,
74
+ keyfilePassphrase: optional(env.OMNIA_HOST_KEYFILE_PASSPHRASE),
75
+ deviceId: optional(env.OMNIA_HOST_DEVICE_ID),
76
+ readOnly: env.OMNIA_HOST_READ_ONLY?.trim() === '1',
77
+ identityFile: optional(env.OMNIA_HOST_IDENTITY_FILE),
78
+ serviceType: optional(env.OMNIA_HOST_SERVICE_TYPE) ?? 'omnia-router',
48
79
  };
49
80
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Host identity + manifest layer (opt-in).
3
+ *
4
+ * - Service identity: an identity document derived from the host seed, plus an
5
+ * optional delegated identity claim loaded from OMNIA_HOST_IDENTITY_FILE
6
+ * (operator root → service delegate), validated via verifyIdentityClaim.
7
+ * - Manifest: an EdgeServiceManifest signed at boot with the same signing path
8
+ * (signManifest). No periodic re-signing.
9
+ *
10
+ * Channels remain authenticated purely by WOTS signatures — identity is a
11
+ * host-layer, opt-in concern and never touches @totemsdk/omnia.
12
+ */
13
+ import { type SignedIdentityClaim, type TotemIdentityDocument } from '@totemsdk/identity';
14
+ import { type EdgeServiceManifest, type SignedManifest } from '@totemsdk/manifest';
15
+ import type { OmniaHostConfig } from './config.js';
16
+ import type { HostSigning } from './signing.js';
17
+ export interface HostIdentity {
18
+ document: TotemIdentityDocument;
19
+ delegation?: SignedIdentityClaim;
20
+ }
21
+ export interface HostIdentityAndManifest {
22
+ identity?: HostIdentity;
23
+ manifest: SignedManifest<EdgeServiceManifest>;
24
+ }
25
+ /** Load and validate the delegated identity claim file (if configured). */
26
+ export declare function loadHostIdentity(config: OmniaHostConfig, signing: HostSigning): HostIdentity | undefined;
27
+ /** Sign the boot-time EdgeServiceManifest with the host signing path. */
28
+ export declare function createHostManifest(config: OmniaHostConfig, signing: HostSigning, identity?: HostIdentity): Promise<SignedManifest<EdgeServiceManifest>>;
29
+ /** Boot-time identity + manifest assembly. */
30
+ export declare function createHostIdentityAndManifest(config: OmniaHostConfig, signing: HostSigning): Promise<HostIdentityAndManifest>;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Host identity + manifest layer (opt-in).
4
+ *
5
+ * - Service identity: an identity document derived from the host seed, plus an
6
+ * optional delegated identity claim loaded from OMNIA_HOST_IDENTITY_FILE
7
+ * (operator root → service delegate), validated via verifyIdentityClaim.
8
+ * - Manifest: an EdgeServiceManifest signed at boot with the same signing path
9
+ * (signManifest). No periodic re-signing.
10
+ *
11
+ * Channels remain authenticated purely by WOTS signatures — identity is a
12
+ * host-layer, opt-in concern and never touches @totemsdk/omnia.
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.loadHostIdentity = loadHostIdentity;
19
+ exports.createHostManifest = createHostManifest;
20
+ exports.createHostIdentityAndManifest = createHostIdentityAndManifest;
21
+ const node_fs_1 = __importDefault(require("node:fs"));
22
+ const node_path_1 = __importDefault(require("node:path"));
23
+ const identity_1 = require("@totemsdk/identity");
24
+ const manifest_1 = require("@totemsdk/manifest");
25
+ function isEdgeServiceType(value) {
26
+ return [
27
+ 'sensor',
28
+ 'robot',
29
+ 'mqtt-feed',
30
+ 'proof-index',
31
+ 'lookup-provider',
32
+ 'omnia-router',
33
+ 'calibration-authority',
34
+ 'verifier',
35
+ 'machine-service',
36
+ 'other',
37
+ ].includes(value);
38
+ }
39
+ /** Load and validate the delegated identity claim file (if configured). */
40
+ function loadHostIdentity(config, signing) {
41
+ const document = (0, identity_1.createIdentityDocument)({
42
+ kind: 'service',
43
+ rootAddress: signing.address,
44
+ controllerAddress: signing.address,
45
+ metadata: {
46
+ serviceType: config.serviceType,
47
+ publicKeyDigest: signing.publicKeyDigest,
48
+ },
49
+ });
50
+ if (!config.identityFile) {
51
+ return { document };
52
+ }
53
+ const identityPath = node_path_1.default.resolve(process.cwd(), config.identityFile);
54
+ let raw;
55
+ try {
56
+ raw = node_fs_1.default.readFileSync(identityPath, 'utf8');
57
+ }
58
+ catch (error) {
59
+ throw new Error(`OMNIA_HOST_IDENTITY_FILE could not be read at ${identityPath}: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ let delegation;
62
+ try {
63
+ delegation = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ throw new Error(`OMNIA_HOST_IDENTITY_FILE at ${identityPath} is not valid JSON`);
67
+ }
68
+ const result = (0, identity_1.verifyIdentityClaim)(delegation);
69
+ if (!result.valid) {
70
+ throw new Error(`OMNIA_HOST_IDENTITY_FILE delegation failed verification: ${result.reason ?? 'invalid claim'}`);
71
+ }
72
+ if (delegation.claim.type !== 'delegates_to') {
73
+ throw new Error('OMNIA_HOST_IDENTITY_FILE must contain a delegates_to claim');
74
+ }
75
+ if (delegation.claim.object !== signing.address) {
76
+ throw new Error(`OMNIA_HOST_IDENTITY_FILE delegates to ${delegation.claim.object}, but the host address is ${signing.address}`);
77
+ }
78
+ return { document, delegation };
79
+ }
80
+ /** Sign the boot-time EdgeServiceManifest with the host signing path. */
81
+ async function createHostManifest(config, signing, identity) {
82
+ const serviceType = isEdgeServiceType(config.serviceType) ? config.serviceType : 'other';
83
+ const manifest = {
84
+ type: 'edge-service',
85
+ serviceId: `omnia-host:${signing.publicKeyDigest}`,
86
+ name: 'omnia-host',
87
+ version: '0.1.1',
88
+ operatorAddress: identity?.delegation?.claim.issuer ?? signing.address,
89
+ serviceType,
90
+ description: 'Durable Omnia node daemon for channel lifecycle, routing, and control APIs',
91
+ endpoints: [{ type: 'websocket', uri: `ws://${config.host}:${config.port}${config.wsPath}` }],
92
+ capabilities: ['omnia:open-channel', 'omnia:update-channel', 'omnia:route', 'omnia:settle'],
93
+ tags: ['omnia', 'payment-channel', 'daemon'],
94
+ };
95
+ return (0, manifest_1.signManifest)(manifest, signing.perAddressSeed, 0);
96
+ }
97
+ /** Boot-time identity + manifest assembly. */
98
+ async function createHostIdentityAndManifest(config, signing) {
99
+ const identity = loadHostIdentity(config, signing);
100
+ const manifest = await createHostManifest(config, signing, identity);
101
+ return { identity, manifest };
102
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,10 @@ export type { OmniaHostConfig } from './config.js';
2
2
  export { loadConfigFromEnv } from './config.js';
3
3
  export type { OmniaHost } from './lifecycle.js';
4
4
  export { createOmniaHost } from './lifecycle.js';
5
+ export type { HostSigning } from './signing.js';
6
+ export { createHostSigning, hasSigningMaterial, JsonFileStorageAdapter, leaseStorageDir, encryptSeedForKeyfile, } from './signing.js';
7
+ export type { HostIdentity, HostIdentityAndManifest } from './identity.js';
8
+ export { createHostIdentityAndManifest, createHostManifest, loadHostIdentity, } from './identity.js';
5
9
  export { SqliteChannelStore } from './stores/sqlite-store.js';
6
10
  export { OperationStore } from './stores/operations.js';
7
11
  export type { OperationRecord, OperationStatus } from './stores/operations.js';
package/dist/index.js CHANGED
@@ -1,10 +1,20 @@
1
1
  "use strict";
2
2
  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.createOmniaHost = exports.loadConfigFromEnv = void 0;
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;
4
4
  var config_js_1 = require("./config.js");
5
5
  Object.defineProperty(exports, "loadConfigFromEnv", { enumerable: true, get: function () { return config_js_1.loadConfigFromEnv; } });
6
6
  var lifecycle_js_1 = require("./lifecycle.js");
7
7
  Object.defineProperty(exports, "createOmniaHost", { enumerable: true, get: function () { return lifecycle_js_1.createOmniaHost; } });
8
+ var signing_js_1 = require("./signing.js");
9
+ Object.defineProperty(exports, "createHostSigning", { enumerable: true, get: function () { return signing_js_1.createHostSigning; } });
10
+ Object.defineProperty(exports, "hasSigningMaterial", { enumerable: true, get: function () { return signing_js_1.hasSigningMaterial; } });
11
+ Object.defineProperty(exports, "JsonFileStorageAdapter", { enumerable: true, get: function () { return signing_js_1.JsonFileStorageAdapter; } });
12
+ Object.defineProperty(exports, "leaseStorageDir", { enumerable: true, get: function () { return signing_js_1.leaseStorageDir; } });
13
+ Object.defineProperty(exports, "encryptSeedForKeyfile", { enumerable: true, get: function () { return signing_js_1.encryptSeedForKeyfile; } });
14
+ var identity_js_1 = require("./identity.js");
15
+ Object.defineProperty(exports, "createHostIdentityAndManifest", { enumerable: true, get: function () { return identity_js_1.createHostIdentityAndManifest; } });
16
+ Object.defineProperty(exports, "createHostManifest", { enumerable: true, get: function () { return identity_js_1.createHostManifest; } });
17
+ Object.defineProperty(exports, "loadHostIdentity", { enumerable: true, get: function () { return identity_js_1.loadHostIdentity; } });
8
18
  var sqlite_store_js_1 = require("./stores/sqlite-store.js");
9
19
  Object.defineProperty(exports, "SqliteChannelStore", { enumerable: true, get: function () { return sqlite_store_js_1.SqliteChannelStore; } });
10
20
  var operations_js_1 = require("./stores/operations.js");
@@ -29,6 +29,15 @@ export interface OmniaHostDependencies {
29
29
  spliceProposals?: Map<string, SpliceProposal>;
30
30
  spliceAcceptances?: Map<string, SpliceAcceptance>;
31
31
  spliceLeaseProvider?: SpliceLeaseProvider;
32
+ /** Opt-in host identity surface (whoami). */
33
+ identity?: {
34
+ address: string;
35
+ publicKeyDigest: string;
36
+ identityId?: string;
37
+ delegation?: unknown;
38
+ };
39
+ /** Boot-time signed EdgeServiceManifest (getManifest). */
40
+ manifest?: unknown;
32
41
  }
33
42
  export interface OmniaHost {
34
43
  readonly config: OmniaHostConfig;
package/dist/lifecycle.js CHANGED
@@ -77,6 +77,10 @@ function createOmniaHost(config, dependencies = {}) {
77
77
  chainProvider: dependencies.chainProvider ?? chainAdapter,
78
78
  });
79
79
  if (!controlServer) {
80
+ const readOnly = !dependencies.signer || !dependencies.leaseProvider;
81
+ if (readOnly) {
82
+ console.log('[omnia-host] no signing material; starting READ-ONLY');
83
+ }
80
84
  controlServer = (0, jsonrpc_js_1.createControlServer)({
81
85
  host: config.host,
82
86
  port: config.port,
@@ -96,6 +100,9 @@ function createOmniaHost(config, dependencies = {}) {
96
100
  spliceProposals: dependencies.spliceProposals,
97
101
  spliceAcceptances: dependencies.spliceAcceptances,
98
102
  spliceLeaseProvider: dependencies.spliceLeaseProvider,
103
+ readOnly,
104
+ identity: dependencies.identity,
105
+ manifest: dependencies.manifest,
99
106
  refreshRouting: () => routingProvider.rebuild((0, routing_provider_js_1.channelGraphEdges)(channelStore.values())),
100
107
  }),
101
108
  });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Host signing material — derives the channel signer, WOTS lease provider,
3
+ * and Minima address from env-provisioned key material.
4
+ *
5
+ * Key model (matches @totemsdk/omnia's verification path):
6
+ * - `baseSeed` comes from OMNIA_HOST_SEED (BIP39 mnemonic or 32-byte hex)
7
+ * or from an OMNIA_HOST_KEYFILE JSON.
8
+ * - `perAddressSeed = derivePerAddressSeed(baseSeed, localAddressIndex)`.
9
+ * - The channel participant's `publicKeyDigest` is the flat WOTS PK digest
10
+ * `derivePKdigest(perAddressSeed, 0)` — omnia verifies signatures with
11
+ * `wotsVerifyDigest(sig, digest, pkDigest)`, which is key-index agnostic,
12
+ * so the signer always signs with key index 0 of the per-address seed.
13
+ * - The lease indices {addressIndex, l1, l2} are slot bookkeeping for
14
+ * double-sign prevention (watermark monotonicity), not key derivation.
15
+ *
16
+ * The lease journal/watermark persist in a directory next to the channel DB
17
+ * (`<dbPath>.lease/`) so they survive restarts together with the channel store.
18
+ */
19
+ import type { StorageAdapter } from '@totemsdk/core';
20
+ import type { ChannelSigner } from '@totemsdk/omnia';
21
+ import type { WotsLeaseProvider } from '@totemsdk/wots-lease';
22
+ import type { OmniaHostConfig } from './config.js';
23
+ export interface HostSigning {
24
+ signer: ChannelSigner;
25
+ leaseProvider: WotsLeaseProvider;
26
+ address: string;
27
+ publicKeyDigest: string;
28
+ /** 32-byte base seed (used by the identity/manifest layer). */
29
+ baseSeed: Uint8Array;
30
+ /** Per-address seed used for channel signing (key index 0). */
31
+ perAddressSeed: Uint8Array;
32
+ addressIndex: number;
33
+ deviceId: string;
34
+ }
35
+ interface KeyfilePayload {
36
+ seed: string;
37
+ encrypted?: boolean;
38
+ salt?: string;
39
+ iv?: string;
40
+ authTag?: string;
41
+ }
42
+ /** Encrypt a 32-byte seed into a keyfile payload (used by tooling/tests). */
43
+ export declare function encryptSeedForKeyfile(seed: Uint8Array, passphrase: string): KeyfilePayload;
44
+ /**
45
+ * JSON-file-backed StorageAdapter rooted at a directory next to the channel DB.
46
+ * Each key is stored as `<key>.json`; the directory is created on first write.
47
+ */
48
+ export declare class JsonFileStorageAdapter implements StorageAdapter {
49
+ private readonly dir;
50
+ constructor(dir: string);
51
+ private fileFor;
52
+ private ensureDir;
53
+ get<T>(key: string): Promise<T | null>;
54
+ set<T>(key: string, value: T): Promise<void>;
55
+ remove(key: string): Promise<boolean>;
56
+ clear(): Promise<void>;
57
+ keys(): Promise<string[]>;
58
+ has(key: string): Promise<boolean>;
59
+ }
60
+ /** Directory holding the lease journal/watermark next to the channel DB. */
61
+ export declare function leaseStorageDir(dbPath: string): string;
62
+ /**
63
+ * Derive the host's signing stack from config.
64
+ *
65
+ * Throws when no key material is configured. Callers gate this behind
66
+ * `config.readOnly` / key presence checks.
67
+ */
68
+ export declare function createHostSigning(config: OmniaHostConfig): HostSigning;
69
+ /** True when the config carries signing material (seed or keyfile). */
70
+ export declare function hasSigningMaterial(config: OmniaHostConfig): boolean;
71
+ export {};
@@ -0,0 +1,225 @@
1
+ "use strict";
2
+ /**
3
+ * Host signing material — derives the channel signer, WOTS lease provider,
4
+ * and Minima address from env-provisioned key material.
5
+ *
6
+ * Key model (matches @totemsdk/omnia's verification path):
7
+ * - `baseSeed` comes from OMNIA_HOST_SEED (BIP39 mnemonic or 32-byte hex)
8
+ * or from an OMNIA_HOST_KEYFILE JSON.
9
+ * - `perAddressSeed = derivePerAddressSeed(baseSeed, localAddressIndex)`.
10
+ * - The channel participant's `publicKeyDigest` is the flat WOTS PK digest
11
+ * `derivePKdigest(perAddressSeed, 0)` — omnia verifies signatures with
12
+ * `wotsVerifyDigest(sig, digest, pkDigest)`, which is key-index agnostic,
13
+ * so the signer always signs with key index 0 of the per-address seed.
14
+ * - The lease indices {addressIndex, l1, l2} are slot bookkeeping for
15
+ * double-sign prevention (watermark monotonicity), not key derivation.
16
+ *
17
+ * The lease journal/watermark persist in a directory next to the channel DB
18
+ * (`<dbPath>.lease/`) so they survive restarts together with the channel store.
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.JsonFileStorageAdapter = void 0;
25
+ exports.encryptSeedForKeyfile = encryptSeedForKeyfile;
26
+ exports.leaseStorageDir = leaseStorageDir;
27
+ exports.createHostSigning = createHostSigning;
28
+ exports.hasSigningMaterial = hasSigningMaterial;
29
+ const node_fs_1 = __importDefault(require("node:fs"));
30
+ const node_path_1 = __importDefault(require("node:path"));
31
+ const node_crypto_1 = require("node:crypto");
32
+ const core_1 = require("@totemsdk/core");
33
+ const wots_lease_1 = require("@totemsdk/wots-lease");
34
+ const wots_lease_2 = require("@totemsdk/wots-lease");
35
+ const KEYFILE_SCRYPT_N = 16384;
36
+ const KEYFILE_SCRYPT_R = 8;
37
+ const KEYFILE_SCRYPT_P = 1;
38
+ const KEYFILE_KEY_LEN = 32;
39
+ function decryptSeed(payload, passphrase) {
40
+ if (!payload.encrypted) {
41
+ return (0, core_1.hexToBytes)(payload.seed);
42
+ }
43
+ if (!passphrase) {
44
+ throw new Error('OMNIA_HOST_KEYFILE is encrypted — OMNIA_HOST_KEYFILE_PASSPHRASE is required');
45
+ }
46
+ if (!payload.salt || !payload.iv || !payload.authTag) {
47
+ throw new Error('OMNIA_HOST_KEYFILE is marked encrypted but is missing salt/iv/authTag');
48
+ }
49
+ const key = (0, node_crypto_1.scryptSync)(passphrase, Buffer.from(payload.salt, 'hex'), KEYFILE_KEY_LEN, {
50
+ N: KEYFILE_SCRYPT_N,
51
+ r: KEYFILE_SCRYPT_R,
52
+ p: KEYFILE_SCRYPT_P,
53
+ });
54
+ const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', key, Buffer.from(payload.iv, 'hex'));
55
+ decipher.setAuthTag(Buffer.from(payload.authTag, 'hex'));
56
+ const plain = Buffer.concat([
57
+ decipher.update(Buffer.from(payload.seed, 'hex')),
58
+ decipher.final(),
59
+ ]);
60
+ return new Uint8Array(plain);
61
+ }
62
+ /** Encrypt a 32-byte seed into a keyfile payload (used by tooling/tests). */
63
+ function encryptSeedForKeyfile(seed, passphrase) {
64
+ const salt = (0, node_crypto_1.randomBytes)(16);
65
+ const iv = (0, node_crypto_1.randomBytes)(12);
66
+ const key = (0, node_crypto_1.scryptSync)(passphrase, salt, KEYFILE_KEY_LEN, {
67
+ N: KEYFILE_SCRYPT_N,
68
+ r: KEYFILE_SCRYPT_R,
69
+ p: KEYFILE_SCRYPT_P,
70
+ });
71
+ const cipher = (0, node_crypto_1.createCipheriv)('aes-256-gcm', key, iv);
72
+ const encrypted = Buffer.concat([cipher.update(seed), cipher.final()]);
73
+ return {
74
+ seed: encrypted.toString('hex'),
75
+ encrypted: true,
76
+ salt: salt.toString('hex'),
77
+ iv: iv.toString('hex'),
78
+ authTag: cipher.getAuthTag().toString('hex'),
79
+ };
80
+ }
81
+ function resolveBaseSeed(config) {
82
+ if (config.seed) {
83
+ const trimmed = config.seed.trim();
84
+ if (/^(0x)?[0-9a-fA-F]+$/.test(trimmed)) {
85
+ return (0, core_1.hexToBytes)(trimmed);
86
+ }
87
+ return (0, core_1.phraseToSeed)(trimmed);
88
+ }
89
+ if (config.keyfile) {
90
+ const keyfilePath = node_path_1.default.resolve(process.cwd(), config.keyfile);
91
+ let raw;
92
+ try {
93
+ raw = node_fs_1.default.readFileSync(keyfilePath, 'utf8');
94
+ }
95
+ catch (error) {
96
+ throw new Error(`OMNIA_HOST_KEYFILE could not be read at ${keyfilePath}: ${error instanceof Error ? error.message : String(error)}`);
97
+ }
98
+ let payload;
99
+ try {
100
+ payload = JSON.parse(raw);
101
+ }
102
+ catch {
103
+ throw new Error(`OMNIA_HOST_KEYFILE at ${keyfilePath} is not valid JSON`);
104
+ }
105
+ if (typeof payload.seed !== 'string' || payload.seed.length === 0) {
106
+ throw new Error(`OMNIA_HOST_KEYFILE at ${keyfilePath} is missing a "seed" field`);
107
+ }
108
+ const seed = decryptSeed(payload, config.keyfilePassphrase);
109
+ if (seed.length !== 32) {
110
+ throw new Error(`OMNIA_HOST_KEYFILE seed must be 32 bytes; got ${seed.length}`);
111
+ }
112
+ return seed;
113
+ }
114
+ throw new Error('createHostSigning requires OMNIA_HOST_SEED or OMNIA_HOST_KEYFILE');
115
+ }
116
+ /**
117
+ * JSON-file-backed StorageAdapter rooted at a directory next to the channel DB.
118
+ * Each key is stored as `<key>.json`; the directory is created on first write.
119
+ */
120
+ class JsonFileStorageAdapter {
121
+ dir;
122
+ constructor(dir) {
123
+ this.dir = dir;
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 });
131
+ }
132
+ async get(key) {
133
+ const file = this.fileFor(key);
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
+ }
142
+ }
143
+ async set(key, value) {
144
+ this.ensureDir();
145
+ node_fs_1.default.writeFileSync(this.fileFor(key), JSON.stringify(value));
146
+ }
147
+ async remove(key) {
148
+ const file = this.fileFor(key);
149
+ if (!node_fs_1.default.existsSync(file))
150
+ return false;
151
+ node_fs_1.default.unlinkSync(file);
152
+ return true;
153
+ }
154
+ async clear() {
155
+ if (!node_fs_1.default.existsSync(this.dir))
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
+ }
160
+ }
161
+ async keys() {
162
+ if (!node_fs_1.default.existsSync(this.dir))
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));
167
+ }
168
+ async has(key) {
169
+ return node_fs_1.default.existsSync(this.fileFor(key));
170
+ }
171
+ }
172
+ exports.JsonFileStorageAdapter = JsonFileStorageAdapter;
173
+ /** Directory holding the lease journal/watermark next to the channel DB. */
174
+ function leaseStorageDir(dbPath) {
175
+ return `${dbPath}.lease`;
176
+ }
177
+ /** Extract the numeric slot from a `device-N` id, or undefined. */
178
+ function parseDeviceSlot(deviceId) {
179
+ const match = /^device-?(\d+)$/i.exec(deviceId);
180
+ return match ? parseInt(match[1], 10) : undefined;
181
+ }
182
+ /**
183
+ * Derive the host's signing stack from config.
184
+ *
185
+ * Throws when no key material is configured. Callers gate this behind
186
+ * `config.readOnly` / key presence checks.
187
+ */
188
+ function createHostSigning(config) {
189
+ const baseSeed = resolveBaseSeed(config);
190
+ const addressIndex = config.localAddressIndex ?? 0;
191
+ const pinnedSlot = config.deviceId ? parseDeviceSlot(config.deviceId) : undefined;
192
+ const deviceSlot = pinnedSlot ?? (0, wots_lease_2.deviceSlotForAddressIndex)(addressIndex);
193
+ const deviceId = config.deviceId ?? `device-${deviceSlot}`;
194
+ const range = (0, wots_lease_2.allocateDeviceRange)({ deviceSlot, deviceId });
195
+ if (addressIndex < range.startAddressIndex || addressIndex > range.endAddressIndex) {
196
+ throw new Error(`localAddressIndex ${addressIndex} is outside device slot ${deviceSlot} range ` +
197
+ `${range.startAddressIndex}–${range.endAddressIndex}`);
198
+ }
199
+ const perAddressSeed = (0, core_1.derivePerAddressSeed)(baseSeed, addressIndex);
200
+ const kp = (0, core_1.wotsKeypairFromSeed)(perAddressSeed, 0);
201
+ const publicKeyDigest = `0x${(0, core_1.bytesToHex)(kp.pk)}`;
202
+ const address = (0, core_1.wotsAddressFromKeypair)(kp);
203
+ const signer = {
204
+ publicKeyDigest,
205
+ async sign(payload, _indices) {
206
+ return (0, core_1.wotsSign)(perAddressSeed, 0, payload);
207
+ },
208
+ };
209
+ const storage = new JsonFileStorageAdapter(leaseStorageDir(config.dbPath));
210
+ const leaseProvider = new wots_lease_1.LocalLeaseProvider(storage, undefined, deviceId);
211
+ return {
212
+ signer,
213
+ leaseProvider,
214
+ address,
215
+ publicKeyDigest,
216
+ baseSeed,
217
+ perAddressSeed,
218
+ addressIndex,
219
+ deviceId,
220
+ };
221
+ }
222
+ /** True when the config carries signing material (seed or keyfile). */
223
+ function hasSigningMaterial(config) {
224
+ return Boolean(config.seed || config.keyfile);
225
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@totemsdk/omnia-host",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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,14 +22,17 @@
22
22
  "dependencies": {
23
23
  "better-sqlite3": "^12.2.0",
24
24
  "ws": "^8.21.1",
25
- "@totemsdk/chain-provider": "^0.1.5",
26
- "@totemsdk/omnia": "^0.1.9",
27
- "@totemsdk/omnia-factory": "^0.1.8",
25
+ "@totemsdk/chain-provider": "^0.1.6",
26
+ "@totemsdk/core": "^1.2.9",
27
+ "@totemsdk/identity": "^0.1.4",
28
+ "@totemsdk/manifest": "^0.1.3",
29
+ "@totemsdk/omnia": "^1.0.0",
30
+ "@totemsdk/omnia-factory": "^0.1.9",
28
31
  "@totemsdk/omnia-router": "^0.1.5",
29
- "@totemsdk/omnia-vtxo": "^0.1.0",
32
+ "@totemsdk/omnia-splice": "^0.1.8",
33
+ "@totemsdk/omnia-vtxo": "^0.1.1",
30
34
  "@totemsdk/minima-rpc": "^0.1.4",
31
- "@totemsdk/wots-lease": "^0.1.5",
32
- "@totemsdk/omnia-splice": "^0.1.7"
35
+ "@totemsdk/wots-lease": "^1.0.0"
33
36
  },
34
37
  "devDependencies": {
35
38
  "@types/jest": "^30.0.0",
@@ -38,7 +41,8 @@
38
41
  "@types/ws": "^8.5.0",
39
42
  "jest": "^30.4.2",
40
43
  "ts-jest": "^29.4.11",
41
- "typescript": "5.9.3"
44
+ "typescript": "5.9.3",
45
+ "@totemsdk/txpow": "^0.1.7"
42
46
  },
43
47
  "engines": {
44
48
  "node": ">=20.0.0"
@@ -51,7 +55,7 @@
51
55
  "homepage": "https://totem.ing",
52
56
  "repository": {
53
57
  "type": "git",
54
- "url": "git+https://github.com/totem-sdk/totem-sdk.git",
58
+ "url": "git+https://github.com/Totem-Edge/totem-sdk.git",
55
59
  "directory": "packages/omnia-host"
56
60
  },
57
61
  "bugs": {