@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.
@@ -0,0 +1,280 @@
1
+ "use strict";
2
+ /**
3
+ * cdro-mirror.ts — SRAID Phases 4-6 dual-write framework.
4
+ *
5
+ * Phase 4 (decision_receipt CDROs):
6
+ * Every Decision Receipt that lands in the SQLite receipts table is
7
+ * ALSO mirrored as a CDRO into an EdgeStore-backed store. Readers stay
8
+ * pointed at the SQLite table until the parity audit clears.
9
+ *
10
+ * Phase 5 (gateway_manifest CDROs):
11
+ * The build-manifest.ts script emits the legacy dist/manifest.json AND
12
+ * a CDRO form alongside. The integrity heartbeat sends both
13
+ * manifest_sha256 (legacy) and manifest_oid (new) so the control plane
14
+ * sees the migration progress without breaking older gateways.
15
+ *
16
+ * Phase 6 (license CDROs):
17
+ * The control plane mirrors license records as CDROs at issue/rotate
18
+ * time. The CDRO mirror lives in this module's symmetric form on the
19
+ * control plane (see synoi-control/src/licensing/cdro-mirror.ts).
20
+ *
21
+ * Failures are non-fatal. The SQLite write is load-bearing; the CDRO
22
+ * mirror is best-effort. If the mirror fails, the production path keeps
23
+ * working — the gateway-side parity report flags the gap.
24
+ *
25
+ * Storage:
26
+ * - DB path: $SYNOI_DATA_DIR/cdro-mirror.db (separate from vault.db
27
+ * and receipts.db; we don't want CDRO migration to affect cache or
28
+ * receipt verification semantics).
29
+ * - Scope per object_type: `receipts`, `manifest`, `license`.
30
+ */
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.mirrorReceipt = mirrorReceipt;
36
+ exports.buildReceiptCdro = buildReceiptCdro;
37
+ exports.mirrorManifest = mirrorManifest;
38
+ exports.buildManifestCdro = buildManifestCdro;
39
+ exports.getMirroredReceipt = getMirroredReceipt;
40
+ exports.getMirroredManifest = getMirroredManifest;
41
+ exports.cdroMirrorStats = cdroMirrorStats;
42
+ exports._resetForTests = _resetForTests;
43
+ const node_fs_1 = __importDefault(require("node:fs"));
44
+ const node_os_1 = __importDefault(require("node:os"));
45
+ const node_path_1 = __importDefault(require("node:path"));
46
+ const vault_1 = require("@synoi/vault");
47
+ const DEFAULT_DIR = process.env.SYNOI_DATA_DIR ?? node_path_1.default.join(node_os_1.default.homedir(), '.synoi');
48
+ let _store = null;
49
+ let _enabled = process.env.SYNOI_CDRO_MIRROR !== '0';
50
+ const _stats = {
51
+ enabled: _enabled,
52
+ total_writes: 0,
53
+ total_failures: 0,
54
+ by_type: {},
55
+ last_error: null,
56
+ last_error_type: null,
57
+ store_path: null,
58
+ };
59
+ function lazyStore() {
60
+ if (!_enabled)
61
+ return null;
62
+ if (_store)
63
+ return _store;
64
+ try {
65
+ // openDatabase() appends 'synoi.db' to the path. Give it a dedicated
66
+ // subdir so the CDRO-mirror DB lives under
67
+ // $SYNOI_DATA_DIR/cdro-mirror/synoi.db without colliding with the
68
+ // gateway's other SQLite files.
69
+ const dir = node_path_1.default.join(DEFAULT_DIR, 'cdro-mirror');
70
+ node_fs_1.default.mkdirSync(dir, { recursive: true });
71
+ _store = (0, vault_1.openDatabase)({ path: dir, scope: 'mirror' });
72
+ _stats.store_path = node_path_1.default.join(dir, 'synoi.db');
73
+ return _store;
74
+ }
75
+ catch (err) {
76
+ _enabled = false;
77
+ _stats.enabled = false;
78
+ process.stderr.write(`[cdro-mirror] disabled: ${err.message}\n`);
79
+ return null;
80
+ }
81
+ }
82
+ function bumpStats(type, success, err) {
83
+ _stats.total_writes++;
84
+ if (!_stats.by_type[type])
85
+ _stats.by_type[type] = { writes: 0, failures: 0 };
86
+ _stats.by_type[type].writes++;
87
+ if (!success) {
88
+ _stats.total_failures++;
89
+ _stats.by_type[type].failures++;
90
+ if (err) {
91
+ _stats.last_error = err.message;
92
+ _stats.last_error_type = type;
93
+ }
94
+ }
95
+ }
96
+ function mirrorReceipt(input) {
97
+ const store = lazyStore();
98
+ if (!store)
99
+ return;
100
+ try {
101
+ const cdro = buildReceiptCdro(input);
102
+ store.putCDRO(cdro, 'receipts');
103
+ bumpStats('decision_receipt', true);
104
+ }
105
+ catch (err) {
106
+ bumpStats('decision_receipt', false, err);
107
+ process.stderr.write(`[cdro-mirror] receipt err ${input.receipt_id}: ${err.message}\n`);
108
+ }
109
+ }
110
+ function buildReceiptCdro(input) {
111
+ return (0, vault_1.buildCDRO)({
112
+ object_id: input.receipt_id,
113
+ object_type: 'decision_receipt',
114
+ schema_version: '1.0.0',
115
+ canonical_fields: [
116
+ { field_id: 1, type_tag: vault_1.FieldType.STRING, value: input.receipt_id },
117
+ { field_id: 2, type_tag: vault_1.FieldType.STRING, value: input.tenant_id },
118
+ { field_id: 3, type_tag: vault_1.FieldType.STRING, value: input.decision },
119
+ { field_id: 4, type_tag: vault_1.FieldType.STRING, value: input.action_class },
120
+ { field_id: 5, type_tag: vault_1.FieldType.STRING, value: input.risk_level },
121
+ { field_id: 6, type_tag: vault_1.FieldType.STRING, value: input.oid_hex },
122
+ { field_id: 7, type_tag: vault_1.FieldType.UINT64, value: input.recorded_at },
123
+ { field_id: 8, type_tag: vault_1.FieldType.STRING, value: input.signature },
124
+ { field_id: 9, type_tag: vault_1.FieldType.STRING, value: input.signer_key_id },
125
+ ...(input.gateway_manifest_sha256
126
+ ? [{ field_id: 10, type_tag: vault_1.FieldType.STRING, value: input.gateway_manifest_sha256 }]
127
+ : []),
128
+ // N16 Phase 2 — HITL decision identity fields (omit when absent).
129
+ // field_id 11: decided_by — identity of the approver/denier.
130
+ // field_id 12: parent_receipt_id — links this decision receipt to the
131
+ // original action receipt it gates (the chain anchor).
132
+ ...(input.decided_by
133
+ ? [{ field_id: 11, type_tag: vault_1.FieldType.STRING, value: input.decided_by }]
134
+ : []),
135
+ ...(input.parent_receipt_id
136
+ ? [{ field_id: 12, type_tag: vault_1.FieldType.STRING, value: input.parent_receipt_id }]
137
+ : []),
138
+ ],
139
+ field_policies: [
140
+ { field_id: 1, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM },
141
+ { field_id: 2, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM },
142
+ { field_id: 3, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM },
143
+ { field_id: 4, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.ATOM },
144
+ { field_id: 5, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.ATOM },
145
+ { field_id: 6, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM },
146
+ { field_id: 7, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.ATOM },
147
+ { field_id: 8, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM },
148
+ { field_id: 9, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.ATOM },
149
+ ...(input.gateway_manifest_sha256
150
+ ? [{ field_id: 10, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM }]
151
+ : []),
152
+ ...(input.decided_by
153
+ ? [{ field_id: 11, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM }]
154
+ : []),
155
+ ...(input.parent_receipt_id
156
+ ? [{ field_id: 12, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM }]
157
+ : []),
158
+ ],
159
+ provenance: {
160
+ source: 'synoi-gateway',
161
+ ingest_path: 'receipt-store.putReceipt → cdro-mirror.mirrorReceipt',
162
+ ingest_timestamp: Date.now(),
163
+ raw_hash: input.signature,
164
+ },
165
+ truth_quality: {
166
+ birth_level: 'full',
167
+ extraction_depth: 'deep',
168
+ plaintext_availability: 'available',
169
+ provenance_confidence: 1.0,
170
+ trust_level: 'authoritative',
171
+ },
172
+ });
173
+ }
174
+ function mirrorManifest(input) {
175
+ const store = lazyStore();
176
+ if (!store)
177
+ return null;
178
+ try {
179
+ const cdro = buildManifestCdro(input);
180
+ store.putCDRO(cdro, 'manifest');
181
+ bumpStats('gateway_manifest', true);
182
+ // Return the oid_hex for the heartbeat to send alongside manifest_sha256.
183
+ const stored = store.getCDROById(cdro.object_id);
184
+ return stored?.oid_hex ?? null;
185
+ }
186
+ catch (err) {
187
+ bumpStats('gateway_manifest', false, err);
188
+ process.stderr.write(`[cdro-mirror] manifest err: ${err.message}\n`);
189
+ return null;
190
+ }
191
+ }
192
+ function buildManifestCdro(input) {
193
+ // The files list is encoded as a JSON STRING because the CDRO LIST
194
+ // type requires typed-value entries (FieldType+value pairs), not raw
195
+ // objects. JSON-encoding preserves content-addressing — same files
196
+ // → same string → same canonical_hash.
197
+ const filesJson = JSON.stringify([...input.files].sort((a, b) => a.path.localeCompare(b.path)));
198
+ return (0, vault_1.buildCDRO)({
199
+ // Use the manifest_sha256 as object_id so the same manifest content
200
+ // always produces the same object_id (idempotent re-mirror on rebuild).
201
+ object_id: `manifest:${input.manifest_sha256}`,
202
+ object_type: 'gateway_manifest',
203
+ schema_version: '1.0.0',
204
+ canonical_fields: [
205
+ { field_id: 1, type_tag: vault_1.FieldType.STRING, value: input.version },
206
+ { field_id: 2, type_tag: vault_1.FieldType.UINT64, value: input.built_at },
207
+ { field_id: 3, type_tag: vault_1.FieldType.STRING, value: input.manifest_sha256 },
208
+ { field_id: 4, type_tag: vault_1.FieldType.STRING, value: filesJson },
209
+ ],
210
+ field_policies: [
211
+ { field_id: 1, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.ATOM },
212
+ { field_id: 2, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.ATOM },
213
+ { field_id: 3, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 255, bio_level: vault_1.BioLevel.ATOM },
214
+ { field_id: 4, policy: vault_1.FieldPolicyType.REQUIRED_CANONICAL, importance_weight: 200, bio_level: vault_1.BioLevel.MOLECULE },
215
+ ],
216
+ provenance: {
217
+ source: 'synoi-gateway',
218
+ ingest_path: 'build-manifest.ts → cdro-mirror.mirrorManifest',
219
+ ingest_timestamp: Date.now(),
220
+ },
221
+ truth_quality: {
222
+ birth_level: 'full',
223
+ extraction_depth: 'deep',
224
+ plaintext_availability: 'available',
225
+ provenance_confidence: 1.0,
226
+ trust_level: 'authoritative',
227
+ },
228
+ });
229
+ }
230
+ // ── Read-side helpers (for parity audits) ──────────────────────────────────
231
+ function getMirroredReceipt(receipt_id) {
232
+ const store = lazyStore();
233
+ if (!store)
234
+ return null;
235
+ const row = store.getCDROById(receipt_id);
236
+ return row?.cdro ?? null;
237
+ }
238
+ function getMirroredManifest(manifest_sha256) {
239
+ const store = lazyStore();
240
+ if (!store)
241
+ return null;
242
+ const row = store.getCDROById(`manifest:${manifest_sha256}`);
243
+ return row?.cdro ?? null;
244
+ }
245
+ // ── Stats ──────────────────────────────────────────────────────────────────
246
+ function cdroMirrorStats() {
247
+ return { ..._stats, enabled: _enabled };
248
+ }
249
+ /** For tests — reset state, close any open store. */
250
+ function _resetForTests(dir) {
251
+ if (_store) {
252
+ try {
253
+ _store.close();
254
+ }
255
+ catch { }
256
+ }
257
+ _store = null;
258
+ _enabled = process.env.SYNOI_CDRO_MIRROR !== '0';
259
+ _stats.enabled = _enabled;
260
+ _stats.total_writes = 0;
261
+ _stats.total_failures = 0;
262
+ _stats.by_type = {};
263
+ _stats.last_error = null;
264
+ _stats.last_error_type = null;
265
+ _stats.store_path = null;
266
+ if (dir) {
267
+ try {
268
+ node_fs_1.default.rmSync(node_path_1.default.join(dir, 'cdro-mirror.db'), { force: true });
269
+ }
270
+ catch { }
271
+ try {
272
+ node_fs_1.default.rmSync(node_path_1.default.join(dir, 'cdro-mirror.db-wal'), { force: true });
273
+ }
274
+ catch { }
275
+ try {
276
+ node_fs_1.default.rmSync(node_path_1.default.join(dir, 'cdro-mirror.db-shm'), { force: true });
277
+ }
278
+ catch { }
279
+ }
280
+ }
package/dist/daemon.js ADDED
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // SPDX-FileCopyrightText: 2026 SynOI Inc.
4
+ //
5
+ // Dual-licensed: AGPL-3.0-or-later by default; commercial license available
6
+ // via licensing@synoi.systems. See LICENSE + LICENSE.synoi-commercial.md.
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ var __importDefault = (this && this.__importDefault) || function (mod) {
41
+ return (mod && mod.__esModule) ? mod : { "default": mod };
42
+ };
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.startLiteDaemon = startLiteDaemon;
45
+ /**
46
+ * daemon.ts — standalone lite daemon entrypoint (ADR_014 Section 10.3 item 1).
47
+ *
48
+ * Mounts ONLY the /local/* localhost API (localIngestRouter +
49
+ * startLocalIngestSweep). Does NOT import src/index.ts (the full multi-surface
50
+ * gateway monolith) and therefore does NOT import/wire:
51
+ * - the channel registry (getChannelRegistry / registerInstagration,
52
+ * src/gap/channels/*: composio, http, mcp, llm, workflow-webhook/n8n/
53
+ * zapier, slack, sms, voice, mobile-push, home-assistant, ar-overlay) —
54
+ * the premium pricing fence, ADR_014 Section 10.2
55
+ * - the control-plane client (src/license-client.ts — SYNOI_CONTROL_URL /
56
+ * api.synoi.systems license validation)
57
+ * - KMS (src/key-provider.ts AwsKmsKeyProvider, src/gap/signing-oracle.ts
58
+ * VaultKmsSigningOracle — the managed-custody hybrid tier lite has no
59
+ * custody for)
60
+ *
61
+ * This is a SEPARATE module graph from src/index.ts: this file has its own
62
+ * `import express` and its own `app`, not index.ts's. The CI dep-graph
63
+ * assertion (test/daemon-dep-graph.test.ts) enforces the three exclusions
64
+ * above as a BUILD-time property, not a code-review-time one.
65
+ *
66
+ * Clean-room boot contract (ADR_014 Section 10.3 release gate): on a machine
67
+ * that has never seen SynOI, with SYNOI_LITE=1 and NO AWS_*, NO
68
+ * control-plane URL, NO Paddle env, and an empty ~/.synoi, this process:
69
+ * 1. generates and persists a per-instance operator-owned Ed25519 key
70
+ * (gap/lite-signing-key.ts) — first-run only; every later boot loads the
71
+ * SAME key
72
+ * 2. seals the local operator enrollment record with that SAME persisted
73
+ * key (gap/operator-enrollment.ts, Security F1 fix, 2026-07-12) — NOT
74
+ * the gateway's ephemeral SYNOI_ALLOW_EPHEMERAL_KEYS bundle, which used
75
+ * to be regenerated fresh every boot and silently un-enrolled the
76
+ * operator on restart. Lite no longer calls initSigningKeys() at all:
77
+ * it has no dependence on SYNOI_ALLOW_EPHEMERAL_KEYS, on any
78
+ * KeyProvider, or on the gateway's key history for anything.
79
+ * 3. listens on 127.0.0.1 serving /local/* only
80
+ *
81
+ * NOTE (accuracy, not a claim of full independence): SYNOI_ALLOW_EPHEMERAL_
82
+ * KEYS is not read anywhere in THIS file's boot sequence any more (steps 1-2
83
+ * above). It is, today, still read by localIngestRouter's POST /local/gate
84
+ * handler (gap/local-ingest-router.ts:1146-1157) for an UNRELATED reason: a
85
+ * bootstrap-sentinel bypass that lets the hero bundle OID 'capbundle/1' pass
86
+ * the grant-existence check without a real gap_grants row, because lite has
87
+ * no capability-grant issuance ceremony built yet. Until that ships, a lite
88
+ * operator who wants POST /local/gate to accept 'capbundle/1' still needs to
89
+ * set this env var (see packages/gateway-lite/README.md) -- but the daemon's OWN
90
+ * boot, and the enrollment record's survival across restarts, do not.
91
+ *
92
+ * One `gate()`-style POST /local/gate, one operator approve via the
93
+ * signed-assent ceremony, and the resulting receipt self-signs against the
94
+ * operator's own key (receipt_scheme=synoi.receipt/gap-selfsign) and verifies
95
+ * through the published @synoi/verify package — see
96
+ * test/local-ingest-lite-mode.test.ts for the server-side proof and the
97
+ * Builder's report for the full process-level clean-room boot transcript.
98
+ *
99
+ * No em dashes. No AI attribution.
100
+ */
101
+ const express_1 = __importDefault(require("express"));
102
+ const local_ingest_router_1 = __importStar(require("./gap/local-ingest-router"));
103
+ const lite_mode_1 = require("./gap/lite-mode");
104
+ const lite_signing_key_1 = require("./gap/lite-signing-key");
105
+ const DEFAULT_PORT = Number(process.env['SYNOI_LITE_PORT'] ?? process.env['PORT'] ?? 8787);
106
+ /**
107
+ * Boot the standalone lite daemon and return the listening HTTP server.
108
+ * Exported (not just invoked at module scope) so tests and an eventual
109
+ * `npx @synoi/gateway-lite` bin wrapper can both drive it without re-importing
110
+ * this module's require.main guard.
111
+ */
112
+ async function startLiteDaemon(port = DEFAULT_PORT) {
113
+ // Lite mode: decision receipts AND the operator-enrollment seal both
114
+ // self-sign with the per-instance operator key (@synoi/gap `receipt()`
115
+ // shape for receipts, Ed25519-only DSSE for the enrollment seal), never
116
+ // the gateway's KMS-hybrid / ephemeral-bundle tier. Set BEFORE any request
117
+ // can arrive. This single flag (gap/lite-mode.ts) governs both.
118
+ (0, lite_mode_1.setLiteMode)(true);
119
+ // Generate-or-load the persisted operator signing key before accepting any
120
+ // request, so the very first receipt (and the very first enrollment seal)
121
+ // this process ever signs uses the same key every later signature (and
122
+ // every later restart) will use. Also fails loud here, at boot, rather
123
+ // than on the first decide call, if the key file is present but corrupt
124
+ // (see lite-signing-key.ts). No initSigningKeys() call in lite mode: after
125
+ // the Security F1 fix (2026-07-12), lite has no dependence on
126
+ // SYNOI_ALLOW_EPHEMERAL_KEYS, any KeyProvider, or the gateway's key
127
+ // history for anything at all.
128
+ //
129
+ // The ASYNC loader (Security F3, 2026-07-12): tries the OS credential
130
+ // store (Windows DPAPI / macOS Keychain / Linux secret-tool) before
131
+ // falling back to a hardened plaintext file. Must run here, at boot,
132
+ // because the synchronous per-request code paths (receipt signing,
133
+ // enrollment sealing) cannot await a keystore CLI round-trip mid-request;
134
+ // this call populates the same in-process cache those paths read from.
135
+ const liteKeyResult = await (0, lite_signing_key_1.loadOrCreateLiteSigningKeyAsync)();
136
+ const liteKey = liteKeyResult.keyPair;
137
+ const app = (0, express_1.default)();
138
+ app.use('/local', local_ingest_router_1.default);
139
+ // Anything outside /local/* is not part of the lite daemon's surface —
140
+ // there is no /v1, /admin, /internal, /billing, /hitl webhook fan-in, etc.
141
+ // here, unlike the full monolith.
142
+ app.use((_req, res) => {
143
+ res.status(404).json({
144
+ error: 'not_found',
145
+ detail: 'the lite daemon serves /local/* only',
146
+ type: 'not_found',
147
+ });
148
+ });
149
+ return await new Promise((resolve) => {
150
+ const server = app.listen(port, '127.0.0.1', () => {
151
+ const actualPort = (() => {
152
+ const a = server.address();
153
+ return typeof a === 'object' && a !== null ? a.port : port;
154
+ })();
155
+ process.stdout.write(`[synoi-lite] Listening on http://127.0.0.1:${actualPort}\n`);
156
+ process.stdout.write(`[synoi-lite] Operator signing key: ${liteKey.keyId ?? '(unlabeled)'}\n`);
157
+ process.stdout.write(`[synoi-lite] Dashboard: http://127.0.0.1:${actualPort}/local/dashboard\n`);
158
+ process.stdout.write(`[synoi-lite] API base: http://127.0.0.1:${actualPort}/local\n`);
159
+ // Security F3 loud disclosure (2026-07-12 quality gate). Printed on
160
+ // EVERY boot, not just first run: an operator who has forgotten how
161
+ // this key is protected should be reminded every time, not once.
162
+ if (liteKeyResult.backend === 'os-keystore') {
163
+ process.stdout.write(`[synoi-lite] Key custody: your operator signing key is wrapped by this machine's OS ` +
164
+ `credential store (${liteKeyResult.keystorePlatform ?? 'unknown'}). Any process running as ` +
165
+ `YOUR OS user account can still ask the OS to unwrap and use it -- the keystore protects ` +
166
+ `against OTHER users/accounts on this machine, not against other processes running AS you. ` +
167
+ `There is no managed custody and no account recovery: if this machine or your OS profile is ` +
168
+ `lost, the key is gone and every receipt it signed can no longer be re-signed under a ` +
169
+ `continuous identity.\n`);
170
+ }
171
+ else {
172
+ process.stdout.write(`[synoi-lite] Key custody: your operator signing key is stored UNENCRYPTED at ` +
173
+ `${(0, lite_signing_key_1.getLiteSigningKeyPath)()} (OS keystore unavailable on this machine -- see the line above ` +
174
+ `if one printed, or packages/gateway-lite/README.md). Any process running as YOUR OS user account ` +
175
+ `can read this file and sign as the operator. ${process.platform === 'win32' ? 'This file\'s Windows ACL is restricted to your account (icacls owner-only).' : 'This file has POSIX mode 0600 (owner read/write only).'} ` +
176
+ `There is no managed custody and no account recovery: if this file is lost or this machine is ` +
177
+ `lost, the key is gone.\n`);
178
+ }
179
+ if (liteKeyResult.fallbackWarning !== '') {
180
+ process.stdout.write(`[synoi-lite] WARNING: ${liteKeyResult.fallbackWarning}\n`);
181
+ }
182
+ (0, local_ingest_router_1.startLocalIngestSweep)();
183
+ resolve(server);
184
+ });
185
+ });
186
+ }
187
+ // Run directly: `node dist/daemon.js` (built) or `tsx src/daemon.ts` / `ts-node src/daemon.ts` (dev).
188
+ // Mirrors src/index.ts's require.main guard exactly.
189
+ if (require.main === module) {
190
+ startLiteDaemon().catch(err => {
191
+ process.stderr.write(`[synoi-lite] FATAL: ${err instanceof Error ? err.stack : String(err)}\n`);
192
+ process.exit(1);
193
+ });
194
+ }