@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,417 @@
|
|
|
1
|
+
// THE PUBLIC ENTRY BOUNDARY. One mechanism, installed over the declared public
|
|
2
|
+
// surface, holding one invariant:
|
|
3
|
+
//
|
|
4
|
+
// NO PUBLIC H1 ENTRY POINT MAY EXECUTE CALLER-CONTROLLED ACCESSOR,
|
|
5
|
+
// DESTRUCTURING OR COERCION CODE BEFORE THE CLOSED REFUSAL BOUNDARY.
|
|
6
|
+
//
|
|
7
|
+
// WHY THIS FILE EXISTS RATHER THAN THIRTY PATCHES.
|
|
8
|
+
//
|
|
9
|
+
// Round 12B closed six entry points that let a raw `TypeError` escape, wrapped
|
|
10
|
+
// them in `refuseOnRawThrow`, and pinned seven hand-picked sites with a test
|
|
11
|
+
// named for a universal property. Independent review #1 then found roughly
|
|
12
|
+
// THIRTY public entry points with the same shape, and the reason is structural
|
|
13
|
+
// rather than clerical: the seam is the PARAMETER LIST.
|
|
14
|
+
//
|
|
15
|
+
// adoptAuthorityBinding({ tenantLocalId, boundIntentId, authorityBindingId } = {})
|
|
16
|
+
//
|
|
17
|
+
// Destructuring in a parameter list runs `[[Get]]` on the caller's object
|
|
18
|
+
// BEFORE the first statement of the function body — so before any guard, any
|
|
19
|
+
// snapshot, any `refuseOnRawThrow`. A getter runs there. A Proxy `get` trap runs
|
|
20
|
+
// there. `...rest` runs `ownKeys` there. A revoked Proxy throws there. The
|
|
21
|
+
// escaping error carries THE CALLER'S OWN MESSAGE TEXT, which is the leak
|
|
22
|
+
// `errors.mjs` opens by forbidding — a probe put a subject-identifying string in
|
|
23
|
+
// a getter's `TypeError` and read it back out of a public method.
|
|
24
|
+
//
|
|
25
|
+
// No amount of guarding INSIDE the body can close a seam that fires before the
|
|
26
|
+
// body. The only fix that generalizes is to stop the body from ever seeing the
|
|
27
|
+
// caller's object: materialize the argument record at the boundary, and hand the
|
|
28
|
+
// body a frozen, null-prototype, data-only clone. Then the existing
|
|
29
|
+
// destructuring in every method is safe by construction, because there is
|
|
30
|
+
// nothing left to trap.
|
|
31
|
+
//
|
|
32
|
+
// TWO LEVERS, BOTH MECHANICAL:
|
|
33
|
+
//
|
|
34
|
+
// `entryRecord` materializes ONE argument record inside a closed refusal
|
|
35
|
+
// boundary. Accessors are REFUSED, never invoked; symbol
|
|
36
|
+
// keys, non-plain prototypes, arrays and strings are
|
|
37
|
+
// refused; no value is coerced.
|
|
38
|
+
//
|
|
39
|
+
// `entryArray` the same lever for the ARRAY carrier. Added by review #1
|
|
40
|
+
// (H1R-01/02/05), which found the record half of this
|
|
41
|
+
// boundary complete and the array half missing entirely:
|
|
42
|
+
// three public exports validated an array with
|
|
43
|
+
// `Array.isArray` and then CONSUMED it through `for...of`
|
|
44
|
+
// or `.length`/`[i]`, both of which a caller controls.
|
|
45
|
+
//
|
|
46
|
+
// `sealEntryPoints` installs `entryRecord` over a declared method table, in
|
|
47
|
+
// one call, so a method cannot be left out by forgetting
|
|
48
|
+
// to edit it. The table is compared against the contract's
|
|
49
|
+
// own method list by test, so a method added to the surface
|
|
50
|
+
// without a boundary code fails rather than shipping bare.
|
|
51
|
+
//
|
|
52
|
+
// WHAT `entryRecord` DELIBERATELY DOES NOT DO. It is SHALLOW. Members keep their
|
|
53
|
+
// identity — a function stays a function, a nested record stays a reference —
|
|
54
|
+
// because the entry points that take functions (`registerDomainProfile`, and
|
|
55
|
+
// every `clock`) could not otherwise be sealed at all, and because each member
|
|
56
|
+
// already has its own validator that snapshots to the depth that member needs.
|
|
57
|
+
// The boundary's job is to make sure the BODY never touches caller-controlled
|
|
58
|
+
// STRUCTURE; the validators' job is to make sure each MEMBER is what it claims.
|
|
59
|
+
// ROUND-2 REVIEW #1, H1R2-01 — WRAPPING THE WALK WAS NOT WRAPPING THE TRAPS.
|
|
60
|
+
//
|
|
61
|
+
// Every `Reflect.ownKeys`, `getOwnPropertyDescriptor` and `getPrototypeOf`
|
|
62
|
+
// below EXECUTES CALLER CODE. Round 12B put the whole walk inside
|
|
63
|
+
// `refuseOnRawThrow`, which closed the raw-`TypeError` half; it did not close
|
|
64
|
+
// the other half, because a trap that raised a publicly constructed
|
|
65
|
+
// `SdkContractError` was rethrown unchanged with the caller's own text in it.
|
|
66
|
+
//
|
|
67
|
+
// Each trap-invoking primitive now runs inside `runUntrusted`, which is the
|
|
68
|
+
// smallest dynamic extent that is genuinely foreign code. Contract refusals in
|
|
69
|
+
// the loop body sit OUTSIDE those regions and keep their own codes; anything a
|
|
70
|
+
// trap raises is re-coded. `errors.mjs` states the provenance model.
|
|
71
|
+
import { types as nodeTypes } from "node:util";
|
|
72
|
+
import { refuse, runUntrusted } from "./errors.mjs";
|
|
73
|
+
import { stableStringHas } from "./stable-primitives.mjs";
|
|
74
|
+
|
|
75
|
+
// The installer makes a security boundary durable. Capture the ordinary
|
|
76
|
+
// object operations it needs so a later ambient replacement cannot turn the
|
|
77
|
+
// commit phase back into a caller-controlled dispatch point. A Proxy cannot
|
|
78
|
+
// offer transactional property definition, so it is rejected before any
|
|
79
|
+
// target inspection or mutation.
|
|
80
|
+
const isProxy = nodeTypes.isProxy;
|
|
81
|
+
const objectDefineProperty = Object.defineProperty;
|
|
82
|
+
const objectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
83
|
+
const objectKeys = Object.keys;
|
|
84
|
+
const objectFreeze = Object.freeze;
|
|
85
|
+
const reflectApply = Reflect.apply;
|
|
86
|
+
const arrayIsArray = Array.isArray;
|
|
87
|
+
const NativeArray = Array;
|
|
88
|
+
const functionToString = Function.prototype.toString;
|
|
89
|
+
|
|
90
|
+
// A shared frozen empty record, so `undefined` keeps meaning "no arguments" —
|
|
91
|
+
// the behaviour the `= {}` parameter defaults used to provide, without the
|
|
92
|
+
// default-parameter evaluation those defaults performed.
|
|
93
|
+
const NO_ARGUMENTS = Object.freeze(Object.create(null));
|
|
94
|
+
|
|
95
|
+
// THE STRUCTURAL CODE POLICY, stated once for both levers.
|
|
96
|
+
//
|
|
97
|
+
// Three facts are decided identically wherever structure is inspected — "the
|
|
98
|
+
// prototype is not plain", "an unsafe or non-addressable key was found", "an
|
|
99
|
+
// accessor was found where a data property was required" — and by default they
|
|
100
|
+
// carry `canonical.mjs`'s own three codes, so one condition has one name across
|
|
101
|
+
// the whole SDK.
|
|
102
|
+
//
|
|
103
|
+
// A boundary that already publishes its OWN closed code for those facts passes
|
|
104
|
+
// `structuralCode` and gets it. That is not drift: it is the rule
|
|
105
|
+
// `text-safety.mjs` states for the same situation — "this append is malformed",
|
|
106
|
+
// "this history is malformed" and "this envelope is malformed" are different
|
|
107
|
+
// facts about different boundaries and each keeps its own code, but they can
|
|
108
|
+
// never disagree about WHICH value is unsafe, because one implementation
|
|
109
|
+
// decides. `projection.mjs` has published `PROJECTION_EVENT_INVALID` for an
|
|
110
|
+
// accessor on an event since round 10 and keeps it; the node's sealed methods
|
|
111
|
+
// have published `VALUE_ACCESSOR_REFUSED` and keep that.
|
|
112
|
+
//
|
|
113
|
+
// IT IS A PLAIN STRING PARAMETER, NOT AN OPTIONS RECORD, and the reason is this
|
|
114
|
+
// file's own first sentence. The first draft wrote it as
|
|
115
|
+
// `{ structuralCode = null } = {}` — DESTRUCTURING IN A PARAMETER LIST, the
|
|
116
|
+
// exact seam `entryRecord` exists to close, reintroduced into the boundary
|
|
117
|
+
// itself. `entry-position-sweep.test.mjs` caught it on its first run:
|
|
118
|
+
// `entryRecord(x, code, null)` raised a raw TypeError, and a Proxy third
|
|
119
|
+
// argument leaked the caller's own message text straight back out. A scalar has
|
|
120
|
+
// no structure to trap.
|
|
121
|
+
function structuralCodes(structuralCode) {
|
|
122
|
+
return {
|
|
123
|
+
notPlain: structuralCode ?? "VALUE_NOT_PLAIN",
|
|
124
|
+
unsafeKey: structuralCode ?? "VALUE_UNSAFE_KEY",
|
|
125
|
+
accessor: structuralCode ?? "VALUE_ACCESSOR_REFUSED",
|
|
126
|
+
hole: structuralCode ?? "VALUE_NOT_JSON_COMPATIBLE",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Materialize a caller-supplied argument record.
|
|
131
|
+
//
|
|
132
|
+
// Null-prototype, so a field lookup can never reach `Object.prototype` and
|
|
133
|
+
// answer with an inherited function — the shape that turns `record[name]` into
|
|
134
|
+
// an execution seam. Frozen, so nothing downstream can write back into it.
|
|
135
|
+
export function entryRecord(rawArguments, code, structuralCode) {
|
|
136
|
+
const CODES = structuralCodes(structuralCode);
|
|
137
|
+
if (rawArguments === undefined) return NO_ARGUMENTS;
|
|
138
|
+
// A string is an object-ish thing with indexed own properties, and reading
|
|
139
|
+
// one as a record silently yields a per-character map rather than failing.
|
|
140
|
+
if (typeof rawArguments === "string") refuse(code);
|
|
141
|
+
if (rawArguments === null || typeof rawArguments !== "object" ||
|
|
142
|
+
runUntrusted(code, () => arrayIsArray(rawArguments))) {
|
|
143
|
+
refuse(code);
|
|
144
|
+
}
|
|
145
|
+
// Prototype first: a class instance or a `Proxy` over one can carry
|
|
146
|
+
// accessors on its prototype that plain-object callers never have. Reading
|
|
147
|
+
// the prototype of a revoked Proxy throws, which the wrapper closes.
|
|
148
|
+
// THE STRUCTURAL CODES ARE canonical.mjs's OWN, NOT THE CALLER'S.
|
|
149
|
+
//
|
|
150
|
+
// "this is not an argument record at all" is a fact about THIS boundary and
|
|
151
|
+
// takes the caller's code. "an accessor was found where a data property was
|
|
152
|
+
// required", "a symbol key was found", "the prototype is not plain" are the
|
|
153
|
+
// same three facts `dataRecordEntries` already names, and this file answers
|
|
154
|
+
// them with the same three codes. One condition, one code, wherever it is
|
|
155
|
+
// detected — the drift this codebase has paid for three times was always two
|
|
156
|
+
// places describing one rule slightly differently.
|
|
157
|
+
const prototype = runUntrusted(code, () => Object.getPrototypeOf(rawArguments));
|
|
158
|
+
if (prototype !== Object.prototype && prototype !== null) refuse(CODES.notPlain);
|
|
159
|
+
const record = Object.create(null);
|
|
160
|
+
const keys = runUntrusted(code, () => Reflect.ownKeys(rawArguments));
|
|
161
|
+
for (let index = 0; index < keys.length; index += 1) {
|
|
162
|
+
const key = keys[index];
|
|
163
|
+
// A symbol-keyed field is not addressable by any declared parameter, so
|
|
164
|
+
// it can only be a channel. Refused rather than dropped: dropping would
|
|
165
|
+
// mean the record validated is not the record the caller handed over.
|
|
166
|
+
if (typeof key !== "string") refuse(CODES.unsafeKey);
|
|
167
|
+
const descriptor = runUntrusted(code, () => Object.getOwnPropertyDescriptor(rawArguments, key));
|
|
168
|
+
// A Proxy may legally report a key from `ownKeys` and then answer
|
|
169
|
+
// `getOwnPropertyDescriptor` with `undefined` for it. Reading `"value" in
|
|
170
|
+
// undefined` raised a raw TypeError out of `projectStateAt` (review #1,
|
|
171
|
+
// H1R-01); a key that will not describe itself is refused here instead.
|
|
172
|
+
if (descriptor === undefined) refuse(code);
|
|
173
|
+
// THE ACCESSOR IS REFUSED WITHOUT BEING INVOKED. `"value" in descriptor`
|
|
174
|
+
// asks the question `descriptor.get` would answer by RUNNING. A
|
|
175
|
+
// non-enumerable data property is refused by the same rule and for the
|
|
176
|
+
// same reason `dataRecordEntries` refuses it: it is invisible to
|
|
177
|
+
// destructuring and visible to `Reflect.ownKeys`, so copying it would make
|
|
178
|
+
// the body see a field the caller's own spread would not.
|
|
179
|
+
if (!("value" in descriptor) || !descriptor.enumerable) refuse(CODES.accessor);
|
|
180
|
+
// THE DESCRIPTOR'S VALUE, NEVER `rawArguments[key]`. A Proxy can present a
|
|
181
|
+
// perfectly ordinary DATA descriptor and still answer `get` with something
|
|
182
|
+
// else, and answer it differently on every read — which is how the same
|
|
183
|
+
// argument object produced three different projections (review #1,
|
|
184
|
+
// H1R-01). Reading the descriptor is the only read that cannot be
|
|
185
|
+
// re-answered.
|
|
186
|
+
record[key] = descriptor.value;
|
|
187
|
+
}
|
|
188
|
+
return objectFreeze(record);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Bound so a hostile oversized carrier is refused before it is walked or
|
|
192
|
+
// copied. A contract limit, not a tuning knob — the same posture `canonical.mjs`
|
|
193
|
+
// takes toward depth, node count and string length.
|
|
194
|
+
const MAX_ENTRY_ARRAY_LENGTH = 100_000;
|
|
195
|
+
|
|
196
|
+
// Materialize a caller-supplied ARRAY argument.
|
|
197
|
+
//
|
|
198
|
+
// WHY THE ARRAY CARRIER NEEDED ITS OWN LEVER. `Array.isArray` answers one
|
|
199
|
+
// question — "is the carrier an Array" — and every way of then CONSUMING that
|
|
200
|
+
// array is caller-controlled:
|
|
201
|
+
//
|
|
202
|
+
// `for...of` dispatches `value[Symbol.iterator]`, and a plain array may
|
|
203
|
+
// own one. `assertAdapterProvides(adapter, ["EMIT_..."])` with
|
|
204
|
+
// an own iterator yielding a DIFFERENT capability returned
|
|
205
|
+
// `true` for a capability the adapter never declared — the
|
|
206
|
+
// gate certified a sequence it had not validated.
|
|
207
|
+
// `.length`/`[i]` are `[[Get]]`s, which a Proxy over an array traps, so both
|
|
208
|
+
// could throw the caller's own message text out of
|
|
209
|
+
// `projectStateAt`.
|
|
210
|
+
//
|
|
211
|
+
// So the rule is the same one `entryRecord` holds, applied to indices:
|
|
212
|
+
// VALIDATE ONCE, CAPTURE ONCE, USE THE CAPTURE. Elements are read from their
|
|
213
|
+
// DESCRIPTORS in a single `Reflect.ownKeys` walk, and the frozen dense copy
|
|
214
|
+
// returned is the only thing any caller may go on to consume.
|
|
215
|
+
export function entryArray(rawArguments, code, structuralCode, maxLength = MAX_ENTRY_ARRAY_LENGTH) {
|
|
216
|
+
const CODES = structuralCodes(structuralCode);
|
|
217
|
+
// `Array.isArray` is not a pure predicate: it THROWS on a revoked Proxy.
|
|
218
|
+
// It runs inside the wrapper for the same reason `snapshotRecord`'s does.
|
|
219
|
+
if (!runUntrusted(code, () => arrayIsArray(rawArguments))) refuse(code);
|
|
220
|
+
// Plain arrays only, exactly as `canonical.mjs`'s `arrayElements` requires:
|
|
221
|
+
// a subclass could carry a prototype method that consumption dispatches
|
|
222
|
+
// through.
|
|
223
|
+
if (runUntrusted(code, () => Object.getPrototypeOf(rawArguments)) !== Array.prototype) {
|
|
224
|
+
refuse(CODES.notPlain);
|
|
225
|
+
}
|
|
226
|
+
const byIndex = new Map();
|
|
227
|
+
const keys = runUntrusted(code, () => Reflect.ownKeys(rawArguments));
|
|
228
|
+
for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
|
|
229
|
+
const key = keys[keyIndex];
|
|
230
|
+
// A SYMBOL KEY ON AN ARRAY IS SKIPPED, NOT REFUSED — and the asymmetry
|
|
231
|
+
// with `entryRecord` is deliberate. On a record a symbol key is a channel
|
|
232
|
+
// carrying a value the body might read. On an array it can never be an
|
|
233
|
+
// indexed element, so it carries nothing this boundary reads; the one
|
|
234
|
+
// symbol a caller realistically attaches is `Symbol.iterator`, and the
|
|
235
|
+
// contract's answer to a hostile iterator is that IT IS NEVER CONSULTED,
|
|
236
|
+
// not that the array is rejected. `sdk-export-contract.test.mjs` has
|
|
237
|
+
// pinned that acceptance since round 10 and it stays true.
|
|
238
|
+
if (typeof key === "symbol") continue;
|
|
239
|
+
if (key === "length") continue;
|
|
240
|
+
if (!ARRAY_INDEX.test(key)) refuse(CODES.unsafeKey);
|
|
241
|
+
const descriptor = runUntrusted(code, () => Object.getOwnPropertyDescriptor(rawArguments, key));
|
|
242
|
+
if (descriptor === undefined) refuse(code);
|
|
243
|
+
if (!("value" in descriptor) || !descriptor.enumerable) refuse(CODES.accessor);
|
|
244
|
+
byIndex.set(Number(key), descriptor.value);
|
|
245
|
+
if (byIndex.size > maxLength) refuse(code);
|
|
246
|
+
}
|
|
247
|
+
// Dense, built from the CAPTURED indices rather than from a second read of
|
|
248
|
+
// `length` — which is itself a trappable `[[Get]]`. A hole would read as
|
|
249
|
+
// `undefined`, which is not a value any caller of this boundary accepts.
|
|
250
|
+
const elements = new NativeArray(byIndex.size);
|
|
251
|
+
for (let index = 0; index < byIndex.size; index += 1) {
|
|
252
|
+
if (!byIndex.has(index)) refuse(CODES.hole);
|
|
253
|
+
elements[index] = byIndex.get(index);
|
|
254
|
+
}
|
|
255
|
+
return objectFreeze(elements);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const ARRAY_INDEX = /^(?:0|[1-9][0-9]*)$/u;
|
|
259
|
+
|
|
260
|
+
// HOW MANY ARGUMENT SLOTS A FUNCTION ACTUALLY DECLARES.
|
|
261
|
+
//
|
|
262
|
+
// NOT `Function.length`, which stops counting at the first defaulted parameter —
|
|
263
|
+
// so `method({ a, b } = {})` reports 0 and `method(raw)` reports 1, and a
|
|
264
|
+
// wrapper that dropped a second argument would look identical to one that did
|
|
265
|
+
// not. The parameter list is read from the source text and split on TOP-LEVEL
|
|
266
|
+
// commas, so a destructured or defaulted parameter counts as one slot.
|
|
267
|
+
//
|
|
268
|
+
// It lives here rather than in a test because `sealEntryPoints` is the last
|
|
269
|
+
// moment the UNSEALED function exists: after installation every method reports
|
|
270
|
+
// the wrapper's single parameter, and the number this measures is gone
|
|
271
|
+
// (round-2 review #1, H1R2-09).
|
|
272
|
+
function declaredParameterCount(fn) {
|
|
273
|
+
const source = reflectApply(functionToString, fn, []);
|
|
274
|
+
const start = source.indexOf("(");
|
|
275
|
+
if (start < 0) return 0;
|
|
276
|
+
let depth = 0;
|
|
277
|
+
let params = "";
|
|
278
|
+
for (let index = start; index < source.length; index += 1) {
|
|
279
|
+
const char = source[index];
|
|
280
|
+
if (stableStringHas("([{", char)) depth += 1;
|
|
281
|
+
if (stableStringHas(")]}", char)) {
|
|
282
|
+
depth -= 1;
|
|
283
|
+
if (depth === 0) break;
|
|
284
|
+
}
|
|
285
|
+
if (depth >= 1) params += char;
|
|
286
|
+
}
|
|
287
|
+
let inner = 0;
|
|
288
|
+
let slots = 1;
|
|
289
|
+
let seen = false;
|
|
290
|
+
for (const char of params.slice(1)) {
|
|
291
|
+
if (stableStringHas("([{", char)) inner += 1;
|
|
292
|
+
else if (stableStringHas(")]}", char)) inner -= 1;
|
|
293
|
+
else if (char === "," && inner === 0) slots += 1;
|
|
294
|
+
if (!/\s/u.test(char)) seen = true;
|
|
295
|
+
}
|
|
296
|
+
return seen ? slots : 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Install the boundary over a declared method table.
|
|
300
|
+
//
|
|
301
|
+
// `codes` maps method name to the refusal code that method's malformed
|
|
302
|
+
// arguments produce, so a caller still gets the boundary-specific code its
|
|
303
|
+
// sibling validators use rather than one generic entry-point code.
|
|
304
|
+
//
|
|
305
|
+
// The wrapper is the ONLY thing that touches the caller's object; the wrapped
|
|
306
|
+
// method receives the materialized record and may destructure it freely.
|
|
307
|
+
// Returns the frozen table so a test can prove the installed set IS the
|
|
308
|
+
// declared public surface, rather than trusting that the two were kept level.
|
|
309
|
+
// ROUND-2 REVIEW #1, H1R2-07 — THE INSTALLER WAS OUTSIDE THE BOUNDARY IT
|
|
310
|
+
// INSTALLS, AND IT INSTALLED NON-ATOMICALLY.
|
|
311
|
+
//
|
|
312
|
+
// Two defects, one shape. `Object.entries(codes)` ran the caller's accessors
|
|
313
|
+
// before any guard, so a getter on the codes table raised a RAW `TypeError`
|
|
314
|
+
// carrying the caller's own text out of a public export — the exact seam this
|
|
315
|
+
// file's first sentence forbids, in the function that exists to close it. And
|
|
316
|
+
// the loop mutated the prototype AS IT WENT, so a refusal partway through the
|
|
317
|
+
// table left the prototype HALF-SEALED: some methods behind the boundary, some
|
|
318
|
+
// bare, and a returned attestation naming methods that were never wrapped. A
|
|
319
|
+
// partially installed security boundary is worse than an absent one, because
|
|
320
|
+
// the attestation says it is present.
|
|
321
|
+
//
|
|
322
|
+
// Both close the same way the rest of this file works: MATERIALIZE, VALIDATE
|
|
323
|
+
// EVERYTHING, THEN ACT. The codes table is materialized by `entryRecord`, every
|
|
324
|
+
// entry is resolved and checked before a single property is redefined, and the
|
|
325
|
+
// install loop touches the prototype only once the whole table is known good.
|
|
326
|
+
//
|
|
327
|
+
// The pre-seal arity is captured here as well, because it is the only place it
|
|
328
|
+
// still exists: after installation every method reports the wrapper's arity of
|
|
329
|
+
// 1, so a test reading the prototype can no longer observe what the method
|
|
330
|
+
// itself declared (H1R2-09).
|
|
331
|
+
export function sealEntryPoints(rawPrototype, rawCodes) {
|
|
332
|
+
// A Proxy may execute arbitrary traps during descriptor inspection or claim
|
|
333
|
+
// a successful define without making the write. There is no rollback-safe
|
|
334
|
+
// commit over that protocol, so reject it before even validating the table.
|
|
335
|
+
if (rawPrototype === null || typeof rawPrototype !== "object" || isProxy(rawPrototype)) {
|
|
336
|
+
refuse("ENTRY_POINT_NOT_DECLARED");
|
|
337
|
+
}
|
|
338
|
+
const codes = entryRecord(rawCodes, "ENTRY_POINT_NOT_DECLARED");
|
|
339
|
+
// PHASE 1 — resolve and validate the WHOLE table. Nothing is mutated here.
|
|
340
|
+
const names = objectKeys(codes);
|
|
341
|
+
const planned = new Array(names.length);
|
|
342
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
343
|
+
const name = names[index];
|
|
344
|
+
const code = codes[name];
|
|
345
|
+
if (typeof code !== "string") refuse("ENTRY_POINT_NOT_DECLARED");
|
|
346
|
+
const descriptor = runUntrusted(
|
|
347
|
+
"ENTRY_POINT_NOT_DECLARED",
|
|
348
|
+
() => objectGetOwnPropertyDescriptor(rawPrototype, name),
|
|
349
|
+
);
|
|
350
|
+
// A non-configurable slot cannot be replaced reliably. Reject all such
|
|
351
|
+
// tables during phase 1, before a sibling slot has been touched.
|
|
352
|
+
if (!descriptor || typeof descriptor.value !== "function" || descriptor.configurable !== true) {
|
|
353
|
+
refuse("ENTRY_POINT_NOT_DECLARED");
|
|
354
|
+
}
|
|
355
|
+
planned[index] = {
|
|
356
|
+
name, code, descriptor,
|
|
357
|
+
inner: descriptor.value,
|
|
358
|
+
arity: declaredParameterCount(descriptor.value),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
// Nothing in the ordinary-object commit phase can run caller code. Check
|
|
362
|
+
// once more after all planning work so a target altered during validation is
|
|
363
|
+
// refused before its first sibling is installed. (Proxies were rejected
|
|
364
|
+
// above because JavaScript cannot make this guarantee for their traps.)
|
|
365
|
+
for (let index = 0; index < planned.length; index += 1) {
|
|
366
|
+
const { name, descriptor } = planned[index];
|
|
367
|
+
const current = objectGetOwnPropertyDescriptor(rawPrototype, name);
|
|
368
|
+
if (!current || current.value !== descriptor.value ||
|
|
369
|
+
current.enumerable !== descriptor.enumerable ||
|
|
370
|
+
current.configurable !== descriptor.configurable ||
|
|
371
|
+
current.writable !== descriptor.writable) {
|
|
372
|
+
refuse("ENTRY_POINT_NOT_DECLARED");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// PHASE 2 — install. Every entry is already known good, so this cannot refuse
|
|
376
|
+
// partway and cannot leave the prototype in a mixed state.
|
|
377
|
+
const arities = Object.create(null);
|
|
378
|
+
const installedCodes = Object.create(null);
|
|
379
|
+
for (let index = 0; index < planned.length; index += 1) {
|
|
380
|
+
const { name, code, descriptor, inner, arity } = planned[index];
|
|
381
|
+
// Defined through a computed-name literal so the sealed function keeps the
|
|
382
|
+
// method's own name. The surface guard reads names off the prototype, and a
|
|
383
|
+
// wrapper reporting `name: ""` would make every stack trace and every
|
|
384
|
+
// reflective check read as anonymous.
|
|
385
|
+
const sealed = {
|
|
386
|
+
[name](rawArguments) {
|
|
387
|
+
return reflectApply(inner, this, [entryRecord(rawArguments, code)]);
|
|
388
|
+
},
|
|
389
|
+
}[name];
|
|
390
|
+
// The wrapper itself is the entry-security boundary. It cannot remain
|
|
391
|
+
// writable/configurable after attestation or a later assignment could
|
|
392
|
+
// restore a raw caller-facing method.
|
|
393
|
+
objectDefineProperty(rawPrototype, name, {
|
|
394
|
+
value: sealed,
|
|
395
|
+
enumerable: descriptor.enumerable,
|
|
396
|
+
writable: false,
|
|
397
|
+
configurable: false,
|
|
398
|
+
});
|
|
399
|
+
const installed = objectGetOwnPropertyDescriptor(rawPrototype, name);
|
|
400
|
+
if (!installed || installed.value !== sealed || installed.writable !== false ||
|
|
401
|
+
installed.configurable !== false || installed.enumerable !== descriptor.enumerable) {
|
|
402
|
+
// This is unreachable for the preflighted ordinary-object commit, but a
|
|
403
|
+
// false attestation is never acceptable if a runtime violates it.
|
|
404
|
+
refuse("ENTRY_POINT_NOT_DECLARED");
|
|
405
|
+
}
|
|
406
|
+
arities[name] = arity;
|
|
407
|
+
installedCodes[name] = code;
|
|
408
|
+
}
|
|
409
|
+
return objectFreeze({
|
|
410
|
+
codes: objectFreeze(installedCodes),
|
|
411
|
+
// The declared parameter count of each method BEFORE sealing. Read by
|
|
412
|
+
// `entry-position-sweep.test.mjs`, which previously asserted arity against
|
|
413
|
+
// the installed wrapper — a value this function fixes at 1, so the
|
|
414
|
+
// assertion could never fail and the drift it guarded was invisible.
|
|
415
|
+
declaredArity: objectFreeze(arities),
|
|
416
|
+
});
|
|
417
|
+
}
|