@totemsdk/omnia-host 0.12.0 → 0.13.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/dist/api/methods.js +106 -32
- package/dist/index.d.ts +5 -1
- package/dist/index.js +11 -1
- package/dist/mfa.d.ts +77 -0
- package/dist/mfa.js +152 -0
- package/dist/observe/snapshot.js +4 -4
- package/dist/signer-port.d.ts +34 -1
- package/dist/signer-port.js +53 -1
- package/dist/stores/audit.d.ts +24 -0
- package/dist/stores/audit.js +49 -0
- package/package.json +4 -4
package/dist/api/methods.js
CHANGED
|
@@ -1697,21 +1697,7 @@ function createHostMethods(context) {
|
|
|
1697
1697
|
});
|
|
1698
1698
|
if (!route)
|
|
1699
1699
|
return { success: false, error: 'No route found', errorCode: 'ROUTE_NOT_FOUND' };
|
|
1700
|
-
return {
|
|
1701
|
-
success: true,
|
|
1702
|
-
route: {
|
|
1703
|
-
...route,
|
|
1704
|
-
totalFees: route.totalFees.toString(),
|
|
1705
|
-
hops: route.hops.map((hop) => ({
|
|
1706
|
-
...hop,
|
|
1707
|
-
amount: hop.amount.toString(),
|
|
1708
|
-
...('isSwap' in hop && hop.isSwap ? {
|
|
1709
|
-
amountIn: hop.amountIn.toString(),
|
|
1710
|
-
amountOut: hop.amountOut.toString(),
|
|
1711
|
-
} : {}),
|
|
1712
|
-
})),
|
|
1713
|
-
},
|
|
1714
|
-
};
|
|
1700
|
+
return { success: true, route: serializeWireRoute(route) };
|
|
1715
1701
|
};
|
|
1716
1702
|
register('totem_omniaGetRoute', getRoute, 'omnia/getRoute');
|
|
1717
1703
|
const getSwapRate = () => ({ success: true, announcements: [] });
|
|
@@ -2529,28 +2515,115 @@ function createHostMethods(context) {
|
|
|
2529
2515
|
};
|
|
2530
2516
|
register('totem_omniaSpliceIn', splice('in'), 'omnia/spliceIn');
|
|
2531
2517
|
register('totem_omniaSpliceOut', splice('out'), 'omnia/spliceOut');
|
|
2518
|
+
function parseWireRoute(value) {
|
|
2519
|
+
if (!value || typeof value !== 'object')
|
|
2520
|
+
throw new Error('route must be an object');
|
|
2521
|
+
const raw = value;
|
|
2522
|
+
if (!Array.isArray(raw.hops) || raw.hops.length === 0)
|
|
2523
|
+
throw new Error('route.hops must be a non-empty array');
|
|
2524
|
+
const hops = raw.hops.map((entry, index) => {
|
|
2525
|
+
if (!entry || typeof entry !== 'object')
|
|
2526
|
+
throw new Error(`route.hops[${index}] must be an object`);
|
|
2527
|
+
const hop = entry;
|
|
2528
|
+
const base = {
|
|
2529
|
+
channelId: String(hop.channelId ?? ''),
|
|
2530
|
+
from: String(hop.from ?? ''),
|
|
2531
|
+
to: String(hop.to ?? ''),
|
|
2532
|
+
amount: typeof hop.amount === 'bigint' ? hop.amount : BigInt(String(hop.amount ?? '0')),
|
|
2533
|
+
tokenId: String(hop.tokenId ?? ''),
|
|
2534
|
+
...(typeof hop.htlcId === 'string' ? { htlcId: hop.htlcId } : {}),
|
|
2535
|
+
};
|
|
2536
|
+
if (hop.isSwap === true) {
|
|
2537
|
+
return {
|
|
2538
|
+
...base,
|
|
2539
|
+
isSwap: true,
|
|
2540
|
+
tokenIn: String(hop.tokenIn ?? ''),
|
|
2541
|
+
tokenOut: String(hop.tokenOut ?? ''),
|
|
2542
|
+
amountIn: typeof hop.amountIn === 'bigint' ? hop.amountIn : BigInt(String(hop.amountIn ?? '0')),
|
|
2543
|
+
amountOut: typeof hop.amountOut === 'bigint' ? hop.amountOut : BigInt(String(hop.amountOut ?? '0')),
|
|
2544
|
+
rate: String(hop.rate ?? ''),
|
|
2545
|
+
inboundChannelId: String(hop.inboundChannelId ?? ''),
|
|
2546
|
+
outboundChannelId: String(hop.outboundChannelId ?? ''),
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
return base;
|
|
2550
|
+
});
|
|
2551
|
+
const route = {
|
|
2552
|
+
hops,
|
|
2553
|
+
totalFees: typeof raw.totalFees === 'bigint' ? raw.totalFees : BigInt(String(raw.totalFees ?? '0')),
|
|
2554
|
+
tokenIn: String(raw.tokenIn ?? ''),
|
|
2555
|
+
tokenOut: String(raw.tokenOut ?? ''),
|
|
2556
|
+
estimatedBlocks: Number(raw.estimatedBlocks ?? 0),
|
|
2557
|
+
};
|
|
2558
|
+
if (hops.some((hop) => hop.isSwap)) {
|
|
2559
|
+
route.swapHops = hops.filter((hop) => hop.isSwap);
|
|
2560
|
+
}
|
|
2561
|
+
return route;
|
|
2562
|
+
}
|
|
2563
|
+
function serializeWireRoute(route) {
|
|
2564
|
+
return {
|
|
2565
|
+
tokenIn: route.tokenIn,
|
|
2566
|
+
tokenOut: route.tokenOut,
|
|
2567
|
+
totalFees: route.totalFees.toString(),
|
|
2568
|
+
estimatedBlocks: route.estimatedBlocks,
|
|
2569
|
+
hops: route.hops.map((hop) => {
|
|
2570
|
+
const base = {
|
|
2571
|
+
channelId: hop.channelId,
|
|
2572
|
+
from: hop.from,
|
|
2573
|
+
to: hop.to,
|
|
2574
|
+
amount: hop.amount.toString(),
|
|
2575
|
+
tokenId: hop.tokenId,
|
|
2576
|
+
...(hop.htlcId ? { htlcId: hop.htlcId } : {}),
|
|
2577
|
+
};
|
|
2578
|
+
if ('isSwap' in hop && hop.isSwap) {
|
|
2579
|
+
return {
|
|
2580
|
+
...base,
|
|
2581
|
+
isSwap: true,
|
|
2582
|
+
tokenIn: hop.tokenIn,
|
|
2583
|
+
tokenOut: hop.tokenOut,
|
|
2584
|
+
amountIn: hop.amountIn.toString(),
|
|
2585
|
+
amountOut: hop.amountOut.toString(),
|
|
2586
|
+
rate: hop.rate,
|
|
2587
|
+
inboundChannelId: hop.inboundChannelId,
|
|
2588
|
+
outboundChannelId: hop.outboundChannelId,
|
|
2589
|
+
};
|
|
2590
|
+
}
|
|
2591
|
+
return base;
|
|
2592
|
+
}),
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2532
2595
|
const payMultiHop = async (raw) => {
|
|
2533
2596
|
const params = paramsObject(raw);
|
|
2534
2597
|
requireMutationContext(context);
|
|
2535
|
-
const
|
|
2536
|
-
if (!
|
|
2537
|
-
throw new Error('
|
|
2538
|
-
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2598
|
+
const from = context.localParticipant?.publicKeyDigest;
|
|
2599
|
+
if (!from)
|
|
2600
|
+
throw new Error('Local participant is not configured');
|
|
2601
|
+
const amount = requiredBigInt(params, 'amount');
|
|
2602
|
+
const tokenId = requiredString(params, 'tokenId');
|
|
2603
|
+
const targetTokenId = typeof params.targetTokenId === 'string' ? params.targetTokenId : undefined;
|
|
2604
|
+
const parsedRoute = params.route
|
|
2605
|
+
? parseWireRoute(params.route)
|
|
2606
|
+
: await context.routing.getRoute({
|
|
2607
|
+
from,
|
|
2608
|
+
to: normalizeRouteNode(requiredString(params, 'toPartyId')),
|
|
2609
|
+
amount,
|
|
2610
|
+
tokenId,
|
|
2611
|
+
...(targetTokenId ? { targetTokenId } : {}),
|
|
2612
|
+
...(typeof params.maxHops === 'number' ? { maxHops: params.maxHops } : {}),
|
|
2613
|
+
});
|
|
2614
|
+
if (!parsedRoute)
|
|
2615
|
+
throw new Error('No route found');
|
|
2616
|
+
const isCrossToken = parsedRoute.hops.some((hop) => 'isSwap' in hop && hop.isSwap === true);
|
|
2544
2617
|
const parsedRequest = {
|
|
2545
|
-
hashlock: requiredString(
|
|
2546
|
-
amount
|
|
2547
|
-
tokenId
|
|
2548
|
-
expiryBlock: requiredBigInt(
|
|
2549
|
-
|
|
2550
|
-
|
|
2618
|
+
hashlock: requiredString(params, 'hashlock'),
|
|
2619
|
+
amount,
|
|
2620
|
+
tokenId,
|
|
2621
|
+
expiryBlock: requiredBigInt(params, 'expiryBlock'),
|
|
2622
|
+
...(typeof params.preimage === 'string' ? { preimage: params.preimage } : {}),
|
|
2623
|
+
...(typeof params.description === 'string' ? { description: params.description } : {}),
|
|
2551
2624
|
};
|
|
2552
|
-
|
|
2553
|
-
|
|
2625
|
+
if (!parsedRequest.preimage)
|
|
2626
|
+
throw new Error('preimage is required');
|
|
2554
2627
|
const ops = {
|
|
2555
2628
|
addHTLC: (channel, htlcParams, leaseProvider) => (0, omnia_1.addHTLC)(channel, htlcParams, leaseProvider, context.signer),
|
|
2556
2629
|
fulfillHTLC: (channel, htlcId, preimage, leaseProvider) => (0, omnia_1.fulfillHTLC)(channel, htlcId, preimage, leaseProvider, context.signer),
|
|
@@ -2588,6 +2661,7 @@ function createHostMethods(context) {
|
|
|
2588
2661
|
success: true,
|
|
2589
2662
|
preimage: result.preimage,
|
|
2590
2663
|
settledHops: result.settledHops,
|
|
2664
|
+
route: serializeWireRoute(parsedRoute),
|
|
2591
2665
|
};
|
|
2592
2666
|
});
|
|
2593
2667
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export { createHostIdentityAndManifest, createHostManifest, loadHostIdentity, }
|
|
|
9
9
|
export { SqliteChannelStore } from './stores/sqlite-store.js';
|
|
10
10
|
export { OperationStore } from './stores/operations.js';
|
|
11
11
|
export type { OperationRecord, OperationStatus } from './stores/operations.js';
|
|
12
|
+
export { SqliteAuditStore, createAuditRecorder } from './stores/audit.js';
|
|
13
|
+
export type { SignerAuditEntry } from './stores/audit.js';
|
|
14
|
+
export { generateTotp, verifyTotp, mfaOperationHash, verifyMfaAssertion, assertMfaAssertion, MemoryMfaReplayGuard, } from './mfa.js';
|
|
15
|
+
export type { MfaAssertion, MfaChallenge, MfaReplayGuard, MfaVerificationOptions, MfaVerificationResult, TotpOptions, } from './mfa.js';
|
|
12
16
|
export { createTotemNodeAdapter } from './chain/totem-node-adapter.js';
|
|
13
17
|
export type { TotemNodeAdapter, TotemNodeAdapterOptions, ConfirmationOptions } from './chain/totem-node-adapter.js';
|
|
14
18
|
export { InProcessRoutingProvider } from './router/routing-provider.js';
|
|
@@ -20,5 +24,5 @@ export type { OperationStoreLike } from './api/methods.js';
|
|
|
20
24
|
export { DisabledAnalyticsStore, DuckDbAnalyticsStore, initializeDuckDb } from './stores/analytics.js';
|
|
21
25
|
export type { AnalyticsEvent, AnalyticsStore, DuckDbConnection } from './stores/analytics.js';
|
|
22
26
|
export { createLocalSignerPort, createPolicySignerPort } from './signer-port.js';
|
|
23
|
-
export type { SignerPolicy } from './signer-port.js';
|
|
27
|
+
export type { SignerAuditRecord, SignerPolicy } from './signer-port.js';
|
|
24
28
|
export * from './observe/index.js';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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;
|
|
17
|
+
exports.createPolicySignerPort = exports.createLocalSignerPort = exports.initializeDuckDb = exports.DuckDbAnalyticsStore = exports.DisabledAnalyticsStore = exports.createHostMethods = exports.GoRoutingProvider = exports.InProcessRoutingProvider = exports.createTotemNodeAdapter = exports.MemoryMfaReplayGuard = exports.assertMfaAssertion = exports.verifyMfaAssertion = exports.mfaOperationHash = exports.verifyTotp = exports.generateTotp = exports.createAuditRecorder = exports.SqliteAuditStore = 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;
|
|
18
18
|
var config_js_1 = require("./config.js");
|
|
19
19
|
Object.defineProperty(exports, "loadConfigFromEnv", { enumerable: true, get: function () { return config_js_1.loadConfigFromEnv; } });
|
|
20
20
|
var lifecycle_js_1 = require("./lifecycle.js");
|
|
@@ -33,6 +33,16 @@ var sqlite_store_js_1 = require("./stores/sqlite-store.js");
|
|
|
33
33
|
Object.defineProperty(exports, "SqliteChannelStore", { enumerable: true, get: function () { return sqlite_store_js_1.SqliteChannelStore; } });
|
|
34
34
|
var operations_js_1 = require("./stores/operations.js");
|
|
35
35
|
Object.defineProperty(exports, "OperationStore", { enumerable: true, get: function () { return operations_js_1.OperationStore; } });
|
|
36
|
+
var audit_js_1 = require("./stores/audit.js");
|
|
37
|
+
Object.defineProperty(exports, "SqliteAuditStore", { enumerable: true, get: function () { return audit_js_1.SqliteAuditStore; } });
|
|
38
|
+
Object.defineProperty(exports, "createAuditRecorder", { enumerable: true, get: function () { return audit_js_1.createAuditRecorder; } });
|
|
39
|
+
var mfa_js_1 = require("./mfa.js");
|
|
40
|
+
Object.defineProperty(exports, "generateTotp", { enumerable: true, get: function () { return mfa_js_1.generateTotp; } });
|
|
41
|
+
Object.defineProperty(exports, "verifyTotp", { enumerable: true, get: function () { return mfa_js_1.verifyTotp; } });
|
|
42
|
+
Object.defineProperty(exports, "mfaOperationHash", { enumerable: true, get: function () { return mfa_js_1.mfaOperationHash; } });
|
|
43
|
+
Object.defineProperty(exports, "verifyMfaAssertion", { enumerable: true, get: function () { return mfa_js_1.verifyMfaAssertion; } });
|
|
44
|
+
Object.defineProperty(exports, "assertMfaAssertion", { enumerable: true, get: function () { return mfa_js_1.assertMfaAssertion; } });
|
|
45
|
+
Object.defineProperty(exports, "MemoryMfaReplayGuard", { enumerable: true, get: function () { return mfa_js_1.MemoryMfaReplayGuard; } });
|
|
36
46
|
var totem_node_adapter_js_1 = require("./chain/totem-node-adapter.js");
|
|
37
47
|
Object.defineProperty(exports, "createTotemNodeAdapter", { enumerable: true, get: function () { return totem_node_adapter_js_1.createTotemNodeAdapter; } });
|
|
38
48
|
var routing_provider_js_1 = require("./router/routing-provider.js");
|
package/dist/mfa.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TOTP + operation-bound MFA assertions (RFC-061 D8).
|
|
3
|
+
*
|
|
4
|
+
* The node/host creates an MfaChallenge for a money-moving operation; the user
|
|
5
|
+
* returns an MfaAssertion (TOTP proof). Verification binds the assertion to the
|
|
6
|
+
* exact operation hash, disclosure and expiry, and consumes it once. Secrets
|
|
7
|
+
* never leave the verifier.
|
|
8
|
+
*/
|
|
9
|
+
export interface TotpOptions {
|
|
10
|
+
/** Current time in ms (default Date.now()). */
|
|
11
|
+
time?: number;
|
|
12
|
+
period?: number;
|
|
13
|
+
digits?: number;
|
|
14
|
+
/** Accepted step window on either side (default 1). */
|
|
15
|
+
window?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function generateTotp(secret: string | Uint8Array, options?: TotpOptions): string;
|
|
18
|
+
export declare function verifyTotp(secret: string | Uint8Array, code: string, options?: TotpOptions): boolean;
|
|
19
|
+
export type MfaPurpose = 'sign' | 'submit' | 'key-rotation' | 'recovery' | 'admin';
|
|
20
|
+
export type MfaMethod = 'totp' | 'webauthn' | 'recovery-code';
|
|
21
|
+
/** Server-issued challenge bound to one exact operation. */
|
|
22
|
+
export interface MfaChallenge {
|
|
23
|
+
challengeId: string;
|
|
24
|
+
purpose: MfaPurpose;
|
|
25
|
+
operationHash: string;
|
|
26
|
+
disclosureId?: string;
|
|
27
|
+
expiresAt: number;
|
|
28
|
+
attempts?: number;
|
|
29
|
+
}
|
|
30
|
+
/** Client-supplied proof for a challenge. */
|
|
31
|
+
export interface MfaAssertion {
|
|
32
|
+
challengeId: string;
|
|
33
|
+
method: MfaMethod;
|
|
34
|
+
proof: string;
|
|
35
|
+
issuedAt: number;
|
|
36
|
+
}
|
|
37
|
+
/** Single-use replay guard (durable in a hosted deployment). */
|
|
38
|
+
export interface MfaReplayGuard {
|
|
39
|
+
has(challengeId: string): boolean;
|
|
40
|
+
add(challengeId: string): void;
|
|
41
|
+
}
|
|
42
|
+
export declare class MemoryMfaReplayGuard implements MfaReplayGuard {
|
|
43
|
+
private readonly used;
|
|
44
|
+
has(challengeId: string): boolean;
|
|
45
|
+
add(challengeId: string): void;
|
|
46
|
+
}
|
|
47
|
+
export interface MfaVerificationOptions {
|
|
48
|
+
secret: string | Uint8Array;
|
|
49
|
+
/** Expected operation hash (see `mfaOperationHash`). */
|
|
50
|
+
operationHash: string;
|
|
51
|
+
disclosureId?: string;
|
|
52
|
+
now?: number;
|
|
53
|
+
period?: number;
|
|
54
|
+
digits?: number;
|
|
55
|
+
window?: number;
|
|
56
|
+
/** Tolerated future clock skew for issuedAt (default 5 minutes). */
|
|
57
|
+
clockSkewMs?: number;
|
|
58
|
+
/** Replay guard; defaults to a process-local guard when omitted. */
|
|
59
|
+
used?: MfaReplayGuard;
|
|
60
|
+
}
|
|
61
|
+
export interface MfaVerificationResult {
|
|
62
|
+
ok: boolean;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
export declare function verifyMfaAssertion(challenge: MfaChallenge, assertion: MfaAssertion, options: MfaVerificationOptions): MfaVerificationResult;
|
|
66
|
+
export declare function assertMfaAssertion(challenge: MfaChallenge, assertion: MfaAssertion, options: MfaVerificationOptions): void;
|
|
67
|
+
export interface MfaOperationInput {
|
|
68
|
+
purpose: MfaPurpose;
|
|
69
|
+
domain: string;
|
|
70
|
+
/** Canonical operation digest (SignRequest.digest). */
|
|
71
|
+
digest: Uint8Array | string;
|
|
72
|
+
disclosureId?: string;
|
|
73
|
+
amount?: string;
|
|
74
|
+
asset?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Stable operation hash binding an MFA challenge to one signing intent. */
|
|
77
|
+
export declare function mfaOperationHash(input: MfaOperationInput): string;
|
package/dist/mfa.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TOTP + operation-bound MFA assertions (RFC-061 D8).
|
|
4
|
+
*
|
|
5
|
+
* The node/host creates an MfaChallenge for a money-moving operation; the user
|
|
6
|
+
* returns an MfaAssertion (TOTP proof). Verification binds the assertion to the
|
|
7
|
+
* exact operation hash, disclosure and expiry, and consumes it once. Secrets
|
|
8
|
+
* never leave the verifier.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.MemoryMfaReplayGuard = void 0;
|
|
12
|
+
exports.generateTotp = generateTotp;
|
|
13
|
+
exports.verifyTotp = verifyTotp;
|
|
14
|
+
exports.verifyMfaAssertion = verifyMfaAssertion;
|
|
15
|
+
exports.assertMfaAssertion = assertMfaAssertion;
|
|
16
|
+
exports.mfaOperationHash = mfaOperationHash;
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
19
|
+
const DEFAULT_PERIOD = 30;
|
|
20
|
+
const DEFAULT_DIGITS = 6;
|
|
21
|
+
function toSecret(secret) {
|
|
22
|
+
if (secret instanceof Uint8Array)
|
|
23
|
+
return secret;
|
|
24
|
+
const clean = secret.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/, '');
|
|
25
|
+
if (!clean)
|
|
26
|
+
throw new Error('MFA secret is empty');
|
|
27
|
+
let bits = 0;
|
|
28
|
+
let value = 0;
|
|
29
|
+
const out = [];
|
|
30
|
+
for (const ch of clean) {
|
|
31
|
+
const index = BASE32.indexOf(ch);
|
|
32
|
+
if (index < 0)
|
|
33
|
+
throw new Error('MFA secret is not valid base32');
|
|
34
|
+
value = (value << 5) | index;
|
|
35
|
+
bits += 5;
|
|
36
|
+
if (bits >= 8) {
|
|
37
|
+
out.push((value >>> (bits - 8)) & 0xff);
|
|
38
|
+
bits -= 8;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return Uint8Array.from(out);
|
|
42
|
+
}
|
|
43
|
+
function hotp(secret, counter, digits) {
|
|
44
|
+
const msg = Buffer.alloc(8);
|
|
45
|
+
msg.writeBigUInt64BE(BigInt(counter));
|
|
46
|
+
const digest = (0, node_crypto_1.createHmac)('sha1', secret).update(msg).digest();
|
|
47
|
+
const offset = digest[digest.length - 1] & 0x0f;
|
|
48
|
+
const binary = ((digest[offset] & 0x7f) << 24) |
|
|
49
|
+
((digest[offset + 1] & 0xff) << 16) |
|
|
50
|
+
((digest[offset + 2] & 0xff) << 8) |
|
|
51
|
+
(digest[offset + 3] & 0xff);
|
|
52
|
+
return String(binary % 10 ** digits).padStart(digits, '0');
|
|
53
|
+
}
|
|
54
|
+
function step(timeMs, period) {
|
|
55
|
+
return Math.floor(timeMs / 1000 / period);
|
|
56
|
+
}
|
|
57
|
+
function generateTotp(secret, options = {}) {
|
|
58
|
+
const period = options.period ?? DEFAULT_PERIOD;
|
|
59
|
+
const digits = options.digits ?? DEFAULT_DIGITS;
|
|
60
|
+
const time = options.time ?? Date.now();
|
|
61
|
+
return hotp(toSecret(secret), step(time, period), digits);
|
|
62
|
+
}
|
|
63
|
+
function verifyTotp(secret, code, options = {}) {
|
|
64
|
+
const period = options.period ?? DEFAULT_PERIOD;
|
|
65
|
+
const digits = options.digits ?? DEFAULT_DIGITS;
|
|
66
|
+
const window = options.window ?? 1;
|
|
67
|
+
const time = options.time ?? Date.now();
|
|
68
|
+
const normalized = code.replace(/\s+/g, '');
|
|
69
|
+
if (!/^\d+$/.test(normalized) || normalized.length !== digits)
|
|
70
|
+
return false;
|
|
71
|
+
const key = toSecret(secret);
|
|
72
|
+
const current = step(time, period);
|
|
73
|
+
for (let delta = -window; delta <= window; delta += 1) {
|
|
74
|
+
const candidate = hotp(key, current + delta, digits);
|
|
75
|
+
if (candidate.length === normalized.length &&
|
|
76
|
+
(0, node_crypto_1.timingSafeEqual)(Buffer.from(candidate), Buffer.from(normalized))) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
class MemoryMfaReplayGuard {
|
|
83
|
+
used = new Set();
|
|
84
|
+
has(challengeId) {
|
|
85
|
+
return this.used.has(challengeId);
|
|
86
|
+
}
|
|
87
|
+
add(challengeId) {
|
|
88
|
+
this.used.add(challengeId);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.MemoryMfaReplayGuard = MemoryMfaReplayGuard;
|
|
92
|
+
const defaultGuard = new MemoryMfaReplayGuard();
|
|
93
|
+
function verifyMfaAssertion(challenge, assertion, options) {
|
|
94
|
+
const now = options.now ?? Date.now();
|
|
95
|
+
if (!challenge.challengeId || assertion.challengeId !== challenge.challengeId) {
|
|
96
|
+
return { ok: false, reason: 'MFA assertion does not match the challenge' };
|
|
97
|
+
}
|
|
98
|
+
if (challenge.expiresAt <= now)
|
|
99
|
+
return { ok: false, reason: 'MFA challenge has expired' };
|
|
100
|
+
if (challenge.attempts !== undefined && challenge.attempts <= 0) {
|
|
101
|
+
return { ok: false, reason: 'MFA challenge has no attempts left' };
|
|
102
|
+
}
|
|
103
|
+
if (challenge.operationHash !== options.operationHash) {
|
|
104
|
+
return { ok: false, reason: 'MFA challenge does not match the operation' };
|
|
105
|
+
}
|
|
106
|
+
if (challenge.disclosureId !== undefined && challenge.disclosureId !== options.disclosureId) {
|
|
107
|
+
return { ok: false, reason: 'MFA challenge does not match the disclosure' };
|
|
108
|
+
}
|
|
109
|
+
const skew = options.clockSkewMs ?? 5 * 60_000;
|
|
110
|
+
if (!Number.isFinite(assertion.issuedAt) ||
|
|
111
|
+
assertion.issuedAt <= 0 ||
|
|
112
|
+
assertion.issuedAt > now + skew) {
|
|
113
|
+
return { ok: false, reason: 'MFA assertion has an invalid issuedAt' };
|
|
114
|
+
}
|
|
115
|
+
const used = options.used ?? defaultGuard;
|
|
116
|
+
if (used.has(challenge.challengeId)) {
|
|
117
|
+
return { ok: false, reason: 'MFA assertion has already been used' };
|
|
118
|
+
}
|
|
119
|
+
if (assertion.method !== 'totp') {
|
|
120
|
+
return { ok: false, reason: `unsupported MFA method: ${assertion.method}` };
|
|
121
|
+
}
|
|
122
|
+
if (!verifyTotp(options.secret, assertion.proof, {
|
|
123
|
+
time: assertion.issuedAt,
|
|
124
|
+
period: options.period,
|
|
125
|
+
digits: options.digits,
|
|
126
|
+
window: options.window,
|
|
127
|
+
})) {
|
|
128
|
+
return { ok: false, reason: 'MFA proof is invalid' };
|
|
129
|
+
}
|
|
130
|
+
used.add(challenge.challengeId);
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
function assertMfaAssertion(challenge, assertion, options) {
|
|
134
|
+
const result = verifyMfaAssertion(challenge, assertion, options);
|
|
135
|
+
if (!result.ok)
|
|
136
|
+
throw new Error(result.reason ?? 'MFA verification failed');
|
|
137
|
+
}
|
|
138
|
+
/** Stable operation hash binding an MFA challenge to one signing intent. */
|
|
139
|
+
function mfaOperationHash(input) {
|
|
140
|
+
const digest = input.digest instanceof Uint8Array
|
|
141
|
+
? Buffer.from(input.digest).toString('hex')
|
|
142
|
+
: String(input.digest).replace(/^0x/i, '').toLowerCase();
|
|
143
|
+
const canonical = JSON.stringify({
|
|
144
|
+
purpose: input.purpose,
|
|
145
|
+
domain: input.domain,
|
|
146
|
+
digest,
|
|
147
|
+
disclosureId: input.disclosureId ?? null,
|
|
148
|
+
amount: input.amount ?? null,
|
|
149
|
+
asset: input.asset ?? null,
|
|
150
|
+
});
|
|
151
|
+
return '0x' + (0, node_crypto_1.createHash)('sha256').update(canonical, 'utf8').digest('hex');
|
|
152
|
+
}
|
package/dist/observe/snapshot.js
CHANGED
|
@@ -40,7 +40,7 @@ function channelView(channel) {
|
|
|
40
40
|
currentSequence: channel.currentSequence,
|
|
41
41
|
fundingCoinId: channel.fundingCoinId,
|
|
42
42
|
fundingTxId: channel.fundingTxId,
|
|
43
|
-
parties: channel.parties,
|
|
43
|
+
parties: toJson(channel.parties),
|
|
44
44
|
balances: Object.fromEntries(Object.entries(channel.balances).map(([k, v]) => [k, v.toString()])),
|
|
45
45
|
pendingHTLCs: (channel.pendingHTLCs ?? []).map((h) => toJson(h)),
|
|
46
46
|
updatedAt: channel.updatedAt,
|
|
@@ -54,7 +54,7 @@ function factoryView(factory) {
|
|
|
54
54
|
status: f.status,
|
|
55
55
|
tokenId: f.tokenId,
|
|
56
56
|
tokenScale: f.tokenScale ?? 0,
|
|
57
|
-
participants: f.participants,
|
|
57
|
+
participants: toJson(f.participants),
|
|
58
58
|
allocations: Object.fromEntries(Object.entries(allocations).map(([k, v]) => [k, v.toString()])),
|
|
59
59
|
updatedAt: f.updatedAt,
|
|
60
60
|
};
|
|
@@ -72,12 +72,12 @@ function operationsView(operations) {
|
|
|
72
72
|
continue;
|
|
73
73
|
if (r.operationId)
|
|
74
74
|
seen.add(r.operationId);
|
|
75
|
-
out.push({
|
|
75
|
+
out.push(toJson({
|
|
76
76
|
operationId: r.operationId,
|
|
77
77
|
status: record.status ?? status,
|
|
78
78
|
updatedAt: record.updatedAt,
|
|
79
79
|
error: record.error,
|
|
80
|
-
});
|
|
80
|
+
}));
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
return out;
|
package/dist/signer-port.d.ts
CHANGED
|
@@ -5,9 +5,24 @@
|
|
|
5
5
|
* contract. A hosted deployment supplies its own `SignerPort` (KMS/HSM/MPC +
|
|
6
6
|
* policy) instead; the host code is agnostic.
|
|
7
7
|
*/
|
|
8
|
-
import { type CustodyMode, type SignDomain, type SignRequest, type SignerPort } from '@totemsdk/omnia';
|
|
8
|
+
import { type CustodyAcknowledgement, type CustodyMode, type SignDomain, type SignRequest, type SignerPort } from '@totemsdk/omnia';
|
|
9
9
|
import type { HostSigning } from './signing.js';
|
|
10
10
|
export declare function createLocalSignerPort(signing: HostSigning): SignerPort;
|
|
11
|
+
/** Durable audit record for every policy-signer decision (RFC-061 D6). */
|
|
12
|
+
export interface SignerAuditRecord {
|
|
13
|
+
requestId: string;
|
|
14
|
+
domain: SignDomain;
|
|
15
|
+
mode: CustodyMode;
|
|
16
|
+
disclosureId: string;
|
|
17
|
+
acknowledgementId?: string;
|
|
18
|
+
signer: string;
|
|
19
|
+
decision: 'allow' | 'deny';
|
|
20
|
+
reason?: string;
|
|
21
|
+
amount?: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
occurredAt?: number;
|
|
24
|
+
mfaVerified?: boolean;
|
|
25
|
+
}
|
|
11
26
|
/** Hosted/KMS policy checks applied before delegating to the underlying port. */
|
|
12
27
|
export interface SignerPolicy {
|
|
13
28
|
allowedDomains?: SignDomain[];
|
|
@@ -16,6 +31,24 @@ export interface SignerPolicy {
|
|
|
16
31
|
maxAmount?: bigint;
|
|
17
32
|
/** Require a disclosure hash/version on the custody context. */
|
|
18
33
|
requireDisclosure?: boolean;
|
|
34
|
+
/** RFC-061 signer gate: exact disclosure acknowledgement + policy decision. */
|
|
35
|
+
custody?: {
|
|
36
|
+
acknowledgement?: CustodyAcknowledgement;
|
|
37
|
+
subjectId?: string;
|
|
38
|
+
policyDecision?: 'allow' | 'deny';
|
|
39
|
+
now?: number;
|
|
40
|
+
};
|
|
41
|
+
/** Durable audit sink; called with allow/deny decisions. */
|
|
42
|
+
audit?: (record: SignerAuditRecord) => void | Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* RFC-061 D8: verify a request-scoped, operation-bound MFA assertion.
|
|
45
|
+
* Defaults to non-self custody; a `minAmount` can scope it to larger moves.
|
|
46
|
+
*/
|
|
47
|
+
mfa?: {
|
|
48
|
+
verify: (request: SignRequest) => void | Promise<void>;
|
|
49
|
+
modes?: CustodyMode[];
|
|
50
|
+
minAmount?: bigint;
|
|
51
|
+
};
|
|
19
52
|
/** Additional operator approval hook (KMS/HSM/MPC or quorum). */
|
|
20
53
|
authorize?: (request: SignRequest) => void | Promise<void>;
|
|
21
54
|
}
|
package/dist/signer-port.js
CHANGED
|
@@ -56,6 +56,32 @@ function createPolicySignerPort(base, policy = {}) {
|
|
|
56
56
|
if (!request.signers.includes(base.publicKeyDigest)) {
|
|
57
57
|
throw new Error(`signer ${base.publicKeyDigest} is not a required signer for ${request.requestId}`);
|
|
58
58
|
}
|
|
59
|
+
const ack = policy.custody?.acknowledgement;
|
|
60
|
+
const auditBase = {
|
|
61
|
+
requestId: request.requestId,
|
|
62
|
+
domain: request.domain,
|
|
63
|
+
mode: request.custody.mode,
|
|
64
|
+
disclosureId: (0, omnia_1.custodyDisclosureId)(request.custody),
|
|
65
|
+
...(ack
|
|
66
|
+
? { acknowledgementId: `${ack.disclosureId}:${ack.subjectId}:${ack.acknowledgedAt}` }
|
|
67
|
+
: {}),
|
|
68
|
+
signer: base.publicKeyDigest,
|
|
69
|
+
amount: request.context.amount,
|
|
70
|
+
summary: request.context.summary,
|
|
71
|
+
occurredAt: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
(0, omnia_1.assertCustodyAllowsSigning)(request, policy.custody ?? {});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
await policy.audit?.({
|
|
78
|
+
...auditBase,
|
|
79
|
+
decision: 'deny',
|
|
80
|
+
mfaVerified: false,
|
|
81
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
82
|
+
});
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
59
85
|
if (policy.allowedDomains && !policy.allowedDomains.includes(request.domain)) {
|
|
60
86
|
throw new Error(`domain ${request.domain} is not allowed by policy`);
|
|
61
87
|
}
|
|
@@ -72,8 +98,34 @@ function createPolicySignerPort(base, policy = {}) {
|
|
|
72
98
|
throw new Error(`request amount ${parsed} exceeds policy limit ${policy.maxAmount}`);
|
|
73
99
|
}
|
|
74
100
|
}
|
|
101
|
+
let mfaVerified = false;
|
|
102
|
+
if (policy.mfa) {
|
|
103
|
+
const modes = policy.mfa.modes ?? ['trust-minimised', 'hosted'];
|
|
104
|
+
const modeRequired = modes.includes(request.custody.mode);
|
|
105
|
+
let amountRequired = true;
|
|
106
|
+
if (policy.mfa.minAmount !== undefined && request.context.amount !== undefined) {
|
|
107
|
+
amountRequired = BigInt(request.context.amount) >= policy.mfa.minAmount;
|
|
108
|
+
}
|
|
109
|
+
if (modeRequired && amountRequired) {
|
|
110
|
+
try {
|
|
111
|
+
await policy.mfa.verify(request);
|
|
112
|
+
mfaVerified = true;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
await policy.audit?.({
|
|
116
|
+
...auditBase,
|
|
117
|
+
decision: 'deny',
|
|
118
|
+
mfaVerified: false,
|
|
119
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
120
|
+
});
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
75
125
|
await policy.authorize?.(request);
|
|
76
|
-
|
|
126
|
+
const signature = await base.sign(request);
|
|
127
|
+
await policy.audit?.({ ...auditBase, decision: 'allow', mfaVerified });
|
|
128
|
+
return signature;
|
|
77
129
|
},
|
|
78
130
|
};
|
|
79
131
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable signer audit store (RFC-061 D6).
|
|
3
|
+
*
|
|
4
|
+
* Every allow/deny decision from `createPolicySignerPort` is appended here
|
|
5
|
+
* through `@totemsdk/storage` (SyncSqliteStore; same codec/envelope as the
|
|
6
|
+
* host's other local state). Audit records never contain keys, seeds,
|
|
7
|
+
* preimages or raw signatures.
|
|
8
|
+
*/
|
|
9
|
+
import type { SignerAuditRecord } from '../signer-port.js';
|
|
10
|
+
export interface SignerAuditEntry extends SignerAuditRecord {
|
|
11
|
+
auditId: string;
|
|
12
|
+
recordedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/** Storage-backed, append-only signer audit log. */
|
|
15
|
+
export declare class SqliteAuditStore {
|
|
16
|
+
private readonly kv;
|
|
17
|
+
constructor(path?: string);
|
|
18
|
+
append(record: SignerAuditRecord): SignerAuditEntry;
|
|
19
|
+
/** Newest first. */
|
|
20
|
+
list(limit?: number): SignerAuditEntry[];
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
|
23
|
+
/** Adapt the store to the `SignerPolicy.audit` callback. */
|
|
24
|
+
export declare function createAuditRecorder(store: SqliteAuditStore): (record: SignerAuditRecord) => SignerAuditEntry;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Durable signer audit store (RFC-061 D6).
|
|
4
|
+
*
|
|
5
|
+
* Every allow/deny decision from `createPolicySignerPort` is appended here
|
|
6
|
+
* through `@totemsdk/storage` (SyncSqliteStore; same codec/envelope as the
|
|
7
|
+
* host's other local state). Audit records never contain keys, seeds,
|
|
8
|
+
* preimages or raw signatures.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.SqliteAuditStore = void 0;
|
|
12
|
+
exports.createAuditRecorder = createAuditRecorder;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
14
|
+
const sqlite_sync_1 = require("@totemsdk/storage/sqlite-sync");
|
|
15
|
+
const PREFIX = 'omnia:audit:';
|
|
16
|
+
/** Storage-backed, append-only signer audit log. */
|
|
17
|
+
class SqliteAuditStore {
|
|
18
|
+
kv;
|
|
19
|
+
constructor(path = ':memory:') {
|
|
20
|
+
this.kv = new sqlite_sync_1.SyncSqliteStore(path);
|
|
21
|
+
}
|
|
22
|
+
append(record) {
|
|
23
|
+
const recordedAt = Date.now();
|
|
24
|
+
const auditId = (0, node_crypto_1.randomUUID)();
|
|
25
|
+
const entry = { ...record, auditId, recordedAt };
|
|
26
|
+
this.kv.set(`${PREFIX}${String(recordedAt).padStart(13, '0')}:${auditId}`, entry);
|
|
27
|
+
return entry;
|
|
28
|
+
}
|
|
29
|
+
/** Newest first. */
|
|
30
|
+
list(limit = 100) {
|
|
31
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 100;
|
|
32
|
+
return this.kv
|
|
33
|
+
.keys()
|
|
34
|
+
.filter((key) => key.startsWith(PREFIX))
|
|
35
|
+
.sort()
|
|
36
|
+
.reverse()
|
|
37
|
+
.slice(0, safeLimit)
|
|
38
|
+
.map((key) => this.kv.get(key))
|
|
39
|
+
.filter((entry) => entry !== null);
|
|
40
|
+
}
|
|
41
|
+
close() {
|
|
42
|
+
this.kv.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.SqliteAuditStore = SqliteAuditStore;
|
|
46
|
+
/** Adapt the store to the `SignerPolicy.audit` callback. */
|
|
47
|
+
function createAuditRecorder(store) {
|
|
48
|
+
return (record) => store.append(record);
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@totemsdk/omnia-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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,8 +22,8 @@
|
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"ws": "^8.21.1",
|
|
25
|
-
"@totemsdk/chain-provider": "^0.3.0",
|
|
26
25
|
"@totemsdk/core": "^1.4.0",
|
|
26
|
+
"@totemsdk/chain-provider": "^0.3.0",
|
|
27
27
|
"@totemsdk/identity": "^1.0.2",
|
|
28
28
|
"@totemsdk/liquidity-bond": "^0.3.1",
|
|
29
29
|
"@totemsdk/manifest": "^2.0.2",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"@totemsdk/omnia-factory": "^0.7.0",
|
|
33
33
|
"@totemsdk/omnia-router": "^0.4.0",
|
|
34
34
|
"@totemsdk/omnia-splice": "^0.3.0",
|
|
35
|
-
"@totemsdk/omnia-pool": "^0.5.3",
|
|
36
35
|
"@totemsdk/p2p-hub": "^0.1.0",
|
|
37
|
-
"@totemsdk/storage": "^0.2.
|
|
36
|
+
"@totemsdk/storage": "^0.2.1",
|
|
38
37
|
"@totemsdk/omnia-vtxo": "^0.6.1",
|
|
38
|
+
"@totemsdk/omnia-pool": "^0.5.3",
|
|
39
39
|
"@totemsdk/wots-lease": "^1.0.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|