Repository navigation
Mask license keys where a key can reach a message, not on the path every detection takes - #85
Merged
Merged
Conversation
jwrosewell
marked this pull request as ready for review
November 12, 2026 23:44
This was referenced Nov 13, 2026
Draft
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Node's release build failed its performance gate on
Ubuntu_ARM_Node_24at25,484 detections a second, against a ten result history average of 33,358
and the gate's floor of 27,413.
The cause is #58,
merge commit
efbea083,
which made
LicenseKeys.maskthe first line ofPipeline.log.FlowDatalogs on every
processand on every element read, so the mask ran for everydetection whether or not anything was listening for the message.
Measured on one
ubuntu-22.04-armrunner, every tree built whole in its ownfolder, five rounds each with the order rotating, run
43182592963:
fdc2a3dd, the only parent of #58's mergeefbea083, #58 as mergedefbea083with that one line removedThe two trees in the first two rows are one commit apart, so 18.4 per cent is
the cost of #58 and nothing else. Removing the one line gives 17.6 per cent
of it back and lands within the spread of the tree before #58, whose rounds
ran 32,851 to 34,547 against 31,423 to 34,304. So the line is effectively all
of the cost and there is no residue to hunt.
The new test counts how often the mask actually runs, and it is 575 calls
over 25 detections, which is 23 a detection rather than the two the file's
shape suggests.
FlowDatalogs onprocess, onstopand on every read,Evidence.addlogs for each key, andPipelineBuilderputs the sequence,JavaScript and HTTP elements alongside the element under test, so each of
them logs too.
What the specification says
pipeline-specification/features/logging.md,
pinned at
a0a7008a, sets the guarantee at lines 20 and 21:and names the one message that matters most here, at lines 32 to 36:
and asks, at lines 15 and 16, for the cost this draft is about:
The specification says a key must not reach a log entry. It does not say
where the masking has to happen, so masking where a key can enter a message
satisfies it, and it is what
.NET #119,
Java #85,
PHP #52 did on
10 November, and Go #79
on 9 November. Not one of them touched its Pipeline or Flow Data logging. Node
was the only language that put the mask on the generic path.
What changed
Two production files, 17 lines added and 5 removed, most of which is the
documentation of why. The rest of the diff is three test files and the
matching JSDoc carried into
types/pipeline.d.ts, which mirrors the sourceverbatim.
pipeline/fiftyone.pipeline.core/pipeline.jslogno longer masks the message it emits. It still masks the one it writesto the console, which is the branch taken for an
'error'that has no errorlistener, because the console is the end of the line and nothing else would
mask it.
A detection's own messages are
'info'and'debug', and a clouddetection also logs
'warn'(cloudRequestEngine.jslines 528 and702), none of which takes that branch. Two kinds of detection
do take it. A failed one masks a message
FlowData.setErrorhas alreadymasked, which
LicenseKeys.maskreturns unchanged. A cloud detection thatcarries on with a missing or null element logs
'error'fromcloudEngine.jslines 139 and 147, not throughsetError, and with no error listener that message is masked there too. Themissing element message is logged once for the life of the engine, and the
null element one on each request that has a null element, so a successful
detection with every element present never pays the mask.
pipeline/fiftyone.pipeline.engines/dataFileUpdateService.jsThis file's own
logwrapper now masks, which covers all of its roughlytwenty messages in one place and is never on the detection path. The file
already imported
LicenseKeysfor its console branch.This part is not optional. #58 masked at the cloud request engine, at the
51Did client and in
FlowData.setError, and it never touched the data updateservice, whose messages name the update URL that carries the licence keys.
Those were covered only by the generic mask, which is why
tests/dataUpdateKeyMasking.test.jspassed with no change to that file.Taking the mask off
Pipeline.logand stopping there would have publishedlicence keys in update log entries, against the specification lines quoted
above.
Which places a key can reach, and how I satisfied myself there are no others
I listed every
.log(,._log(andeventEmitter.emit(in production codeacross the repository, leaving out tests, examples and
console.log. Thatis 48 call sites in 10 files, every one of them under
pipeline/and nonein
device-detection,ip-intelligenceorlocation. I then read eachone's message.
cloudRequestEngine.jslines 315 to 344, every error built from a cloud answerdidClient.jslines 702 and 706, the body and the errors of a redeem answermaskLicenceat line 1047, because the package depends on nothing in the PipelineflowData.jsline 143,setErrorerrorslist the JSON Builder sends to the browser is masked toodataFileUpdateService.js, every messagelogwrapperPipeline.log's console branchThe places I read and left alone, with the reason, because a reviewer should
weigh these rather than take my word:
flowData.jslines 121, 164 and189,
evidence.jslines202 and
208,
setHeadersElement.jslines 173 and
208 and
pipeline.jsline 124 nameelement data keys, property names and evidence key names, never a
value. These are the messages a detection produces.
shareUsage.jslogs gzip and network errors fromhttps://devices-v4.51degrees.com/new.ashx, which carries no licence key.cloudEngine.jslines 139 and 147 name element keys, a product hint andthe cloud's own
nullreasontext.cloudRequestEngine.jsline 528 names property names, andline 702 names
an evidence key, its value and the keys it conflicts with.
javascriptbuilder.jsline 333 names the callback URL the applicationconfigured, and line 400 an object name.
The last three are the ones I was unsure about, so I will say plainly what
the change does to them. Each is built from text a caller or the cloud
supplies, so a 51Degrees licence key does not reach them through our own
code, but a caller could in principle put one in a JavaScript builder
endpoint or in evidence. Today's generic mask would catch such a case only
where the text literally reads
license=orlicence=, becausePipeline.logcallsLicenseKeys.mask(message)with no keys, and withno keys
maskTextskips its key loop and only runs theKeyParameterreplacement. So the generic path was already far narrower than it looks, and
what it covered that these call sites do not is the caller's own text with a
licence parameter in it. I judged that not worth 13 per cent of every
detection. If a reviewer disagrees, the cheapest answer is a mask at
javascriptbuilder.jsline 333, which is behind a log-once flag and wouldcost nothing.
The test #58 wrote that this draft changes
The first full build,
43186177965,
failed on two tests in
tests/licenseKeys.test.jsthat #58 wrote, both pinning that
Pipeline.logmasks. They needed twodifferent answers.
a pipeline error entry with no listener is masked on the consoleiskept and passes unchanged, because the mask stayed in that branch.
a pipeline log entry is maskedasserted the behaviour this draftdeliberately changes, so it is now
a pipeline log entry is passed on as it is, with a comment above it naming where the guarantee lives instead.A reviewer should look hard at the second one. It is a test of a security
property being changed rather than fixed, and I would rather it were
challenged than waved through. The guarantee it used to carry is now carried
by
cloudLicenseKeyMasking.test.js,didClient.test.js,dataUpdateKeyMasking.test.jsand thean error kept on the flow data is maskedtest beside it, all of which pass untouched.What the draft is worth
Run
43189086425,
five arms built whole on one
ubuntu-22.04-armrunner, five rounds each withthe order rotating, the same 109,324,901 byte TAC Enterprise data file and
2,726,867 byte 20,000 User-Agent evidence the gate uses. The log prints each
arm's
Pipeline.loganddataFileUpdateService.jslines, so which tree ranis checked rather than assumed.
The arms ran on
b1bc4e9d. This head,629ae019,differs from it in three comments and one JSDoc block carried into
types/pipeline.d.ts, with no change to any code, so the figures stand.fdc2a3dd, before #58rchead74cdc781rchead, with aninfoand adebuglistener attachedThe draft is 13.1 per cent faster than
rcwith nothing listening and 14.7per cent faster with a logger attached, which is the condition an application
is actually in. It clears the gate floor of 27,413 by 2.8 per cent.
It does not get back to the tree before #58, and it will not. The draft
sits 13.4 per cent below b0. That gap is not masking, and here is the
evidence. In run
43182592963
the same change was applied to two different trees. On
efbea083it measured32,263 and on today's
rchead it measured 29,154, a difference of 9.6 percent between the same code on trees three days apart. So the rest of the gap
is the cost of what merged into
rcbetweenefbea083on 10 November and
74cdc781today, it is a separate fault, and nobody is on it. On a slow runner Node
could still miss the gate with this draft merged.
Why not make the masking lazy instead
The other shape was to keep the mask on
Pipeline.logand skip it whennothing is listening for the message, which would have kept Node's broader
guarantee. It was measured in the same run and it loses on the figures, not
on taste.
rcheadWith a logger attached the lazy shape is worse than doing nothing, 24,661
against 25,212, because it still masks every message it delivers and pays a
listenerCountcall on top. It only helps where nothing listens, which isthe performance gate's case and not a customer's. Taking the mask off is 15.1
per cent better in the same condition.
Runs
43188579949
on
95d34ef, which is today'srcwith the three test files and noproduction change. 4 failed, 36 passed. The four are the two new cost
tests, each counting 575 calls where 0 is asked, the new data update
service test and the rewritten pipeline log test.
43189007832
on
43f1842, the same branch with the two production files added. 40passed, 40 total. ESLint reports 0 errors and 1 warning, and that warning
is on
pipeline.jsline 57 and is identical onrc.43182592963
and
43189086425.
43199280160,
green. All 7
Setting 'PullRequestSha'lines read9fd11ad5b227ae528e78a7d02038d0afe654b28e, the head before the last commit, Configureprinted "Left out 2 macOS entries", and each of the 6 jobs ran two suites,
being 1,237 passed with 2 skipped and 132 passed with 4 skipped. The build
before it,
43189129810,
was green on
b1bc4e9d, which is this head less the two documentationcommits. The earlier build
43186177965
on
f18dd1cfailed, for the reason in "The test Mask license keys in log entries and error messages #58 wrote that this draftchanges" above.
The last commit,
629ae019,changes only the comment in
Pipeline.logon which detections reach itsconsole branch, and no code, so it merges on its quick checks and a review,
without a new full build.
macOS was left out of this draft's full build under the 9 November decision.
The rc head is built with macOS before it merges into main.
What remains
rcbetween 10 and 12 November has noowner. It is named by the two arms above and it is not this draft's
business, but Node will not return to 33,000 detections a second until
someone bisects it. It should be an issue of its own before the release
is judged.
javascriptbuilder.jsline 333 is the cheapest to add if a reviewer wantsit.
pipeline.jsline 124 emits'info'straight onto the event emitterrather than through
log. Its message is'processing 'and an element'sdata key, so no key can reach it and this draft leaves it alone, but it is
a piece of per request work that nothing can switch off.