enigma-memory 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 +55 -0
- package/README.md +379 -0
- package/apps/cli/bin/enigma.mjs +1117 -0
- package/apps/desktop/src/app.js +1327 -0
- package/apps/desktop/src/index.html +1373 -0
- package/apps/desktop/src/styles.css +386 -0
- package/apps/gateway/bin/enigma-gateway.mjs +52 -0
- package/apps/gateway/src/server.mjs +1403 -0
- package/apps/native-host/README.md +265 -0
- package/apps/native-host/bin/enigma-native-host.mjs +499 -0
- package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
- package/apps/relay/bin/enigma-relay.mjs +52 -0
- package/apps/relay/src/server.mjs +1571 -0
- package/apps/verifier/bin/enigma-verify.mjs +240 -0
- package/deploy/docker-compose.production.example.yml +150 -0
- package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
- package/package.json +196 -0
- package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
- package/packages/adapters/src/index.js +183 -0
- package/packages/boundary/src/index.js +468 -0
- package/packages/connectors/src/index.js +533 -0
- package/packages/core/src/index.js +766 -0
- package/packages/enterprise/src/index.js +505 -0
- package/packages/importers/src/index.js +1083 -0
- package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
- package/packages/mcp-server/src/index.js +1185 -0
- package/packages/mesh/src/index.js +531 -0
- package/packages/metering/PACKAGE_CONTRACT.md +20 -0
- package/packages/metering/src/index.js +246 -0
- package/packages/optimizer/src/index.js +707 -0
- package/packages/passport/src/index.js +651 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
- package/packages/settlement/src/index.js +369 -0
- package/packages/storage/PACKAGE_CONTRACT.md +23 -0
- package/packages/storage/src/index.js +392 -0
- package/packages/vault/src/index.js +904 -0
- package/scripts/build-ai-orchestration-plan.mjs +248 -0
- package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
- package/scripts/build-cloudflare-token-policy.mjs +239 -0
- package/scripts/build-cloudflare-token-request.mjs +314 -0
- package/scripts/build-demo-assets.mjs +263 -0
- package/scripts/build-edge-backend-workers.mjs +353 -0
- package/scripts/build-goal-completion-audit.mjs +398 -0
- package/scripts/build-hosted-probe-worker.mjs +191 -0
- package/scripts/build-operator-acceptance-packet.mjs +254 -0
- package/scripts/build-operator-evidence-starter.mjs +525 -0
- package/scripts/build-production-backend-env-kit.mjs +384 -0
- package/scripts/build-production-dependency-report.mjs +369 -0
- package/scripts/build-production-handoff-packet.mjs +475 -0
- package/scripts/build-production-readiness-manifest.mjs +209 -0
- package/scripts/build-production-status-board.mjs +297 -0
- package/scripts/build-production-storage-migration.mjs +86 -0
- package/scripts/build-production-workplan.mjs +371 -0
- package/scripts/build-review-packet.mjs +594 -0
- package/scripts/check.mjs +419 -0
- package/scripts/cloudflare-ops.mjs +1672 -0
- package/scripts/cloudflare-secret-env.mjs +128 -0
- package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
- package/scripts/infrastructure-readiness.mjs +944 -0
- package/scripts/install-enigma-local.mjs +270 -0
- package/scripts/memory-optimization-benchmark.mjs +242 -0
- package/scripts/monitor-live-endpoints.mjs +358 -0
- package/scripts/release-audit.mjs +5588 -0
- package/scripts/release-provenance.mjs +250 -0
- package/scripts/run-backend-readiness-smoke.mjs +220 -0
- package/scripts/validate-backup-restore-drill.mjs +166 -0
- package/scripts/validate-cloudflare-credentials.mjs +112 -0
- package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
- package/scripts/validate-domain-tls.mjs +248 -0
- package/scripts/validate-hosted-backend-live.mjs +319 -0
- package/scripts/validate-incident-drill.mjs +221 -0
- package/scripts/validate-kms-custody.mjs +235 -0
- package/scripts/validate-legal-compliance-approval.mjs +274 -0
- package/scripts/validate-monitoring-alerting.mjs +263 -0
- package/scripts/validate-network-access-policy.mjs +285 -0
- package/scripts/validate-operator-acceptance.mjs +298 -0
- package/scripts/validate-production-manifests.mjs +325 -0
- package/scripts/validate-public-site-security.mjs +252 -0
- package/scripts/validate-security-threat-model.mjs +285 -0
- package/scripts/validate-service-settlement.mjs +256 -0
- package/scripts/validate-support-sla.mjs +190 -0
- package/scripts/validate-tenant-policy-approval.mjs +228 -0
- package/scripts/validate-usage-metering.mjs +224 -0
- package/scripts/validate-whitepaper-claims.mjs +166 -0
- package/specs/boundary-manifest-v1.schema.json +35 -0
- package/specs/capsule-v1.schema.json +55 -0
- package/specs/claim-boundary-manifest-v1.schema.json +22 -0
- package/specs/connector-profile-v1.schema.json +62 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -0
- package/specs/enterprise-policy-v1.schema.json +59 -0
- package/specs/gateway-request-v1.schema.json +32 -0
- package/specs/goal-completion-audit-v1.schema.json +70 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
- package/specs/import-report-v1.schema.json +214 -0
- package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
- package/specs/memory-event-v1.schema.json +42 -0
- package/specs/mesh-node-v1.schema.json +126 -0
- package/specs/passport-v1.schema.json +50 -0
- package/specs/production-handoff-packet-v1.schema.json +149 -0
- package/specs/receipt-v1.schema.json +61 -0
- package/specs/relay-record-v1.schema.json +43 -0
- package/specs/state-checkpoint-v1.schema.json +37 -0
- package/specs/trust-bundle-v1.schema.json +56 -0
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
EMPTY_MERKLE_ROOT,
|
|
8
|
+
canonicalize,
|
|
9
|
+
generateSigningKeyPair,
|
|
10
|
+
sha256Hex,
|
|
11
|
+
signPayload,
|
|
12
|
+
verifySignature
|
|
13
|
+
} from '../../../packages/core/src/index.js';
|
|
14
|
+
import {
|
|
15
|
+
createMeshNode,
|
|
16
|
+
createRelayStore,
|
|
17
|
+
createWitnessCheckpoint,
|
|
18
|
+
pushRelayRecord,
|
|
19
|
+
pullRelayRecord,
|
|
20
|
+
verifyWitnessCheckpoint
|
|
21
|
+
} from '../../../packages/mesh/src/index.js';
|
|
22
|
+
import {
|
|
23
|
+
normalizeDependencyEvidence,
|
|
24
|
+
readinessEvidenceRefs,
|
|
25
|
+
} from '../../../packages/adapters/src/index.js';
|
|
26
|
+
|
|
27
|
+
const SERVICE_SCHEMA = 'enigma.relay_service.v1';
|
|
28
|
+
const PAIRING_CHALLENGE_SCHEMA = 'enigma.pairing_challenge.v1';
|
|
29
|
+
const PAIRING_COMPLETION_SCHEMA = 'enigma.pairing_completion.v1';
|
|
30
|
+
const CLIENT_AUTH_SCHEMA = 'enigma.relay_client_authorization.v1';
|
|
31
|
+
const RELAY_STATE_SCHEMA = 'enigma.relay_state.v1';
|
|
32
|
+
const RELAY_STATE_VERSION = 1;
|
|
33
|
+
const DEFAULT_NOW = '1970-01-01T00:00:00.000Z';
|
|
34
|
+
const DEFAULT_CHALLENGE_TTL_MS = 5 * 60 * 1000;
|
|
35
|
+
const MAX_JSON_BODY_BYTES = 64 * 1024;
|
|
36
|
+
const ROOT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
37
|
+
const SIGNATURE_ALG = 'Ed25519';
|
|
38
|
+
const RELAY_AUTH_MODES = new Set(['paired_client_signature_required', 'unauthenticated_local_demo']);
|
|
39
|
+
const LOCAL_READINESS_MODES = new Set(['local', 'demo', 'development', 'test']);
|
|
40
|
+
const PRODUCTION_READINESS_MODES = new Set(['production', 'hosted', 'byoc', 'customer_byoc', 'on_prem', 'onprem']);
|
|
41
|
+
const SHARED_PRODUCTION_DEPENDENCY_ALIASES = Object.freeze({
|
|
42
|
+
network_access_policy: {
|
|
43
|
+
options: ['network_access_policy', 'networkAccessPolicy', 'network_policy', 'networkPolicy', 'network_access_policy_ref', 'networkAccessPolicyRef'],
|
|
44
|
+
env: ['ENIGMA_NETWORK_ACCESS_POLICY_REF', 'ENIGMA_NETWORK_POLICY_REF'],
|
|
45
|
+
},
|
|
46
|
+
kms_custody: {
|
|
47
|
+
options: ['kms_custody', 'kmsCustody', 'key_custody', 'keyCustody', 'kms_custody_ref', 'kmsCustodyRef'],
|
|
48
|
+
env: ['ENIGMA_KMS_CUSTODY_REF', 'ENIGMA_KEY_CUSTODY_REF'],
|
|
49
|
+
},
|
|
50
|
+
tenant_policy_approval: {
|
|
51
|
+
options: ['tenant_policy_approval', 'tenantPolicyApproval', 'tenant_policy', 'tenantPolicy', 'tenant_policy_approval_ref', 'tenantPolicyApprovalRef'],
|
|
52
|
+
env: ['ENIGMA_TENANT_POLICY_APPROVAL_REF', 'ENIGMA_TENANT_POLICY_REF'],
|
|
53
|
+
},
|
|
54
|
+
usage_metering: {
|
|
55
|
+
options: ['usage_metering', 'usageMetering', 'metering', 'metering_ref', 'meteringRef', 'usage_metering_ref', 'usageMeteringRef'],
|
|
56
|
+
env: ['ENIGMA_USAGE_METERING_REF', 'ENIGMA_METERING_REF'],
|
|
57
|
+
},
|
|
58
|
+
service_settlement: {
|
|
59
|
+
options: ['service_settlement', 'serviceSettlement', 'settlement', 'settlement_ref', 'settlementRef', 'service_settlement_ref', 'serviceSettlementRef'],
|
|
60
|
+
env: ['ENIGMA_SERVICE_SETTLEMENT_REF', 'ENIGMA_SETTLEMENT_REF'],
|
|
61
|
+
},
|
|
62
|
+
monitoring_alerting: {
|
|
63
|
+
options: ['monitoring_alerting', 'monitoringAlerting', 'monitoring_alerting_ref', 'monitoringAlertingRef'],
|
|
64
|
+
env: ['ENIGMA_MONITORING_ALERTING_REF'],
|
|
65
|
+
},
|
|
66
|
+
public_site_security: {
|
|
67
|
+
options: ['public_site_security', 'publicSiteSecurity', 'public_site_security_ref', 'publicSiteSecurityRef'],
|
|
68
|
+
env: ['ENIGMA_PUBLIC_SITE_SECURITY_REF', 'ENIGMA_SITE_SECURITY_REF'],
|
|
69
|
+
},
|
|
70
|
+
security_threat_model: {
|
|
71
|
+
options: ['security_threat_model', 'securityThreatModel', 'threat_model', 'threatModel', 'security_threat_model_ref', 'securityThreatModelRef'],
|
|
72
|
+
env: ['ENIGMA_SECURITY_THREAT_MODEL_REF', 'ENIGMA_THREAT_MODEL_REF'],
|
|
73
|
+
},
|
|
74
|
+
legal_compliance_approval: {
|
|
75
|
+
options: ['legal_compliance_approval', 'legalComplianceApproval', 'legal_compliance', 'legalCompliance', 'legal_compliance_ref', 'legalComplianceRef'],
|
|
76
|
+
env: ['ENIGMA_LEGAL_COMPLIANCE_REF', 'ENIGMA_LEGAL_COMPLIANCE_APPROVAL_REF'],
|
|
77
|
+
},
|
|
78
|
+
support_sla: {
|
|
79
|
+
options: ['support_sla', 'supportSla', 'sla', 'sla_ref', 'slaRef', 'support_sla_ref', 'supportSlaRef'],
|
|
80
|
+
env: ['ENIGMA_SUPPORT_SLA_REF', 'ENIGMA_SLA_REF'],
|
|
81
|
+
},
|
|
82
|
+
incident_drill: {
|
|
83
|
+
options: ['incident_drill', 'incidentDrill', 'incident_drill_ref', 'incidentDrillRef'],
|
|
84
|
+
env: ['ENIGMA_INCIDENT_DRILL_REF', 'ENIGMA_INCIDENT_RESPONSE_DRILL_REF'],
|
|
85
|
+
},
|
|
86
|
+
backup_restore_drill: {
|
|
87
|
+
options: ['backup_restore_drill', 'backupRestoreDrill', 'backup_restore_drill_ref', 'backupRestoreDrillRef'],
|
|
88
|
+
env: ['ENIGMA_BACKUP_RESTORE_DRILL_REF', 'ENIGMA_RESTORE_DRILL_REF'],
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const SHARED_PRODUCTION_DEPENDENCY_KEYS = Object.freeze(Object.keys(SHARED_PRODUCTION_DEPENDENCY_ALIASES));
|
|
92
|
+
const LOCAL_STORAGE_KINDS = new Set(['memory', 'in_memory', 'in-memory', 'local', 'file', 'state_file', 'state-file', 'json']);
|
|
93
|
+
const DURABLE_STORAGE_KINDS = new Set(['postgres', 'postgresql', 'mysql', 'mariadb', 'database', 'dynamodb', 'r2', 's3', 'gcs', 'azure_blob', 'blob', 'kv', 'foundationdb']);
|
|
94
|
+
const FORBIDDEN_PLAINTEXT_KEYS = new Set([
|
|
95
|
+
'plaintext',
|
|
96
|
+
'plaintextmemory',
|
|
97
|
+
'plainmemory',
|
|
98
|
+
'cleartext',
|
|
99
|
+
'rawmemory',
|
|
100
|
+
'memoryplaintext',
|
|
101
|
+
'memory',
|
|
102
|
+
'memories',
|
|
103
|
+
'text',
|
|
104
|
+
'content',
|
|
105
|
+
'body',
|
|
106
|
+
'rawbody',
|
|
107
|
+
'requestbody',
|
|
108
|
+
'rawrequestbody',
|
|
109
|
+
'prompt',
|
|
110
|
+
'prompttext',
|
|
111
|
+
'completion',
|
|
112
|
+
'message',
|
|
113
|
+
'messagetext',
|
|
114
|
+
'conversation',
|
|
115
|
+
'conversationtext',
|
|
116
|
+
'transcript',
|
|
117
|
+
'transcripttext'
|
|
118
|
+
]);
|
|
119
|
+
const RELAY_RECORD_KEYS = new Set([
|
|
120
|
+
'record_id',
|
|
121
|
+
'recordId',
|
|
122
|
+
'capsule_id',
|
|
123
|
+
'capsuleId',
|
|
124
|
+
'encrypted_payload_hash',
|
|
125
|
+
'encryptedPayloadHash',
|
|
126
|
+
'payload_hash',
|
|
127
|
+
'payloadHash',
|
|
128
|
+
'opaque_encrypted_record',
|
|
129
|
+
'opaqueEncryptedRecord',
|
|
130
|
+
'encrypted_payload',
|
|
131
|
+
'encryptedPayload',
|
|
132
|
+
'ciphertext',
|
|
133
|
+
'received_at',
|
|
134
|
+
'receivedAt',
|
|
135
|
+
'expires_at',
|
|
136
|
+
'expiresAt'
|
|
137
|
+
]);
|
|
138
|
+
const WITNESS_CHECKPOINT_KEYS = new Set([
|
|
139
|
+
'subject_node_id',
|
|
140
|
+
'subjectNodeId',
|
|
141
|
+
'subject',
|
|
142
|
+
'epoch',
|
|
143
|
+
'checkpoint_root',
|
|
144
|
+
'checkpointRoot',
|
|
145
|
+
'receipt_log_root',
|
|
146
|
+
'receiptLogRoot',
|
|
147
|
+
'active_set_root',
|
|
148
|
+
'activeSetRoot',
|
|
149
|
+
'active_memory_root',
|
|
150
|
+
'activeMemoryRoot',
|
|
151
|
+
'previous_witness_hash',
|
|
152
|
+
'previousWitnessHash',
|
|
153
|
+
'issued_at',
|
|
154
|
+
'issuedAt'
|
|
155
|
+
]);
|
|
156
|
+
const PAIRING_CHALLENGE_KEYS = new Set([
|
|
157
|
+
'public_key',
|
|
158
|
+
'publicKey',
|
|
159
|
+
'client_public_key',
|
|
160
|
+
'clientPublicKey',
|
|
161
|
+
'device_public_key',
|
|
162
|
+
'devicePublicKey',
|
|
163
|
+
'account_public_key',
|
|
164
|
+
'accountPublicKey'
|
|
165
|
+
]);
|
|
166
|
+
const PAIRING_COMPLETE_KEYS = new Set([
|
|
167
|
+
'challenge_id',
|
|
168
|
+
'challengeId',
|
|
169
|
+
'public_key',
|
|
170
|
+
'publicKey',
|
|
171
|
+
'client_public_key',
|
|
172
|
+
'clientPublicKey',
|
|
173
|
+
'signature',
|
|
174
|
+
'client_signature',
|
|
175
|
+
'clientSignature'
|
|
176
|
+
]);
|
|
177
|
+
const RELAY_STATE_KEYS = new Set([
|
|
178
|
+
'schema',
|
|
179
|
+
'version',
|
|
180
|
+
'generated_at',
|
|
181
|
+
'node',
|
|
182
|
+
'relay_store',
|
|
183
|
+
'relay_records',
|
|
184
|
+
'witness_log',
|
|
185
|
+
'completed_pairings',
|
|
186
|
+
'authorization'
|
|
187
|
+
]);
|
|
188
|
+
const RELAY_STATE_NODE_KEYS = new Set([
|
|
189
|
+
'schema',
|
|
190
|
+
'node_id',
|
|
191
|
+
'signer',
|
|
192
|
+
'publicKey',
|
|
193
|
+
'privateKey',
|
|
194
|
+
'trust_descriptor',
|
|
195
|
+
'public_descriptor'
|
|
196
|
+
]);
|
|
197
|
+
const RELAY_STATE_STORE_KEYS = new Set(['schema', 'store_id', 'node_id', 'created_at', 'metadata']);
|
|
198
|
+
const RELAY_STATE_STORE_METADATA_KEYS = new Set(['record_count']);
|
|
199
|
+
const RELAY_STATE_AUTHORIZATION_KEYS = new Set(['mode', 'allow_unauthenticated']);
|
|
200
|
+
const RELAY_STATE_RECORD_KEYS = new Set([
|
|
201
|
+
'schema',
|
|
202
|
+
'record_id',
|
|
203
|
+
'store_id',
|
|
204
|
+
'capsule_id',
|
|
205
|
+
'encrypted_payload_hash',
|
|
206
|
+
'payload_storage',
|
|
207
|
+
'received_at',
|
|
208
|
+
'expires_at',
|
|
209
|
+
'signature'
|
|
210
|
+
]);
|
|
211
|
+
const RELAY_STATE_PAIRING_KEYS = new Set([
|
|
212
|
+
'schema',
|
|
213
|
+
'pairing_id',
|
|
214
|
+
'challenge_id',
|
|
215
|
+
'relay_node_id',
|
|
216
|
+
'client_public_key',
|
|
217
|
+
'client_public_key_hash',
|
|
218
|
+
'issued_at',
|
|
219
|
+
'signature'
|
|
220
|
+
]);
|
|
221
|
+
const SIGNATURE_KEYS = new Set(['alg', 'key_id', 'value']);
|
|
222
|
+
|
|
223
|
+
function isPlainRecord(value) {
|
|
224
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeKeyName(key) {
|
|
228
|
+
return String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isForbiddenPlaintextKey(key) {
|
|
232
|
+
return FORBIDDEN_PLAINTEXT_KEYS.has(normalizeKeyName(key));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function hasPlaintextLookingField(value, seen = new WeakSet()) {
|
|
236
|
+
if (value === null || typeof value !== 'object') return false;
|
|
237
|
+
if (seen.has(value)) return false;
|
|
238
|
+
seen.add(value);
|
|
239
|
+
if (Array.isArray(value)) return value.some((item) => hasPlaintextLookingField(item, seen));
|
|
240
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
241
|
+
if (isForbiddenPlaintextKey(key) && nested !== undefined && nested !== null && nested !== '') return true;
|
|
242
|
+
if (hasPlaintextLookingField(nested, seen)) return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function assertNoPlaintextLookingFields(value, context) {
|
|
248
|
+
if (hasPlaintextLookingField(value)) throw new Error(`${context} contains plaintext-looking memory fields`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function assertPlainRecord(value, context) {
|
|
252
|
+
if (!isPlainRecord(value)) throw new TypeError(`${context} must be a JSON object`);
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function assertOnlyKeys(value, allowed, context) {
|
|
257
|
+
for (const key of Object.keys(value)) {
|
|
258
|
+
if (!allowed.has(key)) throw new Error(`${context} field ${key} is not allowed`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function assertRoot(value, field) {
|
|
263
|
+
if (typeof value !== 'string' || !ROOT_RE.test(value)) throw new TypeError(`${field} must be a sha256 root`);
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function assertString(value, field) {
|
|
268
|
+
if (typeof value !== 'string' || value.length === 0) throw new TypeError(`${field} must be a non-empty string`);
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function assertIsoDate(value, field) {
|
|
273
|
+
const string = assertString(value, field);
|
|
274
|
+
if (Number.isNaN(Date.parse(string))) throw new TypeError(`${field} must be an ISO date string`);
|
|
275
|
+
return string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function signaturePayload(record) {
|
|
279
|
+
const { signature, descriptor_hash, capsule_hash, witness_hash, grant_hash, relay_record_id, ...unsigned } = record;
|
|
280
|
+
return unsigned;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function cloneJson(value, context) {
|
|
284
|
+
try {
|
|
285
|
+
return JSON.parse(JSON.stringify(value));
|
|
286
|
+
} catch {
|
|
287
|
+
throw new TypeError(`${context} must be JSON serializable`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function assertSignature(signature, context) {
|
|
292
|
+
const record = assertPlainRecord(signature, `${context} signature`);
|
|
293
|
+
assertOnlyKeys(record, SIGNATURE_KEYS, `${context} signature`);
|
|
294
|
+
if (record.alg !== SIGNATURE_ALG) throw new Error(`${context} signature algorithm is not supported`);
|
|
295
|
+
assertString(record.key_id, `${context} signature key_id`);
|
|
296
|
+
assertString(record.value, `${context} signature value`);
|
|
297
|
+
return record;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function assertSignedBy(record, publicKey, context) {
|
|
301
|
+
assertSignature(record.signature, context);
|
|
302
|
+
if (!verifySignature({ payload: signaturePayload(record), signature: record.signature, publicKey })) {
|
|
303
|
+
throw new Error(`${context} signature mismatch`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function signedRecordHash(record, hashField) {
|
|
308
|
+
const { [hashField]: _ignored, ...signed } = record;
|
|
309
|
+
return hashValue(signed);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function nowIso(state) {
|
|
313
|
+
if (typeof state.now === 'function') return state.now();
|
|
314
|
+
if (typeof state.now === 'string') return state.now;
|
|
315
|
+
return new Date().toISOString();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function futureIso(baseIso, ttlMs) {
|
|
319
|
+
const baseMs = Date.parse(baseIso);
|
|
320
|
+
const start = Number.isFinite(baseMs) ? baseMs : Date.now();
|
|
321
|
+
return new Date(start + ttlMs).toISOString();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function jsonResponse(res, statusCode, payload) {
|
|
325
|
+
const body = JSON.stringify(payload);
|
|
326
|
+
res.writeHead(statusCode, {
|
|
327
|
+
'content-type': 'application/json; charset=utf-8',
|
|
328
|
+
'cache-control': 'no-store',
|
|
329
|
+
'content-length': Buffer.byteLength(body)
|
|
330
|
+
});
|
|
331
|
+
res.end(body);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function ok(payload = {}) {
|
|
335
|
+
return { ok: true, ...payload };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function fail(message) {
|
|
339
|
+
return { ok: false, error: message };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function firstDefined(...values) {
|
|
343
|
+
return values.find((value) => value !== undefined);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function nonEmptyString(value) {
|
|
347
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function normalizedBoolean(value) {
|
|
351
|
+
if (typeof value === 'boolean') return value;
|
|
352
|
+
if (!nonEmptyString(value)) return undefined;
|
|
353
|
+
switch (value.trim().toLowerCase()) {
|
|
354
|
+
case '1':
|
|
355
|
+
case 'true':
|
|
356
|
+
case 'yes':
|
|
357
|
+
case 'y':
|
|
358
|
+
case 'on':
|
|
359
|
+
case 'enabled':
|
|
360
|
+
case 'required':
|
|
361
|
+
case 'require':
|
|
362
|
+
return true;
|
|
363
|
+
case '0':
|
|
364
|
+
case 'false':
|
|
365
|
+
case 'no':
|
|
366
|
+
case 'n':
|
|
367
|
+
case 'off':
|
|
368
|
+
case 'disabled':
|
|
369
|
+
return false;
|
|
370
|
+
default:
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function envValue(...keys) {
|
|
376
|
+
const env = globalThis.process?.env;
|
|
377
|
+
if (!isPlainRecord(env)) return undefined;
|
|
378
|
+
for (const key of keys) {
|
|
379
|
+
const value = env[key];
|
|
380
|
+
if (nonEmptyString(value)) return value;
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function envBoolean(...keys) {
|
|
386
|
+
const env = globalThis.process?.env;
|
|
387
|
+
if (!isPlainRecord(env)) return undefined;
|
|
388
|
+
for (const key of keys) {
|
|
389
|
+
const value = normalizedBoolean(env[key]);
|
|
390
|
+
if (value !== undefined) return value;
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function envProductionModeFallback() {
|
|
396
|
+
const nodeEnv = envValue('NODE_ENV');
|
|
397
|
+
return nonEmptyString(nodeEnv) && nodeEnv.trim().toLowerCase() === 'production' ? 'production' : undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function readinessFailClosedFromOptions(options) {
|
|
401
|
+
return normalizedBoolean(firstDefined(
|
|
402
|
+
options.readinessFailClosed,
|
|
403
|
+
options.readiness_fail_closed,
|
|
404
|
+
options.failClosed,
|
|
405
|
+
options.fail_closed,
|
|
406
|
+
envBoolean('ENIGMA_READINESS_FAIL_CLOSED', 'ENIGMA_DISABLE_LOCAL_DEMO_FALLBACK')
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function readinessModeFromOptions(options) {
|
|
411
|
+
return firstDefined(
|
|
412
|
+
options.mode,
|
|
413
|
+
options.environment,
|
|
414
|
+
options.readinessMode,
|
|
415
|
+
options.readiness_mode,
|
|
416
|
+
envValue('ENIGMA_BACKEND_MODE'),
|
|
417
|
+
readinessFailClosedFromOptions(options) === true ? 'production' : undefined,
|
|
418
|
+
envProductionModeFallback()
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function productionLikeFromOptions(options) {
|
|
423
|
+
return options.production === true
|
|
424
|
+
|| options.productionLike === true
|
|
425
|
+
|| options.production_like === true
|
|
426
|
+
|| options.hosted === true
|
|
427
|
+
|| options.byoc === true
|
|
428
|
+
|| readinessFailClosedFromOptions(options) === true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function optionValue(record, ...keys) {
|
|
432
|
+
if (!isPlainRecord(record)) return undefined;
|
|
433
|
+
for (const key of keys) {
|
|
434
|
+
if (Object.prototype.hasOwnProperty.call(record, key)) return record[key];
|
|
435
|
+
}
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function readinessRecord(state) {
|
|
440
|
+
const readiness = optionValue(state, 'production_readiness', 'productionReadiness', 'readiness');
|
|
441
|
+
return isPlainRecord(readiness) ? readiness : {};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function readinessValue(state, ...keys) {
|
|
445
|
+
const direct = optionValue(state, ...keys);
|
|
446
|
+
if (direct !== undefined) return direct;
|
|
447
|
+
return optionValue(readinessRecord(state), ...keys);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sharedProductionDependencyRefsFromOptions(options) {
|
|
451
|
+
const refs = {};
|
|
452
|
+
for (const key of SHARED_PRODUCTION_DEPENDENCY_KEYS) {
|
|
453
|
+
const aliases = SHARED_PRODUCTION_DEPENDENCY_ALIASES[key];
|
|
454
|
+
refs[key] = firstDefined(
|
|
455
|
+
...aliases.options.map((name) => options[name]),
|
|
456
|
+
envValue(...aliases.env)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return refs;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function productionDependencyValue(state, key) {
|
|
463
|
+
const aliases = SHARED_PRODUCTION_DEPENDENCY_ALIASES[key];
|
|
464
|
+
return readinessValue(state, key, ...aliases.options);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function productionDependencyConfigured(state, key) {
|
|
468
|
+
return hasConfiguredReference(productionDependencyValue(state, key));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function normalizedReadinessMode(state) {
|
|
472
|
+
const mode = readinessValue(state, 'mode', 'environment', 'readinessMode', 'readiness_mode');
|
|
473
|
+
return nonEmptyString(mode) ? mode.trim().toLowerCase() : 'local';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isProductionLikeState(state) {
|
|
477
|
+
const mode = normalizedReadinessMode(state);
|
|
478
|
+
return PRODUCTION_READINESS_MODES.has(mode)
|
|
479
|
+
|| readinessValue(state, 'production', 'production_like', 'productionLike', 'hosted', 'byoc') === true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function isLocalReadinessMode(state) {
|
|
483
|
+
return LOCAL_READINESS_MODES.has(normalizedReadinessMode(state));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function readinessKind(value) {
|
|
487
|
+
if (nonEmptyString(value)) return value.trim().toLowerCase();
|
|
488
|
+
if (!isPlainRecord(value)) return undefined;
|
|
489
|
+
const kind = firstDefined(value.kind, value.type, value.backend, value.driver, value.provider);
|
|
490
|
+
return nonEmptyString(kind) ? kind.trim().toLowerCase() : undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function hasConfiguredReference(value) {
|
|
494
|
+
if (value === true) return true;
|
|
495
|
+
if (nonEmptyString(value)) return true;
|
|
496
|
+
if (!isPlainRecord(value)) return false;
|
|
497
|
+
if (value.configured === true || value.enabled === true || value.ready === true) return true;
|
|
498
|
+
return ['ref', 'reference', 'id', 'name', 'uri', 'url', 'arn', 'path', 'target', 'sink', 'key_ref', 'keyRef', 'resource'].some((key) => nonEmptyString(value[key]));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function hasDurableStorage(value) {
|
|
502
|
+
if (value === true) return true;
|
|
503
|
+
const kind = readinessKind(value);
|
|
504
|
+
if (kind !== undefined) {
|
|
505
|
+
if (LOCAL_STORAGE_KINDS.has(kind)) return false;
|
|
506
|
+
if (DURABLE_STORAGE_KINDS.has(kind)) return true;
|
|
507
|
+
}
|
|
508
|
+
if (nonEmptyString(value)) return !LOCAL_STORAGE_KINDS.has(value.trim().toLowerCase());
|
|
509
|
+
if (!isPlainRecord(value)) return false;
|
|
510
|
+
if (value.durable === true || value.production === true) return true;
|
|
511
|
+
if (value.in_memory === true || value.inMemory === true || value.local === true || value.demo === true) return false;
|
|
512
|
+
return hasConfiguredReference(value);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function operatorAcceptanceIsGo(value) {
|
|
516
|
+
if (nonEmptyString(value)) return value.trim().toLowerCase() === 'go';
|
|
517
|
+
if (!isPlainRecord(value)) return false;
|
|
518
|
+
return operatorAcceptanceIsGo(firstDefined(value.status, value.decision, value.state, value.operator_acceptance, value.operatorAcceptance));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function unauthenticatedLocalDemoEnabled(state) {
|
|
522
|
+
return state.allowUnauthenticated === true
|
|
523
|
+
|| state.allow_unauthenticated === true
|
|
524
|
+
|| readinessValue(state, 'unauthenticated_local_demo', 'unauthenticatedLocalDemo') === true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function relayProductionReadinessFromOptions(options) {
|
|
528
|
+
return {
|
|
529
|
+
mode: readinessModeFromOptions(options),
|
|
530
|
+
production_like: productionLikeFromOptions(options),
|
|
531
|
+
backend_host: firstDefined(
|
|
532
|
+
options.backend_host,
|
|
533
|
+
options.backendHost,
|
|
534
|
+
options.backend_host_ref,
|
|
535
|
+
options.backendHostRef,
|
|
536
|
+
options.host_ref,
|
|
537
|
+
options.hostRef,
|
|
538
|
+
options.deployment_ref,
|
|
539
|
+
options.deploymentRef,
|
|
540
|
+
envValue('ENIGMA_BACKEND_HOST_REF', 'ENIGMA_RELAY_BACKEND_HOST_REF', 'ENIGMA_RELAY_DEPLOYMENT_REF')
|
|
541
|
+
),
|
|
542
|
+
dns_tls: firstDefined(
|
|
543
|
+
options.dns_tls,
|
|
544
|
+
options.dnsTls,
|
|
545
|
+
options.dns_tls_ref,
|
|
546
|
+
options.dnsTlsRef,
|
|
547
|
+
options.tls_ref,
|
|
548
|
+
options.tlsRef,
|
|
549
|
+
envValue('ENIGMA_DNS_TLS_REF', 'ENIGMA_RELAY_DNS_TLS_REF', 'ENIGMA_TLS_REF')
|
|
550
|
+
),
|
|
551
|
+
durable_storage: firstDefined(
|
|
552
|
+
options.durable_storage,
|
|
553
|
+
options.durableStorage,
|
|
554
|
+
options.storage,
|
|
555
|
+
options.storage_ref,
|
|
556
|
+
options.storageRef,
|
|
557
|
+
envValue('ENIGMA_EXTERNAL_STORAGE_DSN', 'ENIGMA_EXTERNAL_STORAGE_DSN_FILE', 'ENIGMA_EXTERNAL_STORAGE_REF', 'ENIGMA_RELAY_STORAGE_REF', 'ENIGMA_DURABLE_STORAGE_REF')
|
|
558
|
+
),
|
|
559
|
+
kms_or_secret_custody: firstDefined(
|
|
560
|
+
options.kms_or_secret_custody,
|
|
561
|
+
options.kmsOrSecretCustody,
|
|
562
|
+
options.kms,
|
|
563
|
+
options.kms_ref,
|
|
564
|
+
options.kmsRef,
|
|
565
|
+
options.kmsKeyRef,
|
|
566
|
+
options.kms_key_ref,
|
|
567
|
+
options.secrets_manager,
|
|
568
|
+
options.secretsManager,
|
|
569
|
+
envValue('ENIGMA_KMS_KEY_REF', 'ENIGMA_KMS_REF', 'ENIGMA_EXTERNAL_KMS_REF', 'ENIGMA_SECRETS_MANAGER_REF')
|
|
570
|
+
),
|
|
571
|
+
siem_or_log_sink: firstDefined(
|
|
572
|
+
options.siem_or_log_sink,
|
|
573
|
+
options.siemOrLogSink,
|
|
574
|
+
options.siem,
|
|
575
|
+
options.siem_ref,
|
|
576
|
+
options.siemRef,
|
|
577
|
+
options.audit_sink,
|
|
578
|
+
options.auditSink,
|
|
579
|
+
options.log_sink,
|
|
580
|
+
options.logSink,
|
|
581
|
+
envValue('ENIGMA_SIEM_EXPORT_ENDPOINT', 'ENIGMA_SIEM_EXPORT_ENDPOINT_FILE', 'ENIGMA_SIEM_REF', 'ENIGMA_AUDIT_SINK_REF', 'ENIGMA_LOG_SINK_REF')
|
|
582
|
+
),
|
|
583
|
+
backup_restore: firstDefined(
|
|
584
|
+
options.backup_restore,
|
|
585
|
+
options.backupRestore,
|
|
586
|
+
options.backup,
|
|
587
|
+
options.backup_target,
|
|
588
|
+
options.backupTarget,
|
|
589
|
+
options.restore_target,
|
|
590
|
+
options.restoreTarget,
|
|
591
|
+
envValue('ENIGMA_BACKUP_TARGET_URI', 'ENIGMA_BACKUP_TARGET_URI_FILE', 'ENIGMA_BACKUP_TARGET_REF', 'ENIGMA_RESTORE_TARGET_REF')
|
|
592
|
+
),
|
|
593
|
+
monitoring: firstDefined(
|
|
594
|
+
options.monitoring,
|
|
595
|
+
options.monitoring_ref,
|
|
596
|
+
options.monitoringRef,
|
|
597
|
+
options.alerting,
|
|
598
|
+
options.alerting_ref,
|
|
599
|
+
options.alertingRef,
|
|
600
|
+
envValue('ENIGMA_MONITORING_REF', 'ENIGMA_RELAY_MONITORING_REF', 'ENIGMA_ALERTING_REF')
|
|
601
|
+
),
|
|
602
|
+
runtime_auth: firstDefined(
|
|
603
|
+
options.runtime_auth,
|
|
604
|
+
options.runtimeAuth,
|
|
605
|
+
options.runtime_auth_ref,
|
|
606
|
+
options.runtimeAuthRef,
|
|
607
|
+
options.paired_client_auth,
|
|
608
|
+
options.pairedClientAuth,
|
|
609
|
+
options.auth_ref,
|
|
610
|
+
options.authRef,
|
|
611
|
+
envValue('ENIGMA_RUNTIME_AUTH_REF', 'ENIGMA_RELAY_RUNTIME_AUTH_REF', 'ENIGMA_PAIRED_CLIENT_AUTH_REF')
|
|
612
|
+
),
|
|
613
|
+
...sharedProductionDependencyRefsFromOptions(options),
|
|
614
|
+
durable_storage_configured: hasDurableStorage(firstDefined(
|
|
615
|
+
options.durable_storage_configured,
|
|
616
|
+
options.durableStorageConfigured,
|
|
617
|
+
options.durable_storage,
|
|
618
|
+
options.durableStorage,
|
|
619
|
+
options.storage,
|
|
620
|
+
options.storage_ref,
|
|
621
|
+
options.storageRef,
|
|
622
|
+
envValue(
|
|
623
|
+
'ENIGMA_EXTERNAL_STORAGE_DSN',
|
|
624
|
+
'ENIGMA_EXTERNAL_STORAGE_DSN_FILE',
|
|
625
|
+
'ENIGMA_EXTERNAL_STORAGE_REF',
|
|
626
|
+
'ENIGMA_EXTERNAL_STORAGE_REF_FILE',
|
|
627
|
+
'ENIGMA_STORAGE_REF',
|
|
628
|
+
'ENIGMA_STORAGE_REF_FILE',
|
|
629
|
+
'ENIGMA_RELAY_STORAGE_REF',
|
|
630
|
+
'ENIGMA_RELAY_STORAGE_REF_FILE',
|
|
631
|
+
'ENIGMA_DURABLE_STORAGE_REF',
|
|
632
|
+
'ENIGMA_DURABLE_STORAGE_REF_FILE',
|
|
633
|
+
'ENIGMA_STATE_BACKEND',
|
|
634
|
+
'ENIGMA_RELAY_STATE_BACKEND'
|
|
635
|
+
)
|
|
636
|
+
)),
|
|
637
|
+
kms_configured: hasConfiguredReference(firstDefined(
|
|
638
|
+
options.kms_configured,
|
|
639
|
+
options.kmsConfigured,
|
|
640
|
+
options.kms,
|
|
641
|
+
options.kms_ref,
|
|
642
|
+
options.kmsRef,
|
|
643
|
+
options.kmsKeyRef,
|
|
644
|
+
options.kms_key_ref,
|
|
645
|
+
options.secrets_manager,
|
|
646
|
+
options.secretsManager,
|
|
647
|
+
envValue(
|
|
648
|
+
'ENIGMA_KMS_KEY_REF',
|
|
649
|
+
'ENIGMA_KMS_KEY_REF_FILE',
|
|
650
|
+
'ENIGMA_KMS_REF',
|
|
651
|
+
'ENIGMA_KMS_REF_FILE',
|
|
652
|
+
'ENIGMA_EXTERNAL_KMS_REF',
|
|
653
|
+
'ENIGMA_EXTERNAL_KMS_REF_FILE',
|
|
654
|
+
'ENIGMA_SECRETS_MANAGER_REF',
|
|
655
|
+
'ENIGMA_SECRETS_MANAGER_REF_FILE'
|
|
656
|
+
)
|
|
657
|
+
)),
|
|
658
|
+
siem_configured: hasConfiguredReference(firstDefined(
|
|
659
|
+
options.siem_configured,
|
|
660
|
+
options.siemConfigured,
|
|
661
|
+
options.siem,
|
|
662
|
+
options.siem_ref,
|
|
663
|
+
options.siemRef,
|
|
664
|
+
options.audit,
|
|
665
|
+
options.audit_sink,
|
|
666
|
+
options.auditSink,
|
|
667
|
+
options.log_sink,
|
|
668
|
+
options.logSink,
|
|
669
|
+
envValue(
|
|
670
|
+
'ENIGMA_SIEM_EXPORT_ENDPOINT',
|
|
671
|
+
'ENIGMA_SIEM_EXPORT_ENDPOINT_FILE',
|
|
672
|
+
'ENIGMA_SIEM_REF',
|
|
673
|
+
'ENIGMA_SIEM_REF_FILE',
|
|
674
|
+
'ENIGMA_AUDIT_SINK_REF',
|
|
675
|
+
'ENIGMA_AUDIT_SINK_REF_FILE',
|
|
676
|
+
'ENIGMA_LOG_SINK_REF',
|
|
677
|
+
'ENIGMA_LOG_SINK_REF_FILE'
|
|
678
|
+
)
|
|
679
|
+
)),
|
|
680
|
+
backup_configured: hasConfiguredReference(firstDefined(
|
|
681
|
+
options.backup_configured,
|
|
682
|
+
options.backupConfigured,
|
|
683
|
+
options.backup,
|
|
684
|
+
options.backup_target,
|
|
685
|
+
options.backupTarget,
|
|
686
|
+
options.restore_target,
|
|
687
|
+
options.restoreTarget,
|
|
688
|
+
envValue(
|
|
689
|
+
'ENIGMA_BACKUP_TARGET_URI',
|
|
690
|
+
'ENIGMA_BACKUP_TARGET_URI_FILE',
|
|
691
|
+
'ENIGMA_BACKUP_TARGET',
|
|
692
|
+
'ENIGMA_BACKUP_TARGET_FILE',
|
|
693
|
+
'ENIGMA_BACKUP_TARGET_REF',
|
|
694
|
+
'ENIGMA_BACKUP_TARGET_REF_FILE',
|
|
695
|
+
'ENIGMA_RESTORE_TARGET_REF',
|
|
696
|
+
'ENIGMA_RESTORE_TARGET_REF_FILE'
|
|
697
|
+
)
|
|
698
|
+
)),
|
|
699
|
+
operator_acceptance: firstDefined(
|
|
700
|
+
options.operator_acceptance,
|
|
701
|
+
options.operatorAcceptance,
|
|
702
|
+
options.acceptance,
|
|
703
|
+
options.go_live,
|
|
704
|
+
options.goLive,
|
|
705
|
+
envValue(
|
|
706
|
+
'ENIGMA_OPERATOR_ACCEPTANCE',
|
|
707
|
+
'ENIGMA_OPERATOR_ACCEPTANCE_DECISION',
|
|
708
|
+
'ENIGMA_GO_LIVE_DECISION',
|
|
709
|
+
'ENIGMA_GO_LIVE'
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function relayReadinessCheck(check, okValue, missing) {
|
|
716
|
+
const ok = okValue === true;
|
|
717
|
+
return ok ? { check, ok: true } : { check, ok: false, missing };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function productionEvidenceRef(key, value, status = 'verified') {
|
|
721
|
+
if (value === undefined || value === null || value === false || value === true) return undefined;
|
|
722
|
+
if (nonEmptyString(value)) return { status, provider: 'operator', ref: value };
|
|
723
|
+
if (!isPlainRecord(value)) return undefined;
|
|
724
|
+
const ref = firstDefined(
|
|
725
|
+
value.ref,
|
|
726
|
+
value.reference,
|
|
727
|
+
value.id,
|
|
728
|
+
value.name,
|
|
729
|
+
value.uri,
|
|
730
|
+
value.url,
|
|
731
|
+
value.arn,
|
|
732
|
+
value.path,
|
|
733
|
+
value.target,
|
|
734
|
+
value.sink,
|
|
735
|
+
value.key_ref,
|
|
736
|
+
value.keyRef,
|
|
737
|
+
value.resource
|
|
738
|
+
);
|
|
739
|
+
if (!nonEmptyString(ref)) return undefined;
|
|
740
|
+
return {
|
|
741
|
+
...value,
|
|
742
|
+
key,
|
|
743
|
+
ref,
|
|
744
|
+
status: value.status ?? status,
|
|
745
|
+
provider: value.provider ?? 'operator',
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function relayProductionEvidence(state) {
|
|
750
|
+
const evidence = {};
|
|
751
|
+
const add = (key, value, status = 'verified') => {
|
|
752
|
+
const ref = productionEvidenceRef(key, value, status);
|
|
753
|
+
if (ref !== undefined) evidence[key] = ref;
|
|
754
|
+
};
|
|
755
|
+
add('backend_host', readinessValue(state, 'backend_host', 'backendHost', 'backend_host_ref', 'backendHostRef', 'host_ref', 'hostRef', 'deployment_ref', 'deploymentRef'));
|
|
756
|
+
add('dns_tls', readinessValue(state, 'dns_tls', 'dnsTls', 'dns_tls_ref', 'dnsTlsRef', 'tls_ref', 'tlsRef'));
|
|
757
|
+
add('durable_storage', readinessValue(state, 'durable_storage', 'durableStorage', 'storage', 'storage_ref', 'storageRef'));
|
|
758
|
+
add('kms_or_secret_custody', readinessValue(state, 'kms_or_secret_custody', 'kmsOrSecretCustody', 'kms', 'kms_ref', 'kmsRef', 'kms_key_ref', 'kmsKeyRef', 'secrets_manager', 'secretsManager'));
|
|
759
|
+
add('backup_restore', readinessValue(state, 'backup_restore', 'backupRestore', 'backup', 'backup_target', 'backupTarget', 'restore_target', 'restoreTarget'));
|
|
760
|
+
add('monitoring', readinessValue(state, 'monitoring', 'monitoring_ref', 'monitoringRef', 'alerting', 'alerting_ref', 'alertingRef'));
|
|
761
|
+
add('siem_or_log_sink', readinessValue(state, 'siem_or_log_sink', 'siemOrLogSink', 'siem', 'siem_ref', 'siemRef', 'audit_sink', 'auditSink', 'log_sink', 'logSink'));
|
|
762
|
+
add('operator_acceptance', readinessValue(state, 'operator_acceptance', 'operatorAcceptance', 'acceptance', 'go_live', 'goLive'), 'go');
|
|
763
|
+
add('runtime_auth', readinessValue(state, 'runtime_auth', 'runtimeAuth', 'runtime_auth_ref', 'runtimeAuthRef', 'paired_client_auth', 'pairedClientAuth', 'auth_ref', 'authRef'));
|
|
764
|
+
for (const key of SHARED_PRODUCTION_DEPENDENCY_KEYS) add(key, productionDependencyValue(state, key));
|
|
765
|
+
try {
|
|
766
|
+
const normalized = normalizeDependencyEvidence(evidence, {
|
|
767
|
+
generated_at: nowIso(state),
|
|
768
|
+
required_keys: [
|
|
769
|
+
'backend_host',
|
|
770
|
+
'dns_tls',
|
|
771
|
+
'durable_storage',
|
|
772
|
+
'kms_or_secret_custody',
|
|
773
|
+
'backup_restore',
|
|
774
|
+
'monitoring',
|
|
775
|
+
'siem_or_log_sink',
|
|
776
|
+
'operator_acceptance',
|
|
777
|
+
'runtime_auth',
|
|
778
|
+
...SHARED_PRODUCTION_DEPENDENCY_KEYS,
|
|
779
|
+
],
|
|
780
|
+
});
|
|
781
|
+
return {
|
|
782
|
+
ok: normalized.ok,
|
|
783
|
+
refs: readinessEvidenceRefs(normalized.evidence),
|
|
784
|
+
missing_keys: normalized.missing_keys,
|
|
785
|
+
};
|
|
786
|
+
} catch {
|
|
787
|
+
return {
|
|
788
|
+
ok: false,
|
|
789
|
+
refs: {},
|
|
790
|
+
missing_keys: ['production_dependency_evidence'],
|
|
791
|
+
error: 'production dependency evidence refs are invalid or unsafe',
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function relayReadiness(state) {
|
|
797
|
+
const productionLike = isProductionLikeState(state);
|
|
798
|
+
const localMode = isLocalReadinessMode(state);
|
|
799
|
+
const evidence = productionLike ? relayProductionEvidence(state) : { ok: true, refs: {}, missing_keys: [] };
|
|
800
|
+
const backendHost = hasConfiguredReference(readinessValue(state, 'backend_host', 'backendHost', 'backend_host_ref', 'backendHostRef', 'host_ref', 'hostRef', 'deployment_ref', 'deploymentRef'));
|
|
801
|
+
const dnsTls = hasConfiguredReference(readinessValue(state, 'dns_tls', 'dnsTls', 'dns_tls_ref', 'dnsTlsRef', 'tls_ref', 'tlsRef'));
|
|
802
|
+
const monitoring = hasConfiguredReference(readinessValue(state, 'monitoring', 'monitoring_ref', 'monitoringRef', 'alerting', 'alerting_ref', 'alertingRef'));
|
|
803
|
+
const runtimeAuth = hasConfiguredReference(readinessValue(state, 'runtime_auth', 'runtimeAuth', 'runtime_auth_ref', 'runtimeAuthRef', 'paired_client_auth', 'pairedClientAuth', 'auth_ref', 'authRef'));
|
|
804
|
+
const durableStorage = hasDurableStorage(firstDefined(
|
|
805
|
+
readinessValue(state, 'durable_storage', 'durableStorage', 'storage', 'storage_ref', 'storageRef'),
|
|
806
|
+
readinessValue(state, 'durable_storage_configured')
|
|
807
|
+
));
|
|
808
|
+
const kms = hasConfiguredReference(firstDefined(
|
|
809
|
+
readinessValue(state, 'kms', 'kms_ref', 'kmsRef', 'kms_key_ref', 'kmsKeyRef', 'secrets_manager', 'secretsManager'),
|
|
810
|
+
readinessValue(state, 'kms_configured')
|
|
811
|
+
));
|
|
812
|
+
const siem = hasConfiguredReference(firstDefined(
|
|
813
|
+
readinessValue(state, 'siem', 'siem_ref', 'siemRef', 'audit', 'audit_sink', 'auditSink', 'log_sink', 'logSink'),
|
|
814
|
+
readinessValue(state, 'siem_configured')
|
|
815
|
+
));
|
|
816
|
+
const backup = hasConfiguredReference(firstDefined(
|
|
817
|
+
readinessValue(state, 'backup', 'backup_target', 'backupTarget', 'restore_target', 'restoreTarget'),
|
|
818
|
+
readinessValue(state, 'backup_configured')
|
|
819
|
+
));
|
|
820
|
+
const operatorAccepted = operatorAcceptanceIsGo(firstDefined(
|
|
821
|
+
readinessValue(state, 'operator_acceptance', 'operatorAcceptance', 'acceptance', 'go_live', 'goLive'),
|
|
822
|
+
readinessValue(state, 'operator_acceptance')
|
|
823
|
+
));
|
|
824
|
+
const checks = productionLike
|
|
825
|
+
? [
|
|
826
|
+
relayReadinessCheck('paired_client_auth_required', !unauthenticatedLocalDemoEnabled(state), 'unauthenticated local demo auth must be disabled in production readiness'),
|
|
827
|
+
relayReadinessCheck('backend_host', backendHost, 'production backend host reference is not configured'),
|
|
828
|
+
relayReadinessCheck('dns_tls', dnsTls, 'production DNS/TLS reference is not configured'),
|
|
829
|
+
relayReadinessCheck('runtime_auth', runtimeAuth, 'production paired-client/runtime auth evidence is not configured'),
|
|
830
|
+
relayReadinessCheck('durable_storage', durableStorage, 'production durable storage is not configured'),
|
|
831
|
+
relayReadinessCheck('kms_or_secrets_manager', kms, 'production KMS/secrets-manager reference is not configured'),
|
|
832
|
+
relayReadinessCheck('siem_or_audit_sink', siem, 'production SIEM/audit routing is not configured'),
|
|
833
|
+
relayReadinessCheck('backup_target', backup, 'production backup/restore target is not configured'),
|
|
834
|
+
relayReadinessCheck('monitoring', monitoring, 'production monitoring/alerting reference is not configured'),
|
|
835
|
+
...SHARED_PRODUCTION_DEPENDENCY_KEYS.map((key) => relayReadinessCheck(key, productionDependencyConfigured(state, key), `production ${key.replaceAll('_', ' ')} evidence is not configured`)),
|
|
836
|
+
relayReadinessCheck('operator_acceptance', operatorAccepted, 'operator acceptance is not go'),
|
|
837
|
+
relayReadinessCheck('production_evidence_refs', evidence.ok, evidence.error ?? 'production dependency evidence refs are incomplete')
|
|
838
|
+
]
|
|
839
|
+
: [
|
|
840
|
+
relayReadinessCheck('non_production_local_mode', localMode, 'relay readiness is only green for non-production local mode')
|
|
841
|
+
];
|
|
842
|
+
const missingChecks = checks.filter((check) => check.ok !== true).map((check) => check.check);
|
|
843
|
+
const ready = checks.every((check) => check.ok === true);
|
|
844
|
+
return {
|
|
845
|
+
statusCode: ready ? 200 : 503,
|
|
846
|
+
body: {
|
|
847
|
+
ok: ready,
|
|
848
|
+
service: 'enigma-relay',
|
|
849
|
+
status: ready ? 'ready' : 'not_ready',
|
|
850
|
+
mode: normalizedReadinessMode(state),
|
|
851
|
+
claim_boundary: 'static distribution may be live; relay production backend requires backend host, DNS/TLS, runtime auth, real storage, KMS/secrets, SIEM/audit, backups, monitoring, network policy, KMS custody, tenant policy, usage metering, settlement, public-site security, threat model, legal/compliance, support SLA, incident/restore drills, and operator acceptance',
|
|
852
|
+
checks,
|
|
853
|
+
missing_checks: missingChecks,
|
|
854
|
+
evidence_refs: evidence.refs,
|
|
855
|
+
missing_evidence_refs: evidence.missing_keys,
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async function readJsonBody(req) {
|
|
861
|
+
let size = 0;
|
|
862
|
+
const chunks = [];
|
|
863
|
+
for await (const chunk of req) {
|
|
864
|
+
size += chunk.length;
|
|
865
|
+
if (size > MAX_JSON_BODY_BYTES) throw new Error('JSON body too large');
|
|
866
|
+
chunks.push(chunk);
|
|
867
|
+
}
|
|
868
|
+
if (chunks.length === 0) return {};
|
|
869
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
870
|
+
if (text.trim().length === 0) return {};
|
|
871
|
+
return JSON.parse(text);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function publicKeyFromPairingInput(body) {
|
|
875
|
+
const publicKey = body.public_key ?? body.publicKey ?? body.client_public_key ?? body.clientPublicKey ?? body.device_public_key ?? body.devicePublicKey ?? body.account_public_key ?? body.accountPublicKey;
|
|
876
|
+
if (typeof publicKey !== 'string' || publicKey.length === 0) throw new TypeError('public_key is required');
|
|
877
|
+
return publicKey;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function relaySignature(state, payload) {
|
|
881
|
+
return {
|
|
882
|
+
alg: 'Ed25519',
|
|
883
|
+
key_id: state.node.signer.key_id,
|
|
884
|
+
value: signPayload({ payload, privateKey: state.node.privateKey })
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function hashValue(value) {
|
|
889
|
+
return `sha256:${sha256Hex(typeof value === 'string' ? value : canonicalize(value))}`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function headerValue(req, name) {
|
|
893
|
+
const lower = name.toLowerCase();
|
|
894
|
+
const raw = typeof req.getHeader === 'function' ? req.getHeader(name) : (req.headers?.[lower] ?? req.headers?.[name]);
|
|
895
|
+
if (Array.isArray(raw)) return raw[0];
|
|
896
|
+
return raw === undefined ? undefined : String(raw);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function sortedQueryEntries(url) {
|
|
900
|
+
return [...url.searchParams.entries()].sort(([leftKey, leftValue], [rightKey, rightValue]) =>
|
|
901
|
+
leftKey.localeCompare(rightKey) || leftValue.localeCompare(rightValue)
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function relayRequestHash(method, url, body = undefined) {
|
|
906
|
+
const request = {
|
|
907
|
+
method: String(method).toUpperCase(),
|
|
908
|
+
path: url.pathname,
|
|
909
|
+
query: sortedQueryEntries(url)
|
|
910
|
+
};
|
|
911
|
+
if (body !== undefined) request.body = body;
|
|
912
|
+
return hashValue(request);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function relayAuthorizationPayload(state, pairingId, operation, requestHash) {
|
|
916
|
+
return {
|
|
917
|
+
schema: CLIENT_AUTH_SCHEMA,
|
|
918
|
+
relay_node_id: state.node.node_id,
|
|
919
|
+
pairing_id: pairingId,
|
|
920
|
+
operation,
|
|
921
|
+
request_hash: requestHash
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function requireRelayAuthorization(state, req, operation, requestHash) {
|
|
926
|
+
if (state.allowUnauthenticated === true) {
|
|
927
|
+
return {
|
|
928
|
+
mode: 'unauthenticated_local_demo',
|
|
929
|
+
authenticated: false,
|
|
930
|
+
request_hash: requestHash
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
const pairingId = headerValue(req, 'x-enigma-pairing-id');
|
|
934
|
+
const signature = headerValue(req, 'x-enigma-client-signature');
|
|
935
|
+
if (!pairingId || !signature) throw new Error('relay client authorization required');
|
|
936
|
+
const pairing = state.pairings.get(pairingId);
|
|
937
|
+
if (pairing === undefined) throw new Error('unknown relay pairing');
|
|
938
|
+
const payload = relayAuthorizationPayload(state, pairingId, operation, requestHash);
|
|
939
|
+
if (!verifySignature({ payload, signature, publicKey: pairing.client_public_key })) {
|
|
940
|
+
throw new Error('relay client authorization signature mismatch');
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
mode: 'paired_client_signature',
|
|
944
|
+
authenticated: true,
|
|
945
|
+
pairing_id: pairingId,
|
|
946
|
+
client_public_key_hash: pairing.client_public_key_hash,
|
|
947
|
+
request_hash: requestHash
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function normalizeRelayRecordInput(input) {
|
|
952
|
+
const request = assertPlainRecord(input, 'relay request');
|
|
953
|
+
assertNoPlaintextLookingFields(request, 'relay request');
|
|
954
|
+
const hasRecordEnvelope = Object.prototype.hasOwnProperty.call(request, 'record');
|
|
955
|
+
if (hasRecordEnvelope && Object.keys(request).length !== 1) throw new Error('relay request envelope only allows record');
|
|
956
|
+
const body = assertPlainRecord(hasRecordEnvelope ? request.record : request, 'relay record');
|
|
957
|
+
assertOnlyKeys(body, RELAY_RECORD_KEYS, 'relay record');
|
|
958
|
+
return body;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function normalizeWitnessInput(input) {
|
|
962
|
+
const request = assertPlainRecord(input, 'witness request');
|
|
963
|
+
assertNoPlaintextLookingFields(request, 'witness request');
|
|
964
|
+
const hasCheckpointEnvelope = Object.prototype.hasOwnProperty.call(request, 'checkpoint');
|
|
965
|
+
if (hasCheckpointEnvelope && Object.keys(request).length !== 1) throw new Error('witness request envelope only allows checkpoint');
|
|
966
|
+
const body = assertPlainRecord(hasCheckpointEnvelope ? request.checkpoint : request, 'witness checkpoint');
|
|
967
|
+
assertOnlyKeys(body, WITNESS_CHECKPOINT_KEYS, 'witness checkpoint');
|
|
968
|
+
const checkpointRoot = body.checkpoint_root ?? body.checkpointRoot;
|
|
969
|
+
const receiptRoot = body.receipt_log_root ?? body.receiptLogRoot;
|
|
970
|
+
const activeRoot = body.active_set_root ?? body.activeSetRoot ?? body.active_memory_root ?? body.activeMemoryRoot;
|
|
971
|
+
if (checkpointRoot !== undefined) assertRoot(checkpointRoot, 'checkpoint_root');
|
|
972
|
+
if (receiptRoot !== undefined) assertRoot(receiptRoot, 'receipt_log_root');
|
|
973
|
+
if (activeRoot !== undefined) assertRoot(activeRoot, 'active_set_root');
|
|
974
|
+
return body;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function createPairingChallenge(state, input) {
|
|
978
|
+
const body = assertPlainRecord(input, 'pairing challenge');
|
|
979
|
+
assertNoPlaintextLookingFields(body, 'pairing challenge');
|
|
980
|
+
assertOnlyKeys(body, PAIRING_CHALLENGE_KEYS, 'pairing challenge');
|
|
981
|
+
const clientPublicKey = publicKeyFromPairingInput(body);
|
|
982
|
+
const issuedAt = nowIso(state);
|
|
983
|
+
const nonce = randomBytes(32).toString('base64url');
|
|
984
|
+
const challenge = {
|
|
985
|
+
schema: PAIRING_CHALLENGE_SCHEMA,
|
|
986
|
+
challenge_id: `pch_${sha256Hex(canonicalize({ clientPublicKey, nonce })).slice(0, 32)}`,
|
|
987
|
+
relay_node_id: state.node.node_id,
|
|
988
|
+
client_public_key: clientPublicKey,
|
|
989
|
+
client_public_key_hash: `sha256:${sha256Hex(clientPublicKey)}`,
|
|
990
|
+
nonce,
|
|
991
|
+
issued_at: issuedAt,
|
|
992
|
+
expires_at: futureIso(issuedAt, state.challengeTtlMs)
|
|
993
|
+
};
|
|
994
|
+
const signed = { ...challenge, signature: relaySignature(state, challenge) };
|
|
995
|
+
state.pairingChallenges.set(challenge.challenge_id, Object.freeze({ challenge, publicKey: clientPublicKey }));
|
|
996
|
+
return signed;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function completePairing(state, input) {
|
|
1000
|
+
const body = assertPlainRecord(input, 'pairing completion');
|
|
1001
|
+
assertNoPlaintextLookingFields(body, 'pairing completion');
|
|
1002
|
+
assertOnlyKeys(body, PAIRING_COMPLETE_KEYS, 'pairing completion');
|
|
1003
|
+
const challengeId = body.challenge_id ?? body.challengeId;
|
|
1004
|
+
if (typeof challengeId !== 'string' || challengeId.length === 0) throw new TypeError('challenge_id is required');
|
|
1005
|
+
const pending = state.pairingChallenges.get(challengeId);
|
|
1006
|
+
if (pending === undefined) throw new Error('unknown pairing challenge');
|
|
1007
|
+
if (Date.parse(pending.challenge.expires_at) <= Date.parse(nowIso(state))) throw new Error('pairing challenge expired');
|
|
1008
|
+
const publicKey = publicKeyFromPairingInput(body);
|
|
1009
|
+
if (publicKey !== pending.publicKey) throw new Error('pairing public key mismatch');
|
|
1010
|
+
const signature = body.client_signature ?? body.clientSignature ?? body.signature;
|
|
1011
|
+
if (!verifySignature({ payload: pending.challenge, signature, publicKey })) throw new Error('pairing challenge signature mismatch');
|
|
1012
|
+
const issuedAt = nowIso(state);
|
|
1013
|
+
const pairing = {
|
|
1014
|
+
schema: PAIRING_COMPLETION_SCHEMA,
|
|
1015
|
+
pairing_id: `pair_${sha256Hex(canonicalize({ challengeId, publicKey, issuedAt })).slice(0, 32)}`,
|
|
1016
|
+
challenge_id: challengeId,
|
|
1017
|
+
relay_node_id: state.node.node_id,
|
|
1018
|
+
client_public_key: publicKey,
|
|
1019
|
+
client_public_key_hash: `sha256:${sha256Hex(publicKey)}`,
|
|
1020
|
+
issued_at: issuedAt
|
|
1021
|
+
};
|
|
1022
|
+
const signed = { ...pairing, signature: relaySignature(state, pairing) };
|
|
1023
|
+
state.pairings.set(signed.pairing_id, Object.freeze(signed));
|
|
1024
|
+
state.pairingChallenges.delete(challengeId);
|
|
1025
|
+
return signed;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function serviceHealth(state) {
|
|
1029
|
+
return {
|
|
1030
|
+
schema: SERVICE_SCHEMA,
|
|
1031
|
+
node: {
|
|
1032
|
+
node_id: state.node.node_id,
|
|
1033
|
+
trust_descriptor: state.node.trust_descriptor
|
|
1034
|
+
},
|
|
1035
|
+
relay: {
|
|
1036
|
+
store_id: state.relayStore.store_id,
|
|
1037
|
+
records: state.relayStore.records.size
|
|
1038
|
+
},
|
|
1039
|
+
witness: {
|
|
1040
|
+
checkpoints: state.witnessLog.length
|
|
1041
|
+
},
|
|
1042
|
+
pairing: {
|
|
1043
|
+
pending_challenges: state.pairingChallenges.size,
|
|
1044
|
+
completed_pairings: state.pairings.size
|
|
1045
|
+
},
|
|
1046
|
+
authorization: {
|
|
1047
|
+
mode: state.allowUnauthenticated === true ? 'unauthenticated_local_demo' : 'paired_client_signature_required',
|
|
1048
|
+
endpoints: ['/relay/push', '/relay/pull', '/witness/checkpoint']
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function relayAuthorizationMode(state) {
|
|
1054
|
+
return state.allowUnauthenticated === true ? 'unauthenticated_local_demo' : 'paired_client_signature_required';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function serializeNodeState(node) {
|
|
1058
|
+
const trustDescriptor = cloneJson(node.trust_descriptor, 'relay node trust descriptor');
|
|
1059
|
+
return {
|
|
1060
|
+
schema: node.schema,
|
|
1061
|
+
node_id: node.node_id,
|
|
1062
|
+
signer: cloneJson(node.signer, 'relay node signer'),
|
|
1063
|
+
publicKey: node.publicKey,
|
|
1064
|
+
privateKey: node.privateKey,
|
|
1065
|
+
trust_descriptor: trustDescriptor,
|
|
1066
|
+
public_descriptor: cloneJson(node.public_descriptor ?? trustDescriptor, 'relay node public descriptor')
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function hydrateNodeState(nodeState) {
|
|
1071
|
+
const node = assertPlainRecord(cloneJson(nodeState, 'relay node state'), 'relay node state');
|
|
1072
|
+
assertNoPlaintextLookingFields(node, 'relay node state');
|
|
1073
|
+
assertOnlyKeys(node, RELAY_STATE_NODE_KEYS, 'relay node state');
|
|
1074
|
+
if (node.schema !== 'enigma.mesh_node.v1') throw new Error('relay node schema mismatch');
|
|
1075
|
+
assertString(node.node_id, 'relay node node_id');
|
|
1076
|
+
assertString(node.publicKey, 'relay node publicKey');
|
|
1077
|
+
assertString(node.privateKey, 'relay node privateKey');
|
|
1078
|
+
const signer = assertPlainRecord(node.signer, 'relay node signer');
|
|
1079
|
+
assertOnlyKeys(signer, new Set(['alg', 'key_id']), 'relay node signer');
|
|
1080
|
+
if (signer.alg !== SIGNATURE_ALG) throw new Error('relay node signer algorithm is not supported');
|
|
1081
|
+
assertString(signer.key_id, 'relay node signer key_id');
|
|
1082
|
+
|
|
1083
|
+
const descriptor = assertPlainRecord(node.trust_descriptor, 'relay node trust descriptor');
|
|
1084
|
+
if (descriptor.schema !== 'enigma.mesh_node.v1') throw new Error('relay node trust descriptor schema mismatch');
|
|
1085
|
+
if (descriptor.node_id !== node.node_id) throw new Error('relay node trust descriptor node_id mismatch');
|
|
1086
|
+
const descriptorKey = assertPlainRecord(descriptor.key, 'relay node trust descriptor key');
|
|
1087
|
+
if (descriptorKey.alg !== SIGNATURE_ALG) throw new Error('relay node trust descriptor key algorithm is not supported');
|
|
1088
|
+
if (descriptorKey.key_id !== signer.key_id) throw new Error('relay node signer key_id mismatch');
|
|
1089
|
+
if (descriptorKey.public_key !== node.publicKey || descriptor.public_key !== node.publicKey) {
|
|
1090
|
+
throw new Error('relay node public key mismatch');
|
|
1091
|
+
}
|
|
1092
|
+
assertSignedBy(descriptor, node.publicKey, 'relay node trust descriptor');
|
|
1093
|
+
assertRoot(descriptor.descriptor_hash, 'relay node trust descriptor descriptor_hash');
|
|
1094
|
+
if (descriptor.descriptor_hash !== signedRecordHash(descriptor, 'descriptor_hash')) {
|
|
1095
|
+
throw new Error('relay node trust descriptor hash mismatch');
|
|
1096
|
+
}
|
|
1097
|
+
const keyCheckPayload = { schema: RELAY_STATE_SCHEMA, purpose: 'relay_state_key_check' };
|
|
1098
|
+
const keyCheckSignature = signPayload({ payload: keyCheckPayload, privateKey: node.privateKey });
|
|
1099
|
+
if (!verifySignature({ payload: keyCheckPayload, signature: keyCheckSignature, publicKey: node.publicKey })) {
|
|
1100
|
+
throw new Error('relay node signing material mismatch');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return Object.freeze({
|
|
1104
|
+
schema: node.schema,
|
|
1105
|
+
node_id: node.node_id,
|
|
1106
|
+
signer: Object.freeze({ ...signer }),
|
|
1107
|
+
publicKey: node.publicKey,
|
|
1108
|
+
privateKey: node.privateKey,
|
|
1109
|
+
trust_descriptor: Object.freeze({ ...descriptor }),
|
|
1110
|
+
public_descriptor: Object.freeze({ ...(node.public_descriptor ?? descriptor) })
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function validateRelayRecord(record, relayStore, node) {
|
|
1115
|
+
const relayRecord = assertPlainRecord(cloneJson(record, 'relay record state'), 'relay record state');
|
|
1116
|
+
assertNoPlaintextLookingFields(relayRecord, 'relay record state');
|
|
1117
|
+
assertOnlyKeys(relayRecord, RELAY_STATE_RECORD_KEYS, 'relay record state');
|
|
1118
|
+
if (relayRecord.schema !== 'enigma.relay_record.v1') throw new Error('relay record schema mismatch');
|
|
1119
|
+
assertString(relayRecord.record_id, 'relay record record_id');
|
|
1120
|
+
if (relayRecord.store_id !== relayStore.store_id) throw new Error('relay record store_id mismatch');
|
|
1121
|
+
assertRoot(relayRecord.encrypted_payload_hash, 'relay record encrypted_payload_hash');
|
|
1122
|
+
if (relayRecord.payload_storage !== 'hash_only') throw new Error('relay record payload_storage must be hash_only');
|
|
1123
|
+
assertIsoDate(relayRecord.received_at, 'relay record received_at');
|
|
1124
|
+
if (relayRecord.expires_at !== null) assertIsoDate(relayRecord.expires_at, 'relay record expires_at');
|
|
1125
|
+
assertSignedBy(relayRecord, node.publicKey, 'relay record');
|
|
1126
|
+
return Object.freeze(relayRecord);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function validatePairing(pairing, node) {
|
|
1130
|
+
const record = assertPlainRecord(cloneJson(pairing, 'completed pairing state'), 'completed pairing state');
|
|
1131
|
+
assertNoPlaintextLookingFields(record, 'completed pairing state');
|
|
1132
|
+
assertOnlyKeys(record, RELAY_STATE_PAIRING_KEYS, 'completed pairing state');
|
|
1133
|
+
if (record.schema !== PAIRING_COMPLETION_SCHEMA) throw new Error('completed pairing schema mismatch');
|
|
1134
|
+
assertString(record.pairing_id, 'completed pairing pairing_id');
|
|
1135
|
+
assertString(record.challenge_id, 'completed pairing challenge_id');
|
|
1136
|
+
if (record.relay_node_id !== node.node_id) throw new Error('completed pairing relay_node_id mismatch');
|
|
1137
|
+
assertString(record.client_public_key, 'completed pairing client_public_key');
|
|
1138
|
+
if (record.client_public_key_hash !== `sha256:${sha256Hex(record.client_public_key)}`) {
|
|
1139
|
+
throw new Error('completed pairing client_public_key_hash mismatch');
|
|
1140
|
+
}
|
|
1141
|
+
assertIsoDate(record.issued_at, 'completed pairing issued_at');
|
|
1142
|
+
assertSignedBy(record, node.publicKey, 'completed pairing');
|
|
1143
|
+
return Object.freeze(record);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function validateWitnessLog(witnessLog, node) {
|
|
1147
|
+
if (!Array.isArray(witnessLog)) throw new TypeError('relay witness_log must be an array');
|
|
1148
|
+
const checkpoints = [];
|
|
1149
|
+
let previousWitnessHash = EMPTY_MERKLE_ROOT;
|
|
1150
|
+
let previousEpoch = -1;
|
|
1151
|
+
for (const checkpointState of witnessLog) {
|
|
1152
|
+
const checkpoint = assertPlainRecord(cloneJson(checkpointState, 'witness checkpoint state'), 'witness checkpoint state');
|
|
1153
|
+
assertNoPlaintextLookingFields(checkpoint, 'witness checkpoint state');
|
|
1154
|
+
assertRoot(checkpoint.witness_hash, 'witness checkpoint witness_hash');
|
|
1155
|
+
if (checkpoint.witness_hash !== signedRecordHash(checkpoint, 'witness_hash')) {
|
|
1156
|
+
throw new Error('witness checkpoint hash mismatch');
|
|
1157
|
+
}
|
|
1158
|
+
const verification = verifyWitnessCheckpoint(checkpoint, {
|
|
1159
|
+
trustDescriptor: node.trust_descriptor,
|
|
1160
|
+
expectedWitnessNodeId: node.node_id,
|
|
1161
|
+
expectedReceiptLogRoot: checkpoint.receipt_log_root,
|
|
1162
|
+
expectedActiveSetRoot: checkpoint.active_set_root,
|
|
1163
|
+
expectedCheckpointRoot: checkpoint.checkpoint_root,
|
|
1164
|
+
expectedPreviousWitnessHash: previousWitnessHash,
|
|
1165
|
+
minimumEpoch: previousEpoch + 1
|
|
1166
|
+
});
|
|
1167
|
+
if (!verification.ok) throw new Error(`witness checkpoint state invalid: ${verification.errors.join(', ')}`);
|
|
1168
|
+
if (checkpoint.epoch <= previousEpoch) throw new Error('witness checkpoint epochs must increase monotonically');
|
|
1169
|
+
checkpoints.push(Object.freeze(checkpoint));
|
|
1170
|
+
previousEpoch = checkpoint.epoch;
|
|
1171
|
+
previousWitnessHash = checkpoint.witness_hash;
|
|
1172
|
+
}
|
|
1173
|
+
return checkpoints;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function hydrateRelayStoreState(storeState, recordsState, node) {
|
|
1177
|
+
const store = assertPlainRecord(cloneJson(storeState, 'relay store state'), 'relay store state');
|
|
1178
|
+
assertNoPlaintextLookingFields(store, 'relay store state');
|
|
1179
|
+
assertOnlyKeys(store, RELAY_STATE_STORE_KEYS, 'relay store state');
|
|
1180
|
+
if (store.schema !== 'enigma.relay_store.v1') throw new Error('relay store schema mismatch');
|
|
1181
|
+
assertString(store.store_id, 'relay store store_id');
|
|
1182
|
+
if (store.node_id !== node.node_id) throw new Error('relay store node_id mismatch');
|
|
1183
|
+
assertIsoDate(store.created_at, 'relay store created_at');
|
|
1184
|
+
const metadata = assertPlainRecord(store.metadata, 'relay store metadata');
|
|
1185
|
+
assertOnlyKeys(metadata, RELAY_STATE_STORE_METADATA_KEYS, 'relay store metadata');
|
|
1186
|
+
if (!Number.isInteger(metadata.record_count) || metadata.record_count < 0) {
|
|
1187
|
+
throw new TypeError('relay store metadata record_count must be a non-negative integer');
|
|
1188
|
+
}
|
|
1189
|
+
if (!Array.isArray(recordsState)) throw new TypeError('relay_records must be an array');
|
|
1190
|
+
if (recordsState.length !== metadata.record_count) throw new Error('relay record_count mismatch');
|
|
1191
|
+
const relayStore = createRelayStore({
|
|
1192
|
+
node,
|
|
1193
|
+
store_id: store.store_id,
|
|
1194
|
+
node_id: store.node_id,
|
|
1195
|
+
created_at: store.created_at
|
|
1196
|
+
});
|
|
1197
|
+
for (const recordState of recordsState) {
|
|
1198
|
+
const record = validateRelayRecord(recordState, relayStore, node);
|
|
1199
|
+
if (relayStore.records.has(record.record_id)) throw new Error('duplicate relay record_id in state');
|
|
1200
|
+
relayStore.records.set(record.record_id, record);
|
|
1201
|
+
}
|
|
1202
|
+
return relayStore;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function authorizationFromSnapshot(authorization) {
|
|
1206
|
+
const record = assertPlainRecord(authorization, 'relay authorization state');
|
|
1207
|
+
assertOnlyKeys(record, RELAY_STATE_AUTHORIZATION_KEYS, 'relay authorization state');
|
|
1208
|
+
if (!RELAY_AUTH_MODES.has(record.mode)) throw new Error('relay authorization mode is not supported');
|
|
1209
|
+
if (typeof record.allow_unauthenticated !== 'boolean') {
|
|
1210
|
+
throw new TypeError('relay authorization allow_unauthenticated must be boolean');
|
|
1211
|
+
}
|
|
1212
|
+
const expectedMode = record.allow_unauthenticated ? 'unauthenticated_local_demo' : 'paired_client_signature_required';
|
|
1213
|
+
if (record.mode !== expectedMode) throw new Error('relay authorization mode mismatch');
|
|
1214
|
+
return record;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export function serializeRelayState(state) {
|
|
1218
|
+
if (!isPlainRecord(state) || !isPlainRecord(state.node) || !isPlainRecord(state.relayStore)) {
|
|
1219
|
+
throw new TypeError('relay state is required');
|
|
1220
|
+
}
|
|
1221
|
+
const records = [...state.relayStore.records.values()].map((record) => cloneJson(record, 'relay record state'));
|
|
1222
|
+
const witnessLog = state.witnessLog.map((checkpoint) => cloneJson(checkpoint, 'witness checkpoint state'));
|
|
1223
|
+
const completedPairings = [...state.pairings.values()].map((pairing) => cloneJson(pairing, 'completed pairing state'));
|
|
1224
|
+
const snapshot = {
|
|
1225
|
+
schema: RELAY_STATE_SCHEMA,
|
|
1226
|
+
version: RELAY_STATE_VERSION,
|
|
1227
|
+
generated_at: nowIso(state),
|
|
1228
|
+
node: serializeNodeState(state.node),
|
|
1229
|
+
relay_store: {
|
|
1230
|
+
schema: state.relayStore.schema,
|
|
1231
|
+
store_id: state.relayStore.store_id,
|
|
1232
|
+
node_id: state.relayStore.node_id,
|
|
1233
|
+
created_at: state.relayStore.created_at,
|
|
1234
|
+
metadata: {
|
|
1235
|
+
record_count: records.length
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
relay_records: records,
|
|
1239
|
+
witness_log: witnessLog,
|
|
1240
|
+
completed_pairings: completedPairings,
|
|
1241
|
+
authorization: {
|
|
1242
|
+
mode: relayAuthorizationMode(state),
|
|
1243
|
+
allow_unauthenticated: state.allowUnauthenticated === true
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
assertNoPlaintextLookingFields(snapshot, 'relay state snapshot');
|
|
1247
|
+
return snapshot;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export function hydrateRelayState(snapshot, options = {}) {
|
|
1251
|
+
const stateSnapshot = typeof snapshot === 'string' ? JSON.parse(snapshot) : cloneJson(snapshot, 'relay state snapshot');
|
|
1252
|
+
const record = assertPlainRecord(stateSnapshot, 'relay state snapshot');
|
|
1253
|
+
assertNoPlaintextLookingFields(record, 'relay state snapshot');
|
|
1254
|
+
assertOnlyKeys(record, RELAY_STATE_KEYS, 'relay state snapshot');
|
|
1255
|
+
if (record.schema !== RELAY_STATE_SCHEMA) throw new Error('relay state schema mismatch');
|
|
1256
|
+
if (record.version !== RELAY_STATE_VERSION) throw new Error('relay state version mismatch');
|
|
1257
|
+
assertIsoDate(record.generated_at, 'relay state generated_at');
|
|
1258
|
+
|
|
1259
|
+
const authorization = authorizationFromSnapshot(record.authorization);
|
|
1260
|
+
const node = hydrateNodeState(record.node);
|
|
1261
|
+
const relayStore = hydrateRelayStoreState(record.relay_store, record.relay_records, node);
|
|
1262
|
+
const witnessLog = validateWitnessLog(record.witness_log, node);
|
|
1263
|
+
if (!Array.isArray(record.completed_pairings)) throw new TypeError('completed_pairings must be an array');
|
|
1264
|
+
const pairings = new Map();
|
|
1265
|
+
for (const pairingState of record.completed_pairings) {
|
|
1266
|
+
const pairing = validatePairing(pairingState, node);
|
|
1267
|
+
if (pairings.has(pairing.pairing_id)) throw new Error('duplicate completed pairing_id in state');
|
|
1268
|
+
pairings.set(pairing.pairing_id, pairing);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const state = createRelayState({
|
|
1272
|
+
node,
|
|
1273
|
+
relayStore,
|
|
1274
|
+
witnessLog,
|
|
1275
|
+
now: options.now,
|
|
1276
|
+
challengeTtlMs: options.challengeTtlMs,
|
|
1277
|
+
allowUnauthenticated: authorization.allow_unauthenticated
|
|
1278
|
+
});
|
|
1279
|
+
state.pairings = pairings;
|
|
1280
|
+
state.pairingChallenges = new Map();
|
|
1281
|
+
return state;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export async function loadRelayStateFromFile(path, options = {}) {
|
|
1285
|
+
const filePath = assertString(path, 'relay state file path');
|
|
1286
|
+
let text;
|
|
1287
|
+
try {
|
|
1288
|
+
text = await readFile(filePath, 'utf8');
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
throw new Error(`unable to read relay state file: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
1291
|
+
}
|
|
1292
|
+
try {
|
|
1293
|
+
return hydrateRelayState(JSON.parse(text), options);
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
throw new Error(`relay state file rejected: ${error instanceof Error ? error.message : 'invalid state'}`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
export async function saveRelayStateToFile(state, path) {
|
|
1300
|
+
const filePath = assertString(path, 'relay state file path');
|
|
1301
|
+
const snapshot = serializeRelayState(state);
|
|
1302
|
+
const tempPath = join(dirname(filePath), `.${basename(filePath)}.${randomBytes(6).toString('hex')}.tmp`);
|
|
1303
|
+
try {
|
|
1304
|
+
await writeFile(tempPath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
|
|
1305
|
+
await rename(tempPath, filePath);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
await unlink(tempPath).catch(() => undefined);
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
return snapshot;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export function createRelayState(options = {}) {
|
|
1314
|
+
const now = options.now ?? options.created_at ?? options.createdAt;
|
|
1315
|
+
const node = options.node ?? createMeshNode({
|
|
1316
|
+
role: options.role ?? 'relay_witness',
|
|
1317
|
+
capabilities: ['relay_store', 'witness_checkpoint', 'pairing_challenge'],
|
|
1318
|
+
endpoints: options.endpoints ?? [],
|
|
1319
|
+
created_at: typeof now === 'string' ? now : DEFAULT_NOW
|
|
1320
|
+
});
|
|
1321
|
+
const state = {
|
|
1322
|
+
schema: SERVICE_SCHEMA,
|
|
1323
|
+
node,
|
|
1324
|
+
relayStore: options.relayStore ?? createRelayStore({ node, created_at: typeof now === 'string' ? now : DEFAULT_NOW }),
|
|
1325
|
+
witnessLog: Array.isArray(options.witnessLog) ? [...options.witnessLog] : [],
|
|
1326
|
+
pairingChallenges: new Map(),
|
|
1327
|
+
pairings: new Map(),
|
|
1328
|
+
now: options.now,
|
|
1329
|
+
challengeTtlMs: options.challengeTtlMs ?? DEFAULT_CHALLENGE_TTL_MS,
|
|
1330
|
+
allowUnauthenticated: options.allowUnauthenticated === true || options.allow_unauthenticated === true
|
|
1331
|
+
};
|
|
1332
|
+
Object.defineProperties(state, {
|
|
1333
|
+
mode: {
|
|
1334
|
+
value: firstDefined(readinessModeFromOptions(options), 'local'),
|
|
1335
|
+
writable: true,
|
|
1336
|
+
configurable: true
|
|
1337
|
+
},
|
|
1338
|
+
production_readiness: {
|
|
1339
|
+
value: relayProductionReadinessFromOptions(options),
|
|
1340
|
+
writable: true,
|
|
1341
|
+
configurable: true
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
return state;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export function createRelayServer(options = {}) {
|
|
1348
|
+
const state = options.state ?? createRelayState(options);
|
|
1349
|
+
const server = createServer((req, res) => {
|
|
1350
|
+
handleRelayRequest(state, req, res).catch((error) => {
|
|
1351
|
+
jsonResponse(res, 500, fail(error instanceof Error ? error.message : 'relay request failed'));
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
server.relayState = state;
|
|
1355
|
+
return server;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
export async function handleRelayRequest(state, req, res) {
|
|
1359
|
+
const method = req.method ?? 'GET';
|
|
1360
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
1361
|
+
|
|
1362
|
+
try {
|
|
1363
|
+
if (method === 'GET' && url.pathname === '/health') {
|
|
1364
|
+
jsonResponse(res, 200, ok({ health: serviceHealth(state) }));
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
if (method === 'GET' && url.pathname === '/livez') {
|
|
1369
|
+
jsonResponse(res, 200, ok({
|
|
1370
|
+
service: 'enigma-relay',
|
|
1371
|
+
status: 'live',
|
|
1372
|
+
process: { pid: process.pid }
|
|
1373
|
+
}));
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if (method === 'GET' && url.pathname === '/readyz') {
|
|
1378
|
+
const readiness = relayReadiness(state);
|
|
1379
|
+
jsonResponse(res, readiness.statusCode, readiness.body);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (method === 'POST' && url.pathname === '/relay/push') {
|
|
1384
|
+
const body = await readJsonBody(req);
|
|
1385
|
+
const authorization = requireRelayAuthorization(state, req, 'relay.push', relayRequestHash(method, url, body));
|
|
1386
|
+
const record = pushRelayRecord(state.relayStore, normalizeRelayRecordInput(body));
|
|
1387
|
+
jsonResponse(res, 201, ok({ record, authorization }));
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
if (method === 'GET' && url.pathname === '/relay/pull') {
|
|
1392
|
+
const id = url.searchParams.get('id');
|
|
1393
|
+
if (id === null || id.length === 0) {
|
|
1394
|
+
jsonResponse(res, 400, fail('id is required'));
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const authorization = requireRelayAuthorization(state, req, 'relay.pull', relayRequestHash(method, url));
|
|
1398
|
+
const record = pullRelayRecord(state.relayStore, id);
|
|
1399
|
+
if (record === null) {
|
|
1400
|
+
jsonResponse(res, 404, fail('relay record not found'));
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
jsonResponse(res, 200, ok({ record, authorization }));
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (method === 'POST' && url.pathname === '/witness/checkpoint') {
|
|
1408
|
+
const body = await readJsonBody(req);
|
|
1409
|
+
const authorization = requireRelayAuthorization(state, req, 'witness.checkpoint', relayRequestHash(method, url, body));
|
|
1410
|
+
const witnessBody = normalizeWitnessInput(body);
|
|
1411
|
+
const lastCheckpoint = state.witnessLog.at(-1);
|
|
1412
|
+
const suppliedEpoch = witnessBody.epoch;
|
|
1413
|
+
const epoch = suppliedEpoch === undefined ? (lastCheckpoint ? lastCheckpoint.epoch + 1 : 0) : suppliedEpoch;
|
|
1414
|
+
if (lastCheckpoint && (!Number.isInteger(epoch) || epoch <= lastCheckpoint.epoch)) {
|
|
1415
|
+
throw new Error('witness epoch must increase monotonically');
|
|
1416
|
+
}
|
|
1417
|
+
const expectedPreviousWitnessHash = lastCheckpoint?.witness_hash ?? EMPTY_MERKLE_ROOT;
|
|
1418
|
+
const suppliedPreviousWitnessHash = witnessBody.previous_witness_hash ?? witnessBody.previousWitnessHash;
|
|
1419
|
+
if (suppliedPreviousWitnessHash !== undefined && assertRoot(suppliedPreviousWitnessHash, 'previous_witness_hash') !== expectedPreviousWitnessHash) {
|
|
1420
|
+
throw new Error('previous_witness_hash continuity mismatch');
|
|
1421
|
+
}
|
|
1422
|
+
const checkpoint = createWitnessCheckpoint({
|
|
1423
|
+
...witnessBody,
|
|
1424
|
+
node: state.node,
|
|
1425
|
+
witness_node_id: state.node.node_id,
|
|
1426
|
+
epoch,
|
|
1427
|
+
previous_witness_hash: expectedPreviousWitnessHash,
|
|
1428
|
+
receipt_log_root: witnessBody.receipt_log_root ?? witnessBody.receiptLogRoot ?? EMPTY_MERKLE_ROOT,
|
|
1429
|
+
active_set_root: witnessBody.active_set_root ?? witnessBody.activeSetRoot ?? witnessBody.active_memory_root ?? witnessBody.activeMemoryRoot ?? EMPTY_MERKLE_ROOT,
|
|
1430
|
+
issued_at: witnessBody.issued_at ?? witnessBody.issuedAt ?? nowIso(state)
|
|
1431
|
+
});
|
|
1432
|
+
const verification = verifyWitnessCheckpoint(checkpoint, {
|
|
1433
|
+
trustDescriptor: state.node.trust_descriptor,
|
|
1434
|
+
expectedWitnessNodeId: state.node.node_id,
|
|
1435
|
+
expectedReceiptLogRoot: checkpoint.receipt_log_root,
|
|
1436
|
+
expectedActiveSetRoot: checkpoint.active_set_root,
|
|
1437
|
+
expectedCheckpointRoot: checkpoint.checkpoint_root,
|
|
1438
|
+
expectedPreviousWitnessHash,
|
|
1439
|
+
minimumEpoch: epoch
|
|
1440
|
+
});
|
|
1441
|
+
if (!verification.ok) throw new Error('witness checkpoint verification failed');
|
|
1442
|
+
state.witnessLog.push(Object.freeze({ ...checkpoint }));
|
|
1443
|
+
jsonResponse(res, 201, ok({ checkpoint, verification, authorization }));
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (method === 'GET' && url.pathname === '/witness/log') {
|
|
1448
|
+
jsonResponse(res, 200, ok({ checkpoints: state.witnessLog.map((checkpoint) => ({ ...checkpoint })) }));
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (method === 'POST' && url.pathname === '/pairing/challenge') {
|
|
1453
|
+
const challenge = createPairingChallenge(state, await readJsonBody(req));
|
|
1454
|
+
jsonResponse(res, 201, ok({ challenge }));
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (method === 'POST' && url.pathname === '/pairing/complete') {
|
|
1459
|
+
const pairing = completePairing(state, await readJsonBody(req));
|
|
1460
|
+
jsonResponse(res, 201, ok({ pairing }));
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
jsonResponse(res, 404, fail('unknown relay path'));
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
jsonResponse(res, 400, fail(error instanceof Error ? error.message : 'bad relay request'));
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
export function runRelayDemo(options = {}) {
|
|
1471
|
+
const state = createRelayState({ now: options.now ?? DEFAULT_NOW });
|
|
1472
|
+
const opaquePayload = options.opaque_encrypted_record ?? 'age1-encrypted-demo-capsule-only';
|
|
1473
|
+
const relayRecord = pushRelayRecord(state.relayStore, normalizeRelayRecordInput({
|
|
1474
|
+
capsule_id: 'cap_demo_relay',
|
|
1475
|
+
opaque_encrypted_record: opaquePayload,
|
|
1476
|
+
received_at: options.now ?? DEFAULT_NOW
|
|
1477
|
+
}));
|
|
1478
|
+
const pulled = pullRelayRecord(state.relayStore, relayRecord.record_id);
|
|
1479
|
+
|
|
1480
|
+
let rejectedPlaintext = false;
|
|
1481
|
+
try {
|
|
1482
|
+
pushRelayRecord(state.relayStore, normalizeRelayRecordInput({
|
|
1483
|
+
plaintext: 'demo memory must never enter relay custody',
|
|
1484
|
+
opaque_encrypted_record: opaquePayload
|
|
1485
|
+
}));
|
|
1486
|
+
} catch {
|
|
1487
|
+
rejectedPlaintext = true;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const checkpoint = createWitnessCheckpoint({
|
|
1491
|
+
node: state.node,
|
|
1492
|
+
witness_node_id: state.node.node_id,
|
|
1493
|
+
subject_node_id: 'subject:demo',
|
|
1494
|
+
receipt_log_root: EMPTY_MERKLE_ROOT,
|
|
1495
|
+
active_set_root: EMPTY_MERKLE_ROOT,
|
|
1496
|
+
issued_at: options.now ?? DEFAULT_NOW,
|
|
1497
|
+
epoch: 1
|
|
1498
|
+
});
|
|
1499
|
+
const witnessVerification = verifyWitnessCheckpoint(checkpoint, {
|
|
1500
|
+
trustDescriptor: state.node.trust_descriptor,
|
|
1501
|
+
expectedWitnessNodeId: state.node.node_id,
|
|
1502
|
+
expectedReceiptLogRoot: EMPTY_MERKLE_ROOT,
|
|
1503
|
+
expectedActiveSetRoot: EMPTY_MERKLE_ROOT,
|
|
1504
|
+
expectedCheckpointRoot: checkpoint.checkpoint_root
|
|
1505
|
+
});
|
|
1506
|
+
state.witnessLog.push(Object.freeze({ ...checkpoint }));
|
|
1507
|
+
|
|
1508
|
+
const client = generateSigningKeyPair();
|
|
1509
|
+
const challenge = createPairingChallenge(state, { public_key: client.publicKey });
|
|
1510
|
+
const challengePayload = state.pairingChallenges.get(challenge.challenge_id).challenge;
|
|
1511
|
+
const clientSignature = {
|
|
1512
|
+
alg: 'Ed25519',
|
|
1513
|
+
key_id: client.key_id,
|
|
1514
|
+
value: signPayload({ payload: challengePayload, privateKey: client.privateKey })
|
|
1515
|
+
};
|
|
1516
|
+
const pairing = completePairing(state, {
|
|
1517
|
+
challenge_id: challenge.challenge_id,
|
|
1518
|
+
public_key: client.publicKey,
|
|
1519
|
+
signature: clientSignature
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
const relayOpaqueOnly = relayRecord.payload_storage === 'hash_only'
|
|
1523
|
+
&& typeof relayRecord.encrypted_payload_hash === 'string'
|
|
1524
|
+
&& pulled?.encrypted_payload_hash === relayRecord.encrypted_payload_hash
|
|
1525
|
+
&& !hasPlaintextLookingField(relayRecord)
|
|
1526
|
+
&& !Object.prototype.hasOwnProperty.call(relayRecord, 'opaque_encrypted_record')
|
|
1527
|
+
&& !Object.prototype.hasOwnProperty.call(relayRecord, 'plaintext');
|
|
1528
|
+
const pairingChallengeOk = typeof challenge.challenge_id === 'string';
|
|
1529
|
+
const pairingOk = pairingChallengeOk
|
|
1530
|
+
&& typeof pairing.pairing_id === 'string'
|
|
1531
|
+
&& pairing.challenge_id === challenge.challenge_id;
|
|
1532
|
+
const okValue = Boolean(
|
|
1533
|
+
relayRecord.record_id
|
|
1534
|
+
&& pulled?.record_id === relayRecord.record_id
|
|
1535
|
+
&& relayOpaqueOnly
|
|
1536
|
+
&& rejectedPlaintext
|
|
1537
|
+
&& witnessVerification.ok
|
|
1538
|
+
&& pairingOk
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
return {
|
|
1542
|
+
ok: okValue,
|
|
1543
|
+
pushed_opaque_record: relayOpaqueOnly,
|
|
1544
|
+
relay_record_id: relayRecord.record_id,
|
|
1545
|
+
rejected_plaintext_record: rejectedPlaintext,
|
|
1546
|
+
witness_checkpoint_verification_ok: witnessVerification.ok,
|
|
1547
|
+
pairing_challenge_ok: pairingChallengeOk,
|
|
1548
|
+
pairing_complete_ok: pairingOk,
|
|
1549
|
+
relay: {
|
|
1550
|
+
pushed_opaque_record: relayOpaqueOnly,
|
|
1551
|
+
record_id: relayRecord.record_id,
|
|
1552
|
+
pulled: pulled?.record_id === relayRecord.record_id,
|
|
1553
|
+
rejected_plaintext_record: rejectedPlaintext
|
|
1554
|
+
},
|
|
1555
|
+
witness: {
|
|
1556
|
+
checkpoint_verification_ok: witnessVerification.ok,
|
|
1557
|
+
witness_checkpoint_id: checkpoint.witness_checkpoint_id,
|
|
1558
|
+
witness_hash: checkpoint.witness_hash
|
|
1559
|
+
},
|
|
1560
|
+
pairing: {
|
|
1561
|
+
challenge_ok: pairingChallengeOk,
|
|
1562
|
+
complete_ok: pairingOk,
|
|
1563
|
+
challenge_id: challenge.challenge_id,
|
|
1564
|
+
pairing_id: pairing.pairing_id
|
|
1565
|
+
},
|
|
1566
|
+
node: {
|
|
1567
|
+
node_id: state.node.node_id,
|
|
1568
|
+
trust_descriptor: state.node.trust_descriptor
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
}
|