@mcpherson-ai/observa-local-node 0.1.3
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/README.md +103 -0
- package/SHA256SUMS +33 -0
- package/artifacts/mcphersonai-observa-adapter-n8n-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-domain-generic-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-hosted-transport-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-n8n-h1-binding-0.1.0.tgz +0 -0
- package/artifacts/mcphersonai-observa-node-0.1.0.tgz +0 -0
- package/distribution/observa-cli/bin/observa-n8n-hosted.mjs +114 -0
- package/distribution/observa-cli/bin/observa.mjs +331 -0
- package/distribution/observa-cli/integrations/n8n/observa-external-hook.cjs +132 -0
- package/distribution/observa-cli/keys/observa-beta-1.public.json +7 -0
- package/distribution/observa-cli/src/artifact.mjs +143 -0
- package/distribution/observa-cli/src/config.mjs +162 -0
- package/distribution/observa-cli/src/errors.mjs +21 -0
- package/distribution/observa-cli/src/hosted-delivery.mjs +148 -0
- package/distribution/observa-cli/src/index.mjs +39 -0
- package/distribution/observa-cli/src/install.mjs +443 -0
- package/distribution/observa-cli/src/lock.mjs +57 -0
- package/distribution/observa-cli/src/manifest-schema.mjs +208 -0
- package/distribution/observa-cli/src/manifest-verify.mjs +122 -0
- package/distribution/observa-cli/src/n8n-hook.mjs +70 -0
- package/distribution/observa-cli/src/pair.mjs +149 -0
- package/distribution/observa-cli/src/re-pair.mjs +171 -0
- package/distribution/observa-cli/src/service.mjs +218 -0
- package/distribution/observa-cli/src/state.mjs +87 -0
- package/distribution/observa-cli/src/status.mjs +191 -0
- package/distribution/observa-cli/src/vocabulary.mjs +47 -0
- package/npm-distribution-provenance.json +1 -0
- package/package.json +36 -0
- package/runtime-adapters/n8n/src/strict-json.mjs +138 -0
- package/sdk/contracts/canonical.mjs +289 -0
- package/sdk/contracts/entry-boundary.mjs +417 -0
- package/sdk/contracts/errors.mjs +334 -0
- package/sdk/contracts/stable-primitives.mjs +141 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// THE CLOSED REFUSAL BOUNDARY.
|
|
2
|
+
//
|
|
3
|
+
// A public error object cannot safely be a durable provenance token: callers
|
|
4
|
+
// can capture it, decorate it, and replay it through a later callback. Every
|
|
5
|
+
// foreign-code boundary therefore emits a *fresh*, code-only refusal instead
|
|
6
|
+
// of passing any previously thrown object through. Internal validation keeps
|
|
7
|
+
// its specific codes by placing narrow `runUntrusted` calls only around the
|
|
8
|
+
// reflection/callback operation that is actually foreign.
|
|
9
|
+
//
|
|
10
|
+
// The object has no caller-controlled detail channel. Its message and
|
|
11
|
+
// detail fields are code-only and immutable; the object itself is frozen.
|
|
12
|
+
// Native Promise results
|
|
13
|
+
// are observed then refused because this synchronous contract cannot allow a
|
|
14
|
+
// later rejection to leak caller text after its closed result has returned.
|
|
15
|
+
|
|
16
|
+
// EVERY CODE THIS PACKAGE MAY EMIT, in one closed list.
|
|
17
|
+
//
|
|
18
|
+
// Derived mechanically from the source set — every literal in a code-carrying
|
|
19
|
+
// argument position of `refuse`, `refuseOnRawThrow`, `runUntrusted`,
|
|
20
|
+
// `snapshotRecord`, `exactKeys`, `entryRecord`, `entryArray`,
|
|
21
|
+
// `assertVocabularyValue`, `validateEventPayload` and their siblings, plus the
|
|
22
|
+
// node's entry-code table and the critical section's phase table.
|
|
23
|
+
//
|
|
24
|
+
// FINAL CLOSURE, INDEPENDENT REVIEW #1 — THAT CLAIM USED TO BE FALSE IN ALL
|
|
25
|
+
// THREE PARTS. The list was not derived, nothing re-derived it, and it was
|
|
26
|
+
// wrong in BOTH directions: `PROJECTION_EVENT_PAYLOAD_INVALID` was emitted by
|
|
27
|
+
// the public `projectStateAt` and not declared here, while
|
|
28
|
+
// `EVIDENCE_OUTSIDE_PROFILE_VOCABULARY` was declared and emitted nowhere. The
|
|
29
|
+
// undeclared one escaped because it is bound to a module const and passed by
|
|
30
|
+
// NAME, so even a literal-only scan would have missed it. What stood in for the
|
|
31
|
+
// guard was a test that called six hand-picked methods and asserted their codes
|
|
32
|
+
// were members — it could not see either divergence.
|
|
33
|
+
//
|
|
34
|
+
// `error-provenance.test.mjs` now performs the derivation this comment
|
|
35
|
+
// describes, resolving single-assignment string consts, and fails on a set
|
|
36
|
+
// difference in EITHER direction. A code added to the code without being
|
|
37
|
+
// declared here fails the suite rather than silently becoming
|
|
38
|
+
// `REFUSAL_CODE_UNDECLARED` at runtime.
|
|
39
|
+
export const REFUSAL_CODES = Object.freeze([
|
|
40
|
+
"ACTION_ALREADY_ATTEMPTED",
|
|
41
|
+
"ADAPTER_CAPABILITIES_INVALID",
|
|
42
|
+
"ADAPTER_CAPABILITY_UNSATISFIED",
|
|
43
|
+
"ADAPTER_CONTRACT_REQUIRED",
|
|
44
|
+
"ADAPTER_DECLARATION_INVALID",
|
|
45
|
+
"ADAPTER_ID_INVALID",
|
|
46
|
+
"ADAPTER_METHOD_NOT_IMPLEMENTED",
|
|
47
|
+
"ADAPTER_VERSION_INVALID",
|
|
48
|
+
"AUTHORITY_BINDING_FIELDS_INVALID",
|
|
49
|
+
"AUTHORITY_BINDING_ID_INVALID",
|
|
50
|
+
"AUTHORITY_BINDING_NOT_REGISTERED",
|
|
51
|
+
"AUTHORITY_BINDING_NOT_SUCCESSOR",
|
|
52
|
+
"AUTHORITY_BINDING_OPTIONS_INVALID",
|
|
53
|
+
"AUTHORITY_BINDING_SUPERSEDED",
|
|
54
|
+
"AUTHORITY_BINDING_TAMPERED",
|
|
55
|
+
"BINDING_ALREADY_SUPERSEDED",
|
|
56
|
+
"BINDING_OUTSIDE_PROFILE_VOCABULARY",
|
|
57
|
+
"BINDING_PROFILE_MISMATCH",
|
|
58
|
+
"BINDING_REVISION_PROFILE_MISMATCH",
|
|
59
|
+
"BINDING_REVISION_REASON_INVALID",
|
|
60
|
+
"BINDING_REVISION_REASON_UNEXPECTED",
|
|
61
|
+
"BINDING_REVISION_TENANT_MISMATCH",
|
|
62
|
+
"BOUND_INTENT_FIELDS_INVALID",
|
|
63
|
+
"BOUND_INTENT_ID_INVALID",
|
|
64
|
+
"BOUND_INTENT_OPTIONS_INVALID",
|
|
65
|
+
"BOUND_INTENT_TAMPERED",
|
|
66
|
+
"CAPABILITY_NOT_DECLARED",
|
|
67
|
+
"CLOCK_INVALID",
|
|
68
|
+
"CLOCK_REQUIRED",
|
|
69
|
+
"COMPARISON_INPUT_INVALID",
|
|
70
|
+
"CORRECTION_AUTHORITY_MISMATCH",
|
|
71
|
+
"CORRECTION_ERA_MISMATCH",
|
|
72
|
+
"CORRECTION_REVISION_NOT_DISTINCT",
|
|
73
|
+
"CORRECTION_RULE_INVALID",
|
|
74
|
+
"CORRECTION_RULE_NOT_REGISTERED",
|
|
75
|
+
"CORRECTION_SEMANTICS_MISSING",
|
|
76
|
+
"CORRECTION_SOURCE_MISMATCH",
|
|
77
|
+
"CORRECTION_TARGET_REQUIRED",
|
|
78
|
+
"CROSS_INTENT_SUPERSESSION_REFUSED",
|
|
79
|
+
"CROSS_TENANT_REFUSED",
|
|
80
|
+
"ENTRY_POINT_NOT_DECLARED",
|
|
81
|
+
"ENVELOPE_ACCESSOR_FIELD",
|
|
82
|
+
"ENVELOPE_BINDING_MISMATCH",
|
|
83
|
+
"ENVELOPE_CONTEXT_INVALID",
|
|
84
|
+
"ENVELOPE_DESCRIPTOR_INVALID",
|
|
85
|
+
"ENVELOPE_INCONSISTENT",
|
|
86
|
+
"ENVELOPE_INVALID_ENUM",
|
|
87
|
+
"ENVELOPE_INVALID_FORMAT",
|
|
88
|
+
"ENVELOPE_INVALID_TYPE",
|
|
89
|
+
"ENVELOPE_MISSING_FIELD",
|
|
90
|
+
"ENVELOPE_NESTED_OBJECT",
|
|
91
|
+
"ENVELOPE_NOT_PLAIN_OBJECT",
|
|
92
|
+
"ENVELOPE_OUTSIDE_PROFILE_VOCABULARY",
|
|
93
|
+
"ENVELOPE_PROJECTION_INVALID",
|
|
94
|
+
"ENVELOPE_SERIALIZED_INPUT",
|
|
95
|
+
"ENVELOPE_SYMBOL_FIELD",
|
|
96
|
+
"ENVELOPE_UNKNOWN_FIELD",
|
|
97
|
+
"EVIDENCE_APPEND_FIELDS_INVALID",
|
|
98
|
+
"EVIDENCE_APPEND_OPTIONS_INVALID",
|
|
99
|
+
"EVIDENCE_BEFORE_ACTION",
|
|
100
|
+
"EVIDENCE_ERA_INVALID",
|
|
101
|
+
"EVIDENCE_FINGERPRINT_INVALID",
|
|
102
|
+
"EVIDENCE_IDENTITY_CONFLICT",
|
|
103
|
+
"EVIDENCE_LOG_GENERATION_INVALID",
|
|
104
|
+
"EVIDENCE_LOG_OPTIONS_INVALID",
|
|
105
|
+
"EVIDENCE_LOG_STALE",
|
|
106
|
+
"EVIDENCE_PAYLOAD_INVALID",
|
|
107
|
+
"EVIDENCE_PAYLOAD_REQUIRED",
|
|
108
|
+
"EVIDENCE_READ_EPOCH_INVALID",
|
|
109
|
+
"EVIDENCE_RECORDED_AT_INVALID",
|
|
110
|
+
"EVIDENCE_RELATION_INVALID",
|
|
111
|
+
"EVIDENCE_SOURCE_INVALID",
|
|
112
|
+
"EVIDENCE_SOURCE_OUTSIDE_PROFILE_VOCABULARY",
|
|
113
|
+
"EVIDENCE_SOURCE_REQUIRED",
|
|
114
|
+
"INTENT_ALREADY_REVISED",
|
|
115
|
+
"INTENT_OUTSIDE_PROFILE_VOCABULARY",
|
|
116
|
+
"INTENT_PROFILE_MISMATCH",
|
|
117
|
+
"INTENT_REVISION_PROFILE_MISMATCH",
|
|
118
|
+
"INTENT_REVISION_REASON_INVALID",
|
|
119
|
+
"INTENT_REVISION_REASON_UNEXPECTED",
|
|
120
|
+
"INTENT_REVISION_TENANT_MISMATCH",
|
|
121
|
+
"INVALID_LOCAL_TENANT",
|
|
122
|
+
"NODE_APPEND_DURING_EVALUATION",
|
|
123
|
+
"NODE_CRITICAL_STATE_MOVED",
|
|
124
|
+
"NODE_METHOD_NOT_IMPLEMENTED",
|
|
125
|
+
"NODE_QUERY_INVALID",
|
|
126
|
+
"NODE_REENTRANT_APPEND",
|
|
127
|
+
"NODE_REENTRANT_OPERATION",
|
|
128
|
+
"NODE_SHAPE_INVALID",
|
|
129
|
+
"NODE_SURFACE_EMITS_AUTHORITY",
|
|
130
|
+
"NON_EVIDENCE_RELATION_REFUSED",
|
|
131
|
+
"NO_APPLICABLE_EVIDENCE",
|
|
132
|
+
"PRIOR_EVIDENCE_APPLICABILITY_REQUIRED",
|
|
133
|
+
"PRIOR_EVIDENCE_APPLICABILITY_UNEXPECTED",
|
|
134
|
+
"PROFILE_BEHAVIOUR_NOT_INSPECTABLE",
|
|
135
|
+
"PROFILE_CLASSIFICATION_INVALID",
|
|
136
|
+
"PROFILE_CONTRACT_VERSION_MISMATCH",
|
|
137
|
+
"PROFILE_DESCRIPTOR_INVALID",
|
|
138
|
+
"PROFILE_EVALUATION_FAILED",
|
|
139
|
+
"PROFILE_NOT_REGISTERED",
|
|
140
|
+
"PROFILE_OUTCOME_CAUSE_REQUIRED",
|
|
141
|
+
"PROFILE_OUTCOME_INCONSISTENT",
|
|
142
|
+
"PROFILE_OUTCOME_INVALID",
|
|
143
|
+
"PROFILE_OUTCOME_OWNER_REQUIRED",
|
|
144
|
+
"PROFILE_OUTCOME_OWNER_UNKNOWN",
|
|
145
|
+
"PROFILE_OUTCOME_REASON_UNKNOWN",
|
|
146
|
+
"PROFILE_REASON_CODE_COLLISION",
|
|
147
|
+
"PROFILE_SHAPE_INVALID",
|
|
148
|
+
"PROFILE_VERSION_REWRITE_ATTEMPT",
|
|
149
|
+
"PROJECTION_EVENTS_INVALID",
|
|
150
|
+
"PROJECTION_EVENT_INVALID",
|
|
151
|
+
"PROJECTION_EVENT_PAYLOAD_INVALID",
|
|
152
|
+
"PROJECTION_INTENT_MISMATCH",
|
|
153
|
+
"PROJECTION_RECOVERY_INCOMPLETE",
|
|
154
|
+
"PROJECTION_SEQUENCE_GAP",
|
|
155
|
+
"PROJECTION_SEQUENCE_INVALID",
|
|
156
|
+
"PROJECTION_STATE_INVALID",
|
|
157
|
+
"READBACK_DEADLINE_NOT_REACHED",
|
|
158
|
+
"READBACK_NOT_PENDING",
|
|
159
|
+
"READBACK_POLICY_INVALID",
|
|
160
|
+
"READBACK_POLICY_REQUIRED",
|
|
161
|
+
"READBACK_STILL_PENDING",
|
|
162
|
+
"RECOVERY_CAUSE_REQUIRED",
|
|
163
|
+
"RECOVERY_CAUSE_UNEXPECTED",
|
|
164
|
+
"RECOVERY_CAUSE_UNKNOWN",
|
|
165
|
+
"RECOVERY_IN_PROGRESS",
|
|
166
|
+
"RECOVERY_NOT_APPLICABLE",
|
|
167
|
+
"RECOVERY_NOT_OPEN",
|
|
168
|
+
"RECOVERY_OWNER_UNKNOWN",
|
|
169
|
+
// ROUND-2 REVIEW #1, H1R2-04. A verdict may not close a repair that no fresh
|
|
170
|
+
// evidence resolved. Distinct from RECOVERY_IN_PROGRESS, which answers the
|
|
171
|
+
// narrower "somebody is already working on it".
|
|
172
|
+
"RECOVERY_UNRESOLVED",
|
|
173
|
+
// ROUND-2 REVIEW #1, H1R2-01. The fallback for a code this package never
|
|
174
|
+
// declared: the only way a non-member string can leave this file.
|
|
175
|
+
"REFUSAL_CODE_UNDECLARED",
|
|
176
|
+
"RELATION_SUPERSESSION_MISMATCH",
|
|
177
|
+
"RUNTIME_CLASS_INVALID",
|
|
178
|
+
"SUPERSESSION_TARGET_ALREADY_SUPERSEDED",
|
|
179
|
+
"SUPERSESSION_TARGET_NOT_EVIDENCE",
|
|
180
|
+
"SUPERSESSION_TARGET_UNKNOWN",
|
|
181
|
+
"TOLERANCE_SPEC_INVALID",
|
|
182
|
+
"TOLERANCE_SPEC_REQUIRED",
|
|
183
|
+
"UNKNOWN_BOUND_INTENT",
|
|
184
|
+
"UNKNOWN_COMPARISON_RULE",
|
|
185
|
+
"UNKNOWN_EVENT_KIND",
|
|
186
|
+
"UNKNOWN_EVIDENCE_EVENT",
|
|
187
|
+
"VALUE_ACCESSOR_REFUSED",
|
|
188
|
+
"VALUE_ACCESS_REFUSED",
|
|
189
|
+
"VALUE_CYCLE",
|
|
190
|
+
"VALUE_NOT_JSON_COMPATIBLE",
|
|
191
|
+
"VALUE_NOT_PLAIN",
|
|
192
|
+
"VALUE_TOO_DEEP",
|
|
193
|
+
"VALUE_TOO_LARGE",
|
|
194
|
+
"VALUE_UNSAFE_KEY",
|
|
195
|
+
"VERIFICATION_BEFORE_ACTION",
|
|
196
|
+
"VOCABULARY_FIELD_UNDECLARED",
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const REGISTRY = new Set(REFUSAL_CODES);
|
|
200
|
+
|
|
201
|
+
export const UNDECLARED_REFUSAL_CODE = "REFUSAL_CODE_UNDECLARED";
|
|
202
|
+
|
|
203
|
+
// A REFUSAL CODE IS A MACHINE TOKEN, AND THAT IS THE WHOLE OF WHAT IT MAY BE.
|
|
204
|
+
//
|
|
205
|
+
// The codes above are the ones THIS package emits. They are not the only codes
|
|
206
|
+
// that may exist, because several public helpers — `snapshotRecord`,
|
|
207
|
+
// `exactKeys`, `entryRecord`, `entryArray`, `assertExportSafeText`,
|
|
208
|
+
// `assertVocabularyValue` — take the CALLER'S OWN code so that an external
|
|
209
|
+
// consumer's boundary answers in its own vocabulary. That is a deliberate part
|
|
210
|
+
// of the contract and predates this round.
|
|
211
|
+
//
|
|
212
|
+
// What was never part of the contract is a code that is not a token at all. The
|
|
213
|
+
// the review drove sentences, JSON fragments and caller-authored text
|
|
214
|
+
// through that parameter and read them back out of a public method. The
|
|
215
|
+
// alphabet below is the same positive-allowlist decision
|
|
216
|
+
// `vocabulary-charset.mjs` records for every other machine vocabulary: ASCII
|
|
217
|
+
// UPPER_SNAKE, bounded, anchored. It admits `MY_CODE`; it cannot express a
|
|
218
|
+
// sentence, a path, a JSON blob, a lowercase word or any character outside
|
|
219
|
+
// `[A-Z0-9_]`, so free text cannot survive contact with it.
|
|
220
|
+
//
|
|
221
|
+
// `runUntrusted` below is the control-flow half: a foreign callback cannot use
|
|
222
|
+
// even a well-formed token to choose the enclosing boundary's result.
|
|
223
|
+
const MACHINE_CODE = /^[A-Z][A-Z0-9_]{0,63}$/u;
|
|
224
|
+
|
|
225
|
+
// Capture the small set of primordials this boundary depends on. A frozen
|
|
226
|
+
// refusal object is not sufficient when membership or trace construction can
|
|
227
|
+
// be redirected through mutable ambient prototypes after module load.
|
|
228
|
+
const setHas = Function.prototype.call.bind(Set.prototype.has);
|
|
229
|
+
const nativePromiseThen = Function.prototype.call.bind(Promise.prototype.then);
|
|
230
|
+
const regexpTest = Function.prototype.call.bind(RegExp.prototype.test);
|
|
231
|
+
const reflectGet = Reflect.get;
|
|
232
|
+
const defineProperty = Object.defineProperty;
|
|
233
|
+
const freeze = Object.freeze;
|
|
234
|
+
|
|
235
|
+
export class SdkContractError extends Error {
|
|
236
|
+
constructor(code) {
|
|
237
|
+
// NORMALIZED BEFORE `super`, so there is no window in which an undeclared
|
|
238
|
+
// string is the message. A non-string, a template-built string carrying
|
|
239
|
+
// caller text, a subclass's chosen code: all become the one fallback.
|
|
240
|
+
const declared = typeof code === "string" && (setHas(REGISTRY, code) || regexpTest(MACHINE_CODE, code))
|
|
241
|
+
? code
|
|
242
|
+
: UNDECLARED_REFUSAL_CODE;
|
|
243
|
+
const message = `Observa SDK contract refused (${declared})`;
|
|
244
|
+
super(message);
|
|
245
|
+
// NON-WRITABLE AND NON-CONFIGURABLE. A subclass runs its own constructor
|
|
246
|
+
// body after `super()` returns, and the round-2 review used exactly that to
|
|
247
|
+
// overwrite `.message` with caller-authored text on an object the
|
|
248
|
+
// boundary then rethrew. Defining the three fields as immutable own
|
|
249
|
+
// properties means the shape survives subclassing.
|
|
250
|
+
defineProperty(this, "name", {
|
|
251
|
+
value: "SdkContractError", writable: false, configurable: false, enumerable: false,
|
|
252
|
+
});
|
|
253
|
+
defineProperty(this, "code", {
|
|
254
|
+
value: declared, writable: false, configurable: false, enumerable: true,
|
|
255
|
+
});
|
|
256
|
+
defineProperty(this, "message", {
|
|
257
|
+
value: message, writable: false, configurable: false, enumerable: false,
|
|
258
|
+
});
|
|
259
|
+
// Engine-generated traces and Error.prepareStackTrace are caller-controlled
|
|
260
|
+
// detail channels. The contract therefore fixes the detail fields
|
|
261
|
+
// to code-only values instead of retaining a lazy trace or chained value.
|
|
262
|
+
defineProperty(this, "stack", {
|
|
263
|
+
value: message, writable: false, configurable: false, enumerable: false,
|
|
264
|
+
});
|
|
265
|
+
defineProperty(this, "cause", {
|
|
266
|
+
value: undefined, writable: false, configurable: false, enumerable: false,
|
|
267
|
+
});
|
|
268
|
+
// No later `defineProperty`, subclass body, or captured-reference replay
|
|
269
|
+
// may add an attacker-bearing field to an emitted refusal.
|
|
270
|
+
freeze(this);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Raise a code-only refusal. This public function intentionally carries NO
|
|
275
|
+
// durable "trusted" brand: a caller can capture any public object and replay
|
|
276
|
+
// it later, so object lifetime is not control-flow provenance.
|
|
277
|
+
export function refuse(code) {
|
|
278
|
+
throw new SdkContractError(code);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// A synchronous H1 boundary cannot safely admit a native Promise: an async
|
|
282
|
+
// profile/clock can reject later with caller text after the synchronous caller
|
|
283
|
+
// has already received a refusal. Attach a rejection sink first, then refuse
|
|
284
|
+
// synchronously. The private-slot call does not dispatch through a caller's
|
|
285
|
+
// `.then` property.
|
|
286
|
+
function isNativePromiseAndObserve(value) {
|
|
287
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) return false;
|
|
288
|
+
try {
|
|
289
|
+
const observed = nativePromiseThen(value, undefined, () => undefined);
|
|
290
|
+
nativePromiseThen(observed, undefined, () => undefined);
|
|
291
|
+
return true;
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// A non-native thenable is also outside this synchronous contract. Reading
|
|
298
|
+
// `then` is fenced here; it is never invoked. Native Promises were handled
|
|
299
|
+
// above so a rejected one is observed before the closed refusal leaves.
|
|
300
|
+
function hasThenableShape(value) {
|
|
301
|
+
if (value === null || (typeof value !== "object" && typeof value !== "function")) return false;
|
|
302
|
+
try {
|
|
303
|
+
return typeof reflectGet(value, "then") === "function";
|
|
304
|
+
} catch {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Run foreign code and collapse everything it can do to one closed code.
|
|
310
|
+
export function runUntrusted(code, fn, thenableCode = code) {
|
|
311
|
+
let result;
|
|
312
|
+
try {
|
|
313
|
+
result = fn();
|
|
314
|
+
} catch {
|
|
315
|
+
refuse(code);
|
|
316
|
+
}
|
|
317
|
+
if (isNativePromiseAndObserve(result) || hasThenableShape(result)) refuse(thenableCode);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Retained as a compatibility query for callers/tests. No public error
|
|
322
|
+
// object is ever a trusted control-flow token; public callers must receive a
|
|
323
|
+
// freshly closed boundary code instead.
|
|
324
|
+
export function isContractRaised(value) {
|
|
325
|
+
void value;
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Compatibility wrapper for a public callback boundary. It intentionally
|
|
330
|
+
// recodes *every* thrown value, including a previously emitted SDK error:
|
|
331
|
+
// public object identity cannot prove trustworthy control-flow provenance.
|
|
332
|
+
export function refuseOnRawThrow(code, fn) {
|
|
333
|
+
return runUntrusted(code, fn);
|
|
334
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SMALL PRIMITIVES FOR SECURITY-RELEVANT, ALREADY-VALIDATED DATA.
|
|
2
|
+
//
|
|
3
|
+
// A frozen Array protects its own elements; it does not freeze
|
|
4
|
+
// Array.prototype. Capability certification and boundary materialization
|
|
5
|
+
// must therefore not dispatch through mutable inherited helpers such as
|
|
6
|
+
// `.includes`, `Symbol.iterator`, spread, or `.push` after validation.
|
|
7
|
+
//
|
|
8
|
+
// These helpers deliberately use indexed access only. Their callers either
|
|
9
|
+
// hold a contract-owned frozen array or have just materialized a caller array
|
|
10
|
+
// with `entryArray`; they are not general-purpose validators for arbitrary
|
|
11
|
+
// objects.
|
|
12
|
+
|
|
13
|
+
const NativeArray = Array;
|
|
14
|
+
const freeze = Object.freeze;
|
|
15
|
+
const isSafeInteger = Number.isSafeInteger;
|
|
16
|
+
|
|
17
|
+
export function stableArrayHas(values, wanted) {
|
|
18
|
+
const length = values.length;
|
|
19
|
+
for (let index = 0; index < length; index += 1) {
|
|
20
|
+
const value = values[index];
|
|
21
|
+
// SameValueZero is the membership semantics Array#includes supplies,
|
|
22
|
+
// without dynamically dispatching to a mutable prototype method.
|
|
23
|
+
if (value === wanted || (value !== value && wanted !== wanted)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Closed grammar checks sometimes use a short string as their vocabulary.
|
|
29
|
+
// Keep those checks off mutable String.prototype for the same reason closed
|
|
30
|
+
// array vocabularies stay off Array.prototype.includes.
|
|
31
|
+
export function stableStringHas(text, wanted) {
|
|
32
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
33
|
+
if (text[index] === wanted) return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function stableArrayCopy(values) {
|
|
39
|
+
const length = values.length;
|
|
40
|
+
const copy = new NativeArray(length);
|
|
41
|
+
for (let index = 0; index < length; index += 1) copy[index] = values[index];
|
|
42
|
+
return freeze(copy);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// SELECT AND PROJECT WITHOUT DISPATCHING THROUGH `Array.prototype` — FINAL
|
|
46
|
+
// CLOSURE, INDEPENDENT REVIEW #1.
|
|
47
|
+
//
|
|
48
|
+
// `Array.prototype.filter` is ambient mutable behaviour, and this package's own
|
|
49
|
+
// threat model has semi-trusted domain profiles executing in-process (the same
|
|
50
|
+
// class already closed for `GOVERNANCE_WORDS` and for `ID_PATTERNS`, H1R2-06).
|
|
51
|
+
// `EvidenceLog.applicableEvidence()` was `Object.freeze(this.#records.filter(…))`
|
|
52
|
+
// — it FROZE A VALUE IT DID NOT OWN, on the assumption that `filter` returns a
|
|
53
|
+
// fresh array. One assignment inside a profile callback (`Array.prototype.filter
|
|
54
|
+
// = function () { return this; }`) made it return the receiver, so
|
|
55
|
+
// `Object.freeze` froze the log's private `#records`.
|
|
56
|
+
//
|
|
57
|
+
// The result was the worst outcome this contract names: a raw uncoded
|
|
58
|
+
// `TypeError` out of public node methods AND an append-only log destroyed
|
|
59
|
+
// beyond remediation — restoring the prototype does not help, because
|
|
60
|
+
// `#records` stays frozen. Verified: `recordSourceUnavailable` still threw a raw
|
|
61
|
+
// TypeError after `Array.prototype.filter` had been fully restored.
|
|
62
|
+
//
|
|
63
|
+
// The same mutable dispatch sat on the evidence-admissibility decision path and
|
|
64
|
+
// the false-VERIFIED gate in the node. These two helpers are the stable
|
|
65
|
+
// replacements: indexed access only, and the array they freeze is one they
|
|
66
|
+
// constructed themselves.
|
|
67
|
+
export function stableFilter(values, predicate) {
|
|
68
|
+
const length = values.length;
|
|
69
|
+
const out = new NativeArray();
|
|
70
|
+
let count = 0;
|
|
71
|
+
for (let index = 0; index < length; index += 1) {
|
|
72
|
+
const value = values[index];
|
|
73
|
+
if (predicate(value, index)) {
|
|
74
|
+
out[count] = value;
|
|
75
|
+
count += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
out.length = count;
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function stableMap(values, project) {
|
|
83
|
+
const length = values.length;
|
|
84
|
+
const out = new NativeArray(length);
|
|
85
|
+
for (let index = 0; index < length; index += 1) out[index] = project(values[index], index);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sort a verified, string-valued snapshot without dispatching through the
|
|
90
|
+
// mutable Array prototype. The small insertion sort is intentional: these
|
|
91
|
+
// callers compare declaration keys, not bulk user data, and the important
|
|
92
|
+
// property is that the ordering mechanism cannot be replaced after import.
|
|
93
|
+
export function stableSortedStringCopy(values) {
|
|
94
|
+
const copy = new NativeArray(values.length);
|
|
95
|
+
for (let index = 0; index < values.length; index += 1) copy[index] = values[index];
|
|
96
|
+
for (let index = 1; index < copy.length; index += 1) {
|
|
97
|
+
const value = copy[index];
|
|
98
|
+
let cursor = index - 1;
|
|
99
|
+
while (cursor >= 0 && copy[cursor] > value) {
|
|
100
|
+
copy[cursor + 1] = copy[cursor];
|
|
101
|
+
cursor -= 1;
|
|
102
|
+
}
|
|
103
|
+
copy[cursor + 1] = value;
|
|
104
|
+
}
|
|
105
|
+
return freeze(copy);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Integer millisecond timestamps are a bounded numeric domain, not every
|
|
109
|
+
// Number.isInteger value. JavaScript accepts integers that cannot represent
|
|
110
|
+
// adjacent milliseconds; allowing them into durable history makes ordinary
|
|
111
|
+
// deadline arithmetic non-deterministic at the precision boundary.
|
|
112
|
+
export function isNonNegativeSafeInteger(value) {
|
|
113
|
+
return isSafeInteger(value) && value >= 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isPositiveSafeInteger(value) {
|
|
117
|
+
return isSafeInteger(value) && value > 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function assertNonNegativeSafeInteger(value, code, refuse) {
|
|
121
|
+
if (!isNonNegativeSafeInteger(value)) refuse(code);
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function assertPositiveSafeInteger(value, code, refuse) {
|
|
126
|
+
if (!isPositiveSafeInteger(value)) refuse(code);
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Checked arithmetic for the millisecond-number API. The equality check is
|
|
131
|
+
// intentional: it detects IEEE-754 rounding even in a case where the rounded
|
|
132
|
+
// result happens to remain numerically greater than the start.
|
|
133
|
+
export function addSafeMilliseconds(start, windowMs, code, refuse) {
|
|
134
|
+
assertNonNegativeSafeInteger(start, code, refuse);
|
|
135
|
+
assertPositiveSafeInteger(windowMs, code, refuse);
|
|
136
|
+
const deadline = start + windowMs;
|
|
137
|
+
if (!isSafeInteger(deadline) || deadline <= start || deadline - start !== windowMs) {
|
|
138
|
+
refuse(code);
|
|
139
|
+
}
|
|
140
|
+
return deadline;
|
|
141
|
+
}
|