@synoi/gateway-lite 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ /**
3
+ * gap/cited-oracle-inputs.ts — cited_oracle_inputs[] receipt schema.
4
+ *
5
+ * Spec: DEMO_PREWORK_SCHEMAS_2026-06-17.md Specification 1 (Sections 1.1-1.9).
6
+ * Status: PARTIAL (visible-receipt citation of oracle inputs SHIPPED for
7
+ * invocation and denial receipt subtypes; freshness-budget and cache-cursor
8
+ * primitives remain PAPER, deferred to ADR_005 revision task #37).
9
+ *
10
+ * PII note (Section 1.8 + ADR_009 Section 9):
11
+ * raw_value for "ofac" subject_type carries the recipient name (GDPR/CCPA).
12
+ * raw_value for "sms_hitl" carries the E.164 phone number.
13
+ * These are tenant-partitioned under tenant:steve-demo-<date> per #47
14
+ * confirmation. raw_value MUST NOT be logged at INFO level; it is only
15
+ * present inside the hybrid-signed receipt body for verifier use.
16
+ *
17
+ * Canonicalization rule (Section 1.3.5):
18
+ * value_hash = sha256("sha256:" prefix NOT included in hash input) over
19
+ * the JCS-canonical form of raw_value. Concretely:
20
+ * value_hash = "sha256:" + sha256hex(canonicalize(raw_value))
21
+ * The gate verifies this before signing; mismatch fails closed with
22
+ * CITED_ORACLE_HASH_MISMATCH error code.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.ORACLE_SUBJECT_TYPES = void 0;
26
+ exports.canonicalizeRawValue = canonicalizeRawValue;
27
+ exports.computeValueHash = computeValueHash;
28
+ exports.verifyValueHash = verifyValueHash;
29
+ exports.validateCitedOracleInput = validateCitedOracleInput;
30
+ exports.validateCitedOracleInputs = validateCitedOracleInputs;
31
+ exports.buildCitedOracleEntry = buildCitedOracleEntry;
32
+ const node_crypto_1 = require("node:crypto");
33
+ const sraid_1 = require("@synoi/sraid");
34
+ exports.ORACLE_SUBJECT_TYPES = new Set([
35
+ 'weather',
36
+ 'ofac',
37
+ 'time',
38
+ 'sms_hitl',
39
+ 'webhook',
40
+ 'cve',
41
+ 'incident_status',
42
+ ]);
43
+ // ── Allowed known keys (strict mode per Section 1.1) ─────────────────────────
44
+ const REQUIRED_KEYS = new Set([
45
+ 'subject_type',
46
+ 'value_hash',
47
+ 'raw_value',
48
+ 'fetched_at',
49
+ 'source_url',
50
+ ]);
51
+ const OPTIONAL_KEYS = new Set([
52
+ 'feed_claimed_at',
53
+ 'source_cursor',
54
+ ]);
55
+ const ALL_KNOWN_KEYS = new Set([
56
+ ...REQUIRED_KEYS,
57
+ ...OPTIONAL_KEYS,
58
+ ]);
59
+ // ── Canonicalization helper (Section 1.3.5) ───────────────────────────────────
60
+ /**
61
+ * Produce the JCS-canonical string form of a raw_value object.
62
+ * This is the exact string whose SHA-256 is used for value_hash.
63
+ * Uses @synoi/sraid canonicalize (RFC 8785 conformant).
64
+ */
65
+ function canonicalizeRawValue(raw_value) {
66
+ return (0, sraid_1.canonicalize)(raw_value);
67
+ }
68
+ // ── value_hash computation ────────────────────────────────────────────────────
69
+ /**
70
+ * Compute value_hash = "sha256:" + sha256hex(canonicalize(raw_value)).
71
+ *
72
+ * This is the hash the gate embeds in each CitedOracleInput entry before
73
+ * signing the receipt. The verifier calls verifyValueHash() to confirm
74
+ * the embedded hash still matches the embedded raw_value.
75
+ */
76
+ function computeValueHash(raw_value) {
77
+ const canonical = canonicalizeRawValue(raw_value);
78
+ const hex = (0, node_crypto_1.createHash)('sha256').update(canonical, 'utf8').digest('hex');
79
+ return 'sha256:' + hex;
80
+ }
81
+ // ── value_hash verification (gate pre-signing check + verifier check) ─────────
82
+ /**
83
+ * Recompute value_hash from raw_value and compare to the embedded hash.
84
+ *
85
+ * Returns true on match, throws on mismatch.
86
+ * Fail-closed: used by the gate before signing the receipt (Section 2.5
87
+ * adapter_value_hash_mismatch) and by the verifier (Section 1.3.5).
88
+ *
89
+ * Error code on mismatch: "adapter_value_hash_mismatch" for gate use,
90
+ * "integrity_error: ...value_hash does not match sha256(JCS(raw_value))"
91
+ * for verifier use (Section 1.7.2 negative #2).
92
+ */
93
+ function verifyValueHash(entry, index) {
94
+ const expected = computeValueHash(entry.raw_value);
95
+ if (entry.value_hash !== expected) {
96
+ throw new Error(`integrity_error: cited_oracle_inputs[${index}].value_hash does not match sha256(JCS(raw_value))` +
97
+ ` (embedded=${entry.value_hash}, computed=${expected})`);
98
+ }
99
+ return true;
100
+ }
101
+ // ── Strict-mode schema validation (Section 1.1 + Section 1.7.2) ──────────────
102
+ /** RFC 3339 UTC ms pattern: must end with Z, must have ms component. */
103
+ const RFC3339_UTC_MS_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
104
+ /**
105
+ * Validate a single cited_oracle_inputs[] entry in strict mode.
106
+ *
107
+ * Checks:
108
+ * - All required fields present (Section 1.7.2 negative #1).
109
+ * - No unknown keys (Section 1.7.2 negative #3 -- strict mode).
110
+ * - null forbidden for optional fields (Section 1.3.3).
111
+ * - subject_type is a known enum value.
112
+ * - value_hash format is "sha256:" + 64 hex chars.
113
+ * - fetched_at and feed_claimed_at match RFC 3339 UTC ms.
114
+ * - value_hash matches sha256(JCS(raw_value)) (Section 1.3.5).
115
+ *
116
+ * Does NOT validate per-subject_type raw_value structure beyond type presence.
117
+ * That validation is left to per-adapter code.
118
+ *
119
+ * @param entry - the raw parsed object (unknown type for safety).
120
+ * @param index - array index for error messages.
121
+ * @returns the typed CitedOracleInput on success; throws CitedOracleError on failure.
122
+ */
123
+ function validateCitedOracleInput(entry, index) {
124
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
125
+ throw {
126
+ code: 'schema_error',
127
+ message: `cited_oracle_inputs[${index}] must be a non-null object`,
128
+ };
129
+ }
130
+ const obj = entry;
131
+ // Unknown key check (strict mode).
132
+ for (const k of Object.keys(obj)) {
133
+ if (!ALL_KNOWN_KEYS.has(k)) {
134
+ throw {
135
+ code: 'schema_error',
136
+ message: `cited_oracle_inputs[${index}] unknown key '${k}'`,
137
+ };
138
+ }
139
+ }
140
+ // Required fields.
141
+ for (const k of REQUIRED_KEYS) {
142
+ if (!(k in obj) || obj[k] === undefined) {
143
+ throw {
144
+ code: 'schema_error',
145
+ message: `cited_oracle_inputs[${index}].${k} missing`,
146
+ };
147
+ }
148
+ }
149
+ // null forbidden for optional fields.
150
+ for (const k of OPTIONAL_KEYS) {
151
+ if (k in obj && obj[k] === null) {
152
+ throw {
153
+ code: 'schema_error',
154
+ message: `cited_oracle_inputs[${index}].${k} must not be null; omit the key instead`,
155
+ };
156
+ }
157
+ }
158
+ const subject_type = obj['subject_type'];
159
+ if (typeof subject_type !== 'string' || !exports.ORACLE_SUBJECT_TYPES.has(subject_type)) {
160
+ throw {
161
+ code: 'schema_error',
162
+ message: `cited_oracle_inputs[${index}].subject_type must be one of: ${[...exports.ORACLE_SUBJECT_TYPES].join(', ')}; got ${JSON.stringify(subject_type)}`,
163
+ };
164
+ }
165
+ const value_hash = obj['value_hash'];
166
+ if (typeof value_hash !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value_hash)) {
167
+ throw {
168
+ code: 'schema_error',
169
+ message: `cited_oracle_inputs[${index}].value_hash must be "sha256:" followed by 64 lowercase hex characters`,
170
+ };
171
+ }
172
+ const raw_value = obj['raw_value'];
173
+ if (typeof raw_value !== 'object' || raw_value === null || Array.isArray(raw_value)) {
174
+ throw {
175
+ code: 'schema_error',
176
+ message: `cited_oracle_inputs[${index}].raw_value must be a non-null object`,
177
+ };
178
+ }
179
+ const fetched_at = obj['fetched_at'];
180
+ if (typeof fetched_at !== 'string' || !RFC3339_UTC_MS_RE.test(fetched_at)) {
181
+ throw {
182
+ code: 'schema_error',
183
+ message: `cited_oracle_inputs[${index}].fetched_at must be RFC 3339 UTC with milliseconds (e.g. 2026-06-17T19:32:18.412Z)`,
184
+ };
185
+ }
186
+ const source_url = obj['source_url'];
187
+ if (typeof source_url !== 'string' || source_url.trim() === '') {
188
+ throw {
189
+ code: 'schema_error',
190
+ message: `cited_oracle_inputs[${index}].source_url must be a non-empty string`,
191
+ };
192
+ }
193
+ if ('feed_claimed_at' in obj && obj['feed_claimed_at'] !== undefined) {
194
+ const fca = obj['feed_claimed_at'];
195
+ if (typeof fca !== 'string' || !RFC3339_UTC_MS_RE.test(fca)) {
196
+ throw {
197
+ code: 'schema_error',
198
+ message: `cited_oracle_inputs[${index}].feed_claimed_at must be RFC 3339 UTC with milliseconds when present`,
199
+ };
200
+ }
201
+ }
202
+ if ('source_cursor' in obj && obj['source_cursor'] !== undefined) {
203
+ if (typeof obj['source_cursor'] !== 'string') {
204
+ throw {
205
+ code: 'schema_error',
206
+ message: `cited_oracle_inputs[${index}].source_cursor must be a string when present`,
207
+ };
208
+ }
209
+ }
210
+ // Integrity check: value_hash must equal sha256(JCS(raw_value)).
211
+ try {
212
+ verifyValueHash({ raw_value: raw_value, value_hash: value_hash }, index);
213
+ }
214
+ catch (err) {
215
+ throw {
216
+ code: 'integrity_error',
217
+ message: err instanceof Error ? err.message : String(err),
218
+ };
219
+ }
220
+ const result = {
221
+ subject_type: subject_type,
222
+ value_hash: value_hash,
223
+ raw_value: raw_value,
224
+ fetched_at: fetched_at,
225
+ source_url: source_url,
226
+ };
227
+ if ('feed_claimed_at' in obj && typeof obj['feed_claimed_at'] === 'string') {
228
+ result.feed_claimed_at = obj['feed_claimed_at'];
229
+ }
230
+ if ('source_cursor' in obj && typeof obj['source_cursor'] === 'string') {
231
+ result.source_cursor = obj['source_cursor'];
232
+ }
233
+ return result;
234
+ }
235
+ /**
236
+ * Validate an entire cited_oracle_inputs array.
237
+ * Returns the typed array on success; throws CitedOracleError on first failure.
238
+ * Array order is preserved (Section 1.3.2).
239
+ * Empty array and omission are both accepted (Section 1.3.4).
240
+ */
241
+ function validateCitedOracleInputs(inputs) {
242
+ if (inputs === undefined || inputs === null)
243
+ return [];
244
+ if (!Array.isArray(inputs)) {
245
+ throw {
246
+ code: 'schema_error',
247
+ message: 'cited_oracle_inputs must be an array when present',
248
+ };
249
+ }
250
+ return inputs.map((entry, i) => validateCitedOracleInput(entry, i));
251
+ }
252
+ // ── Receipt integration helpers ───────────────────────────────────────────────
253
+ /**
254
+ * Build a CitedOracleInput entry from adapter-produced fields.
255
+ *
256
+ * The gate calls this after each oracle fetch and before signing the receipt.
257
+ * If value_hash is provided (from the adapter), it is verified; if not, it is
258
+ * computed here so the adapter does not have to.
259
+ *
260
+ * Fails closed with an error if the provided hash mismatches (Section 2.5:
261
+ * programmer error in the adapter -- emit denial with adapter_value_hash_mismatch).
262
+ */
263
+ function buildCitedOracleEntry(params) {
264
+ const computed_hash = computeValueHash(params.raw_value);
265
+ if (params.value_hash !== undefined && params.value_hash !== computed_hash) {
266
+ throw new Error(`adapter_value_hash_mismatch: subject_type=${params.subject_type} ` +
267
+ `adapter_hash=${params.value_hash} computed=${computed_hash}`);
268
+ }
269
+ const entry = {
270
+ subject_type: params.subject_type,
271
+ value_hash: computed_hash,
272
+ raw_value: params.raw_value,
273
+ fetched_at: params.fetched_at,
274
+ source_url: params.source_url,
275
+ };
276
+ if (params.feed_claimed_at !== undefined) {
277
+ entry.feed_claimed_at = params.feed_claimed_at;
278
+ }
279
+ if (params.source_cursor !== undefined) {
280
+ entry.source_cursor = params.source_cursor;
281
+ }
282
+ return entry;
283
+ }