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.
Files changed (205) hide show
  1. package/LICENSE +20 -0
  2. package/NitroLogger.podspec +29 -0
  3. package/README.md +214 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +124 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/nitrologger/AndroidPlatformIo.kt +100 -0
  9. package/android/src/main/java/com/margelo/nitro/nitrologger/HybridFileSink.kt +292 -0
  10. package/android/src/main/java/com/margelo/nitro/nitrologger/HybridNativeConsoleSink.kt +23 -0
  11. package/android/src/main/java/com/margelo/nitro/nitrologger/LogFileWriter.kt +1328 -0
  12. package/android/src/main/java/com/margelo/nitro/nitrologger/LogRotationPolicy.kt +186 -0
  13. package/android/src/main/java/com/margelo/nitro/nitrologger/LogSecureFile.kt +80 -0
  14. package/android/src/main/java/com/margelo/nitro/nitrologger/LogWriterRegistry.kt +420 -0
  15. package/android/src/main/java/com/margelo/nitro/nitrologger/NativeConsoleWriter.kt +237 -0
  16. package/android/src/main/java/com/margelo/nitro/nitrologger/NitroLoggerPackage.kt +22 -0
  17. package/android/src/main/java/com/margelo/nitro/nitrologger/PlatformIo.kt +165 -0
  18. package/docs/PARITY.md +330 -0
  19. package/docs/PRIVACY.md +341 -0
  20. package/eslint-plugin/README.md +238 -0
  21. package/eslint-plugin/index.js +62 -0
  22. package/eslint-plugin/rules/literal-subsystem.js +102 -0
  23. package/eslint-plugin/rules/no-computed-metadata-key.js +174 -0
  24. package/eslint-plugin/rules/no-derived-correlation.js +171 -0
  25. package/eslint-plugin/rules/no-dynamic-message.js +60 -0
  26. package/eslint-plugin/shared.js +1948 -0
  27. package/ios/Gzip.swift +203 -0
  28. package/ios/HybridFileSink.swift +291 -0
  29. package/ios/HybridNativeConsoleSink.swift +20 -0
  30. package/ios/LogFileWriter.swift +1280 -0
  31. package/ios/LogSecureFile.swift +296 -0
  32. package/ios/LogWriterRegistry.swift +444 -0
  33. package/ios/NativeConsoleWriter.swift +180 -0
  34. package/lib/module/Logger.js +381 -0
  35. package/lib/module/Logger.js.map +1 -0
  36. package/lib/module/ScopedLogger.js +70 -0
  37. package/lib/module/ScopedLogger.js.map +1 -0
  38. package/lib/module/config.js +23 -0
  39. package/lib/module/config.js.map +1 -0
  40. package/lib/module/destinations/Batcher.js +647 -0
  41. package/lib/module/destinations/Batcher.js.map +1 -0
  42. package/lib/module/destinations/ConsoleDestination.js +51 -0
  43. package/lib/module/destinations/ConsoleDestination.js.map +1 -0
  44. package/lib/module/destinations/FileDestination.js +360 -0
  45. package/lib/module/destinations/FileDestination.js.map +1 -0
  46. package/lib/module/destinations/NativeConsoleDestination.js +170 -0
  47. package/lib/module/destinations/NativeConsoleDestination.js.map +1 -0
  48. package/lib/module/destinations/types.js +4 -0
  49. package/lib/module/destinations/types.js.map +1 -0
  50. package/lib/module/formatters/DefaultFormatter.js +136 -0
  51. package/lib/module/formatters/DefaultFormatter.js.map +1 -0
  52. package/lib/module/formatters/JsonLinesFormatter.js +162 -0
  53. package/lib/module/formatters/JsonLinesFormatter.js.map +1 -0
  54. package/lib/module/formatters/timestamp.js +15 -0
  55. package/lib/module/formatters/timestamp.js.map +1 -0
  56. package/lib/module/formatters/types.js +4 -0
  57. package/lib/module/formatters/types.js.map +1 -0
  58. package/lib/module/index.js +31 -0
  59. package/lib/module/index.js.map +1 -0
  60. package/lib/module/integrations/appState.js +81 -0
  61. package/lib/module/integrations/appState.js.map +1 -0
  62. package/lib/module/integrations/errorHandler.js +166 -0
  63. package/lib/module/integrations/errorHandler.js.map +1 -0
  64. package/lib/module/integrations/sanitizeError.js +238 -0
  65. package/lib/module/integrations/sanitizeError.js.map +1 -0
  66. package/lib/module/levels.js +35 -0
  67. package/lib/module/levels.js.map +1 -0
  68. package/lib/module/metadata.js +52 -0
  69. package/lib/module/metadata.js.map +1 -0
  70. package/lib/module/package.json +1 -0
  71. package/lib/module/privacy.js +349 -0
  72. package/lib/module/privacy.js.map +1 -0
  73. package/lib/module/specs/FileSink.nitro.js +4 -0
  74. package/lib/module/specs/FileSink.nitro.js.map +1 -0
  75. package/lib/module/specs/NativeConsoleSink.nitro.js +4 -0
  76. package/lib/module/specs/NativeConsoleSink.nitro.js.map +1 -0
  77. package/lib/module/types.js +4 -0
  78. package/lib/module/types.js.map +1 -0
  79. package/lib/module/utf8.js +42 -0
  80. package/lib/module/utf8.js.map +1 -0
  81. package/lib/typescript/package.json +1 -0
  82. package/lib/typescript/src/Logger.d.ts +122 -0
  83. package/lib/typescript/src/Logger.d.ts.map +1 -0
  84. package/lib/typescript/src/ScopedLogger.d.ts +36 -0
  85. package/lib/typescript/src/ScopedLogger.d.ts.map +1 -0
  86. package/lib/typescript/src/config.d.ts +12 -0
  87. package/lib/typescript/src/config.d.ts.map +1 -0
  88. package/lib/typescript/src/destinations/Batcher.d.ts +244 -0
  89. package/lib/typescript/src/destinations/Batcher.d.ts.map +1 -0
  90. package/lib/typescript/src/destinations/ConsoleDestination.d.ts +27 -0
  91. package/lib/typescript/src/destinations/ConsoleDestination.d.ts.map +1 -0
  92. package/lib/typescript/src/destinations/FileDestination.d.ts +200 -0
  93. package/lib/typescript/src/destinations/FileDestination.d.ts.map +1 -0
  94. package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts +86 -0
  95. package/lib/typescript/src/destinations/NativeConsoleDestination.d.ts.map +1 -0
  96. package/lib/typescript/src/destinations/types.d.ts +19 -0
  97. package/lib/typescript/src/destinations/types.d.ts.map +1 -0
  98. package/lib/typescript/src/formatters/DefaultFormatter.d.ts +22 -0
  99. package/lib/typescript/src/formatters/DefaultFormatter.d.ts.map +1 -0
  100. package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts +73 -0
  101. package/lib/typescript/src/formatters/JsonLinesFormatter.d.ts.map +1 -0
  102. package/lib/typescript/src/formatters/timestamp.d.ts +6 -0
  103. package/lib/typescript/src/formatters/timestamp.d.ts.map +1 -0
  104. package/lib/typescript/src/formatters/types.d.ts +27 -0
  105. package/lib/typescript/src/formatters/types.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +34 -0
  107. package/lib/typescript/src/index.d.ts.map +1 -0
  108. package/lib/typescript/src/integrations/appState.d.ts +33 -0
  109. package/lib/typescript/src/integrations/appState.d.ts.map +1 -0
  110. package/lib/typescript/src/integrations/errorHandler.d.ts +55 -0
  111. package/lib/typescript/src/integrations/errorHandler.d.ts.map +1 -0
  112. package/lib/typescript/src/integrations/sanitizeError.d.ts +89 -0
  113. package/lib/typescript/src/integrations/sanitizeError.d.ts.map +1 -0
  114. package/lib/typescript/src/levels.d.ts +9 -0
  115. package/lib/typescript/src/levels.d.ts.map +1 -0
  116. package/lib/typescript/src/metadata.d.ts +23 -0
  117. package/lib/typescript/src/metadata.d.ts.map +1 -0
  118. package/lib/typescript/src/privacy.d.ts +103 -0
  119. package/lib/typescript/src/privacy.d.ts.map +1 -0
  120. package/lib/typescript/src/specs/FileSink.nitro.d.ts +131 -0
  121. package/lib/typescript/src/specs/FileSink.nitro.d.ts.map +1 -0
  122. package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts +21 -0
  123. package/lib/typescript/src/specs/NativeConsoleSink.nitro.d.ts.map +1 -0
  124. package/lib/typescript/src/types.d.ts +37 -0
  125. package/lib/typescript/src/types.d.ts.map +1 -0
  126. package/lib/typescript/src/utf8.d.ts +15 -0
  127. package/lib/typescript/src/utf8.d.ts.map +1 -0
  128. package/nitro.json +33 -0
  129. package/nitrogen/generated/android/c++/JAppendResult.hpp +79 -0
  130. package/nitrogen/generated/android/c++/JClearOutcome.hpp +88 -0
  131. package/nitrogen/generated/android/c++/JFlushOutcome.hpp +81 -0
  132. package/nitrogen/generated/android/c++/JHybridFileSinkSpec.cpp +120 -0
  133. package/nitrogen/generated/android/c++/JHybridFileSinkSpec.hpp +69 -0
  134. package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.cpp +71 -0
  135. package/nitrogen/generated/android/c++/JHybridNativeConsoleSinkSpec.hpp +64 -0
  136. package/nitrogen/generated/android/c++/JRejectReason.hpp +64 -0
  137. package/nitrogen/generated/android/c++/JRotationConfig.hpp +77 -0
  138. package/nitrogen/generated/android/c++/JSinkStatus.hpp +69 -0
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/AppendResult.kt +76 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/ClearOutcome.kt +66 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/FlushOutcome.kt +81 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridFileSinkSpec.kt +80 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/HybridNativeConsoleSinkSpec.kt +58 -0
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RejectReason.kt +25 -0
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/RotationConfig.kt +76 -0
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/SinkStatus.kt +66 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrologger/nitrologgerOnLoad.kt +35 -0
  148. package/nitrogen/generated/android/nitrologger+autolinking.cmake +83 -0
  149. package/nitrogen/generated/android/nitrologger+autolinking.gradle +27 -0
  150. package/nitrogen/generated/android/nitrologgerOnLoad.cpp +70 -0
  151. package/nitrogen/generated/android/nitrologgerOnLoad.hpp +34 -0
  152. package/nitrogen/generated/ios/NitroLogger+autolinking.rb +62 -0
  153. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.cpp +50 -0
  154. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Bridge.hpp +216 -0
  155. package/nitrogen/generated/ios/NitroLogger-Swift-Cxx-Umbrella.hpp +69 -0
  156. package/nitrogen/generated/ios/NitroLoggerAutolinking.mm +41 -0
  157. package/nitrogen/generated/ios/NitroLoggerAutolinking.swift +38 -0
  158. package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.cpp +11 -0
  159. package/nitrogen/generated/ios/c++/HybridFileSinkSpecSwift.hpp +150 -0
  160. package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.cpp +11 -0
  161. package/nitrogen/generated/ios/c++/HybridNativeConsoleSinkSpecSwift.hpp +87 -0
  162. package/nitrogen/generated/ios/swift/AppendResult.swift +60 -0
  163. package/nitrogen/generated/ios/swift/ClearOutcome.swift +50 -0
  164. package/nitrogen/generated/ios/swift/FlushOutcome.swift +59 -0
  165. package/nitrogen/generated/ios/swift/HybridFileSinkSpec.swift +61 -0
  166. package/nitrogen/generated/ios/swift/HybridFileSinkSpec_cxx.swift +227 -0
  167. package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec.swift +56 -0
  168. package/nitrogen/generated/ios/swift/HybridNativeConsoleSinkSpec_cxx.swift +148 -0
  169. package/nitrogen/generated/ios/swift/RejectReason.swift +48 -0
  170. package/nitrogen/generated/ios/swift/RotationConfig.swift +93 -0
  171. package/nitrogen/generated/ios/swift/SinkStatus.swift +44 -0
  172. package/nitrogen/generated/shared/c++/AppendResult.hpp +105 -0
  173. package/nitrogen/generated/shared/c++/ClearOutcome.hpp +96 -0
  174. package/nitrogen/generated/shared/c++/FlushOutcome.hpp +107 -0
  175. package/nitrogen/generated/shared/c++/HybridFileSinkSpec.cpp +28 -0
  176. package/nitrogen/generated/shared/c++/HybridFileSinkSpec.hpp +84 -0
  177. package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.cpp +22 -0
  178. package/nitrogen/generated/shared/c++/HybridNativeConsoleSinkSpec.hpp +64 -0
  179. package/nitrogen/generated/shared/c++/RejectReason.hpp +84 -0
  180. package/nitrogen/generated/shared/c++/RotationConfig.hpp +103 -0
  181. package/nitrogen/generated/shared/c++/SinkStatus.hpp +95 -0
  182. package/package.json +151 -0
  183. package/src/Logger.ts +417 -0
  184. package/src/ScopedLogger.ts +94 -0
  185. package/src/config.ts +25 -0
  186. package/src/destinations/Batcher.ts +805 -0
  187. package/src/destinations/ConsoleDestination.ts +60 -0
  188. package/src/destinations/FileDestination.ts +473 -0
  189. package/src/destinations/NativeConsoleDestination.ts +220 -0
  190. package/src/destinations/types.ts +19 -0
  191. package/src/formatters/DefaultFormatter.ts +150 -0
  192. package/src/formatters/JsonLinesFormatter.ts +195 -0
  193. package/src/formatters/timestamp.ts +12 -0
  194. package/src/formatters/types.ts +29 -0
  195. package/src/index.tsx +106 -0
  196. package/src/integrations/appState.ts +111 -0
  197. package/src/integrations/errorHandler.ts +223 -0
  198. package/src/integrations/sanitizeError.ts +287 -0
  199. package/src/levels.ts +35 -0
  200. package/src/metadata.ts +52 -0
  201. package/src/privacy.ts +389 -0
  202. package/src/specs/FileSink.nitro.ts +144 -0
  203. package/src/specs/NativeConsoleSink.nitro.ts +21 -0
  204. package/src/types.ts +45 -0
  205. 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
+ };