@synoi/gateway-lite 0.1.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/LICENSE +195 -0
- package/README.md +185 -0
- package/bin/synoi-gateway-lite.js +22 -0
- package/dist/cdro-mirror.js +280 -0
- package/dist/daemon.js +194 -0
- package/dist/gap/cited-oracle-inputs.js +283 -0
- package/dist/gap/lite-dashboard.html +502 -0
- package/dist/gap/lite-keystore.js +351 -0
- package/dist/gap/lite-mode.js +31 -0
- package/dist/gap/lite-signing-key.js +362 -0
- package/dist/gap/local-ingest-router.js +1642 -0
- package/dist/gap/operator-enrollment.js +466 -0
- package/dist/gap/store.js +1063 -0
- package/dist/gap/types.js +15 -0
- package/dist/key-provider.js +275 -0
- package/dist/keys.js +250 -0
- package/dist/native-mldsa.js +82 -0
- package/dist/receipt-store.js +801 -0
- package/dist/shadow-mode.js +85 -0
- package/dist/state-drift/quantize.js +107 -0
- package/dist/tenant-store.js +366 -0
- package/dist/verify-router.js +1755 -0
- package/npm-shrinkwrap.json +1400 -0
- package/package.json +46 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* gap/lite-signing-key.ts — per-instance operator-owned self-sign key.
|
|
4
|
+
*
|
|
5
|
+
* ADR_014 Section 10.1 (public lite distribution scope): the gate that trips
|
|
6
|
+
* on public self-host distribution is keys, not multi-tenancy. The full
|
|
7
|
+
* gateway's receipt-signing identity is loaded via verify-router's
|
|
8
|
+
* initSigningKeys() -> KeyProvider chain (LocalEncryptedFileKeyProvider /
|
|
9
|
+
* AwsKmsKeyProvider), a managed-custody-oriented, Ed25519+ML-DSA-65 hybrid
|
|
10
|
+
* path. The lite self-host daemon has neither managed custody nor a KMS to
|
|
11
|
+
* reach, and must not ship on a shared Foundation X TEST key (a stranger
|
|
12
|
+
* cannot run on Foundation X test keys, ADR_014 Section 10.1).
|
|
13
|
+
*
|
|
14
|
+
* This module generates a single Ed25519 keypair ONCE, on the operator's own
|
|
15
|
+
* machine, so every receipt the daemon signs after first run uses the SAME
|
|
16
|
+
* key. This is the "operator-owned" tier: the private key never leaves the
|
|
17
|
+
* operator's own machine, distinct from a shared/baked-in TEST key and
|
|
18
|
+
* distinct from the gateway's KMS-hybrid managed-custody tier.
|
|
19
|
+
*
|
|
20
|
+
* KEY-AT-REST (Adversary #1 / Security F3, 2026-07-12 quality gate, founder
|
|
21
|
+
* choice "BOTH"):
|
|
22
|
+
* (a) OS credential store where available -- Windows DPAPI, macOS
|
|
23
|
+
* Keychain, Linux libsecret (gap/lite-keystore.ts). Wrapping the
|
|
24
|
+
* private key so only the same OS user/session can unwrap it, using
|
|
25
|
+
* key material the OS itself manages, not a key SynOI's own code
|
|
26
|
+
* chooses or stores.
|
|
27
|
+
* (b) A hardened file fallback when the keystore is unavailable (tool
|
|
28
|
+
* missing, headless/server install, permission denied): the SAME
|
|
29
|
+
* plaintext-JSON shape this module always used, but now with a REAL
|
|
30
|
+
* Windows ACL (icacls owner-only) rather than the POSIX `mode: 0o600`
|
|
31
|
+
* that was always a silent no-op on Windows NTFS.
|
|
32
|
+
* (c) Loud disclosure either way: daemon.ts's boot banner and
|
|
33
|
+
* packages/gateway-lite/README.md both state plainly that ANY process
|
|
34
|
+
* running as the operator's OS account can sign as the operator, that
|
|
35
|
+
* this key has no managed custody and no recovery, and (on the file
|
|
36
|
+
* fallback path specifically) exactly how it is protected per-OS.
|
|
37
|
+
*
|
|
38
|
+
* BACKWARD COMPATIBILITY: an existing lite-signing-key.json written by a
|
|
39
|
+
* pre-fix daemon (schema_version 'lite-signing-key/1', plaintext
|
|
40
|
+
* private_key_hex directly in the file) is read AS-IS, unchanged -- an
|
|
41
|
+
* operator upgrading must not silently lose their operator identity (every
|
|
42
|
+
* receipt they ever signed, and the enrollment record sealed under that
|
|
43
|
+
* key, would become unverifiable under a new key). Only a FRESH first-run
|
|
44
|
+
* key generation uses the new schema_version 'lite-signing-key/2' shape and
|
|
45
|
+
* attempts the OS keystore.
|
|
46
|
+
*
|
|
47
|
+
* Uses @synoi/gap's own Ed25519 keygen (generateReceiptKeyPair) -- no second
|
|
48
|
+
* key-generation primitive in this codebase.
|
|
49
|
+
*
|
|
50
|
+
* No em dashes. No AI attribution.
|
|
51
|
+
*/
|
|
52
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
53
|
+
if (k2 === undefined) k2 = k;
|
|
54
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
55
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
56
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
57
|
+
}
|
|
58
|
+
Object.defineProperty(o, k2, desc);
|
|
59
|
+
}) : (function(o, m, k, k2) {
|
|
60
|
+
if (k2 === undefined) k2 = k;
|
|
61
|
+
o[k2] = m[k];
|
|
62
|
+
}));
|
|
63
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
64
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
65
|
+
}) : function(o, v) {
|
|
66
|
+
o["default"] = v;
|
|
67
|
+
});
|
|
68
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
69
|
+
var ownKeys = function(o) {
|
|
70
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
71
|
+
var ar = [];
|
|
72
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
73
|
+
return ar;
|
|
74
|
+
};
|
|
75
|
+
return ownKeys(o);
|
|
76
|
+
};
|
|
77
|
+
return function (mod) {
|
|
78
|
+
if (mod && mod.__esModule) return mod;
|
|
79
|
+
var result = {};
|
|
80
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
81
|
+
__setModuleDefault(result, mod);
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
})();
|
|
85
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
86
|
+
exports.getLiteSigningKeyPath = getLiteSigningKeyPath;
|
|
87
|
+
exports._resetLiteSigningKeyCache = _resetLiteSigningKeyCache;
|
|
88
|
+
exports.loadOrCreateLiteSigningKeyAsync = loadOrCreateLiteSigningKeyAsync;
|
|
89
|
+
exports.loadOrCreateLiteSigningKey = loadOrCreateLiteSigningKey;
|
|
90
|
+
const fs = __importStar(require("node:fs"));
|
|
91
|
+
const path = __importStar(require("node:path"));
|
|
92
|
+
const os = __importStar(require("node:os"));
|
|
93
|
+
const gap_1 = require("@synoi/gap");
|
|
94
|
+
const lite_keystore_1 = require("./lite-keystore");
|
|
95
|
+
function getLiteSigningKeyPath() {
|
|
96
|
+
return path.join(process.env['SYNOI_DATA_DIR'] ?? path.join(os.homedir(), '.synoi'), 'lite-signing-key.json');
|
|
97
|
+
}
|
|
98
|
+
/** Where the Windows DPAPI-protected private-key blob lives, when that
|
|
99
|
+
* backend is used. A separate file from getLiteSigningKeyPath() so the
|
|
100
|
+
* metadata file (schema_version 'lite-signing-key/2') never itself carries
|
|
101
|
+
* private key material for the keystore-backed backends. */
|
|
102
|
+
function getDpapiBlobPath() {
|
|
103
|
+
return path.join(process.env['SYNOI_DATA_DIR'] ?? path.join(os.homedir(), '.synoi'), 'lite-signing-key.dpapi');
|
|
104
|
+
}
|
|
105
|
+
const KEYSTORE_ACCOUNT = 'lite-operator';
|
|
106
|
+
let _cached = null;
|
|
107
|
+
let _cachedResult = null;
|
|
108
|
+
/** TEST ONLY: drop the in-process cache so a test pointed at a fresh
|
|
109
|
+
* SYNOI_DATA_DIR re-reads (or re-generates) from that directory. */
|
|
110
|
+
function _resetLiteSigningKeyCache() {
|
|
111
|
+
_cached = null;
|
|
112
|
+
_cachedResult = null;
|
|
113
|
+
}
|
|
114
|
+
function hexToBytes32(hex) {
|
|
115
|
+
const buf = Buffer.from(hex, 'hex');
|
|
116
|
+
if (buf.length !== 32)
|
|
117
|
+
throw new Error(`expected 32 bytes, got ${buf.length}`);
|
|
118
|
+
return new Uint8Array(buf);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Async, keystore-aware loader. Call this ONCE at daemon boot (src/
|
|
122
|
+
* daemon.ts), before any request can arrive, and before the synchronous
|
|
123
|
+
* loadOrCreateLiteSigningKey() below is used by the (synchronous) receipt-
|
|
124
|
+
* signing / enrollment-seal call paths -- it populates the same in-process
|
|
125
|
+
* cache those synchronous paths read from.
|
|
126
|
+
*
|
|
127
|
+
* On an EXISTING v1 (pre-fix) file: loaded as-is, unchanged, backend
|
|
128
|
+
* reported as 'file-fallback' (it always was; this call does not migrate it
|
|
129
|
+
* -- migrating would mean re-writing a file whose whole point is to never
|
|
130
|
+
* silently change under the operator).
|
|
131
|
+
*
|
|
132
|
+
* On an EXISTING v2 file: dispatches by its own `backend` field.
|
|
133
|
+
*
|
|
134
|
+
* On NO file (first run): tries the OS keystore; on any failure (including
|
|
135
|
+
* "no keystore for this platform") falls back to a hardened plaintext file
|
|
136
|
+
* and returns a non-empty fallbackWarning.
|
|
137
|
+
*/
|
|
138
|
+
async function loadOrCreateLiteSigningKeyAsync() {
|
|
139
|
+
if (_cachedResult !== null)
|
|
140
|
+
return _cachedResult;
|
|
141
|
+
const metaPath = getLiteSigningKeyPath();
|
|
142
|
+
if (fs.existsSync(metaPath)) {
|
|
143
|
+
const rec = parsePersistedFile(metaPath);
|
|
144
|
+
const result = await loadFromExisting(rec);
|
|
145
|
+
_cached = result.keyPair;
|
|
146
|
+
_cachedResult = result;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
// First run.
|
|
150
|
+
const fresh = (0, gap_1.generateReceiptKeyPair)();
|
|
151
|
+
const keyId = fresh.keyId ?? ('key:' + Buffer.from(fresh.publicKey).toString('hex').slice(0, 16));
|
|
152
|
+
const publicKeyHex = Buffer.from(fresh.publicKey).toString('hex');
|
|
153
|
+
const privateKeyHex = Buffer.from(fresh.privateKey).toString('hex');
|
|
154
|
+
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
|
|
155
|
+
const keystore = (0, lite_keystore_1.createLiteKeystore)(getDpapiBlobPath());
|
|
156
|
+
let usedKeystore = false;
|
|
157
|
+
let fallbackWarning = '';
|
|
158
|
+
if (keystore !== null) {
|
|
159
|
+
try {
|
|
160
|
+
const available = await keystore.isAvailable();
|
|
161
|
+
if (available) {
|
|
162
|
+
await keystore.store(KEYSTORE_ACCOUNT, privateKeyHex);
|
|
163
|
+
usedKeystore = true;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
fallbackWarning = `OS keystore (${keystore.platform}) not available on this machine; using the hardened file fallback.`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
fallbackWarning = `OS keystore (${keystore.platform}) store failed (${err instanceof Error ? err.message : String(err)}); using the hardened file fallback.`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
fallbackWarning = `No OS keystore integration for platform "${(0, lite_keystore_1.detectKeystorePlatform)()}"; using the hardened file fallback.`;
|
|
175
|
+
}
|
|
176
|
+
if (usedKeystore) {
|
|
177
|
+
const record = {
|
|
178
|
+
schema_version: 'lite-signing-key/2',
|
|
179
|
+
backend: 'os-keystore',
|
|
180
|
+
keystore_platform: (0, lite_keystore_1.detectKeystorePlatform)(),
|
|
181
|
+
public_key_hex: publicKeyHex,
|
|
182
|
+
key_id: keyId,
|
|
183
|
+
created_at: new Date().toISOString(),
|
|
184
|
+
};
|
|
185
|
+
writeMetaFile(metaPath, record);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const record = {
|
|
189
|
+
schema_version: 'lite-signing-key/2',
|
|
190
|
+
backend: 'file-fallback',
|
|
191
|
+
public_key_hex: publicKeyHex,
|
|
192
|
+
key_id: keyId,
|
|
193
|
+
created_at: new Date().toISOString(),
|
|
194
|
+
private_key_hex: privateKeyHex,
|
|
195
|
+
};
|
|
196
|
+
// Security re-clear (2026-07-12): the ACL used to be hardened AFTER the
|
|
197
|
+
// plaintext-bearing file was already renamed into place at metaPath, so
|
|
198
|
+
// for the duration of the icacls process spawn (non-trivial: an async
|
|
199
|
+
// child-process round-trip, not a syscall) the private key sat on disk
|
|
200
|
+
// under its FINAL, discoverable name with whatever ACL it inherited from
|
|
201
|
+
// the parent directory -- POSIX mode 0600 on the writeFileSync call is a
|
|
202
|
+
// no-op on NTFS, so on Windows that inherited ACL could be broader than
|
|
203
|
+
// owner-only. Fix: write to the temp path, harden ITS ACL, THEN rename
|
|
204
|
+
// into place, so the file never appears at metaPath without the
|
|
205
|
+
// restrictive ACL already applied. On non-Windows this is a cheap no-op
|
|
206
|
+
// check (POSIX mode 0600 already did the job at write time); on Windows
|
|
207
|
+
// it closes the window entirely.
|
|
208
|
+
const tmp = writeMetaFileTmp(metaPath, record);
|
|
209
|
+
const acl = await (0, lite_keystore_1.hardenFileAcl)(tmp);
|
|
210
|
+
if (!acl.hardened) {
|
|
211
|
+
fallbackWarning += ` ADDITIONALLY: file ACL hardening failed (${acl.detail}) -- the key file may be readable by more than the operator's own account.`;
|
|
212
|
+
}
|
|
213
|
+
fs.renameSync(tmp, metaPath);
|
|
214
|
+
}
|
|
215
|
+
const keyPair = { privateKey: fresh.privateKey, publicKey: fresh.publicKey, keyId };
|
|
216
|
+
const result = {
|
|
217
|
+
keyPair,
|
|
218
|
+
backend: usedKeystore ? 'os-keystore' : 'file-fallback',
|
|
219
|
+
keystorePlatform: (0, lite_keystore_1.detectKeystorePlatform)(),
|
|
220
|
+
fallbackWarning,
|
|
221
|
+
};
|
|
222
|
+
_cached = keyPair;
|
|
223
|
+
_cachedResult = result;
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
async function loadFromExisting(rec) {
|
|
227
|
+
if (rec.schema_version === 'lite-signing-key/1') {
|
|
228
|
+
// Pre-fix file: read as-is, never migrated.
|
|
229
|
+
const privateKey = hexToBytes32(rec.private_key_hex);
|
|
230
|
+
const publicKey = hexToBytes32(rec.public_key_hex);
|
|
231
|
+
return {
|
|
232
|
+
keyPair: { privateKey, publicKey, keyId: rec.key_id },
|
|
233
|
+
backend: 'file-fallback',
|
|
234
|
+
fallbackWarning: '',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// v2.
|
|
238
|
+
if (rec.backend === 'file-fallback') {
|
|
239
|
+
if (rec.private_key_hex === undefined) {
|
|
240
|
+
throw new Error(`[lite-signing-key] v2 file-fallback record missing private_key_hex`);
|
|
241
|
+
}
|
|
242
|
+
const privateKey = hexToBytes32(rec.private_key_hex);
|
|
243
|
+
const publicKey = hexToBytes32(rec.public_key_hex);
|
|
244
|
+
return {
|
|
245
|
+
keyPair: { privateKey, publicKey, keyId: rec.key_id },
|
|
246
|
+
backend: 'file-fallback',
|
|
247
|
+
fallbackWarning: '',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// backend === 'os-keystore': retrieve the private key from the OS store.
|
|
251
|
+
const keystore = (0, lite_keystore_1.createLiteKeystore)(getDpapiBlobPath());
|
|
252
|
+
if (keystore === null) {
|
|
253
|
+
throw new Error(`[lite-signing-key] record says backend=os-keystore (platform=${String(rec.keystore_platform)}) but ` +
|
|
254
|
+
`this process's platform (${(0, lite_keystore_1.detectKeystorePlatform)()}) has no keystore integration. ` +
|
|
255
|
+
'The operator key cannot be recovered on this machine/platform.');
|
|
256
|
+
}
|
|
257
|
+
const privateKeyHex = await keystore.retrieve(KEYSTORE_ACCOUNT);
|
|
258
|
+
if (privateKeyHex === null) {
|
|
259
|
+
throw new Error(`[lite-signing-key] record says backend=os-keystore but the key could not be retrieved from ` +
|
|
260
|
+
`the ${keystore.platform} keystore. It may have been removed, or this process may be running as ` +
|
|
261
|
+
'a different OS user than the one that created it.');
|
|
262
|
+
}
|
|
263
|
+
const privateKey = hexToBytes32(privateKeyHex);
|
|
264
|
+
const publicKey = hexToBytes32(rec.public_key_hex);
|
|
265
|
+
return {
|
|
266
|
+
keyPair: { privateKey, publicKey, keyId: rec.key_id },
|
|
267
|
+
backend: 'os-keystore',
|
|
268
|
+
keystorePlatform: rec.keystore_platform,
|
|
269
|
+
fallbackWarning: '',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function parsePersistedFile(metaPath) {
|
|
273
|
+
let rec;
|
|
274
|
+
try {
|
|
275
|
+
rec = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
throw new Error(`[lite-signing-key] failed to parse ${metaPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
+
}
|
|
280
|
+
if (rec.schema_version !== 'lite-signing-key/1' && rec.schema_version !== 'lite-signing-key/2') {
|
|
281
|
+
throw new Error(`[lite-signing-key] ${metaPath}: unexpected schema_version "${String(rec.schema_version)}"`);
|
|
282
|
+
}
|
|
283
|
+
return rec;
|
|
284
|
+
}
|
|
285
|
+
/** Write `record` to metaPath's temp path and rename it into place in one
|
|
286
|
+
* step, for callers with nothing to do between write and commit (the
|
|
287
|
+
* os-keystore branch: no private key in the file, so there is no ACL
|
|
288
|
+
* window to close). Callers who DO need to act between write and commit
|
|
289
|
+
* (the file-fallback branch, which hardens the ACL first) use
|
|
290
|
+
* writeMetaFileTmp + fs.renameSync directly instead. */
|
|
291
|
+
function writeMetaFile(metaPath, record) {
|
|
292
|
+
const tmp = writeMetaFileTmp(metaPath, record);
|
|
293
|
+
fs.renameSync(tmp, metaPath);
|
|
294
|
+
}
|
|
295
|
+
/** Write `record` to metaPath's temp path ONLY; does not rename. Returns the
|
|
296
|
+
* temp path so the caller can act on it (e.g. harden its ACL) before
|
|
297
|
+
* committing it into place with fs.renameSync. */
|
|
298
|
+
function writeMetaFileTmp(metaPath, record) {
|
|
299
|
+
const tmp = metaPath + '.tmp';
|
|
300
|
+
fs.writeFileSync(tmp, JSON.stringify(record, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
301
|
+
return tmp;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* SYNCHRONOUS loader for the many call sites inside a request handler
|
|
305
|
+
* (receipt signing, enrollment sealing) that cannot await a keystore CLI
|
|
306
|
+
* round-trip mid-request. Returns the in-process cache if
|
|
307
|
+
* loadOrCreateLiteSigningKeyAsync() already ran (the normal daemon boot
|
|
308
|
+
* order: async loader first, then the server starts accepting requests).
|
|
309
|
+
*
|
|
310
|
+
* If called before the async loader has ever run (e.g. a test that only
|
|
311
|
+
* exercises this synchronous path directly, or a hypothetical future
|
|
312
|
+
* caller that skips the boot sequence), falls back to the ORIGINAL
|
|
313
|
+
* synchronous, file-only behavior: read an existing v1 OR v2 file-fallback
|
|
314
|
+
* record directly (no keystore attempt, since that would require an
|
|
315
|
+
* async call), or generate-and-persist a fresh v1-shaped plaintext file on
|
|
316
|
+
* first run. This preserves every existing synchronous test's behavior
|
|
317
|
+
* unchanged. A v2 os-keystore record encountered here (impossible in
|
|
318
|
+
* practice without having gone through the async loader first, since that
|
|
319
|
+
* is the only path that ever WRITES an os-keystore record) throws rather
|
|
320
|
+
* than silently failing to produce a key.
|
|
321
|
+
*/
|
|
322
|
+
function loadOrCreateLiteSigningKey() {
|
|
323
|
+
if (_cached !== null)
|
|
324
|
+
return _cached;
|
|
325
|
+
const p = getLiteSigningKeyPath();
|
|
326
|
+
if (fs.existsSync(p)) {
|
|
327
|
+
const rec = parsePersistedFile(p);
|
|
328
|
+
if (rec.schema_version === 'lite-signing-key/1') {
|
|
329
|
+
const privateKey = hexToBytes32(rec.private_key_hex);
|
|
330
|
+
const publicKey = hexToBytes32(rec.public_key_hex);
|
|
331
|
+
_cached = { privateKey, publicKey, keyId: rec.key_id };
|
|
332
|
+
return _cached;
|
|
333
|
+
}
|
|
334
|
+
if (rec.backend === 'file-fallback' && rec.private_key_hex !== undefined) {
|
|
335
|
+
const privateKey = hexToBytes32(rec.private_key_hex);
|
|
336
|
+
const publicKey = hexToBytes32(rec.public_key_hex);
|
|
337
|
+
_cached = { privateKey, publicKey, keyId: rec.key_id };
|
|
338
|
+
return _cached;
|
|
339
|
+
}
|
|
340
|
+
throw new Error('[lite-signing-key] existing record is backend=os-keystore, which requires the async loader ' +
|
|
341
|
+
'(loadOrCreateLiteSigningKeyAsync, called once at daemon boot) to retrieve the private key. ' +
|
|
342
|
+
'The synchronous loadOrCreateLiteSigningKey() cannot do this mid-request.');
|
|
343
|
+
}
|
|
344
|
+
// First run via the SYNCHRONOUS path only: no keystore attempt (would
|
|
345
|
+
// require awaiting a CLI round-trip). Matches the original v1 behavior
|
|
346
|
+
// exactly, for callers/tests that never invoke the async loader.
|
|
347
|
+
const fresh = (0, gap_1.generateReceiptKeyPair)();
|
|
348
|
+
const keyId = fresh.keyId ?? ('key:' + Buffer.from(fresh.publicKey).toString('hex').slice(0, 16));
|
|
349
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
350
|
+
const record = {
|
|
351
|
+
schema_version: 'lite-signing-key/1',
|
|
352
|
+
private_key_hex: Buffer.from(fresh.privateKey).toString('hex'),
|
|
353
|
+
public_key_hex: Buffer.from(fresh.publicKey).toString('hex'),
|
|
354
|
+
key_id: keyId,
|
|
355
|
+
created_at: new Date().toISOString(),
|
|
356
|
+
};
|
|
357
|
+
const tmp = p + '.tmp';
|
|
358
|
+
fs.writeFileSync(tmp, JSON.stringify(record, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
359
|
+
fs.renameSync(tmp, p);
|
|
360
|
+
_cached = { privateKey: fresh.privateKey, publicKey: fresh.publicKey, keyId };
|
|
361
|
+
return _cached;
|
|
362
|
+
}
|