@cosyte/mllp 0.0.6 → 0.0.8
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/CHANGELOG.md +236 -2
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -76,6 +76,76 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
|
|
|
76
76
|
|
|
77
77
|
### Security
|
|
78
78
|
|
|
79
|
+
- **The PHI scanner refuses a non-regular entry under a scan root, on both routes
|
|
80
|
+
(`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`).** A symbolic link under `test/` or `src/` read CLEAN on
|
|
81
|
+
**both** enumerating routes, so a link pointing at a PHI-bearing file passed the gate twice over.
|
|
82
|
+
Reproduced on `d854e81` with a synthetic name-bearing payload kept outside both walk roots: a link
|
|
83
|
+
to it under `test/` gave `all` mode exit 0 "OK, no hits"; a link to its **directory** did the same
|
|
84
|
+
and took the whole subtree with it; `--staged`, which is this repo's pre-commit gate, exited 0
|
|
85
|
+
after `git add`; and naming the target explicitly exited 1 with every hit. The payload was always
|
|
86
|
+
detectable, the two routes never looked at it.
|
|
87
|
+
|
|
88
|
+
Two mechanisms, two fixes. `walk()` enumerates `Dirent.isFile()`, an **lstat** answer, so a link is
|
|
89
|
+
neither a file nor a directory and fell out of the loop; `isDirectory()` is an lstat answer too,
|
|
90
|
+
which is how a linked directory vanished wholesale. `--staged` read content with
|
|
91
|
+
`git show :<path>`, and git stores a symbolic link as its **target path** under mode `120000`, so
|
|
92
|
+
it scanned the path text.
|
|
93
|
+
|
|
94
|
+
**Neither route follows a link it finds INSIDE a scan root.** Following would read bytes the
|
|
95
|
+
enumeration does not control (outside the repo, a loop, a device, a FIFO that blocks the gate
|
|
96
|
+
forever), and git does not carry those bytes anyway, so a hit on them would be a claim about
|
|
97
|
+
something no commit contains. The enumeration is narrowed instead: a non-regular in-scope entry
|
|
98
|
+
**refuses the scan** (exit 2, the existing "could not complete" code), naming **every** offender.
|
|
99
|
+
`--staged` now reads `git diff --cached --raw -z` so the destination mode is visible. The scan
|
|
100
|
+
roots' own names are matched as well as the prefix, because an entry named exactly `test` or `src`
|
|
101
|
+
does not sit inside a root, it **replaces** one, and a prefix test alone let a staged mode-`120000`
|
|
102
|
+
blob named `test` through at exit 0.
|
|
103
|
+
|
|
104
|
+
**The root itself is the one thing still followed, and a flat "neither route follows a link" would
|
|
105
|
+
be false.** `walk()` opens `test/` and `src/` with `existsSync` + `readdirSync`, and both follow,
|
|
106
|
+
so replacing a walk root with a link to a directory outside the repo makes the walk read straight
|
|
107
|
+
through it. Pre-existing, and the precise reading is **not** "its PHI is reported": the tree beyond
|
|
108
|
+
the link is scanned exactly as the root it replaced would have been, **with that root's own
|
|
109
|
+
limits**. A fixture-like payload behind a linked `test/` is reported (measured, exit 1); the same
|
|
110
|
+
payload behind a linked `src/` gets only the conservative pass and can read clean, exactly as it
|
|
111
|
+
would through a real `src/`. It is link-**neutral**, which is why it is disclosed rather than
|
|
112
|
+
closed: refusing a linked root is a decision about repo layout, not this defect.
|
|
113
|
+
|
|
114
|
+
**`--diff-filter` admits `T` (typechange), and leaving it out would have made the mode check
|
|
115
|
+
unreachable whenever the file being replaced was already tracked.** Replacing a **tracked** regular
|
|
116
|
+
file with a link is neither an add nor a modify; git raises it as `T`, so `--diff-filter=AM` deleted
|
|
117
|
+
the record before any mode could be read and the hook passed a mode-`120000` blob green. Measured on
|
|
118
|
+
git 2.39.5 against a tracked `test/differential/fixtures/*.frame.bin`: with `AM` the raw output for
|
|
119
|
+
that stage is **empty**. Admitting `T` also scans the reverse typechange, a link replaced by a real
|
|
120
|
+
file bearing PHI. Both premises are asserted in the tests rather than trusted.
|
|
121
|
+
|
|
122
|
+
A refusal names the entry's own repo-relative path and an engine-owned kind token, **never the link
|
|
123
|
+
target**, which is working-tree text that can itself carry PHI. Pinned with a synthetic name-bearing
|
|
124
|
+
payload whose target **filename** also carries a name, so the never-echo assertion is not a claim
|
|
125
|
+
about an empty string. 10 of the 14 new cases are red on `d854e81`; the other four are boundary
|
|
126
|
+
controls that must stay green (a gitignored entry stays exempt, an entry outside both roots is not
|
|
127
|
+
refused, the committed corpus still passes, a staged regular file still scans).
|
|
128
|
+
|
|
129
|
+
**Scope, stated rather than left to be read charitably.** "In scope" is each route's own existing
|
|
130
|
+
**root**; the `.ts` / `.md` **name** exemptions deliberately do not carry over to a non-regular
|
|
131
|
+
entry, because they are judgements about a file whose bytes the route could have read. `R`/`C`
|
|
132
|
+
rename and copy are still not enumerated by `--staged` at all: **pre-existing** (`AM` excluded them
|
|
133
|
+
too), disclosed rather than fixed. Explicit-path mode still reads **through** a link and is
|
|
134
|
+
unchanged. The gitlink (mode `160000`) arm is **not** a hole this closes: `--staged`'s scope already
|
|
135
|
+
reached a staged submodule under both roots and `git show` on one fails with `bad object`, so the
|
|
136
|
+
base commit already refused it, and what changed is that the diagnostic now says what the entry is.
|
|
137
|
+
The refuse-a-sweep-that-observed-nothing rule and the vanished-transient tolerance are untouched:
|
|
138
|
+
a non-regular entry that goes away mid-sweep therefore still **refuses**, because that tolerance is
|
|
139
|
+
scoped to the read and these entries are refused at enumeration. That asymmetry is deliberate in
|
|
140
|
+
direction, fail-closed being the correct way for a PHI gate to be wrong, and it is not reachable
|
|
141
|
+
here: nothing in this repo creates a non-regular entry under a scan root, and every one the tests
|
|
142
|
+
create lives in a throwaway repo under `tmpdir()`.
|
|
143
|
+
|
|
144
|
+
**This corrects a claim made under this same heading by the previous scanner change**, which called
|
|
145
|
+
the symlink gap "bounded, because git cannot commit content through a symlink and `--staged` reads
|
|
146
|
+
the index". The first half is true and the conclusion does not follow: the gate's promise is that it
|
|
147
|
+
observed what is under its roots, and both routes reported clean over an entry neither had read.
|
|
148
|
+
|
|
79
149
|
- **BREAKING: ACK-correlation diagnostics no longer carry the control ID (`PHI-WARNING-MESSAGE-LEAK`).**
|
|
80
150
|
The opt-in `correlateByControlId` path built its diagnostics by interpolation, and both ends were
|
|
81
151
|
consumer-controlled and unbounded. The unmatched-ACK path read the **peer's** MSA-2 straight off an
|
|
@@ -185,6 +255,167 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
|
|
|
185
255
|
|
|
186
256
|
### Fixed
|
|
187
257
|
|
|
258
|
+
- **The test suite stopped assuming an idle machine, and the fix was mostly to make it cheaper
|
|
259
|
+
rather than to move a number.** `vitest.config.ts` carried `testTimeout: 10_000` and
|
|
260
|
+
`hookTimeout: 10_000`; the shared `@cosyte/vitest-config` sets neither. Test-only change: no
|
|
261
|
+
runtime, public API or published artifact is affected. `PARSER-TESTTIMEOUT-ASSERTS-AN-IDLE-BOX`.
|
|
262
|
+
|
|
263
|
+
**What the numbers below were measured under, because the condition is the finding.** Every
|
|
264
|
+
figure comes from `pnpm test:coverage`, run on a shared box that had other repos' suites on it
|
|
265
|
+
throughout, with base and head **interleaved** (alternating within the same minutes, three rounds
|
|
266
|
+
each) so contention cancels instead of being attributed to the change. The load-sensitive figures
|
|
267
|
+
additionally come from **four concurrent coverage suites**, which is what a parallel session here
|
|
268
|
+
actually does. Single figures are medians; peaks are called peaks.
|
|
269
|
+
|
|
270
|
+
**The headline, and it is a pass/fail rather than a stopwatch reading: under four concurrent
|
|
271
|
+
coverage suites the base commit FAILED three runs out of three, on correct code, and head passed
|
|
272
|
+
three out of three.** Base red on `test/framing/byte-fidelity.test.ts` (the 1 MiB corpus, 31.1 s,
|
|
273
|
+
over its **own** 30 s budget), on `test/conformance/quirk-corpus.test.ts` (§3.7 large payload,
|
|
274
|
+
10.7 s and 14.2 s) and on `test/scripts/phi-scan.test.ts` (`--staged` negative control, 14.6 s).
|
|
275
|
+
All three were spending that time on **cost rather than subject**, which is why the remedy is the
|
|
276
|
+
trim and not a number.
|
|
277
|
+
|
|
278
|
+
**`hookTimeout: 10_000` was a verbatim no-op and is gone.** Vitest 4.1.4 resolves
|
|
279
|
+
`hookTimeout ??= 1e4` and `testTimeout ??= 5e3` for a non-browser run, read out of this repo's own
|
|
280
|
+
installed copy. The line restated the default exactly.
|
|
281
|
+
|
|
282
|
+
**`testTimeout: 10_000` stays, and the reason is not the one this change set out to give.** After
|
|
283
|
+
the trim, the slowest case in the suite that carries **no budget of its own** is a `fast-check`
|
|
284
|
+
property sweep at **2.6 s peak** under four concurrent coverage suites, and across three loaded
|
|
285
|
+
runs **not one** unbudgeted case passed 5 s. (Every `attw` gate case is budgeted for this count,
|
|
286
|
+
in the bare-trailing-number form described later in this entry, which is why the suite's
|
|
287
|
+
slowest cases are not in it.) So neither 10,000 nor Vitest's 5,000 is what stands
|
|
288
|
+
between this suite and a false red any more, and an earlier draft of this entry was wrong to say
|
|
289
|
+
the framework default would sit close to reddening correct code. The literal is left alone because
|
|
290
|
+
no measurement asks it to move: halving a documented ceiling with nothing near it is churn, and
|
|
291
|
+
`PARSER-TESTTIMEOUT-ASSERTS-AN-IDLE-BOX` is satisfied by removing what was near it. The literal was
|
|
292
|
+
never what asserted an idle box.
|
|
293
|
+
|
|
294
|
+
**Two sites gain a budget, and no pre-existing number changes value.** The larger is `test/tls/**`,
|
|
295
|
+
where every case generates an RSA key pair and completes a real TLS handshake, both CPU-bound and
|
|
296
|
+
proportional to machine load, while what each case asserts is an outcome (a classification, a
|
|
297
|
+
flag, a frame delivered) and never a duration. Over 96 TLS case-runs under four concurrent
|
|
298
|
+
coverage suites the WANT-authorized regression **peaked at 9.70 s**, against the 10 s ceiling it
|
|
299
|
+
used to run under. No TLS case was observed timing out, and the budget is not claimed to fix an
|
|
300
|
+
observed red: a 3 % margin on a load-proportional case is a coin flip, and the budget is what buys
|
|
301
|
+
it out of the shared ceiling. The smaller is the one case this slice adds, the `tsx`/`node` parity
|
|
302
|
+
check below: it is the only case in the suite still paying a `tsx` cold start, and it **peaked at
|
|
303
|
+
9.20 s** under the same four concurrent suites, which is the same argument at 92 % of the ceiling.
|
|
304
|
+
On a quiet box it runs in under 2 s, so the budget looks gratuitous there and is not.
|
|
305
|
+
|
|
306
|
+
**Trim first: the phi-scan suite was paying a `tsx` start per case.** It spawns the scanner dozens
|
|
307
|
+
of times through two helpers, and on this scanner `tsx` costs **466 ms** against **137 ms** for a
|
|
308
|
+
bare `node` (medians of seven, same clean fixture). The suite now spawns `node` directly and relies
|
|
309
|
+
on node's native type stripping; the file went from **29.7 s to 12.4 s** **while gaining a test**.
|
|
310
|
+
The gained test is the backstop: `pnpm phi-scan` still runs `tsx scripts/phi-scan.ts`, so one case
|
|
311
|
+
spawns both runners on a framed violator and on a clean file and requires they agree on exit code,
|
|
312
|
+
stdout and stderr byte for byte. Without it the cheap runner would be testing something the commit
|
|
313
|
+
gate does not run.
|
|
314
|
+
|
|
315
|
+
**Trim second, and this one was the surprise: `expect(a).toEqual(b)` on a megabyte of Buffer was
|
|
316
|
+
the whole cost of the framing corpus test.** `toEqual` walks two Buffers element by element in JS.
|
|
317
|
+
Timed around the assertion alone on a 1 MiB pair it took **8.46 s under coverage** (4.32 s without
|
|
318
|
+
it), against **0.5 ms** for `Buffer.equals` on the same pair and tens of milliseconds for the
|
|
319
|
+
encode and decode round trip the test exists to exercise. The two large-payload assertions (the
|
|
320
|
+
1 MiB byte-fidelity corpus and the quirk corpus's accumulator-growth case) now use a native
|
|
321
|
+
`Buffer.equals` through `test/helpers/bytes.ts`, which reports the first differing offset and both
|
|
322
|
+
bytes on failure, so the diagnostic is better than the truncated structural diff it replaced. Those
|
|
323
|
+
two files together went from **14.9 s to 1.2 s**. The helper's failure paths are pinned by their
|
|
324
|
+
own tests, since a byte comparison that cannot fail is worse than the assertion it replaced.
|
|
325
|
+
|
|
326
|
+
**Said plainly, because it is easy to oversell:** the whole-suite wall moved only from **33.3 s to
|
|
327
|
+
27.8 s** (median of three interleaved rounds). The suite's critical path is
|
|
328
|
+
`test/scripts/attw-gate.test.ts`, which runs a real `npm pack` per case, peaked at 43.9 s in one
|
|
329
|
+
case under four concurrent suites, was not touched here, and already carries a 120 s per-case
|
|
330
|
+
budget. The per-file cuts are large; the total is bounded by a file this change deliberately left
|
|
331
|
+
alone.
|
|
332
|
+
|
|
333
|
+
**One assumption carried in from a sibling repo was measured here and is false here.**
|
|
334
|
+
`pnpm test:coverage` is **not** materially slower than a bare `pnpm test` on this suite: four
|
|
335
|
+
interleaved rounds put the two within noise of each other, because the critical path is
|
|
336
|
+
`attw-gate`'s real `npm pack` subprocesses, which v8 coverage does not instrument. Coverage is
|
|
337
|
+
still the right thing to measure under, for the different reason that CI runs **both** `pnpm test`
|
|
338
|
+
and `pnpm test:coverage`, so the suite is paid for twice per matrix leg.
|
|
339
|
+
|
|
340
|
+
**The rule this leaves behind, stated once:** a case that needs more than the shared ceiling
|
|
341
|
+
states its own budget at its own site and says why. **No list of those sites is kept anywhere,
|
|
342
|
+
because three drafts of this entry kept one and all three named the wrong set.** Read them off the
|
|
343
|
+
tests, and note there are **two spellings**: the options object (`it(name, { timeout: N }, fn)`,
|
|
344
|
+
`describe(name, { timeout: N }, fn)`) and a **bare trailing number** (`it(name, fn, N)`, and the
|
|
345
|
+
same on `describe`), which is
|
|
346
|
+
the form the `attw` gate uses through a named constant. A search for `timeout:` alone finds
|
|
347
|
+
neither the second form nor the constant, and does turn up `spawnSync`'s unrelated child-process
|
|
348
|
+
kill timeout. The quirk corpus's large-payload case was briefly given a budget and then had it
|
|
349
|
+
taken away again, because after the trim it no longer needed one, and a budget that changes
|
|
350
|
+
nothing is the same defect as the `hookTimeout` line above.
|
|
351
|
+
|
|
352
|
+
**Disclosed, not fixed, and it outranks everything above: the tightest idle-box assumption in
|
|
353
|
+
`test/tls/**` is not a test budget at all.** Both TLS files poll with a local
|
|
354
|
+
`waitFor(cond, timeoutMs = 3000)` helper, and two cases pass 5,000 ms explicitly. Those are
|
|
355
|
+
wall-clock ceilings on a loaded box exactly the way `testTimeout` was, they are unchanged here and
|
|
356
|
+
unchanged from base, and no per-suite budget relaxes them. They did not red in any run measured
|
|
357
|
+
for this slice. Tightening them is its own slice; widening them is not obviously right either,
|
|
358
|
+
since a poll ceiling is also what stops a hung case from burning the whole 60 s budget.
|
|
359
|
+
|
|
360
|
+
**Disclosed, not fixed.** `engines.node` says `>=22.0.0`, while node's type stripping is on by
|
|
361
|
+
default only from 22.18, so the phi-scan suite now needs the newer 22 that CI's 22 and 24 matrix
|
|
362
|
+
already resolves to. Narrowing `engines` is a consumer-facing change and belongs in its own slice.
|
|
363
|
+
|
|
364
|
+
- **The `attw` publish gate no longer passes on a tarball that carries no types.** `pnpm attw` was
|
|
365
|
+
`attw --pack . --profile node16`, and `@arethetypeswrong/cli@0.18.4`'s `getExitCode.js` opens with
|
|
366
|
+
`if (!analysis.types) return 0`, so the problem list is never read and no `--profile`,
|
|
367
|
+
`--ignore-rules` or config setting reaches that early return. For a package that ships types, "does
|
|
368
|
+
not contain types" means the declarations were not in the tarball, which is a broken publish
|
|
369
|
+
reported as a pass. A false red costs an hour; a false green merges.
|
|
370
|
+
`ATTW-FALSE-GREEN-PORT`, porting the remedy that shipped in `terminology#28` (`bf153cb`).
|
|
371
|
+
|
|
372
|
+
**Measured on this package at `0.0.6` with zero concurrency**, under the old invocation:
|
|
373
|
+
`rm -rf dist && pnpm attw` printed "This package does not contain types." and exited **0**.
|
|
374
|
+
Concurrency supplies only the condition, so the remedy is not a lock, a lease or a build queue: the
|
|
375
|
+
gate has to be able to say its own inputs were missing, whatever removed them.
|
|
376
|
+
|
|
377
|
+
**The exit-0 path needs the tarball to carry no declaration at all**, which is a boundary specific
|
|
378
|
+
to how this package is built and is stated here because it is easy to get wrong. `tsup` emits a
|
|
379
|
+
shared type chunk (`dist/index-<hash>.d.ts`) beside the three entry declarations, so deleting only
|
|
380
|
+
the six entry `.d.ts`/`.d.cts` files leaves that chunk in the tarball and `attw` reds honestly
|
|
381
|
+
(`❌ No types`, UntypedResolution, exit 1); the same removal **plus** `dist/index-*.d.*ts` is what
|
|
382
|
+
exits 0. The build window lands in the exit-0 state regardless, because `tsup` writes JS in one
|
|
383
|
+
pass and every declaration in a later one, so no moment exists where only some are present: polling
|
|
384
|
+
two real clean builds here for `dist/index.mjs` and then the first declaration of any kind gave
|
|
385
|
+
windows of **4.25s and 3.31s**, holding JS and zero declarations throughout. The absolute timings
|
|
386
|
+
move run to run, so no single figure is quoted as "the" window; the stable claim is that the gap is
|
|
387
|
+
seconds rather than milliseconds, which is wide enough for a concurrent build or `pnpm clean` to
|
|
388
|
+
land `attw` in it.
|
|
389
|
+
|
|
390
|
+
`pnpm attw` is now `node scripts/attw.mjs --profile node16`. The wrapper hardcodes `--pack .` and
|
|
391
|
+
forwards the rest, so **`--profile node16` is preserved**, not dropped. Two nets, which catch
|
|
392
|
+
different things: a **preflight** that every relative path `package.json` promises (`main`,
|
|
393
|
+
`module`, `types`, `typings`, and every string leaf of `exports`) exists and is non-empty, which
|
|
394
|
+
catches the build window and names the missing file; and a **post-check** that promotes `attw`'s
|
|
395
|
+
untyped sentence to a failure, which catches declarations present on disk but excluded from the
|
|
396
|
+
tarball by `files`/`.npmignore` (no instance of that is on record here). This package declares
|
|
397
|
+
three subpaths, so the preflight has **twelve** artifact paths to check. Because the post-check
|
|
398
|
+
reads a string, the options that would hide it are refused by name and wholesale rather than by
|
|
399
|
+
value: `--quiet`/`-q`, `--format`/`-f`, `--config-path`, and a `.attw.json` setting `quiet` or
|
|
400
|
+
`format`. The first three of those were re-measured here, on this package's own untyped pack and
|
|
401
|
+
under its own `--profile node16`, each handing back exit 0 with the sentence unreadable;
|
|
402
|
+
`--config-path` is refused by inference, not measurement.
|
|
403
|
+
|
|
404
|
+
**Disclosed gap:** the preflight cannot see that shared type chunk go missing, because
|
|
405
|
+
`package.json` names it nowhere. It is left to the post-check and to `attw` itself rather than
|
|
406
|
+
papered over with a glob. For the same reason the preflight's failure message does not promise
|
|
407
|
+
which way `attw` would have gone, since it cannot tell the two states apart.
|
|
408
|
+
|
|
409
|
+
`test/scripts/attw-gate.test.ts` pins both nets against the real binary, including **the upstream
|
|
410
|
+
exit 0 itself** (so an `attw` upgrade that rewords the sentence or fixes the exit code reds the
|
|
411
|
+
suite rather than letting the net go slack), that the preflight reaches a **subpath's `require`
|
|
412
|
+
branch** and not just the root entry, **the boundary above** (a tarball keeping a shared chunk reds
|
|
413
|
+
on `attw`'s own account, so the failure message must not claim the exit-0 counterfactual), that
|
|
414
|
+
**`--profile node16` survives the wrapper** (the fixture is red under the default profile and green
|
|
415
|
+
under `node16`), a **negative control** on a well-formed package, and that a **real `attw` failure
|
|
416
|
+
still fails** with `attw`'s own exit status. Build and
|
|
417
|
+
packaging only: no runtime, framing, transport, or public-API change.
|
|
418
|
+
|
|
188
419
|
- **The PHI scanner no longer refuses an entire sweep because a transient file went away while it
|
|
189
420
|
was reading.** `all` mode lists `test/` + `src/`, then reads each file; anything created and
|
|
190
421
|
removed inside that window made a read throw `ENOENT` and took the whole scan down with it
|
|
@@ -261,8 +492,11 @@ begins its public history at `0.0.x`, per the cosyte version ladder (`0.0.x` unt
|
|
|
261
492
|
|
|
262
493
|
**Two scope notes surfaced by the same review, one pre-existing and one introduced here.**
|
|
263
494
|
PRE-EXISTING: `walk()` tests `e.isFile()`, which is false for a symlink Dirent, so a symlinked
|
|
264
|
-
file under `test/` or `src/` is never swept in `all` mode
|
|
265
|
-
|
|
495
|
+
file under `test/` or `src/` is never swept in `all` mode. **The "bounded" reading recorded here
|
|
496
|
+
was wrong and is corrected under Security above (`PHI-SCAN-SYMLINK-BLIND-ON-BOTH-ROUTES`): git
|
|
497
|
+
not committing content through a link does not make a route that reports clean over an unread
|
|
498
|
+
entry safe, and `--staged` reading the index is exactly how the link's target path text got
|
|
499
|
+
scanned in place of any content.** INTRODUCED by this change: a
|
|
266
500
|
tolerated skip exits **0** with only a stderr line, including in CI, where the tree is at rest and
|
|
267
501
|
a skip is therefore anomalous by definition. On the base commit that same input exited 2. Both are
|
|
268
502
|
stated rather than changed: the first is not this defect, and the second **is** the shipped design
|
package/dist/index.cjs
CHANGED