0.5.11 — An option a verb never reads is now refused, so a misspelled password on key export can no longer leave a private key unprotected. - #175
Conversation
… `password` on key export can no longer leave a private key unprotected.
Eleven verbs accepted any option object and read only the keys they knew, so a typo, or an option borrowed from a sibling verb, did nothing and said nothing. The worst of those was `pki.key.export`, which does not encrypt: passing it a `password` wrote the private key in the clear from a call site that read as though the key were protected. Each verb now refuses a key it does not read, and the message names the option or the verb the caller most likely wanted.
Fixed:
- `pki.key.export(key, { password })` no longer writes an unprotected private key. Export serializes; it has never encrypted. The option was ignored, so the file on disk was a plaintext PKCS#8 while the call site named a password. It now throws `key/bad-input` naming `pki.key.encrypt(key, password)`, whose result is what to export.
- An options argument of `false`, `0`, `""` or `NaN` is refused rather than read as no options at all. The check that an options argument is an object ran after the argument had already been replaced by an empty object, and that replacement treated any falsy value as absent, so those four reached the body as `{}`. `pki.cms.sign`, `pki.cms.countersign` and `pki.cms.verify` carried this before this release; the verbs gaining option checks here would have inherited it. `null` and `undefined` still mean no options, and a Buffer in the options position is now named as the argument-order mistake it is.
Changed:
- `pki.key` (encrypt, decrypt, export, import, generate, publicFromPrivate), `pki.path.validate`, `pki.path.build`, `pki.lint.certificate` and `pki.ocsp` (buildRequest, sign, verify) throw `<domain>/bad-input` on an option they do not read, instead of ignoring it. A call passing an option that did nothing before will now fail; the message names the unknown key, so the fix is to correct or drop it.
- Where two verbs spell the same idea differently, the refusal says so. `pki.path.validate` takes `trustAnchor` and `pki.path.build` takes `trustAnchors`, and each names the other, because carrying the wrong spelling between them previously bought no anchoring and no error. `pki.key.encrypt` chooses `iterations` while `pki.key.decrypt` caps `maxIterations`; `pki.ocsp` spells the nonce three ways across buildRequest, sign and verify because it means three different things.
- `pki.path.build` accepts every `pki.path.validate` option, since it forwards them to each internal validation. That union is derived from validate's own list rather than restated, so the two cannot drift apart.
Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01cc105e68
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The refusal of unknown options walked own enumerable names, which is the subset
`Object.keys` reports and not the set a later `opts.foo` can resolve. Two
ordinary JavaScript objects answered the lookup while showing the check nothing:
`Object.create({ password: "pw" })` puts the value on the prototype, and
`Object.defineProperty(o, "password", { enumerable: false })` puts it where the
enumeration cannot see it. Either one meant `pki.key.export` accepted the bag,
ignored the password it could read, and returned the private key in the clear --
the case the refusal exists to prevent, reached by a different object shape.
Both were reproduced against the shipped verb before the fix and after it.
The check now reads every own name, enumerable or not, plus the inherited
enumerable ones. Inherited NON-enumerable members are deliberately left alone.
`constructor`, and the methods class syntax defines, are machinery no caller
means as an option, and this repository has already settled that a class
instance is a legitimate options bag -- guard.bytes.snapshotDeep copies one and
keeps its prototype so its methods still resolve. A rule that refused non-plain
objects, or that flagged everything up the chain, would have contradicted that
and rejected calls that work today. The first attempt did exactly that and the
suite caught it on `constructor`.
The vectors cover both directions: the two hidden spellings are refused, and a
null-prototype bag and an inherited KNOWN option are still accepted, so the
widened check cannot quietly start rejecting valid callers.
Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb4ed35ac8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Two more shapes reached `opts.password` while the check saw nothing, and both
meant `pki.key.export` returned an unprotected private key. A class getter is
non-enumerable, so a rule keyed on enumerability missed it. A value that is both
inherited AND non-enumerable was missed by every earlier reading.
Enumerability is gone from the test, because it recorded how a member was
WRITTEN rather than what it means: class syntax defines methods non-enumerable,
`Proto.m = fn` defines them enumerable, and the previous rule accepted one style
and rejected the other. What is skipped now is a name inherited as a data
property holding a function -- a method, either way it was written, including
`constructor`. An accessor is never skipped: a getter exists to answer with a
value, which is what an option is. An instance of a caller's own class remains a
usable options bag, which this toolkit had already settled elsewhere.
One case is traded away and is written down rather than left implicit: an
unknown option supplied as an inherited method is not reported. Its value would
be a function, and the function-valued options here are passed as own
properties, so reaching it needs a caller to both misspell one and put it on a
prototype.
The second half is that accepting a wider surface is only half a change.
`pki.path.build` recognized an inherited option and then built its forwarding
set with `Object.keys`, so `Object.create({ requireRevocation: true })` passed
the check and never reached a single internal validate -- a caller who demanded
a determined revocation result could have got a valid path without one. Both
copies now read the same surface, from one exported definition, so the set that
is accepted and the set that is forwarded cannot differ. Fixing only the second
copy left the first one dropping it, which is how the first attempt at this went.
Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2604012b57
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…c chain
A name is skipped as a method when it is a data property holding a function
that the prototype chain above it also supplies under that name. Ownness no
longer decides it. Ownness records whether the object has been through
guard.bytes.snapshotDeep, which copies every readable name onto the copy and
keeps the prototype, so the same method is inherited before the snapshot and
own after it. A rule keyed on ownness called that method behavior at one check
site and an unknown option at the next, and every check running after a
snapshot refused an options bag that is an instance of the caller's own class.
Value identity against the chain above answers the same on both sides, so one
rule now covers every call site rather than the ones that happen to run first.
Object.prototype is compared against a snapshot of its own names taken while
guard-identifier loads. Stopping the walk at Object.prototype excluded the
whole object, so a runtime carrying Object.prototype.password answered
opts.password on a plain {} and pki.key.export returned an unprotected key for
a bag holding nothing. The built-ins are skipped by identity and any name added
afterwards is reported. A name planted before the module loads is baked into
the snapshot; require order is the limit of what this can see.
A prototype chain is finite only while it is acyclic, and a Proxy whose
getPrototypeOf trap returns the proxy makes one while its target stays
extensible. Reading an object's option names, copying it, and every instanceof
inside the copy walk that chain, and none of them terminated: the first two
looped and the third exhausted the stack, so a raw RangeError escaped from a
guard whose contract is a typed error. Reading the names stops at an object
already visited, which loses nothing because a second lap yields no new name,
and copying refuses the shape with the caller's own code.
Each half is proven by neutering it and watching a named vector fail: the
promoted-method reading against cms.sign and ocsp.buildRequest, the inherited
reading against key.export, and each cycle test against its own guard suite.
The refusal messages and the comments added by this branch drop the
mid-sentence aside and the capitalized emphasis.
Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac23119df0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…wers A Proxy resolves ownKeys and get from two independent traps, so the two need not agree. One reporting no keys while its get returns "pw" for password presents an object every enumeration calls empty and every read calls populated: pki.key.export accepted such a bag and returned 48 bytes of plaintext PKCS#8, which is the case the unknown-option refusal exists to prevent, and guard.bytes.snapshotDeep copied it to an object holding nothing while the original still answered, losing a supplied field with no fault raised. No walk closes that. The readable names are whatever a trap decides at the moment it is asked, and asking again may differ, so the enumeration is not made more thorough and the object is refused. That is the rule guard-bytes already states for a value it cannot copy: an argument is copied or refused, never half-taken. The test is util.types.isProxy, by identity. Probing traps for a contradiction is a race the liar wins, since it can answer consistently for as long as the check looks and differ afterwards. A Proxy with no traps of its own is refused on the same rule rather than admitted for behaving today. Both refusals are proven by neutering each and watching its named vector fail: key.export and assertKnownKeys for the options door, snapshotDeep for the copy. Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 836ff9adee
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A Symbol-named option is reported. Both walks enumerated with Object.getOwnPropertyNames, which never returns one, so a Symbol key was accepted in silence and the copy dropped it before the check read the copy. Both now use Reflect.ownKeys, and the refused name reaches the message as its description, before either the default wording or a caller's own builder sees it, since JSON.stringify returns undefined for a Symbol. A name planted on Object.prototype before this package loads is reported, whether it holds a value or a function. The built-ins were recognized by reading Object.prototype at load, which is the polluted runtime answering the question about itself, so a name already there was taken for one and key.export returned a plaintext private key. They are now the twelve members the language specifies, each still required to have the shape a real one has: a data property holding a function, or the __proto__ accessor. Object.prototype is decided there and the method rule does not run on it, since that rule exempts an inherited function and a planted one is both. A guard-identifier test compares the list against the live object, so an engine that adds a member fails there rather than reporting it to every caller as an unknown option. A verb called with no options is handed a bag with no prototype, so it inherits nothing and its caller is never refused for a name they did not write. A Proxy is refused anywhere on the chain rather than only as the object itself. An ordinary object inheriting from one resolves the read through it while not being one, which left a line of indirection open in both the check and the copy. Copying no longer drops a name the caller added. A Date came back as its instant alone, and an own `constructor` was skipped along with the inherited one a class defines; in both cases the copy held nothing while the original still answered. Each is proven by neutering it and watching its named vector fail. The pre-load plants run in a child process, since the plant has to precede the require that takes the snapshot. Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6fbc720bd5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Reading a prototype chain runs a getPrototypeOf trap, so every walk of one runs the caller's code. The copy walked the chain looking for a cycle before it had decided the value was a Proxy at all, and a trap that throws then left as its own raw Error from a boundary whose whole contract is a typed one. The Proxy test now comes first, ahead of the cycle walk and of every instanceof below it, and it reaches getPrototypeOf only on links it has already cleared. The cycle test stays underneath as a second line and is unreachable while the first one holds: setPrototypeOf refuses to build a cycle out of ordinary objects, so a Proxy is the only way to have one. readableNames takes the caller's error factory, like every other function in the guard family. It walks the chain, and it was the one door that could not refuse a Proxy before doing so, which left a hostile trap escaping untyped from a function this module exports for exactly the caller who has not checked yet. Both call sites in path-validate pass their own class and code. A primitive in the options position is refused by the verb that checks types rather than raising a TypeError about Reflect.ownKeys, which the caller never called. The walk starts above the value instead of on it. The error factory is checked at entry rather than at the moment it is needed, which is the one moment something has already gone wrong. What that catches is a missing or non-callable factory; a class cannot be told from a factory by typeof, so calling E without new stays a convention its own vector pins. Each is proven by neutering it and watching its named vector fail. Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Line 383 in bf6805d
When an unsafe opaque value has a hostile constructor accessor, this diagnostic reads it after deciding to reject the value, allowing the accessor's raw exception to escape instead of the caller's typed domain error. For example, a RegExp with an enumerable foo property and an own get constructor() { throw new Error("boom"); } makes snapshotDeep throw that untyped Error; through any fixedCall producer such as pki.cms.sign, this bypasses the promised <domain>/bad-input boundary. Format the kind without consulting another caller-controlled property, or wrap this read through _raise.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
The mechanism does not reproduce on either runtime, and the suite claim does not hold, but the fragility behind it is real and is now pinned by vectors. Measured, on a freshly generated key and on an imported one, for Ed25519, ECDSA P-256 and RSASSA-PKCS1-v1_5, before and after reading No On the suite claim: What the report gets right is that enumerability of an implementation's own slots is an assumption about a runtime this library does not control, and widening the walk to Excluding symbols from the opaque pass-through test is not the remedy: an opaque handle is passed through by reference, so a field a caller hangs on one stays theirs to change after every check has read it, and a symbol key would then be the way around the refusal that exists for exactly that. There is also no way to tell an implementation's symbol from a caller's. So enumerability stays the line, the same line already used for names, and both directions are now vectors on a real CryptoKey: one asserts a platform key carries no enumerable own symbol after its surface has been read and still passes through by reference, the other asserts a key carrying a caller-added symbol field is refused. If a future runtime ever does cache under an enumerable symbol, the first fails at that upgrade instead of an operator finding their keys refused in production. |
…al key The property walks report symbol keys, so a caller cannot hang a field on an argument under one and have every check pass over it. That widening reaches the opaque pass-through test, which decides whether a handle carries caller-added fields, and enumerability is the line there for a symbol exactly as it is for a name: an implementation's own slots are non-enumerable, and what a caller adds they add by assignment. That line is an assumption about a runtime this library does not control, so it is now measured rather than assumed. A node:crypto CryptoKey carries no enumerable own symbol on Node 24 or 26, generated or imported, for Ed25519, ECDSA and RSA, before or after the surface reads where a lazy cache would land. Two vectors hold both ends: a platform key still passes through by reference after its surface has been read, and a key carrying a caller-added symbol field is refused. A runtime that ever did cache under an enumerable symbol would fail the first at the upgrade rather than refusing an operator's key in production. Excluding symbols there is not available: a handle is passed through by reference, so a field hung on one stays the caller's to change after every check has read it, and no test tells an implementation's symbol from a caller's. A symbol behaves as a name in the rest of the copy too, which is the point of the widening rather than a side effect: a class method declared under a symbol survives and still runs, and an accessor that throws is the caller's typed error under either kind of key. Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e7401eaf71
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…prototype pki.path.build forwards each validate option to every internal validate call, and chose what to forward by indexing a build-only table with the option name. A table read that way answers for any name Object.prototype carries, so on a runtime carrying softFail the caller's own value was taken for a build-only option and left out of the forwarded set. That set was a plain object, so the pass that reads it back found the inherited value instead. A caller who wrote softFail: false could receive a valid path built on an undetermined revocation result they had refused to waive. Both halves are fixed and each is proven by its own vector. Own membership decides whether a name is build-only, which is what carries the caller's stated value through in either direction: a stated false must not be dropped, and a stated true must not be either. The forwarded set and the per-anchor options built from it have no prototype, so a build-only name excluded from them stays excluded rather than being re-acquired and handed to validate, which does not accept it and would refuse the whole call. The vectors run in a child process, since the plant has to precede the require that reads the prototype. test/helpers/polluted-prototype-preload.js is that plant, and it names options the verb recognizes on purpose: an unrecognized one is already refused, and a name the caller never states is genuinely what their bag answers, so neither settles anything. The case in between is the one that needed pinning. The wider shape, a registry indexed by a caller-supplied name, is a separate sweep: constructor and toString are truthy in any such table with no pollution at all. Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
An array, a byte view, a Map, a Set or an ArrayBuffer used as an options bag reports only what the caller added to it. A verb reads an option by name whatever the argument's type, so these shapes are supported, but their own surface is structural: length is intrinsic on an empty array and the prototypes carry buffer, byteLength, size and BYTES_PER_ELEMENT. Reading the whole chain reported all of it and the verb then refused a shape it supports. length and the indices are skipped for an array and a byte view only, since the other kinds have no own length and skipping the name there would discard one the caller had put on themselves. guard-bytes states the same rule for which names it copies, and the two are one rule seen from each side. pki.path.build builds the set it forwards from the validate-option contract rather than by enumerating the bag. Enumerating reads whatever the property walk reports, and that walk skips a method so a caller's class keeps its own; a known option whose value were a bare function would be indistinguishable from one and would be dropped. No option takes a bare function today, so this is a hardening rather than a live defect and no vector fails without it. It is here because asking the contract cannot drop a known option whatever its value. The spelling gate reads the files git does not track yet, alongside the ones it does. Built on `git ls-files` alone it answered for the committed tree, so a file written in the same session was invisible locally and read for the first time by CI, which is how a British spelling in a new test helper cost a full run on a tree whose gates were green. Its canary now asserts the file set itself, and clears its probe before writing as well as after, so a run that dies partway cannot leave a planted misspelling in the repository root for the next run and the linter to trip over. Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46e15c51ca
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…he property can still move, so a key handle reaches the verb it was passed to and a field the caller can still change is refused; and a Date copy answers a Date method with the language's rather than a method written onto the value Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5ad8aa923c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…an HMAC key whose handle holds raw bytes reaches the verb it was passed to alongside the signing keys Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a28a02b984
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… field inherited from an object a caller built over the shared typed-array prototype is reported as the unknown option it is Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…efusal Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59faa057e3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…uestion, so a throwing trap cannot leave the copy carrying the caller's own error Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc45bd1a26
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…olds, so a Date carrying its own calendar methods cannot change what is printed beside a certificate Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 759a70c6d3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…where nothing can be put in front of it, since an extensible value takes an own property of the same name at any time Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…re that implementation keeps its internals under a symbol, since no verb reads an option under one; a name a verb could read is still refused on the same handle Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 34386bb84e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…n the name, so a plain field named after a Date method reaches the unknown-option check instead of vanishing Signed-off-by: dotCooCoo <RobertLeeLW@gmail.com>
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Eleven verbs accepted any options object and read only the keys they knew, so a typo, or an option borrowed from a sibling verb, did nothing and reported nothing.
pki.key(encrypt, decrypt, export, import, generate, publicFromPrivate),pki.path.validate,pki.path.build,pki.lint.certificateandpki.ocsp(buildRequest, sign, verify) now refuse a key they do not read.The one that mattered:
pki.key.exportserializes and has never encrypted, soexport(privateKey, { password })wrote the private key in the clear from a call site that read as though it were protected. It now refuses and namespki.key.encrypt(key, password), whose result is what to export.Where two verbs spell one idea differently the refusal says which is which —
path.validatetakestrustAnchorandpath.buildtakestrustAnchors;key.encryptchoosesiterationswhilekey.decryptcapsmaxIterations; the OCSP nonce means something different in each of buildRequest, sign and verify.Three defects underneath the first one
Each was found by review after the fix above, and each was a way past it.
The check ran after the default.
opts = opts || {}preceded the shape check, and||treatsfalse,0,""andNaNas absent, so those four arrived as{}and were accepted.pki.cms.sign,pki.cms.countersignandpki.cms.verifyhad already shipped that way. Normalization is now one primitive that returns the value, so the safe order is the only order available.The check could not see half the property surface. It walked
Object.keys— own enumerable names — whileopts.fooresolves through prototypes, non-enumerable properties and accessors. A prototype value, a non-enumerable own value, and a class getter each answeredopts.passwordwhile showing the check nothing, and each returned an unprotected key. Enumerability is now out of the test entirely: it recorded how a member was written (class syntax defines methods non-enumerable,Proto.m = fndefines them enumerable), not what it means.Accepting a wider surface is half a change.
path.buildrecognized an inheritedrequireRevocation: trueand then built its forwarding set withObject.keys, so it never reached a single internalvalidate— a caller who demanded a determined revocation result could have got a valid path without one. Both copies now read one exported definition.Deliberately allowed
An instance of a caller's own class stays a usable options bag; this toolkit had already settled that (
guard.bytes.snapshotDeepcopies one and keeps its prototype so its methods resolve). So an inherited data property holding a function is skipped as behaviour. The residual is stated in the code: an unknown option supplied as an inherited method is not reported — its value would be a function, and the function-valued options here are passed as own properties.Test plan
eslint --max-warnings 0 .— cleannpm run gates— codebase-patterns, comment blocks, api snapshot, spellingSMOKE_PARALLEL=64 node test/smoke.js— 12644 checks across 118 filesnode scripts/test-integration.js— interop green (run byrelease.js push)gitleaks— no leaksNo wire format, encoder or decoder changed. The API surface snapshot is unchanged; the behavior change is that a call passing an option that previously did nothing now fails, naming the key.