@shieldedtech/moth-wallet 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/contract/args-parser.d.ts +29 -0
- package/dist/contract/args-parser.d.ts.map +1 -0
- package/dist/contract/args-parser.js +123 -0
- package/dist/contract/args-parser.js.map +1 -0
- package/dist/contract/artifact-loader.d.ts +26 -0
- package/dist/contract/artifact-loader.d.ts.map +1 -0
- package/dist/contract/artifact-loader.js +129 -0
- package/dist/contract/artifact-loader.js.map +1 -0
- package/dist/contract/call.d.ts +32 -0
- package/dist/contract/call.d.ts.map +1 -0
- package/dist/contract/call.js +245 -0
- package/dist/contract/call.js.map +1 -0
- package/dist/contract/deploy.d.ts +36 -0
- package/dist/contract/deploy.d.ts.map +1 -0
- package/dist/contract/deploy.js +388 -0
- package/dist/contract/deploy.js.map +1 -0
- package/dist/contract/fungible-token.d.ts +40 -0
- package/dist/contract/fungible-token.d.ts.map +1 -0
- package/dist/contract/fungible-token.js +88 -0
- package/dist/contract/fungible-token.js.map +1 -0
- package/dist/contract/initial-private-state.d.ts +18 -0
- package/dist/contract/initial-private-state.d.ts.map +1 -0
- package/dist/contract/initial-private-state.js +44 -0
- package/dist/contract/initial-private-state.js.map +1 -0
- package/dist/contract/maintenance.d.ts +72 -0
- package/dist/contract/maintenance.d.ts.map +1 -0
- package/dist/contract/maintenance.js +430 -0
- package/dist/contract/maintenance.js.map +1 -0
- package/dist/contract/state.d.ts +16 -0
- package/dist/contract/state.d.ts.map +1 -0
- package/dist/contract/state.js +18 -0
- package/dist/contract/state.js.map +1 -0
- package/dist/contract/witness-loader.d.ts +3 -0
- package/dist/contract/witness-loader.d.ts.map +1 -0
- package/dist/contract/witness-loader.js +27 -0
- package/dist/contract/witness-loader.js.map +1 -0
- package/dist/daemon/api-keys.d.ts +66 -0
- package/dist/daemon/api-keys.d.ts.map +1 -0
- package/dist/daemon/api-keys.js +222 -0
- package/dist/daemon/api-keys.js.map +1 -0
- package/dist/daemon/audit-log.d.ts +65 -0
- package/dist/daemon/audit-log.d.ts.map +1 -0
- package/dist/daemon/audit-log.js +73 -0
- package/dist/daemon/audit-log.js.map +1 -0
- package/dist/daemon/client.d.ts +63 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +214 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/confirmation-queue.d.ts +46 -0
- package/dist/daemon/confirmation-queue.d.ts.map +1 -0
- package/dist/daemon/confirmation-queue.js +92 -0
- package/dist/daemon/confirmation-queue.js.map +1 -0
- package/dist/daemon/index.d.ts +23 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +26 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/protocol.d.ts +40 -0
- package/dist/daemon/protocol.d.ts.map +1 -0
- package/dist/daemon/protocol.js +104 -0
- package/dist/daemon/protocol.js.map +1 -0
- package/dist/daemon/server.d.ts +158 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +297 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/wallet-handlers.d.ts +37 -0
- package/dist/daemon/wallet-handlers.d.ts.map +1 -0
- package/dist/daemon/wallet-handlers.js +533 -0
- package/dist/daemon/wallet-handlers.js.map +1 -0
- package/dist/daemon/wallet-rpc-parsers.d.ts +14 -0
- package/dist/daemon/wallet-rpc-parsers.d.ts.map +1 -0
- package/dist/daemon/wallet-rpc-parsers.js +259 -0
- package/dist/daemon/wallet-rpc-parsers.js.map +1 -0
- package/dist/daemon/wallet-rpc-types.d.ts +138 -0
- package/dist/daemon/wallet-rpc-types.d.ts.map +1 -0
- package/dist/daemon/wallet-rpc-types.js +11 -0
- package/dist/daemon/wallet-rpc-types.js.map +1 -0
- package/dist/diagnostics/fs-timing-store.d.ts +29 -0
- package/dist/diagnostics/fs-timing-store.d.ts.map +1 -0
- package/dist/diagnostics/fs-timing-store.js +81 -0
- package/dist/diagnostics/fs-timing-store.js.map +1 -0
- package/dist/diagnostics/timings.d.ts +64 -0
- package/dist/diagnostics/timings.d.ts.map +1 -0
- package/dist/diagnostics/timings.js +100 -0
- package/dist/diagnostics/timings.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/network/indexer-client.d.ts +66 -0
- package/dist/network/indexer-client.d.ts.map +1 -0
- package/dist/network/indexer-client.js +112 -0
- package/dist/network/indexer-client.js.map +1 -0
- package/dist/network/node-client.d.ts +27 -0
- package/dist/network/node-client.d.ts.map +1 -0
- package/dist/network/node-client.js +94 -0
- package/dist/network/node-client.js.map +1 -0
- package/dist/proof/client.d.ts +15 -0
- package/dist/proof/client.d.ts.map +1 -0
- package/dist/proof/client.js +77 -0
- package/dist/proof/client.js.map +1 -0
- package/dist/proof/provider.d.ts +12 -0
- package/dist/proof/provider.d.ts.map +1 -0
- package/dist/proof/provider.js +96 -0
- package/dist/proof/provider.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +2 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/midnight-provider.d.ts +11 -0
- package/dist/providers/midnight-provider.d.ts.map +1 -0
- package/dist/providers/midnight-provider.js +30 -0
- package/dist/providers/midnight-provider.js.map +1 -0
- package/dist/storage/adapter.d.ts +8 -0
- package/dist/storage/adapter.d.ts.map +1 -0
- package/dist/storage/adapter.js +2 -0
- package/dist/storage/adapter.js.map +1 -0
- package/dist/storage/fs-adapter.d.ts +14 -0
- package/dist/storage/fs-adapter.d.ts.map +1 -0
- package/dist/storage/fs-adapter.js +126 -0
- package/dist/storage/fs-adapter.js.map +1 -0
- package/dist/storage/safe-path.d.ts +13 -0
- package/dist/storage/safe-path.d.ts.map +1 -0
- package/dist/storage/safe-path.js +35 -0
- package/dist/storage/safe-path.js.map +1 -0
- package/dist/sync/activity.d.ts +53 -0
- package/dist/sync/activity.d.ts.map +1 -0
- package/dist/sync/activity.js +92 -0
- package/dist/sync/activity.js.map +1 -0
- package/dist/sync/dust-registration-estimate.d.ts +54 -0
- package/dist/sync/dust-registration-estimate.d.ts.map +1 -0
- package/dist/sync/dust-registration-estimate.js +103 -0
- package/dist/sync/dust-registration-estimate.js.map +1 -0
- package/dist/sync/node-sync-store.d.ts +10 -0
- package/dist/sync/node-sync-store.d.ts.map +1 -0
- package/dist/sync/node-sync-store.js +42 -0
- package/dist/sync/node-sync-store.js.map +1 -0
- package/dist/sync/operations.d.ts +138 -0
- package/dist/sync/operations.d.ts.map +1 -0
- package/dist/sync/operations.js +395 -0
- package/dist/sync/operations.js.map +1 -0
- package/dist/sync/preseed-parts.d.ts +18 -0
- package/dist/sync/preseed-parts.d.ts.map +1 -0
- package/dist/sync/preseed-parts.js +33 -0
- package/dist/sync/preseed-parts.js.map +1 -0
- package/dist/sync/preseed.d.ts +95 -0
- package/dist/sync/preseed.d.ts.map +1 -0
- package/dist/sync/preseed.js +365 -0
- package/dist/sync/preseed.js.map +1 -0
- package/dist/sync/progress.d.ts +45 -0
- package/dist/sync/progress.d.ts.map +1 -0
- package/dist/sync/progress.js +51 -0
- package/dist/sync/progress.js.map +1 -0
- package/dist/sync/sdk-dedup.d.ts +53 -0
- package/dist/sync/sdk-dedup.d.ts.map +1 -0
- package/dist/sync/sdk-dedup.js +134 -0
- package/dist/sync/sdk-dedup.js.map +1 -0
- package/dist/sync/sync-store.d.ts +33 -0
- package/dist/sync/sync-store.d.ts.map +1 -0
- package/dist/sync/sync-store.js +45 -0
- package/dist/sync/sync-store.js.map +1 -0
- package/dist/sync/wallet-sync.d.ts +153 -0
- package/dist/sync/wallet-sync.d.ts.map +1 -0
- package/dist/sync/wallet-sync.js +943 -0
- package/dist/sync/wallet-sync.js.map +1 -0
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +33 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/exit-codes.d.ts +8 -0
- package/dist/types/exit-codes.d.ts.map +1 -0
- package/dist/types/exit-codes.js +7 -0
- package/dist/types/exit-codes.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/network.d.ts +65 -0
- package/dist/types/network.d.ts.map +1 -0
- package/dist/types/network.js +108 -0
- package/dist/types/network.js.map +1 -0
- package/dist/types/tokens.d.ts +3 -0
- package/dist/types/tokens.d.ts.map +1 -0
- package/dist/types/tokens.js +11 -0
- package/dist/types/tokens.js.map +1 -0
- package/dist/types/transaction.d.ts +13 -0
- package/dist/types/transaction.d.ts.map +1 -0
- package/dist/types/transaction.js +2 -0
- package/dist/types/transaction.js.map +1 -0
- package/dist/types/wallet.d.ts +82 -0
- package/dist/types/wallet.d.ts.map +1 -0
- package/dist/types/wallet.js +2 -0
- package/dist/types/wallet.js.map +1 -0
- package/dist/wallet/address-validate.d.ts +7 -0
- package/dist/wallet/address-validate.d.ts.map +1 -0
- package/dist/wallet/address-validate.js +28 -0
- package/dist/wallet/address-validate.js.map +1 -0
- package/dist/wallet/address.d.ts +32 -0
- package/dist/wallet/address.d.ts.map +1 -0
- package/dist/wallet/address.js +107 -0
- package/dist/wallet/address.js.map +1 -0
- package/dist/wallet/app-secret.d.ts +15 -0
- package/dist/wallet/app-secret.d.ts.map +1 -0
- package/dist/wallet/app-secret.js +45 -0
- package/dist/wallet/app-secret.js.map +1 -0
- package/dist/wallet/balance-format.d.ts +12 -0
- package/dist/wallet/balance-format.d.ts.map +1 -0
- package/dist/wallet/balance-format.js +51 -0
- package/dist/wallet/balance-format.js.map +1 -0
- package/dist/wallet/batch-transfer.d.ts +36 -0
- package/dist/wallet/batch-transfer.d.ts.map +1 -0
- package/dist/wallet/batch-transfer.js +93 -0
- package/dist/wallet/batch-transfer.js.map +1 -0
- package/dist/wallet/keystore.d.ts +16 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +76 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/dist/wallet/manager.d.ts +77 -0
- package/dist/wallet/manager.d.ts.map +1 -0
- package/dist/wallet/manager.js +474 -0
- package/dist/wallet/manager.js.map +1 -0
- package/dist/wallet/mnemonic.d.ts +5 -0
- package/dist/wallet/mnemonic.d.ts.map +1 -0
- package/dist/wallet/mnemonic.js +28 -0
- package/dist/wallet/mnemonic.js.map +1 -0
- package/dist/wallet/sign-message.d.ts +20 -0
- package/dist/wallet/sign-message.d.ts.map +1 -0
- package/dist/wallet/sign-message.js +58 -0
- package/dist/wallet/sign-message.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface ApiKeyRecord {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly salt: string;
|
|
5
|
+
readonly hashedSecret: string;
|
|
6
|
+
readonly createdAt: string;
|
|
7
|
+
readonly revokedAt?: string;
|
|
8
|
+
/** Scopes this key holds. Defaults to ['write'] (full daemon access)
|
|
9
|
+
* on records that predate scopes. Enforced by the daemon's
|
|
10
|
+
* per-method gate (server.ts): a read-only key is rejected from
|
|
11
|
+
* every write verb. */
|
|
12
|
+
readonly scopes?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ApiKeyGenerated {
|
|
15
|
+
readonly record: ApiKeyRecord;
|
|
16
|
+
/** The plaintext token `<id>.<secret-hex>`. Returned ONCE to the
|
|
17
|
+
* caller (the `key gen` CLI command surfaces it on stdout); the
|
|
18
|
+
* store never persists this value. */
|
|
19
|
+
readonly token: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ApiKeyAuthResult {
|
|
22
|
+
readonly apiKeyId: string;
|
|
23
|
+
readonly label: string;
|
|
24
|
+
readonly scopes: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
export declare class ApiKeyStore {
|
|
27
|
+
private readonly dir;
|
|
28
|
+
constructor(opts?: {
|
|
29
|
+
dir?: string;
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Generate, persist, and return a new API key. The plaintext token
|
|
33
|
+
* is surfaced ONCE in the returned `token` field and never
|
|
34
|
+
* persisted — once the CLI has shown it to the operator there is
|
|
35
|
+
* no way to recover it.
|
|
36
|
+
*
|
|
37
|
+
* `scopes` defaults to `['write']` (full daemon access) so
|
|
38
|
+
* existing call sites that don't pass it keep their previous
|
|
39
|
+
* semantics. A read-only key passes `['read']`; a key that needs
|
|
40
|
+
* both passes `['read', 'write']`.
|
|
41
|
+
*/
|
|
42
|
+
generate(label: string, scopes?: readonly string[]): ApiKeyGenerated;
|
|
43
|
+
/**
|
|
44
|
+
* Verify a wire token. Returns the auth result on success or null
|
|
45
|
+
* on any failure (unknown id, wrong secret, revoked key). Always
|
|
46
|
+
* runs the hash compare even when the id is unknown so the timing
|
|
47
|
+
* profile doesn't leak id presence.
|
|
48
|
+
*/
|
|
49
|
+
verify(token: string): ApiKeyAuthResult | null;
|
|
50
|
+
list(): readonly ApiKeyRecord[];
|
|
51
|
+
/**
|
|
52
|
+
* Mark a key revoked. Returns the updated record, or null if no
|
|
53
|
+
* key with that id exists. Revoked records are kept on disk so
|
|
54
|
+
* audit lines reference a real id.
|
|
55
|
+
*/
|
|
56
|
+
revoke(id: string): ApiKeyRecord | null;
|
|
57
|
+
/** Whether the store currently has at least one non-revoked key.
|
|
58
|
+
* Used by `daemon serve` to refuse a TCP bind that would have no
|
|
59
|
+
* way to authenticate any caller. */
|
|
60
|
+
hasActiveKey(): boolean;
|
|
61
|
+
/** Filesystem location — exposed for tests + CLI status output. */
|
|
62
|
+
get directory(): string;
|
|
63
|
+
private read;
|
|
64
|
+
private write;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=api-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.d.ts","sourceRoot":"","sources":["../../src/daemon/api-keys.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;4BAGwB;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;;2CAEuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,IAAI,GAAE;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAM;IAKrC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,SAAS,MAAM,EAAc,GAAG,eAAe;IA4B/E;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI;IA0C9C,IAAI,IAAI,SAAS,YAAY,EAAE;IAc/B;;;;OAIG;IACH,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI;IAYvC;;0CAEsC;IACtC,YAAY,IAAI,OAAO;IAIvB,mEAAmE;IACnE,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,OAAO,CAAC,IAAI;IAeZ,OAAO,CAAC,KAAK;CAUd"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// API-key store for the daemon's stage-2 AuthN. Keys live as
|
|
2
|
+
// individual JSON files under ~/.moth/api-keys/ (mode 0700 dir,
|
|
3
|
+
// 0600 files). Each file is one record:
|
|
4
|
+
//
|
|
5
|
+
// {
|
|
6
|
+
// "id": "<8-char hex prefix>",
|
|
7
|
+
// "label": "ci-bot",
|
|
8
|
+
// "salt": "<hex>",
|
|
9
|
+
// "hashedSecret": "<hex>",
|
|
10
|
+
// "createdAt": "<iso>",
|
|
11
|
+
// "revokedAt": "<iso?>",
|
|
12
|
+
// "scopes": ["read", "write"] // reserved; stage-2.5
|
|
13
|
+
// }
|
|
14
|
+
//
|
|
15
|
+
// The token presented on the wire is `<id>.<secret-hex>`; the
|
|
16
|
+
// server splits on the dot, looks up the record by id, and
|
|
17
|
+
// timing-safe-compares sha256(salt || secret) against
|
|
18
|
+
// `hashedSecret`. Plain SHA-256 is fine because the secret is 32
|
|
19
|
+
// bytes of CSPRNG output — brute force is not viable. No need
|
|
20
|
+
// for Argon2id's CPU/memory cost on a high-entropy secret; the
|
|
21
|
+
// cost there exists to slow human-passphrase guessing.
|
|
22
|
+
import { randomBytes, timingSafeEqual, createHash } from 'node:crypto';
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { homedir } from 'node:os';
|
|
25
|
+
import { join, resolve, sep } from 'node:path';
|
|
26
|
+
/**
|
|
27
|
+
* The on-disk id of a key is exactly 8 lowercase hex chars — derived
|
|
28
|
+
* from the first 4 random bytes of the key's secret. Tokens that
|
|
29
|
+
* don't parse to this shape can never have been generated by us, so
|
|
30
|
+
* every read/write path validates against this regex BEFORE touching
|
|
31
|
+
* the filesystem. Without it, a malicious token like
|
|
32
|
+
* `../../etc/passwd.<anything>` would let `read()` attempt to stat
|
|
33
|
+
* arbitrary files inside the user's home — a path-traversal primitive
|
|
34
|
+
* that future bugs could compound into a real exploit even though
|
|
35
|
+
* the JSON.parse + hash check would still reject the contents.
|
|
36
|
+
*/
|
|
37
|
+
const ID_RE = /^[0-9a-f]{8}$/;
|
|
38
|
+
export class ApiKeyStore {
|
|
39
|
+
dir;
|
|
40
|
+
constructor(opts = {}) {
|
|
41
|
+
this.dir = opts.dir ?? join(homedir(), '.moth', 'api-keys');
|
|
42
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Generate, persist, and return a new API key. The plaintext token
|
|
46
|
+
* is surfaced ONCE in the returned `token` field and never
|
|
47
|
+
* persisted — once the CLI has shown it to the operator there is
|
|
48
|
+
* no way to recover it.
|
|
49
|
+
*
|
|
50
|
+
* `scopes` defaults to `['write']` (full daemon access) so
|
|
51
|
+
* existing call sites that don't pass it keep their previous
|
|
52
|
+
* semantics. A read-only key passes `['read']`; a key that needs
|
|
53
|
+
* both passes `['read', 'write']`.
|
|
54
|
+
*/
|
|
55
|
+
generate(label, scopes = ['write']) {
|
|
56
|
+
if (!label.trim())
|
|
57
|
+
throw new Error('API key label is required');
|
|
58
|
+
const VALID = new Set(['read', 'write']);
|
|
59
|
+
const normalised = Array.from(new Set(scopes.map((s) => s.trim()))).filter((s) => s.length > 0);
|
|
60
|
+
if (normalised.length === 0) {
|
|
61
|
+
throw new Error('at least one scope is required (read, write, or both)');
|
|
62
|
+
}
|
|
63
|
+
for (const s of normalised) {
|
|
64
|
+
if (!VALID.has(s)) {
|
|
65
|
+
throw new Error(`unknown scope "${s}"; valid: read, write`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const secretBytes = randomBytes(32);
|
|
69
|
+
const id = secretBytes.subarray(0, 4).toString('hex'); // 8-char hex id
|
|
70
|
+
const salt = randomBytes(16).toString('hex');
|
|
71
|
+
const hashedSecret = hashSecret(salt, secretBytes.toString('hex'));
|
|
72
|
+
const record = {
|
|
73
|
+
id,
|
|
74
|
+
label: label.trim(),
|
|
75
|
+
salt,
|
|
76
|
+
hashedSecret,
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
scopes: normalised,
|
|
79
|
+
};
|
|
80
|
+
this.write(record);
|
|
81
|
+
return { record, token: `${id}.${secretBytes.toString('hex')}` };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Verify a wire token. Returns the auth result on success or null
|
|
85
|
+
* on any failure (unknown id, wrong secret, revoked key). Always
|
|
86
|
+
* runs the hash compare even when the id is unknown so the timing
|
|
87
|
+
* profile doesn't leak id presence.
|
|
88
|
+
*/
|
|
89
|
+
verify(token) {
|
|
90
|
+
const dot = token.indexOf('.');
|
|
91
|
+
if (dot <= 0 || dot === token.length - 1) {
|
|
92
|
+
// Still hash a dummy to keep timing stable.
|
|
93
|
+
timingSafeCompareHex(hashSecret('00'.repeat(16), '00'.repeat(32)), hashSecret('00'.repeat(16), '00'.repeat(32)));
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const id = token.slice(0, dot);
|
|
97
|
+
const secret = token.slice(dot + 1);
|
|
98
|
+
// Validate the id shape BEFORE any filesystem use. An id that
|
|
99
|
+
// doesn't match the canonical 8-hex format could never have been
|
|
100
|
+
// generated by us, so we reject it as failed-auth without
|
|
101
|
+
// touching the disk. Defense-in-depth against path-traversal
|
|
102
|
+
// tokens like `../../etc/passwd.<anything>`.
|
|
103
|
+
if (!ID_RE.test(id)) {
|
|
104
|
+
timingSafeCompareHex(hashSecret('00'.repeat(16), secret), hashSecret('00'.repeat(16), '00'.repeat(32)));
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const record = this.read(id);
|
|
108
|
+
if (!record || record.revokedAt) {
|
|
109
|
+
// Run a dummy hash to keep the timing profile uniform.
|
|
110
|
+
timingSafeCompareHex(hashSecret('00'.repeat(16), secret), hashSecret('00'.repeat(16), '00'.repeat(32)));
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const presented = hashSecret(record.salt, secret);
|
|
114
|
+
if (!timingSafeCompareHex(presented, record.hashedSecret))
|
|
115
|
+
return null;
|
|
116
|
+
return {
|
|
117
|
+
apiKeyId: record.id,
|
|
118
|
+
label: record.label,
|
|
119
|
+
scopes: record.scopes ?? ['write'],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
list() {
|
|
123
|
+
if (!existsSync(this.dir))
|
|
124
|
+
return [];
|
|
125
|
+
const out = [];
|
|
126
|
+
for (const name of readdirSync(this.dir)) {
|
|
127
|
+
if (!name.endsWith('.key'))
|
|
128
|
+
continue;
|
|
129
|
+
try {
|
|
130
|
+
out.push(JSON.parse(readFileSync(join(this.dir, name), 'utf-8')));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* skip unreadable / malformed entries */
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Mark a key revoked. Returns the updated record, or null if no
|
|
140
|
+
* key with that id exists. Revoked records are kept on disk so
|
|
141
|
+
* audit lines reference a real id.
|
|
142
|
+
*/
|
|
143
|
+
revoke(id) {
|
|
144
|
+
const existing = this.read(id);
|
|
145
|
+
if (!existing)
|
|
146
|
+
return null;
|
|
147
|
+
if (existing.revokedAt)
|
|
148
|
+
return existing;
|
|
149
|
+
const updated = {
|
|
150
|
+
...existing,
|
|
151
|
+
revokedAt: new Date().toISOString(),
|
|
152
|
+
};
|
|
153
|
+
this.write(updated);
|
|
154
|
+
return updated;
|
|
155
|
+
}
|
|
156
|
+
/** Whether the store currently has at least one non-revoked key.
|
|
157
|
+
* Used by `daemon serve` to refuse a TCP bind that would have no
|
|
158
|
+
* way to authenticate any caller. */
|
|
159
|
+
hasActiveKey() {
|
|
160
|
+
return this.list().some((r) => !r.revokedAt);
|
|
161
|
+
}
|
|
162
|
+
/** Filesystem location — exposed for tests + CLI status output. */
|
|
163
|
+
get directory() {
|
|
164
|
+
return this.dir;
|
|
165
|
+
}
|
|
166
|
+
read(id) {
|
|
167
|
+
// Belt-and-suspenders: re-validate at the IO boundary. Callers
|
|
168
|
+
// are supposed to gate on ID_RE before getting here, but a
|
|
169
|
+
// future refactor could miss a path. Cheap to check twice.
|
|
170
|
+
if (!ID_RE.test(id))
|
|
171
|
+
return null;
|
|
172
|
+
const path = join(this.dir, `${id}.key`);
|
|
173
|
+
if (!isInside(this.dir, path))
|
|
174
|
+
return null;
|
|
175
|
+
if (!existsSync(path))
|
|
176
|
+
return null;
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
write(record) {
|
|
185
|
+
if (!ID_RE.test(record.id)) {
|
|
186
|
+
throw new Error(`refusing to write key with malformed id: ${record.id}`);
|
|
187
|
+
}
|
|
188
|
+
const path = join(this.dir, `${record.id}.key`);
|
|
189
|
+
if (!isInside(this.dir, path)) {
|
|
190
|
+
throw new Error(`refusing to write key outside ${this.dir}`);
|
|
191
|
+
}
|
|
192
|
+
writeFileSync(path, JSON.stringify(record, null, 2) + '\n', { mode: 0o600 });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* True iff `child`, resolved against the filesystem, is the same as
|
|
197
|
+
* or a descendant of `parent`. Used as a defense-in-depth check
|
|
198
|
+
* after the id regex — even if `..` somehow appeared in a path
|
|
199
|
+
* (e.g. because Node added a quirk), this would catch it before any
|
|
200
|
+
* read/write touched a file outside the api-keys directory.
|
|
201
|
+
*/
|
|
202
|
+
function isInside(parent, child) {
|
|
203
|
+
const p = resolve(parent);
|
|
204
|
+
const c = resolve(child);
|
|
205
|
+
return c === p || c.startsWith(p + sep);
|
|
206
|
+
}
|
|
207
|
+
function hashSecret(saltHex, secretHex) {
|
|
208
|
+
const h = createHash('sha256');
|
|
209
|
+
h.update(Buffer.from(saltHex, 'hex'));
|
|
210
|
+
h.update(Buffer.from(secretHex, 'hex'));
|
|
211
|
+
return h.digest('hex');
|
|
212
|
+
}
|
|
213
|
+
function timingSafeCompareHex(a, b) {
|
|
214
|
+
if (a.length !== b.length)
|
|
215
|
+
return false;
|
|
216
|
+
const ab = Buffer.from(a, 'hex');
|
|
217
|
+
const bb = Buffer.from(b, 'hex');
|
|
218
|
+
if (ab.length !== bb.length)
|
|
219
|
+
return false;
|
|
220
|
+
return timingSafeEqual(ab, bb);
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=api-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../../src/daemon/api-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type AuditDecision = 'auto-approve' | 'user-approve' | 'user-denied';
|
|
2
|
+
export interface AuditRpcEntry {
|
|
3
|
+
readonly ts: string;
|
|
4
|
+
readonly kind: 'rpc';
|
|
5
|
+
readonly wallet: string;
|
|
6
|
+
readonly network: string;
|
|
7
|
+
readonly verb: string;
|
|
8
|
+
readonly summary: string;
|
|
9
|
+
readonly details?: readonly string[];
|
|
10
|
+
readonly decision: AuditDecision;
|
|
11
|
+
readonly txHash?: string;
|
|
12
|
+
readonly contractAddress?: string;
|
|
13
|
+
readonly status?: string;
|
|
14
|
+
readonly error?: {
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly message: string;
|
|
17
|
+
};
|
|
18
|
+
/** Connection transport ('unix' | 'tcp') the RPC arrived on. */
|
|
19
|
+
readonly transport?: 'unix' | 'tcp';
|
|
20
|
+
/** API key id once the connection authenticated; absent on Unix
|
|
21
|
+
* transport without an auth handler. */
|
|
22
|
+
readonly apiKeyId?: string;
|
|
23
|
+
/** Connection-id (per-daemon-process) for correlating multiple
|
|
24
|
+
* RPCs from the same client connection. */
|
|
25
|
+
readonly connId?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface AuditLifecycleEntry {
|
|
28
|
+
readonly ts: string;
|
|
29
|
+
readonly kind: 'lifecycle';
|
|
30
|
+
readonly wallet: string;
|
|
31
|
+
readonly network: string;
|
|
32
|
+
readonly event: 'daemon-start' | 'daemon-stop' | 'socket-bound' | 'sync-complete' | 'shutdown-signal' | 'auth-success' | 'auth-failure';
|
|
33
|
+
readonly message?: string;
|
|
34
|
+
readonly apiKeyId?: string;
|
|
35
|
+
}
|
|
36
|
+
export type AuditEntry = AuditRpcEntry | AuditLifecycleEntry;
|
|
37
|
+
export interface AuditLogOptions {
|
|
38
|
+
/** Override the default `~/.moth/` directory. Used by tests. */
|
|
39
|
+
readonly dir?: string;
|
|
40
|
+
/** Override the file name. Defaults to `daemon-audit.log`. */
|
|
41
|
+
readonly filename?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare class AuditLog {
|
|
44
|
+
private readonly path;
|
|
45
|
+
constructor(opts?: AuditLogOptions);
|
|
46
|
+
/** Append one JSONL entry. Best-effort, silent on error. */
|
|
47
|
+
record(entry: AuditEntry): void;
|
|
48
|
+
/** Convenience: stamp `ts` with the current time and record an
|
|
49
|
+
* RPC entry. Split from the lifecycle variant so TypeScript can
|
|
50
|
+
* narrow on the discriminated union at the call site. */
|
|
51
|
+
recordRpc(entry: Omit<AuditRpcEntry, 'ts' | 'kind'>): void;
|
|
52
|
+
/** Same as `recordRpc` for lifecycle events. */
|
|
53
|
+
recordLifecycle(entry: Omit<AuditLifecycleEntry, 'ts' | 'kind'>): void;
|
|
54
|
+
/** Rotate the live file to `<path>.YYYY-MM-DD` when the file's
|
|
55
|
+
* most-recent mtime is on a different day than today. Called
|
|
56
|
+
* before every append; the per-write cost is one stat() syscall.
|
|
57
|
+
* Idempotent — once today's file exists, the check returns
|
|
58
|
+
* immediately. */
|
|
59
|
+
private maybeRotate;
|
|
60
|
+
/** Test helper: full path to the live log file. */
|
|
61
|
+
get filePath(): string;
|
|
62
|
+
/** Test helper: directory the log lives in. */
|
|
63
|
+
get directory(): string;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=audit-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.d.ts","sourceRoot":"","sources":["../../src/daemon/audit-log.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,cAAc,GAAG,aAAa,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAC,CAAC;IACnE,gEAAgE;IAChE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IACpC;6CACyC;IACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;gDAC4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EACV,cAAc,GACd,aAAa,GACb,cAAc,GACd,eAAe,GACf,iBAAiB,GACjB,cAAc,GACd,cAAc,CAAC;IACnB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,mBAAmB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,gEAAgE;IAChE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,GAAE,eAAoB;IAMtC,4DAA4D;IAC5D,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAS/B;;8DAE0D;IAC1D,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI;IAI1D,gDAAgD;IAChD,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,mBAAmB,EAAE,IAAI,GAAG,MAAM,CAAC,GAAG,IAAI;IAItE;;;;uBAImB;IACnB,OAAO,CAAC,WAAW;IAcnB,mDAAmD;IACnD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAED,+CAA+C;IAC/C,IAAI,SAAS,IAAI,MAAM,CAEtB;CACF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Append-only JSONL audit log for the wallet daemon. One JSON object
|
|
2
|
+
// per line. Rotated daily — when the first write of the day sees a
|
|
3
|
+
// log file last modified on an earlier day, it renames the file to
|
|
4
|
+
// `daemon-audit.log.YYYY-MM-DD` and starts a fresh one.
|
|
5
|
+
//
|
|
6
|
+
// Stage-1.5 implementation. Stage 3+ will add hash-chaining for
|
|
7
|
+
// tamper evidence (Trillian-style); for now the file is plain JSONL
|
|
8
|
+
// and trusts the host's filesystem mode (`0600`).
|
|
9
|
+
//
|
|
10
|
+
// All writes are best-effort and silent on error: the audit log
|
|
11
|
+
// must never crash the daemon. Errors during write are dropped on
|
|
12
|
+
// the floor — if they matter, you'll notice the missing entries
|
|
13
|
+
// later.
|
|
14
|
+
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
export class AuditLog {
|
|
18
|
+
path;
|
|
19
|
+
constructor(opts = {}) {
|
|
20
|
+
const dir = opts.dir ?? join(homedir(), '.moth');
|
|
21
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
22
|
+
this.path = join(dir, opts.filename ?? 'daemon-audit.log');
|
|
23
|
+
}
|
|
24
|
+
/** Append one JSONL entry. Best-effort, silent on error. */
|
|
25
|
+
record(entry) {
|
|
26
|
+
this.maybeRotate();
|
|
27
|
+
try {
|
|
28
|
+
appendFileSync(this.path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* audit must never crash the daemon */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Convenience: stamp `ts` with the current time and record an
|
|
35
|
+
* RPC entry. Split from the lifecycle variant so TypeScript can
|
|
36
|
+
* narrow on the discriminated union at the call site. */
|
|
37
|
+
recordRpc(entry) {
|
|
38
|
+
this.record({ ...entry, kind: 'rpc', ts: new Date().toISOString() });
|
|
39
|
+
}
|
|
40
|
+
/** Same as `recordRpc` for lifecycle events. */
|
|
41
|
+
recordLifecycle(entry) {
|
|
42
|
+
this.record({ ...entry, kind: 'lifecycle', ts: new Date().toISOString() });
|
|
43
|
+
}
|
|
44
|
+
/** Rotate the live file to `<path>.YYYY-MM-DD` when the file's
|
|
45
|
+
* most-recent mtime is on a different day than today. Called
|
|
46
|
+
* before every append; the per-write cost is one stat() syscall.
|
|
47
|
+
* Idempotent — once today's file exists, the check returns
|
|
48
|
+
* immediately. */
|
|
49
|
+
maybeRotate() {
|
|
50
|
+
if (!existsSync(this.path))
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
const stat = statSync(this.path);
|
|
54
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
55
|
+
const fileDay = stat.mtime.toISOString().slice(0, 10);
|
|
56
|
+
if (fileDay !== today) {
|
|
57
|
+
renameSync(this.path, `${this.path}.${fileDay}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* skip rotation on any error; the next write will retry */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Test helper: full path to the live log file. */
|
|
65
|
+
get filePath() {
|
|
66
|
+
return this.path;
|
|
67
|
+
}
|
|
68
|
+
/** Test helper: directory the log lives in. */
|
|
69
|
+
get directory() {
|
|
70
|
+
return dirname(this.path);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.js","sourceRoot":"","sources":["../../src/daemon/audit-log.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,mEAAmE;AACnE,mEAAmE;AACnE,wDAAwD;AACxD,EAAE;AACF,gEAAgE;AAChE,oEAAoE;AACpE,kDAAkD;AAClD,EAAE;AACF,gEAAgE;AAChE,kEAAkE;AAClE,gEAAgE;AAChE,SAAS;AAET,OAAO,EAAC,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AACpF,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,WAAW,CAAC;AAqDxC,MAAM,OAAO,QAAQ;IACF,IAAI,CAAS;IAE9B,YAAY,OAAwB,EAAE;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;QACjD,SAAS,CAAC,GAAG,EAAE,EAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,IAAI,kBAAkB,CAAC,CAAC;IAC7D,CAAC;IAED,4DAA4D;IAC5D,MAAM,CAAC,KAAiB;QACtB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,IAAI,CAAC;YACH,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,EAAC,IAAI,EAAE,KAAK,EAAC,CAAC,CAAC;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,uCAAuC;QACzC,CAAC;IACH,CAAC;IAED;;8DAE0D;IAC1D,SAAS,CAAC,KAAyC;QACjD,IAAI,CAAC,MAAM,CAAC,EAAC,GAAG,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAC,CAAC,CAAC;IACrE,CAAC;IAED,gDAAgD;IAChD,eAAe,CAAC,KAA+C;QAC7D,IAAI,CAAC,MAAM,CAAC,EAAC,GAAG,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAC,CAAC,CAAC;IAC3E,CAAC;IAED;;;;uBAImB;IACX,WAAW;QACjB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,OAAO;QACnC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjC,MAAM,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACpD,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtD,IAAI,OAAO,KAAK,KAAK,EAAE,CAAC;gBACtB,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,2DAA2D;QAC7D,CAAC;IACH,CAAC;IAED,mDAAmD;IACnD,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,+CAA+C;IAC/C,IAAI,SAAS;QACX,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;CACF"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DaemonBind } from './server.js';
|
|
2
|
+
export interface DaemonClient {
|
|
3
|
+
/** Issue an RPC; resolves with the daemon's result or rejects with a
|
|
4
|
+
* DaemonProtocolError carrying the daemon-reported code+message. */
|
|
5
|
+
call<R = unknown>(method: string, params?: unknown, opts?: {
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}): Promise<R>;
|
|
8
|
+
/** Close the underlying socket. In-flight calls reject with code CLOSED. */
|
|
9
|
+
close(): void;
|
|
10
|
+
readonly closed: boolean;
|
|
11
|
+
/** Daemon-reported build identifier from the version handshake. */
|
|
12
|
+
readonly daemonVersion: string;
|
|
13
|
+
/** API key id this client authenticated as. Present when a `token`
|
|
14
|
+
* was passed to `connectDaemonBind`; absent for Unix-transport
|
|
15
|
+
* clients hitting a daemon with no auth handler. */
|
|
16
|
+
readonly apiKeyId?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ConnectDaemonOptions {
|
|
19
|
+
/** Override the per-call default. Individual call() invocations can
|
|
20
|
+
* still pass their own timeoutMs. */
|
|
21
|
+
readonly defaultTimeoutMs?: number;
|
|
22
|
+
/** Optional logger hook. Defaults to silent. */
|
|
23
|
+
readonly onLog?: (level: 'info' | 'warn' | 'error', msg: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Stage-2 token in `<id>.<secret>` format. When set, the client
|
|
26
|
+
* issues an `auth` RPC after the version handshake and returns
|
|
27
|
+
* null if it fails (treating "bad token" the same as "no daemon"
|
|
28
|
+
* — see the connectDaemon docstring). Omit on Unix-transport
|
|
29
|
+
* connections to a daemon with no auth handler.
|
|
30
|
+
*/
|
|
31
|
+
readonly token?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Try to connect to the daemon at the given Unix-socket path. Stage-1
|
|
35
|
+
* entry point — kept for backwards compatibility. For TCP transports
|
|
36
|
+
* use `connectDaemonTcp(host, port)` or the unified
|
|
37
|
+
* `connectDaemonBind(bind)`.
|
|
38
|
+
*
|
|
39
|
+
* Returns:
|
|
40
|
+
* - DaemonClient on successful connect + handshake
|
|
41
|
+
* - null if the file doesn't exist, the connect is refused, or the
|
|
42
|
+
* handshake reports an incompatible protocol version
|
|
43
|
+
*
|
|
44
|
+
* Throws only on genuinely-unexpected conditions (filesystem error,
|
|
45
|
+
* malformed daemon response). Network-style failures are folded into
|
|
46
|
+
* null so the CLI can fall back to standalone without a try/catch.
|
|
47
|
+
*/
|
|
48
|
+
export declare function connectDaemon(socketPath: string, opts?: ConnectDaemonOptions): Promise<DaemonClient | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Stage-2 entry point. Connect to a daemon listening on loopback TCP
|
|
51
|
+
* (or, at stage 3, a TLS-terminating reverse proxy in front of the
|
|
52
|
+
* daemon's loopback bind). Same fall-back-to-null semantics as
|
|
53
|
+
* `connectDaemon`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function connectDaemonTcp(host: string, port: number, opts?: ConnectDaemonOptions): Promise<DaemonClient | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Transport-agnostic entry point. Both `connectDaemon` and
|
|
58
|
+
* `connectDaemonTcp` route through here. The handshake + dispatcher
|
|
59
|
+
* setup is identical across transports — only the underlying socket
|
|
60
|
+
* factory differs.
|
|
61
|
+
*/
|
|
62
|
+
export declare function connectDaemonBind(bind: DaemonBind, opts?: ConnectDaemonOptions): Promise<DaemonClient | null>;
|
|
63
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/daemon/client.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,YAAY;IAC3B;yEACqE;IACrE,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7F,4EAA4E;IAC5E,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B;;yDAEqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,oBAAoB;IACnC;0CACsC;IACtC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,gDAAgD;IAChD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAE9B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAE9B;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,EAChB,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA2D9B"}
|