@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,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
|
+
}
|