react-native-nitro-logger 0.1.0
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/LICENSE +20 -0
- package/NitroLogger.podspec +29 -0
- package/README.md +214 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
- package/docs/PARITY.md +330 -0
- package/docs/PRIVACY.md +341 -0
- package/eslint-plugin/README.md +238 -0
- package/eslint-plugin/index.js +62 -0
- package/eslint-plugin/rules/literal-subsystem.js +102 -0
- package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
- package/eslint-plugin/rules/no-derived-correlation.js +171 -0
- package/eslint-plugin/rules/no-dynamic-message.js +60 -0
- package/eslint-plugin/shared.js +1948 -0
- package/ios/Gzip.swift +203 -0
- package/ios/HybridFileSink.swift +291 -0
- package/ios/HybridNativeConsoleSink.swift +20 -0
- package/ios/LogFileWriter.swift +1280 -0
- package/ios/LogSecureFile.swift +296 -0
- package/ios/LogWriterRegistry.swift +444 -0
- package/ios/NativeConsoleWriter.swift +180 -0
- package/lib/module/Logger.js +381 -0
- package/lib/module/Logger.js.map +1 -0
- package/lib/module/ScopedLogger.js +70 -0
- package/lib/module/ScopedLogger.js.map +1 -0
- package/lib/module/config.js +23 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/destinations/Batcher.js +647 -0
- package/lib/module/destinations/Batcher.js.map +1 -0
- package/lib/module/destinations/ConsoleDestination.js +51 -0
- package/lib/module/destinations/ConsoleDestination.js.map +1 -0
- package/lib/module/destinations/FileDestination.js +360 -0
- package/lib/module/destinations/FileDestination.js.map +1 -0
- package/lib/module/destinations/NativeConsoleDestination.js +170 -0
- package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
- package/lib/module/destinations/types.js +4 -0
- package/lib/module/destinations/types.js.map +1 -0
- package/lib/module/formatters/DefaultFormatter.js +136 -0
- package/lib/module/formatters/DefaultFormatter.js.map +1 -0
- package/lib/module/formatters/JsonLinesFormatter.js +162 -0
- package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
- package/lib/module/formatters/timestamp.js +15 -0
- package/lib/module/formatters/timestamp.js.map +1 -0
- package/lib/module/formatters/types.js +4 -0
- package/lib/module/formatters/types.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/integrations/appState.js +81 -0
- package/lib/module/integrations/appState.js.map +1 -0
- package/lib/module/integrations/errorHandler.js +166 -0
- package/lib/module/integrations/errorHandler.js.map +1 -0
- package/lib/module/integrations/sanitizeError.js +238 -0
- package/lib/module/integrations/sanitizeError.js.map +1 -0
- package/lib/module/levels.js +35 -0
- package/lib/module/levels.js.map +1 -0
- package/lib/module/metadata.js +52 -0
- package/lib/module/metadata.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy.js +349 -0
- package/lib/module/privacy.js.map +1 -0
- package/lib/module/specs/FileSink.nitro.js +4 -0
- package/lib/module/specs/FileSink.nitro.js.map +1 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
- package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utf8.js +42 -0
- package/lib/module/utf8.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/Logger.d.ts +122 -0
- package/lib/typescript/src/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ScopedLogger.d.ts +36 -0
- package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +12 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
- package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
- package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
- package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
- package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
- package/lib/typescript/src/destinations/types.d.ts +19 -0
- package/lib/typescript/src/destinations/types.d.ts.map +1 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
- package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
- package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
- package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
- package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
- package/lib/typescript/src/formatters/types.d.ts +27 -0
- package/lib/typescript/src/formatters/types.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +34 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/integrations/appState.d.ts +33 -0
- package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
- package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
- package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
- package/lib/typescript/src/levels.d.ts +9 -0
- package/lib/typescript/src/levels.d.ts.map +1 -0
- package/lib/typescript/src/metadata.d.ts +23 -0
- package/lib/typescript/src/metadata.d.ts.map +1 -0
- package/lib/typescript/src/privacy.d.ts +103 -0
- package/lib/typescript/src/privacy.d.ts.map +1 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
- package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
- package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +37 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utf8.d.ts +15 -0
- package/lib/typescript/src/utf8.d.ts.map +1 -0
- package/nitro.json +33 -0
- package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
- package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
- package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
- package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
- package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
- package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
- package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
- package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
- package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
- package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
- package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
- package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
- package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
- package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
- package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
- package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
- package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
- package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
- package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
- package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
- package/package.json +151 -0
- package/src/Logger.ts +417 -0
- package/src/ScopedLogger.ts +94 -0
- package/src/config.ts +25 -0
- package/src/destinations/Batcher.ts +805 -0
- package/src/destinations/ConsoleDestination.ts +60 -0
- package/src/destinations/FileDestination.ts +473 -0
- package/src/destinations/NativeConsoleDestination.ts +220 -0
- package/src/destinations/types.ts +19 -0
- package/src/formatters/DefaultFormatter.ts +150 -0
- package/src/formatters/JsonLinesFormatter.ts +195 -0
- package/src/formatters/timestamp.ts +12 -0
- package/src/formatters/types.ts +29 -0
- package/src/index.tsx +106 -0
- package/src/integrations/appState.ts +111 -0
- package/src/integrations/errorHandler.ts +223 -0
- package/src/integrations/sanitizeError.ts +287 -0
- package/src/levels.ts +35 -0
- package/src/metadata.ts +52 -0
- package/src/privacy.ts +389 -0
- package/src/specs/FileSink.nitro.ts +144 -0
- package/src/specs/NativeConsoleSink.nitro.ts +21 -0
- package/src/types.ts +45 -0
- package/src/utf8.ts +39 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# `react-native-nitro-logger/eslint-plugin`
|
|
2
|
+
|
|
3
|
+
The build-time half of the privacy contract.
|
|
4
|
+
|
|
5
|
+
Under `privacyDefault('private')` the runtime redacts metadata **values**. It
|
|
6
|
+
can never redact the fields around them: by the time a message string,
|
|
7
|
+
subsystem, correlation ID, or metadata key reaches the logger, any
|
|
8
|
+
interpolation already happened and the original data is gone. Those four fields
|
|
9
|
+
are *public by contract*, and these rules are what make that contract true.
|
|
10
|
+
|
|
11
|
+
Under the default `privacyDefault('public')` profile, values are not redacted
|
|
12
|
+
either — they are public because you declared them so. These rules matter more
|
|
13
|
+
there, not less.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// eslint.config.js
|
|
17
|
+
import nitroLogger from 'react-native-nitro-logger/eslint-plugin';
|
|
18
|
+
|
|
19
|
+
export default [nitroLogger.configs.strict];
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configs
|
|
23
|
+
|
|
24
|
+
| Config | Rules | For |
|
|
25
|
+
| ------------- | ----------------------------------------------------------- | ------------------------------------------------------- |
|
|
26
|
+
| `recommended` | `no-dynamic-message`, `no-computed-metadata-key` | OSS apps. Catches what is almost always a mistake. |
|
|
27
|
+
| `strict` | those plus `no-derived-correlation`, `literal-subsystem` | Apps under `privacyDefault('private')` — PHI, PII. |
|
|
28
|
+
|
|
29
|
+
Both fail closed on metadata: an object the rules cannot open is reported,
|
|
30
|
+
because that is exactly the case where they cannot tell a reviewed key from a
|
|
31
|
+
patient identifier.
|
|
32
|
+
|
|
33
|
+
## Configure `loggerModules` first
|
|
34
|
+
|
|
35
|
+
Almost every app re-exports the logger from its own module:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// src/logging.ts
|
|
39
|
+
export { Log } from 'react-native-nitro-logger';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The rules trust a logger by **provenance, not by name** — a binding called
|
|
43
|
+
`Log` imported from somewhere unknown could be anything, and treating it as the
|
|
44
|
+
singleton would let its `newCorrelationId()` mint approved IDs out of medical
|
|
45
|
+
record numbers. So out of the box, `import { Log } from '../logging'` is
|
|
46
|
+
checked but not trusted, and correct code reports:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
no-derived-correlation Correlation IDs must come from newCorrelationId()…
|
|
50
|
+
no-computed-metadata-key Metadata must be an object literal at the call site…
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Tell the plugin which modules legitimately hand out the logger:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
{
|
|
57
|
+
rules: {
|
|
58
|
+
'nitro-logger/no-derived-correlation': [
|
|
59
|
+
'error',
|
|
60
|
+
{ loggerModules: ['react-native-nitro-logger', '@/logging'] },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
List every module that re-exports it. This is a trust boundary — anything named
|
|
67
|
+
here is believed to hand out the real Logger, so keep it to modules you own.
|
|
68
|
+
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
71
|
+
### `no-dynamic-message`
|
|
72
|
+
|
|
73
|
+
Messages must be string literals or constants that resolve to one.
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
Log.info(`patient ${id} admitted`); // error: interpolation cannot be redacted
|
|
77
|
+
Log.info('patient admitted', { patientRef: pub(ref) }); // ok
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `no-computed-metadata-key`
|
|
81
|
+
|
|
82
|
+
Metadata keys must be literal and, when a catalog is configured, drawn from it.
|
|
83
|
+
A computed key or a spread hides the key set from review — and `patientId` is
|
|
84
|
+
very often a value like `patient123`, which passes the runtime's shape check and
|
|
85
|
+
reaches the destination intact.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
Log.info('m', { [patientId]: v }); // error: computed
|
|
89
|
+
Log.info('m', { ...record }); // error: spread
|
|
90
|
+
Log.info('m', buildMetadata(p)); // error: unanalyzable
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `no-derived-correlation`
|
|
94
|
+
|
|
95
|
+
Correlation IDs appear on every line of a scope, so they must be opaque and
|
|
96
|
+
freshly random. Reusing a record identifier turns the whole scope into a
|
|
97
|
+
re-identification key, and hashing does not fix it — a hash of a small
|
|
98
|
+
identifier space is trivially reversible.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
Log.scoped(patient.mrn); // error
|
|
102
|
+
Log.scoped(hash(patient.mrn)); // error — still 1:1 with the patient
|
|
103
|
+
Log.scoped(Log.newCorrelationId()); // ok
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `literal-subsystem`
|
|
107
|
+
|
|
108
|
+
Subsystem names must be literal. A computed name is both a leak risk and a
|
|
109
|
+
functional bug: `Log.subsystem('network', 'debug')` can only match names known
|
|
110
|
+
at configuration time, so a dynamic name silently misses every level override
|
|
111
|
+
the app configured.
|
|
112
|
+
|
|
113
|
+
## Options
|
|
114
|
+
|
|
115
|
+
Every rule accepts `loggerNames`, `loggerModules`, and `singletonName`.
|
|
116
|
+
`no-computed-metadata-key` additionally accepts `catalog` and
|
|
117
|
+
`allowOpaqueMetadata`.
|
|
118
|
+
|
|
119
|
+
| Option | Default | Meaning |
|
|
120
|
+
| --------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
121
|
+
| `loggerNames` | `['Log', 'log', 'logger']` | Identifiers to treat as logger receivers. Widens what is **checked**, never what is **trusted**. |
|
|
122
|
+
| `loggerModules` | `['react-native-nitro-logger']` | Modules an import of the logger may come from. This is what grants trust — see above. |
|
|
123
|
+
| `singletonName` | first of `loggerNames` | The name meaning the Logger itself rather than a ScopedLogger. Decides argument **shape**, since `Log.log` and `scope.log` differ. |
|
|
124
|
+
| `catalog` | none | Approved metadata keys. Mirrors the runtime's contract, so a key lint accepts is one the runtime will keep. |
|
|
125
|
+
| `allowOpaqueMetadata` | `false` | Stop reporting metadata objects the rules cannot open. A `catalog` overrides this — a catalog promises every key was reviewed. |
|
|
126
|
+
|
|
127
|
+
## What the rules cannot see
|
|
128
|
+
|
|
129
|
+
They are a control, not a proof. Analysis is per-file and stops at the module
|
|
130
|
+
boundary: metadata assembled in another file and imported arrives opaque, and is
|
|
131
|
+
reported as `unanalyzable` rather than approved.
|
|
132
|
+
|
|
133
|
+
### Loggers installed by a function call are not followed
|
|
134
|
+
|
|
135
|
+
This is the one limitation worth knowing before you rely on these rules.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
function configure(target, fn) {
|
|
139
|
+
target.emit = fn;
|
|
140
|
+
}
|
|
141
|
+
configure(handlers, Log.info);
|
|
142
|
+
handlers.emit(`patient ${id} admitted`, { [mrn]: v }); // NOT reported
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The rules see `handlers.emit(...)` and cannot tell it is a logger call. That
|
|
146
|
+
call site is then outside **all four rules at once**, not just the message:
|
|
147
|
+
|
|
148
|
+
| Field | Normally enforced by | At a helper-wired call site |
|
|
149
|
+
| -------------- | ------------------------- | --------------------------- |
|
|
150
|
+
| message | `no-dynamic-message` | unchecked |
|
|
151
|
+
| metadata keys | `no-computed-metadata-key`| unchecked |
|
|
152
|
+
| correlation ID | `no-derived-correlation` | unchecked |
|
|
153
|
+
| subsystem | `literal-subsystem` | unchecked |
|
|
154
|
+
|
|
155
|
+
What the runtime still does for you at such a call site depends entirely on
|
|
156
|
+
which privacy profile you are running, and the difference is large:
|
|
157
|
+
|
|
158
|
+
| | `privacyDefault('private')` | `privacyDefault('public')` — the default |
|
|
159
|
+
| ------------------- | ----------------------------------------------- | ---------------------------------------- |
|
|
160
|
+
| Unwrapped **value** | redacted to `<private>` | **logged as written** |
|
|
161
|
+
| `pub(v)` / `priv(v)`| renders / redacts | renders / redacts |
|
|
162
|
+
| **Key** | catalog mandatory; without one all metadata drops| **any key matching `^[A-Za-z0-9._-]{1,64}$` is kept** |
|
|
163
|
+
|
|
164
|
+
Under `'private'` the runtime is the backstop the lint rules are not: a
|
|
165
|
+
forgotten wrapper hides data rather than leaking it, and a key you never
|
|
166
|
+
approved never appears. This gap then costs you the message, subsystem, and
|
|
167
|
+
correlation checks — real, but not the metadata.
|
|
168
|
+
|
|
169
|
+
Under the default `'public'` profile there is no backstop at all. Values are
|
|
170
|
+
public because you said they were, and `patient123` satisfies the key pattern.
|
|
171
|
+
A helper-wired `handlers.emit('m', { [mrn]: name })` reaches the destination
|
|
172
|
+
with the key and the value both intact.
|
|
173
|
+
|
|
174
|
+
**So if you log anything sensitive, set `privacyDefault('private')` and a
|
|
175
|
+
catalog.** That is worth doing regardless of this limitation, and it is what
|
|
176
|
+
makes the limitation survivable.
|
|
177
|
+
|
|
178
|
+
This holds whether `configure` is imported or defined right above the call.
|
|
179
|
+
|
|
180
|
+
It is deliberate. Following it correctly means interprocedural dataflow
|
|
181
|
+
analysis — parameter defaults, destructuring at any depth, rest patterns,
|
|
182
|
+
spread arguments, reassignment, shadowing, and every way a function can be
|
|
183
|
+
invoked — and each of those is a way to get it wrong in one of two directions.
|
|
184
|
+
An earlier revision of this plugin did follow local helpers. It went through
|
|
185
|
+
seven review rounds, kept admitting spellings it had missed, and had begun
|
|
186
|
+
reporting ordinary code: `configure(h, { [key]: () => Log.info('setup') })` is
|
|
187
|
+
a callback that logs a literal, not a logger installed on `h`. A rule that
|
|
188
|
+
reports ordinary code gets switched off, and a rule that is switched off
|
|
189
|
+
protects nothing.
|
|
190
|
+
|
|
191
|
+
**What to do instead.** Moving the helper into the same file does not help —
|
|
192
|
+
the local `configure` above is just as invisible. What the rules follow is a
|
|
193
|
+
logger that reaches the call site without passing through a call:
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
Log.info(msg); // direct call
|
|
197
|
+
const s = Log.scoped(Log.newCorrelationId()); // scoped logger, then s.info(msg)
|
|
198
|
+
const emit = Log.info; // const alias of a method
|
|
199
|
+
handlers.emit = Log.info; // direct property assignment
|
|
200
|
+
const handlers = { emit: Log.info }; // object literal
|
|
201
|
+
Object.assign(handlers, { emit: Log.info }); // Object.assign
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Those six forms are the supported set. Anything that hands the logger to a
|
|
205
|
+
function and lets the function install it is not.
|
|
206
|
+
|
|
207
|
+
**Which methods are checked.** The six level helpers (`verbose`, `debug`,
|
|
208
|
+
`info`, `warning`, `error`, `todo`), plus `log` and `logMessage` — the latter
|
|
209
|
+
because it is public and is what every other emitting method delegates to.
|
|
210
|
+
`scoped`, `subsystem` and `resetSubsystem` are checked for their own arguments.
|
|
211
|
+
|
|
212
|
+
That list is not maintained by hand against memory: a test enumerates
|
|
213
|
+
`Logger.prototype` and `ScopedLogger.prototype` and fails on any method that is
|
|
214
|
+
neither covered by a rule nor named in an explicit not-emitting list. This
|
|
215
|
+
matters because it is how coverage drifts — `logMessage` was public from the
|
|
216
|
+
start, was known to the plugin as a trusted method, and was checked by nothing,
|
|
217
|
+
so a `logMessage` call with an interpolated message linted clean while the
|
|
218
|
+
identical `log` call errored. Adding an emitting method now fails the suite
|
|
219
|
+
until somebody decides which side it falls on.
|
|
220
|
+
|
|
221
|
+
Within an options object both `metadata` and `scopeMetadata` are inspected.
|
|
222
|
+
They reach the same redaction path, and reading only the first left the other
|
|
223
|
+
half of the same pipeline unchecked.
|
|
224
|
+
|
|
225
|
+
Two more behaviours follow from taking the module boundary seriously, and both
|
|
226
|
+
look strict on first contact:
|
|
227
|
+
|
|
228
|
+
- **`const` protects the binding, not the object.** A metadata object that is
|
|
229
|
+
written through, or handed to a function the rules cannot see inside, is not
|
|
230
|
+
a constant.
|
|
231
|
+
- **A table of constants is only constant if nothing in it executes.** A
|
|
232
|
+
literal containing a getter, a method, a function- or class-valued property,
|
|
233
|
+
or `__proto__` resolves to nothing at all. Getters are the sharp case: one
|
|
234
|
+
runs while the logger walks the object, and can rewrite its own siblings
|
|
235
|
+
before they are read. Keep tables to plain data and they stay resolvable.
|
|
236
|
+
- **Unprovable is reported.** A spread argument, an opaque options object, a
|
|
237
|
+
computed method name — each is a finding. Falling silent on the calls it
|
|
238
|
+
cannot read is how a lint rule becomes decorative.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const noDynamicMessage = require('./rules/no-dynamic-message');
|
|
4
|
+
const noComputedMetadataKey = require('./rules/no-computed-metadata-key');
|
|
5
|
+
const noDerivedCorrelation = require('./rules/no-derived-correlation');
|
|
6
|
+
const literalSubsystem = require('./rules/literal-subsystem');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build-time half of the privacy contract.
|
|
10
|
+
*
|
|
11
|
+
* The runtime redacts metadata *values*. It cannot redact the fields around
|
|
12
|
+
* them: by the time a message string, subsystem, correlation ID, or metadata
|
|
13
|
+
* key reaches the logger, any interpolation already happened and the
|
|
14
|
+
* original data is gone. Those fields are public by contract, and these
|
|
15
|
+
* rules are what makes the contract true.
|
|
16
|
+
*
|
|
17
|
+
* Two configs:
|
|
18
|
+
* - `recommended` — the OSS profile. Catches the leaks that are almost
|
|
19
|
+
* always mistakes: interpolated messages, and metadata whose key set
|
|
20
|
+
* cannot be read at the call site.
|
|
21
|
+
* - `strict` — the profile for apps under a `privacyDefault('private')`
|
|
22
|
+
* regime (PHI, PII). Adds correlation-ID provenance and literal
|
|
23
|
+
* subsystems, and accepts a `catalog` so lint enforces the same approved
|
|
24
|
+
* key list the runtime does.
|
|
25
|
+
*
|
|
26
|
+
* Both fail closed on metadata: an object the rules cannot open is reported,
|
|
27
|
+
* because that is exactly the case where they cannot tell a reviewed key
|
|
28
|
+
* from a patient identifier. Projects with nothing sensitive to protect can
|
|
29
|
+
* set `allowOpaqueMetadata` on `no-computed-metadata-key`.
|
|
30
|
+
*/
|
|
31
|
+
const plugin = {
|
|
32
|
+
meta: {
|
|
33
|
+
name: 'react-native-nitro-logger',
|
|
34
|
+
},
|
|
35
|
+
rules: {
|
|
36
|
+
'no-dynamic-message': noDynamicMessage,
|
|
37
|
+
'no-computed-metadata-key': noComputedMetadataKey,
|
|
38
|
+
'no-derived-correlation': noDerivedCorrelation,
|
|
39
|
+
'literal-subsystem': literalSubsystem,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
plugin.configs = {
|
|
44
|
+
recommended: {
|
|
45
|
+
plugins: { 'nitro-logger': plugin },
|
|
46
|
+
rules: {
|
|
47
|
+
'nitro-logger/no-dynamic-message': 'error',
|
|
48
|
+
'nitro-logger/no-computed-metadata-key': 'error',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
strict: {
|
|
52
|
+
plugins: { 'nitro-logger': plugin },
|
|
53
|
+
rules: {
|
|
54
|
+
'nitro-logger/no-dynamic-message': 'error',
|
|
55
|
+
'nitro-logger/no-computed-metadata-key': 'error',
|
|
56
|
+
'nitro-logger/no-derived-correlation': 'error',
|
|
57
|
+
'nitro-logger/literal-subsystem': 'error',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
module.exports = plugin;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
describeLogCall,
|
|
5
|
+
describeScopedCall,
|
|
6
|
+
describeSubsystemConfigCall,
|
|
7
|
+
isOmitted,
|
|
8
|
+
isStaticString,
|
|
9
|
+
maybeLogger,
|
|
10
|
+
subsystemArguments,
|
|
11
|
+
} = require('../shared');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Subsystems are the filtering mechanism — they replace SwiftLogger's
|
|
15
|
+
* file/function/line, which JS cannot capture. They belong to a small fixed
|
|
16
|
+
* vocabulary the team chooses, so they must be literal.
|
|
17
|
+
*
|
|
18
|
+
* A computed subsystem is both a leak risk (it renders unredacted in every
|
|
19
|
+
* line) and a functional bug: `Log.subsystem('network', 'debug')` can only
|
|
20
|
+
* match names known at configuration time, so a dynamic name silently
|
|
21
|
+
* misses every level override the app configured.
|
|
22
|
+
*/
|
|
23
|
+
module.exports = {
|
|
24
|
+
meta: {
|
|
25
|
+
type: 'problem',
|
|
26
|
+
docs: {
|
|
27
|
+
description:
|
|
28
|
+
'require subsystem names to be static strings from a fixed vocabulary',
|
|
29
|
+
},
|
|
30
|
+
schema: [
|
|
31
|
+
{
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
loggerNames: { type: 'array', items: { type: 'string' } },
|
|
35
|
+
loggerModules: { type: 'array', items: { type: 'string' } },
|
|
36
|
+
singletonName: { type: 'string' },
|
|
37
|
+
},
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
messages: {
|
|
42
|
+
dynamic:
|
|
43
|
+
'Subsystem names must be string literals or approved constants. A computed name renders unredacted and silently misses the level overrides configured for it.',
|
|
44
|
+
unanalyzable:
|
|
45
|
+
'This subsystem cannot be resolved to a literal name. Pass the name directly, or a `const` string this rule can follow.',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
create(context) {
|
|
50
|
+
function check(node) {
|
|
51
|
+
// `Log.scoped(id, undefined, md)` carries no subsystem at all, which is
|
|
52
|
+
// what the runtime sees too.
|
|
53
|
+
if (!node || isOmitted(context, node)) return;
|
|
54
|
+
if (!isStaticString(context, node)) {
|
|
55
|
+
context.report({ node, messageId: 'dynamic' });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
CallExpression(node) {
|
|
61
|
+
const call = describeLogCall(context, node);
|
|
62
|
+
if (call) {
|
|
63
|
+
// A spread cannot shift the subsystem out of reach silently: the
|
|
64
|
+
// Logger's level helpers take one, so the position exists.
|
|
65
|
+
if (call.spreadArgs && maybeLogger(call.receiver)) {
|
|
66
|
+
context.report({ node, messageId: 'unanalyzable' });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const result of subsystemArguments(context, call)) {
|
|
70
|
+
if (result.kind === 'unanalyzable') {
|
|
71
|
+
context.report({ node: result.node, messageId: 'unanalyzable' });
|
|
72
|
+
} else {
|
|
73
|
+
check(result.node);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// `Log.subsystem(name, level)` / `Log.resetSubsystem(name)`
|
|
80
|
+
const config = describeSubsystemConfigCall(context, node);
|
|
81
|
+
if (config) {
|
|
82
|
+
if (config.spreadArgs) {
|
|
83
|
+
context.report({ node, messageId: 'unanalyzable' });
|
|
84
|
+
} else {
|
|
85
|
+
check(config.args[0]);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// `Log.scoped(correlation, subsystem, metadata)` — the subsystem
|
|
91
|
+
// here renders on every message the scope emits.
|
|
92
|
+
const scoped = describeScopedCall(context, node);
|
|
93
|
+
if (!scoped) return;
|
|
94
|
+
if (scoped.spreadArgs) {
|
|
95
|
+
context.report({ node, messageId: 'unanalyzable' });
|
|
96
|
+
} else {
|
|
97
|
+
check(scoped.args[1]);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
describeLogCall,
|
|
5
|
+
describeScopedCall,
|
|
6
|
+
isOmitted,
|
|
7
|
+
metadataArguments,
|
|
8
|
+
resolveObjectLiteral,
|
|
9
|
+
} = require('../shared');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The runtime's own key contract, mirrored so a catalog that lint accepts is
|
|
13
|
+
* one the runtime can actually enforce. Kept in sync with
|
|
14
|
+
* `METADATA_KEY_PATTERN_SOURCE`, `MAX_CATALOG_SIZE` and `DROPPED_COUNT_KEY`
|
|
15
|
+
* in `src/privacy.ts` by a test that imports both.
|
|
16
|
+
*/
|
|
17
|
+
const METADATA_KEY_PATTERN_SOURCE = '^[A-Za-z0-9._-]{1,64}$';
|
|
18
|
+
const MAX_CATALOG_SIZE = 4096;
|
|
19
|
+
const DROPPED_COUNT_KEY = 'droppedMetadataCount';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Metadata keys are structure, not data.
|
|
23
|
+
*
|
|
24
|
+
* The runtime drops keys that fail the shape regex or the approved-key
|
|
25
|
+
* catalog, but it can only do that for keys it can see. A computed key
|
|
26
|
+
* (`{ [patientId]: … }`) or a spread (`{ ...record }`) hides the key set
|
|
27
|
+
* from review entirely. Worse, `patientId` is very often a value like
|
|
28
|
+
* `patient123`, which satisfies the runtime's shape regex — so in the OSS
|
|
29
|
+
* profile, where no catalog is configured, that key reaches the destination
|
|
30
|
+
* intact. Lint is the only control on that path.
|
|
31
|
+
*
|
|
32
|
+
* Because of that, an *unresolvable* metadata expression matters as much as
|
|
33
|
+
* a visibly computed one: `const md = { [patientId]: v }; Log.info('m', md)`
|
|
34
|
+
* and `Log.info('m', buildMetadata(patient))` leak identically. Aliases are
|
|
35
|
+
* followed to their object literal, and when that fails the expression is
|
|
36
|
+
* reported.
|
|
37
|
+
*
|
|
38
|
+
* That is on by default. An opaque metadata object is precisely the case the
|
|
39
|
+
* rule cannot see through, so exempting it would leave the rule enforcing
|
|
40
|
+
* only the mistakes people do not make. `allowOpaqueMetadata` turns it off
|
|
41
|
+
* for projects with nothing sensitive to protect, and the option is named to
|
|
42
|
+
* say what it costs.
|
|
43
|
+
*/
|
|
44
|
+
module.exports = {
|
|
45
|
+
meta: {
|
|
46
|
+
type: 'problem',
|
|
47
|
+
docs: {
|
|
48
|
+
description:
|
|
49
|
+
'require metadata keys to be literal, and optionally drawn from the approved catalog',
|
|
50
|
+
},
|
|
51
|
+
schema: [
|
|
52
|
+
{
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
loggerNames: { type: 'array', items: { type: 'string' } },
|
|
56
|
+
loggerModules: { type: 'array', items: { type: 'string' } },
|
|
57
|
+
singletonName: { type: 'string' },
|
|
58
|
+
// The catalog mirrors the runtime's contract exactly. A key the
|
|
59
|
+
// runtime would reject is worse than useless in lint: it approves
|
|
60
|
+
// at build time something that is silently dropped in production.
|
|
61
|
+
catalog: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
items: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
pattern: METADATA_KEY_PATTERN_SOURCE,
|
|
66
|
+
not: { const: DROPPED_COUNT_KEY },
|
|
67
|
+
},
|
|
68
|
+
maxItems: MAX_CATALOG_SIZE,
|
|
69
|
+
uniqueItems: true,
|
|
70
|
+
},
|
|
71
|
+
allowOpaqueMetadata: { type: 'boolean' },
|
|
72
|
+
},
|
|
73
|
+
additionalProperties: false,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
messages: {
|
|
77
|
+
computed:
|
|
78
|
+
'Metadata keys must be literal. A computed key hides the key set from review and is dropped at runtime — put the varying part in the value, wrapped in pub() or priv().',
|
|
79
|
+
spread:
|
|
80
|
+
'Spreading an object into metadata hides its keys. List the keys you intend to log explicitly.',
|
|
81
|
+
unapproved:
|
|
82
|
+
'Metadata key "{{key}}" is not in the approved catalog, so the runtime will drop it. Add it to the catalog or use an approved key.',
|
|
83
|
+
unanalyzable:
|
|
84
|
+
'Metadata must be an object literal at the call site so its keys can be reviewed. This expression hides them — inline the keys, or build the object as a `const` this rule can follow.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
create(context) {
|
|
89
|
+
const options = context.options?.[0] ?? {};
|
|
90
|
+
const catalog = Array.isArray(options.catalog)
|
|
91
|
+
? new Set(options.catalog)
|
|
92
|
+
: null;
|
|
93
|
+
// A catalog is a promise that every key was reviewed, so it overrides an
|
|
94
|
+
// explicit opt-out rather than coexisting with it.
|
|
95
|
+
const requireLiteral =
|
|
96
|
+
catalog !== null || options.allowOpaqueMetadata !== true;
|
|
97
|
+
|
|
98
|
+
function reportOpaque(node) {
|
|
99
|
+
if (requireLiteral && node) {
|
|
100
|
+
context.report({ node, messageId: 'unanalyzable' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkMetadataExpression(node) {
|
|
105
|
+
if (!node) return;
|
|
106
|
+
const object = resolveObjectLiteral(context, node);
|
|
107
|
+
if (!object) {
|
|
108
|
+
reportOpaque(node);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const property of object.properties) {
|
|
113
|
+
if (property.type === 'SpreadElement') {
|
|
114
|
+
context.report({ node: property, messageId: 'spread' });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (property.computed) {
|
|
118
|
+
context.report({ node: property.key, messageId: 'computed' });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!catalog) continue;
|
|
122
|
+
// A numeric key is a string key at runtime: `{ 42: v }` reaches the
|
|
123
|
+
// logger as "42", which the shape regex accepts.
|
|
124
|
+
const key =
|
|
125
|
+
property.key.type === 'Identifier'
|
|
126
|
+
? property.key.name
|
|
127
|
+
: property.key.type === 'Literal'
|
|
128
|
+
? String(property.key.value)
|
|
129
|
+
: null;
|
|
130
|
+
if (key !== null && !catalog.has(key)) {
|
|
131
|
+
context.report({
|
|
132
|
+
node: property.key,
|
|
133
|
+
messageId: 'unapproved',
|
|
134
|
+
data: { key },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
CallExpression(node) {
|
|
142
|
+
const call = describeLogCall(context, node);
|
|
143
|
+
if (call) {
|
|
144
|
+
if (call.spreadArgs) {
|
|
145
|
+
// `Log.info(...args)` — the metadata position is unknowable.
|
|
146
|
+
reportOpaque(node);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
for (const result of metadataArguments(context, call)) {
|
|
150
|
+
if (result.kind === 'unanalyzable') reportOpaque(result.node);
|
|
151
|
+
else checkMetadataExpression(result.node);
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// `Log.scoped(correlation, subsystem, metadata)` carries metadata
|
|
157
|
+
// for every message the scope emits, so it gets the same treatment.
|
|
158
|
+
const scoped = describeScopedCall(context, node);
|
|
159
|
+
if (!scoped) return;
|
|
160
|
+
if (scoped.spreadArgs) {
|
|
161
|
+
// `Log.scoped(...args)` — the metadata position is unknowable, and
|
|
162
|
+
// a scope's metadata rides every message it emits.
|
|
163
|
+
reportOpaque(node);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// `Log.scoped(id, 'sub', undefined)` states there is no metadata,
|
|
167
|
+
// which is what the runtime sees too.
|
|
168
|
+
if (!isOmitted(context, scoped.args[2])) {
|
|
169
|
+
checkMetadataExpression(scoped.args[2]);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
};
|