Repository navigation
PER-16493: log only the PDP and API URL when the client starts - #137
Conversation
The constructor's debug line printed the whole config. It now names the PDP and the API URL, which is what the line is for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…no options - The init line passes its fields to pino as an object: the PDP and API URL origins (no user, password, path or query) and the settings that change the SDK's behaviour (timeout, throwOnError, multiTenancy, proxyFactsViaPdp, the facts-sync and retry settings). - LoggerFactory no longer passes prettyPrint, which pino 8 rejects, so log.json / PERMIT_LOG_JSON=true no longer stop the client from starting; the output stays JSON either way, as it was by default. - pino redacts token and Authorization fields as a backstop. - Tests run the child without PERMIT_* variables, check the exact fields, cover log.json, and report a failed spawn's cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… URLs without credentials
- The logger's err serializer turns an axios error into its type, message,
code, status, method and URL. pino's standard serializer copied its config,
request and response, which hold the Authorization header; the redact paths
stay as a second layer.
- redactUrl drops a URL's user, password, query and fragment. PDP connection
errors and the HTTP debug lines show URLs through it.
- PERMIT_LOG_JSON is read leniently ('true'/'1', any case), so no value stops
the client from starting; log.json is documented as having no effect.
- The init-log test's child process has a 20s timeout.
- Tests: an axios error with the token in its headers, raw request and URL
serializes without them; PERMIT_LOG_JSON values that JSON.parse rejected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ep no Authorization header - checkAllTenants posted an object holding the Authorization header and the query as the request body, so the PDP got the key in the body and no query. It now posts the query (user, action, resource, merged context) as the body, with the key in the Authorization header, and reads allowed_tenants, the field the PDP returns. The resource keeps only the tenant it names. - PermitApiError redacts the Authorization header in the axios error it carries (config, response config, raw request header text), and keeps originalError non-enumerable, so logging or inspecting the error does not print the key. originalError, response and request still work. - Tests: the all-tenants request's body and header; an API error inspected at depth prints no key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…egex CodeQL flagged the pattern's two adjacent wildcards as polynomial on long header text. A line scan does the same replacement in linear time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Changes requested: 1 HIGH, 3 MEDIUM, 6 LOW.
Blocking:
- HIGH
src/enforcement/enforcer.ts:377: thecheckAllTenantserror path needs the same redaction asPermitApiError
Non-blocking:
- MEDIUM
src/api/base.ts:18: the redaction covers less than its comment says, and the test uses a hand-made request - MEDIUM
src/api/base.ts:52: a network failure surfaces as aPermitApiErrorwith an empty message and no visible cause - MEDIUM
src/tests/unit/key-exposure.spec.ts:79: five of the new protections can be removed with every unit test still passing - LOW
src/index.ts:95: thePermitclass's TSDoc block now documentsurlOrigin - LOW
src/index.ts:109:urlOriginlogs"null"for a PDP value without a scheme - LOW
src/utils/redact-url.ts:6: the query and fragment are cut before the user info is removed - LOW
src/logger.ts:39: theredactbackstop covers one nesting level and two header spellings - LOW
src/api/base.ts:54:originalErroris now read-only at runtime - LOW
src/tests/unit/init-log.spec.ts:71: the expected key list is filtered by the actual record
The earlier code-scanning thread on src/api/base.ts (polynomial regex) is fixed at this head.
Details are in the inline comments on each line.
…rrors hide the request and say why - checkAllTenants redacts the axios error it rethrows, as PermitApiError does, and logs it through the err serializer (pino ignored the extra argument). - The axios error's request (the live Node request, which holds the headers) is no longer enumerable on the error or its response, so printing or inspecting the error doesn't reach it. It stays readable. - A PermitApiError without an API message (a refused connection, a timeout) takes axios's message instead of an empty one. - originalError stays writable, as the public field was; only enumerability changes. - Tests use real errors from a 127.0.0.1 server and a refused port, and cover the checkAllTenants URL, timeout and context store, and the PDP URL in connection errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t paths LoggerFactory.createLogger takes an optional destination (stdout by default), so a test can read what the SDK's own logger writes: an axios error through the err serializer, the redact paths, and the HTTP debug lines without a URL's credentials or query. The redact comment now says exactly which paths it covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ine logs redacted URLs - redactUrl removes the user info first, up to the last '@' before the path, so a '?' or '#' in a password can't hide the '@', then the query and fragment. - The init line logs redactUrl(pdp) and redactUrl(apiUrl) instead of the URL origin, which is "null" for a value without a scheme such as localhost:7766. - initLogFields sits above the Permit class's TSDoc block again, so the class keeps its docs in index.d.ts. - The init-log test sets timeout, retry and pdpRetry and expects all ten keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
left a comment
There was a problem hiding this comment.
Approved — no CRITICAL or HIGH issues found.
All ten threads from the previous review are verified at 9801dc1, including the blocking checkAllTenants one; replies are on each thread.
Non-blocking:
- MEDIUM
src/enforcement/enforcer.ts:391— no test checks thecheckAllTenantsfailure log lines - LOW
src/api/base.ts:47— a non-enumerable data property still shows in some inspection modes - LOW
src/utils/redact-url.ts:11— some inputs still come back with the wrong part removed or kept - LOW
src/logger.ts:27— theerrserializer fallback keeps a URL field unredacted - LOW
src/logger.ts:48— two of the six redact paths aren't pinned by a test - LOW
src/tests/unit/key-exposure.spec.ts:43—refusedUrl()connects to a port it has just released
Details are in the inline comments on each line.
… an accessor - Permit.checkAllTenants no longer logs the failure the enforcer has already logged, so a failure gives one error line. - `request` on an axios error and its response is a non-enumerable accessor rather than a data property, so inspection modes that list hidden properties show [Getter/Setter] instead of the request. It stays readable and writable. - The checkAllTenants failure tests capture the SDK logger, assert one record with the error's code (and status), and make a further call on the same client to check it still sends the key. - The tests use a server that drops each connection instead of a port that was just released, which another listener could take in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; pin each redact path - For an error that isn't an axios error, the serializer passes a string `input` (Node's ERR_INVALID_URL carries the whole URL there) through redactUrl. - The redact test is table-driven, one case per path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The input is trimmed, and one or more slashes after the scheme are accepted, as Node's URL parser (which axios uses) accepts both. - For a valid URL, a '?' or '#' starts the query or fragment, so an '@' after it is not taken as the end of the user info. - For a URL with a scheme that doesn't parse, everything up to the last '@' is removed, which also covers a '/' in a password. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
left a comment
There was a problem hiding this comment.
Approved — no CRITICAL or HIGH issues found.
Non-blocking:
- LOW
src/utils/redact-url.ts:10— a few malformed inputs still come back with their user info - LOW
src/logger.ts:31— theinputredaction can rewrite the caller's own object
Still open from the previous review: none. All six items are verified at 08494f3 (replies on each thread).
Details are in the inline comments on each line.
- An input that Node's URL parser accepts as http: or https: is rebuilt from the parsed URL (scheme, host, path), so the parser's own clean-up (tabs and newlines dropped, '\' read as '/') applies before anything is removed. - Anything else has everything up to the last '@' removed, whether or not a scheme was found, then the query and fragment; a scheme in front is kept. - The scheme and host now come back lowercased, and `http:/host` as `http://host`; a URL with an empty path drops its trailing '/'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pino returns a value that isn't error-like as the caller's own object, so the redacted input goes on a copy and the logged value is never written to (a frozen one no longer makes the log call throw). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
left a comment
There was a problem hiding this comment.
Approved — no CRITICAL or HIGH issues found.
Both items from the previous review are verified fixed at 1e527da (replies on each thread).
Non-blocking:
- MEDIUM
src/tests/unit/redact-url.spec.ts:34— no test pins the cut at the last@in the fallback - LOW
src/utils/redact-url.ts:23— HTTP debug lines now cut a relative path at its last@ - LOW
src/utils/redact-url.ts:3— the header comment and two spec comments describe the old fallback
Details are in the inline comments on each line.
…st-@ cut; redactUrl comments match Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
left a comment
There was a problem hiding this comment.
Approved — no CRITICAL or HIGH issues found.
All three items from the previous review are verified fixed at f215370 (replies on each thread).
Non-blocking:
- LOW
src/utils/http-logger.ts:16— the logged URL carriesparams, andgetUrican returnundefined - LOW
src/utils/http-logger.ts:18— no test covers the fallback whengetUrithrows
Details are in the inline comments on each line.
…t the getUri fallback Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
left a comment
There was a problem hiding this comment.
Approved — no CRITICAL or HIGH issues found.
Non-blocking:
- LOW
src/utils/http-logger.ts:19— aurlthat isn't a string makes the interceptor throw
Details are in the inline comments on each line.
…built only at debug Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zeevmoney
left a comment
There was a problem hiding this comment.
Approved — no CRITICAL or HIGH issues found.
Non-blocking:
- LOW
src/logger.ts:24— theerrserializer still assumesconfig.urlis a string
Details are in the inline comments on each line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linear: PER-16493
Changes
Init line. The constructor's debug line (
Permit.io SDK initialized) now passes pino an object with:pdpandapiUrlthroughredactUrl: scheme, host, port and path, without user, password, query or fragment (a value without a scheme, such aslocalhost:7766, is logged without everything up to its last@, its query or its fragment)timeout,throwOnError,multiTenancy,proxyFactsViaPdp,factsSyncTimeout(Policy),retry,pdpRetryAs fields, they can be filtered in JSON log tooling.
log.json.LoggerFactoryno longer passesprettyPrint, which pino 8 rejects.log.json: trueandPERMIT_LOG_JSON=trueno longer stop the client from starting. The output stays JSON either way, as it already was by default, so no existing log pipeline sees a different format. A real pretty mode is left to 3.0.pino
redact. It covers atokenfield and aheaders.Authorization/headers.authorizationfield, at the top level of a logged object or one level down, as a backstop for an object a log call passes. Errors go through theerrserializer instead.This is a patch for the 2.x line. The 3.0.0 refactor in #134 reworks the same code.
Tests
src/tests/unit/init-log.spec.tsstarts a client in a child process (withoutPERMIT_*variables, so the shell can't change the config) and reads what it prints, because the logger writes to the process's stdout file descriptor. It checks:log.jsonandPERMIT_LOG_JSON=truestartRan locally:
yarn lint,yarn build,yarn test:unit(87 passed).Second review pass (3223122)
errserializer turns an axios error into its type, message, code, status, method and URL. Theredactpaths stay as a second layer.redactUrldrops a URL's user, password, query and fragment. PDP connection errors and the HTTP debug lines show URLs through it.PERMIT_LOG_JSON: read leniently (trueor1, any case), so no value stops the client from starting.log.jsonis documented as having no effect.PERMIT_LOG_JSONvalues thatJSON.parseused to rejectyarn test:unit: 89 passed;yarn lint: 0 errorsThird pass (8913b83)
checkAllTenants: it now posts the query (user, action, resource, merged context) as the body, with the SDK's request headers, and readsallowed_tenants, the field the PDP returns. The resource keeps only the tenant it names: the answer lists every tenant, so the default tenant isn't filled in.PermitApiError:originalErroris no longer enumerable, and request headers in the axios error it carries are redacted.originalError,responseandrequestare still readable; the redacted header inoriginalError.configstays redacted if that config is replayed.yarn test:unit: 91 passed.Fourth pass (7aacac9, d8ad051, 9801dc1)
checkAllTenantserrors: an axios error is rethrown as it is (same type for callers), with the same header redaction asPermitApiError, and logged through theerrserializer. (Permit.checkAllTenantslogged it again until the fifth pass; now only the enforcer does.)PermitApiError:requestis non-enumerable on the axios error and its response, as axios'stoJSON()already leaves it out (an accessor since the fifth pass).originalErroris writable and configurable again. When the API response carries nomessage(no response at all, or a body without that field),error.messageis axios's message, such asRequest failed with status code 422orconnect ECONNREFUSED ..., instead of an empty string.LoggerFactory.createLogger: takes an optional destination stream (stdout stays the default), so tests can read the SDK logger's output.redactUrl: removes the user info before cutting the query and fragment. The init line logspdpandapiUrlthrough it and keeps the path.checkAllTenants; the connection-error messages ofcheck,bulkCheckandgetUserPermissions; the SDK logger'serrserializer andredactpaths; the HTTP debug lines;redact-url.spec.ts.yarn test:unit: 114 passed;yarn lint: 0 errors.Fifth pass (e9d7882, 943d546, 08494f3)
checkAllTenantserrors: logged once, by the enforcer.Permit.checkAllTenantsreturns the enforcer's result and doesn't log the failure again.requeston the error and its response is a non-enumerable get/set accessor instead of a data property. It stays readable and writable.errserializer: for an error that isn't an axios error, a stringinputfield (Node'sERR_INVALID_URLcarries the URL there) goes throughredactUrl.redactUrl: trims the input and accepts one or more slashes after the scheme. For a URL thatnew URL()parses, the query and fragment are cut before the user info is taken; for a URL with a scheme that doesn't parse, everything up to the last@is removed.checkAllTenantsfailure tests capture the SDK logger, assert one record with the error's code (and status), and check that the next call on the same client still sends the key; a server that drops each connection replaces the released-port helper; one test perredactpath; anERR_INVALID_URLlogged through the SDK logger; newredact-url.spec.tsinputs.yarn test:unit: 126 passed;yarn lint: 0 errors.Sixth pass (1b9cc5a, 1e527da)
redactUrl: an input that Node's URL parser accepts ashttp:orhttps:is rebuilt from the parsed URL (scheme, host, port, path), so the parser's own clean-up applies first. Anything else loses everything up to its last@, then its query and fragment; a scheme in front is kept. The scheme and host now come back lowercased,http:/hostashttp://host, and a/-only path is dropped (the init line'sapiUrlforhttps://api.example.com/ishttps://api.example.com).errserializer: the redactedinputgoes on a copy, so the logged value is never written to (a frozen one no longer makes the log call throw).redact-url.spec.tsinputs; a frozen, non-error-like value logged through the SDK logger.yarn test:unit: 133 passed;yarn lint: 0 errors.Seventh pass (f215370)
redactUrl(axiosInstance.getUri(config)), the URL the request goes to withbaseURLjoined, so a relative path such as/users/a@example.com/xkeeps its@and goes through the parse branch. IfgetUrithrows, the line falls back toconfig.url.redactUrl: comments only; the header and the spec describe the two branches as they work.@(they pin the cut at the last one, and the query cut); an HTTP debug-line test with abaseURLthat has user info, a path with@and a query param.yarn test:unit: 136 passed;yarn lint: 0 errors.Eighth pass (f1f375c)
getUri({ ...config, params: null }), so it carries no query and an app'sparamsSerializerdoesn't run for it (null, becauseundefinedfalls back to the instance'sparams). A request with neitherurlnorbaseURLlogs an empty URL.paramswith a countingparamsSerializerthat is never called; a request with neitherurlnorbaseURL; agetUrithat throws, where both lines show the redactedconfig.url.yarn test:unit: 139 passed;yarn lint: 0 errors.Ninth pass (7441f37)
getUri(or theconfig.urlfallback) returns is turned into a string (''fornullorundefined), so aurlgiven as aURLobject is sent, and logged without its user info or query, instead of making the request fail. Both lines are built only when the logger's level includesdebug; at other levelsgetUriandredactUrldon't run.URLobject with user info asurl, atwarnanddebug;getUrinot called atinfo.yarn test:unit: 142 passed;yarn lint: 0 errors.Worth a line in the release notes:
originalErrorand the axios error'srequestno longer show up inObject.keysor a spread of the error (requestis now an accessor),error.messageis no longer empty when the API gives no message, and acheckAllTenantsfailure is logged once instead of twice.Description updated during review to match d17b3c5 (wording made neutral for a public repo).
Description updated during review to match 9801dc1 (init-line URLs keep the path; redact scope; fourth-pass changes).
Description updated during review to match 08494f3 (fifth-pass changes;
checkAllTenantslogs once).Description updated during review to match 1e527da (sixth-pass changes).
Description updated during review to match f215370 (seventh-pass changes).
Description updated during review to match f1f375c (eighth-pass changes).
Description updated during review to match 7441f37 (ninth-pass changes).
🤖 Generated with Claude Code