{"_id":"@onlineapps/logger-contract","_rev":"5-72a5bc107d782aa424f3297940428144","name":"@onlineapps/logger-contract","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@onlineapps/logger-contract","version":"1.0.0","keywords":["logger","contract","validation","fail-fast","oadrive"],"author":{"name":"OnlineApps"},"license":"MIT","_id":"@onlineapps/logger-contract@1.0.0","maintainers":[{"name":"onlineapps","email":"npmjs@onlineapps.cz"}],"dist":{"shasum":"6995f17bacef044147821ddc8585381002abc7bc","tarball":"https://registry.npmjs.org/@onlineapps/logger-contract/-/logger-contract-1.0.0.tgz","fileCount":3,"integrity":"sha512-exjNUJVxZkT9zbVlqr4NVoEag2gCmOgK54KgdQaSBftV7qSZAFXpidu2UtPr9s6umj+DjpZEP0s+zYHk2GyDFA==","signatures":[{"sig":"MEUCIFLzexxYC0XUY+HzquKbGfEzvxa2J1Ye9jSLvpRsaBkUAiEAjDQSZa3qcuxUX3CRAkwRmiBl/urpxQZI33OXZL2M10k=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":8233},"main":"src/index.js","engines":{"node":">=18"},"gitHead":"ec4991495eb252b7531e315b895a1d746080d912","scripts":{"docs":"jsdoc2md --files 'src/**/*.js' > API.md.tmp && mv API.md.tmp API.md || (rm -f API.md.tmp; exit 1)","test":"npm run test:unit","test:unit":"jest tests/unit","test:watch":"jest --watch","test:coverage":"jest --coverage"},"_npmUser":{"name":"onlineapps","email":"npmjs@onlineapps.cz"},"_npmVersion":"11.12.1","description":"The one definition of what a logger is on the OA Drive platform - four methods, validated fail-fast, no console fallback","directories":{},"_nodeVersion":"25.9.0","dependencies":{},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","jsdoc-to-markdown":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/logger-contract_1.0.0_1788717101787_0.23420028448553953","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@onlineapps/logger-contract","version":"1.1.0","keywords":["logger","contract","validation","fail-fast","oadrive"],"author":{"name":"OnlineApps"},"license":"MIT","_id":"@onlineapps/logger-contract@1.1.0","maintainers":[{"name":"onlineapps","email":"npmjs@onlineapps.cz"}],"dist":{"shasum":"7b5655cfd5b00c90a8c8b6f26ddc53ece3b1e5ad","tarball":"https://registry.npmjs.org/@onlineapps/logger-contract/-/logger-contract-1.1.0.tgz","fileCount":3,"integrity":"sha512-mNFjQ4fiD12YdpCpstJ/JawBC+hyiV+jBU9pcW0tbXldR/ohknF4nAh0RvE0ts61dEG7b8Ou/+LArmgD3geUrA==","signatures":[{"sig":"MEYCIQCSkfkhnuZIVHwhVvjBkOguz3yiUdkSu/7kg4Tj8IhwygIhAODdsYi6/e1gElDOsPYtyQi6jFyNgaggsexmllZbyHC+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCRfpnYM8T8Kj+G8mk2D4O93IIDj4ArVNNbQBFEcMgTAQIgJibCBNwHVA3ZUVnpT0eETB6xi5WQc3SVhaJnM0iyoPU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":10965},"main":"src/index.js","engines":{"node":">=18"},"gitHead":"8b187384e4defac6df74bcbeabcbe14053a6058e","scripts":{"docs":"jsdoc2md --files 'src/**/*.js' > API.md.tmp && mv API.md.tmp API.md || (rm -f API.md.tmp; exit 1)","test":"npm run test:unit","test:unit":"jest tests/unit","test:watch":"jest --watch","test:coverage":"jest --coverage"},"_npmUser":{"name":"onlineapps","email":"npmjs@onlineapps.cz"},"_npmVersion":"11.12.1","description":"The one definition of what a logger is on the OA Drive platform - four methods, validated fail-fast, no console fallback","directories":{},"_nodeVersion":"25.9.0","dependencies":{},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","jsdoc-to-markdown":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/logger-contract_1.1.0_1788861325226_0.034907790540439354","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@onlineapps/logger-contract","version":"1.2.0","keywords":["logger","contract","validation","fail-fast","oadrive"],"author":{"name":"OnlineApps"},"license":"MIT","_id":"@onlineapps/logger-contract@1.2.0","maintainers":[{"name":"onlineapps","email":"npmjs@onlineapps.cz"}],"oa":{"category":"core"},"dist":{"shasum":"f0fd2532a0fbd27bf4dbc90dc318a1db9ff91ea8","tarball":"https://registry.npmjs.org/@onlineapps/logger-contract/-/logger-contract-1.2.0.tgz","fileCount":5,"integrity":"sha512-J64VOgU0xzRosHTFphhJIGwAbwp7+g/W8ARxdA2j60XOuLuH6rCUgJFXV8P5j2OhpoWdFqFCYG3BSaalkr8VBg==","signatures":[{"sig":"MEQCIB7KeHICo4Ee5x9duqLuHcy555/n9z4s5zBr17pM/3ENAiA8fZmrkTO+Wjm1C5D3na6u7NgQrEouHtuumZ2/quTWrA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIQCIE4trFUzCtEzIfCjpj3B7CpLhSnrAGeHQh2PTBZSLvQIgKb0GoubLIox2hwDavyrWYMJhKRBUcGdaPK/HNENQTI8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":24238},"main":"src/index.js","engines":{"node":">=24.0.0 <25"},"gitHead":"f8324c75f32bad4ab4695b3733f2cf60a466c6ea","scripts":{"docs":"jsdoc2md --files 'src/**/*.js' > API.md.tmp && mv API.md.tmp API.md || (rm -f API.md.tmp; exit 1)","test":"npm run test:unit","test:unit":"jest tests/unit","test:watch":"jest --watch","test:coverage":"jest --coverage"},"_npmUser":{"name":"onlineapps","email":"npmjs@onlineapps.cz"},"_npmVersion":"11.12.1","description":"The one definition of what a logger is on the OA Drive platform - four methods, validated fail-fast, no console fallback","directories":{},"_nodeVersion":"25.9.0","dependencies":{},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","jsdoc-to-markdown":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/logger-contract_1.2.0_1789419283304_0.3507515434554791","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@onlineapps/logger-contract","version":"1.3.0","keywords":["logger","contract","validation","fail-fast","oadrive"],"author":{"name":"OnlineApps"},"license":"MIT","_id":"@onlineapps/logger-contract@1.3.0","maintainers":[{"name":"onlineapps","email":"npmjs@onlineapps.cz"}],"oa":{"category":"core"},"dist":{"shasum":"718b0bf53a5c58fc4e17af52ed7e2e396f0acecd","tarball":"https://registry.npmjs.org/@onlineapps/logger-contract/-/logger-contract-1.3.0.tgz","fileCount":5,"integrity":"sha512-kupoMTrgikDOyZ879MNaiB/81/NAyQ6ohSLdwswbqtC1mfaq3RGKfo7UDlBSgHpHZ7apZWwEaEAK5yfxd2vnbA==","signatures":[{"sig":"MEUCIEL+f0ASejqZJUwpyidPkgijrXeBA9esVbtqus5/r1SLAiEA1HZLvIbjvApU7It4kN7djzHJi5CUQ9VPN0bsN9qJ6Eg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQCR0UjeC/0uz4Hs7UZrswU7xjjhfPJJUFHFrePXv2s4VgIhANvYqEERjt88HtARRigmgF5SM9hwczbjhS/ilysJtmxJ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":27904},"main":"src/index.js","engines":{"node":">=24.0.0 <25"},"gitHead":"f26821a8b1c6cdf3295de2670768804a2a976306","scripts":{"docs":"jsdoc2md --files 'src/**/*.js' > API.md.tmp && mv API.md.tmp API.md || (rm -f API.md.tmp; exit 1)","test":"npm run test:unit","test:unit":"jest tests/unit","test:watch":"jest --watch","test:coverage":"jest --coverage"},"_npmUser":{"name":"onlineapps","email":"npmjs@onlineapps.cz"},"_npmVersion":"11.12.1","description":"The one definition of what a logger is on the OA Drive platform - four methods, validated fail-fast, no console fallback","directories":{},"_nodeVersion":"25.9.0","dependencies":{},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","jsdoc-to-markdown":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/logger-contract_1.3.0_1789476256824_0.4259359058835628","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"oa":{"category":"core"},"_id":"@onlineapps/logger-contract@2.0.0","dist":{"shasum":"e553c76ec809becb5216bee1552f2e55a9cd506c","tarball":"https://registry.npmjs.org/@onlineapps/logger-contract/-/logger-contract-2.0.0.tgz","fileCount":5,"integrity":"sha512-qM8pk82QddEFbPY8sYsXWxo7ukJEC9bt09xzScmwo6Ow2d9B7rHnMRjg6rHsYYYG343rR+pRQ4HAzJi0pglkgQ==","signatures":[{"sig":"MEQCICc//Ixkjy0kQsnILdene0zRivkaBjMHJoqhNMfJozdWAiBNvNltpV8vdbA/efyaia1FidbZJoUJWS8wglm0y2Ohew==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIANPt9n3vWWkSNC+b2/7XQTbKQeSDtV6GssdZw67QMOLAiEAvHIFAEymo8W3RNTrpPNFk0Z/4DzjcQHZoRsLZuAnK5o="}],"unpackedSize":30363},"main":"src/index.js","name":"@onlineapps/logger-contract","author":{"name":"OnlineApps"},"engines":{"node":">=24.0.0 <25"},"gitHead":"1a7baeb1b8f5239a07a0abcd019c7479a3e3d2d7","license":"MIT","scripts":{"docs":"jsdoc2md --files 'src/**/*.js' > API.md.tmp && mv API.md.tmp API.md || (rm -f API.md.tmp; exit 1)","test":"npm run test:unit","test:unit":"jest tests/unit","test:watch":"jest --watch","test:coverage":"jest --coverage"},"version":"2.0.0","_npmUser":{"name":"onlineapps","email":"npmjs@onlineapps.cz"},"keywords":["logger","contract","validation","fail-fast","oadrive"],"_npmVersion":"11.12.1","description":"The one definition of what a logger is on the OA Drive platform - four methods, validated fail-fast, no console fallback","directories":{},"maintainers":[{"name":"onlineapps","email":"npmjs@onlineapps.cz"}],"_nodeVersion":"25.9.0","dependencies":{},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","jsdoc-to-markdown":"^8.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/logger-contract_2.0.0_1789489749271_0.715003956234848"}}},"time":{"created":"2026-09-06T17:51:41.614Z","modified":"2026-09-15T16:29:09.532Z","1.0.0":"2026-09-06T17:51:41.917Z","1.1.0":"2026-09-08T09:55:25.312Z","1.2.0":"2026-09-14T20:54:43.439Z","1.3.0":"2026-09-15T12:44:16.924Z","2.0.0":"2026-09-15T16:29:09.351Z"},"author":{"name":"OnlineApps"},"license":"MIT","keywords":["logger","contract","validation","fail-fast","oadrive"],"description":"The one definition of what a logger is on the OA Drive platform - four methods, validated fail-fast, no console fallback","maintainers":[{"name":"onlineapps","email":"npmjs@onlineapps.cz"}],"readme":"> Status: current\n> Owns: the one definition of what a logger is on this platform — `info`, `warn`, `error`, `debug`, asserted at the point of injection — and what may reach one: the redaction of credentials in connection URLs and of declared sensitive fields\n\n<!-- BEGIN GENERATED: library-uniform — regenerate: npx oa-sync-template readme-uniform --all -->\nUniform: [library/core](../connector/conn-orch-validator/manifests/library.manifest.json)\n\nDuty sections that apply:\n\n- `all`: L-MAIN, L-ENGINES, L-TESTS, L-TEST-SCRIPT, L-PACK-TESTS, L-PINS, L-NO-FILE-RANGE, L-CHANGELOG, L-README, L-README-REGION, L-CONSUMER\n- `core`: L-CORE-DEPS, L-CORE-EXPORTS\n<!-- END GENERATED: library-uniform -->\n\n# @onlineapps/logger-contract\n\n[![npm version](https://img.shields.io/npm/v/@onlineapps/logger-contract)](https://www.npmjs.com/package/@onlineapps/logger-contract)\n\nThe one definition of what a logger is on the OA Drive platform: an object\nexposing `info`, `warn`, `error` and `debug`, validated at the point of\ninjection.\n\n## Why it is a package\n\nThe same check lived as a byte-identical `src/loggerContract.js` in three\npackages — `@onlineapps/conn-orch-registry`, `@onlineapps/error-handler-core`,\n`@onlineapps/service-common` — with a fourth copy of the constant in\n`@onlineapps/service-wrapper`. The three files agreed on every executable line\nand differed only in a JSDoc example: four owners of one contract, and nothing\nthat fails when they drift.\n\nThey could not import each other, because they sit on different layers and none\nmay depend on the others (`architecture-principles.md` §7). A contract shared\nacross layers has to live below all of them — hence this package: layer L1,\n**zero dependencies**, nothing to depend back on.\n\nRecorded decision:\n[`connector-logger-contract.md`](../../docs/governance/confirmations/connector-logger-contract.md)\n004 (with 001, 002 and 003 behind it).\n\n## Installation\n\n```bash\nnpm install @onlineapps/logger-contract\n```\n\n## Usage\n\n```js\nconst { assertLogger, LOGGER_METHODS } = require('@onlineapps/logger-contract');\n\nclass QueueManager {\n  constructor({ logger }) {\n    // Validates and assigns in one statement, so the field never holds an\n    // unchecked value.\n    this.logger = assertLogger(\n      'QueueManager',\n      logger,\n      'queue lifecycle events reach the service log'\n    );\n  }\n}\n```\n\n### `assertLogger(context, logger, reason[, fix])`\n\n| Parameter | Type | Meaning |\n|---|---|---|\n| `context` | `string` | The caller named in the message, e.g. `'QueueManager'` |\n| `logger` | `Object` | The candidate logger |\n| `reason` | `string` | Why this component needs one, in one clause |\n| `fix` | `string` | Optional. What the caller should pass, e.g. `'pass config.logger'`. Default: `'pass options.logger (e.g. the logger your service already built)'` |\n\nReturns the same `logger`. Throws when it is absent or does not implement all\nfour methods, and when `fix` is given but is not a non-empty string.\n\n`fix` exists because the Fix half of `[Context] Problem - Expected/Fix` has to be\nactionable, and the default names one key. A constructor reading `config.logger`\nor a helper reading `context.logger` would otherwise send its caller to a key\ntheir API does not have:\n\n```js\nassertLogger('WorkflowOrchestrator', config.logger, 'the router it builds writes where the service writes',\n  'pass config.logger (e.g. the logger your service already built)');\n```\n\nIt applies to the **required** branch only. The incomplete branch ends `Fix: pass\na logger implementing all four`, which is about the object rather than about\nwhere it is passed, and is already right for every caller.\n\nA bad `fix` is refused at entry even when the logger itself is fine — deferring\nthat check would surface the typo on the one day the logger is missing.\n\n### `LOGGER_METHODS`\n\n`['info', 'warn', 'error', 'debug']`, frozen. Frozen because the array is now\nshared across packages within one process: an accidental `push` by any consumer\nwould silently redefine the contract for every other consumer loaded from the\nsame `node_modules`.\n\n## Redaction — what may reach a log\n\n`assertLogger` states what a logger **is**. The three functions below state what\nmay **reach** one, which is the same concern seen from the other end — and it has\nthe same layering problem: every layer logs, so every layer needs the redactor,\nand a redactor owned by one of them cannot be imported by the others\n(`architecture-principles.md` §7). Three packages therefore kept a copy of the URL\nredactor — `@onlineapps/service-common` (the Redis URL, d.245/d.245b),\n`@onlineapps/mq-client-core` (the AMQP URL, d.448) and\n`@onlineapps/conn-orch-registry` (deleted by d.446b in favour of the core's\nexport) — which is the state `change-discipline.md` § One rail per concern calls a\ndefect.\n\nThe move here was forced rather than merely tidy: gate G7 refused\n`@onlineapps/conn-orch-orchestrator` (category `orchestration`, which may pin\ncore, connector and orchestration) for depending on `@onlineapps/service-common`\n(category `runtime`) — a dependency it had taken on only to reach these helpers.\n**Core is the layer every category already pins**, and this package is core with\nzero `@onlineapps` dependencies, so the rule now lands where all of its users can\nreach it.\n\n### `redactUrl(url)` · `UNPARSEABLE_PLACEHOLDER`\n\nReturns an **allow-list** of the URL: scheme, host (hostname:port) and path, and\nnothing else. The answer is composed from those three parts, so userinfo, query\nstring and fragment never reach the log — the whole userinfo, because the account\nname is the other half of the same credential, and the query and fragment because\nboth brokers accept the credential there too.\n\n```js\nconst { redactUrl } = require('@onlineapps/logger-contract');\n\nlogger.info(`Connecting to ${redactUrl(process.env.RABBITMQ_URL)}`);\n// amqp://oa_dev:s3cr3t@queuer:5672/oa_vhost?heartbeat=60  →  amqp://queuer:5672/oa_vhost\n// redis://cache-host:6379?password=s3cr3t                 →  redis://cache-host:6379\n```\n\nWhy an allow-list and not a list of fields to blank out: until d.476 the function\ncleared `username` and `password` and returned the rest of the URL as it stood, so\n`redis://host:6379?password=SEKRET` and `redis://host:6379#SEKRET` carried the\npassword into the log in full (measured by INFRA-monitoring, 2026-09-15). A\ndeny-list must enumerate every place a secret can hide, and for Redis and AMQP\nthat place is not only the userinfo; an allow-list only has to name what the\nreader of the log came for, so a place nobody thought of falls outside the answer\ninstead of inside it. The cost is the query: `?heartbeat=60` is gone from the\nredacted line as well, because the function does not decide which parameter is a\nsecret — it decides which parts are an endpoint.\n\nA value that is not a parseable URL is answered with\n`UNPARSEABLE_PLACEHOLDER` (`'<unparseable-url>'`) rather than echoed — that is\nexactly the case where nobody can say whether the string holds a credential.\nA bare `host:port` is one of them: `new URL('queuer:5672')` *succeeds*, reading\n`queuer:` as a scheme, so the guard is on the parsed host, not on the throw.\nThe function **never throws**: its callers are log lines and error messages,\nwhere a throw would replace the report with a second incident.\n\nMeasured leaks behind the rule: the broker password on stdout at every connect\n(`api_service_hello`, 2026-09-07) and the Redis password in three boot log lines\n(2026-09-11) — both durable and searchable in Loki.\n\n### `redactSensitiveDeep(value, fieldNames)` · `sensitiveFieldsForOperation(serviceSpec, operationName)` · `REDACTED_PLACEHOLDER`\n\nDeep-clones `value` and replaces every object property named in `fieldNames`\nwith `REDACTED_PLACEHOLDER` (`'[REDACTED]'`), at any depth. It never mutates the\ninput; `Date` is cloned, `Buffer` passes through untouched, a cycle becomes\n`'[Circular]'`, and an empty field list returns the original value unchanged.\n\nWhich fields are sensitive is declared by the operation, not guessed:\n`sensitiveFieldsForOperation` reads `operations[<name>].sensitive_input_fields`\nfrom a service spec (the `registry:service:<name>:spec` shape, or a local\n`operations.json`).\n\n**Not knowing is not the same as knowing there is nothing.** The function has\ntwo negative answers and they mean opposite things:\n\n| Answer | Meaning | What the caller does |\n|---|---|---|\n| `[]` | the operation is known and declares no sensitive field | log the payload as it is |\n| `null` | undecidable — spec missing or unusable, operation not in it, or its `sensitive_input_fields` present but not a list | fail fast, write nothing |\n\n`fieldNames` is required: `redactSensitiveDeep(value)` with the argument omitted\nthrows the same way as `null`. An omitted list is a defect in the caller, not a\nstatement that nothing is sensitive — only `[]` (or an empty `Set`) says that.\n\n`redactSensitiveDeep` enforces the second row: given `null` it throws\n`[redactSensitiveDeep] Sensitive field list is unknown (null) - refusing to\nproduce a log copy that may carry secrets. Fix: resolve the operation spec\nfirst, or pass [] when the operation declares no sensitive fields.` A caller\nthat silently redacted nothing would persist the secret it was asked to hide,\nwhich is the fail-open this contract removes (`architecture-principles.md`\n§3 no fallbacks, §4 fail-fast).\n\n```js\nconst { redactSensitiveDeep, sensitiveFieldsForOperation } = require('@onlineapps/logger-contract');\n\nconst fields = sensitiveFieldsForOperation(spec, 'set-secret');   // ['value'] | [] | null\nif (fields === null) throw new Error('[trace] Operation spec unknown - cannot redact. Fix: load the spec before writing the trace copy.');\ntrace.write(redactSensitiveDeep(cookbook, fields));               // observability copy only\n```\n\nThis is for observability, log and trace copies **only**. The execution message\nthat carries the value to its owning handler MUST NOT be redacted. Contract:\n[`docs/architecture/secretbox.md`](../../docs/architecture/secretbox.md) §8.\n\n## Behaviour\n\nFail-fast, no fallback to `console` (`architecture-principles.md` §3, §4).\nA component that demands a logger and never writes to it is a declaration\nwithout a consumer; the fix is to log, not to drop the requirement\n(confirmation 002).\n\nMessages follow `[Context] Problem - Expected/Fix`\n(`architecture-principles.md` §5):\n\n```\n[QueueManager] logger is required - Expected: a logger with info/warn/error/debug,\nso queue lifecycle events reach the service log. Fix: pass options.logger\n(e.g. the logger your service already built).\n\n[RegistryClient] logger is incomplete - Expected: info, warn, error, debug as\nfunctions; missing: debug. Fix: pass a logger implementing all four.\n```\n\nAnything exposing the four methods passes, `console` included — the contract is\nabout the shape, not about a particular implementation.\n\n## Tests\n\n```bash\nnpm test\n```\n\nThree suites: `loggerContract` (the four methods, both failure paths, both\nmessage shapes, plus the control cases that must stay green — a complete logger,\n`console`, a logger with extra methods, one built from a class), `redactUrl` and\n`redactSensitive`. The latter two are the merged coverage of the suites that\ntravelled with the functions, so the new owner carries the whole of it.\n\n## Generated API reference\n\n[`API.md`](./API.md) is generated from the JSDoc by `npm run docs`. It is a\nrepository artefact and is deliberately not listed in `files`, so it does not\nship in the tarball.\n","readmeFilename":"README.md"}