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
package/src/privacy.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LogMetadata,
|
|
3
|
+
LogPrimitive,
|
|
4
|
+
LogValue,
|
|
5
|
+
RedactedMetadata,
|
|
6
|
+
} from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The privacy layer, deliberately a single module.
|
|
10
|
+
*
|
|
11
|
+
* Marker inspection and redaction live together so that payload recovery
|
|
12
|
+
* stays closure-private. If `inspectMarker` were exported, anyone could deep
|
|
13
|
+
* import the compiled module and read back a `priv()` payload in production,
|
|
14
|
+
* which would defeat the whole no-reveal contract regardless of what the
|
|
15
|
+
* package entry point re-exports. Nothing here that can return a payload
|
|
16
|
+
* crosses the module boundary.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** What a redacted value renders as in a log line. */
|
|
20
|
+
export const PRIVATE_PLACEHOLDER = '<private>';
|
|
21
|
+
|
|
22
|
+
/** What a marker renders as if it is stringified directly (interpolated into
|
|
23
|
+
* a message, spread, inspected). Never the payload — a marker leaking its
|
|
24
|
+
* value through `String()` would defeat the whole contract. */
|
|
25
|
+
const MARKER_PLACEHOLDER = '<redacted>';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Metadata keys are structural, never data. Anything outside this shape is
|
|
29
|
+
* a sign a value was used as a key — the classic `patient123: …` leak.
|
|
30
|
+
*
|
|
31
|
+
* The enforcing RegExp is module-private and only its source is exported. A
|
|
32
|
+
* shared RegExp object is mutable: a consumer could assign an own `test`
|
|
33
|
+
* method or call `RegExp.prototype.compile` on it and silently disable key
|
|
34
|
+
* validation for the whole process. Callers that want to pre-check a key
|
|
35
|
+
* build their own from {@link METADATA_KEY_PATTERN_SOURCE}.
|
|
36
|
+
*/
|
|
37
|
+
const METADATA_KEY_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
|
|
38
|
+
|
|
39
|
+
/** Source text of the key rule, for callers that want their own RegExp. */
|
|
40
|
+
export const METADATA_KEY_PATTERN_SOURCE = '^[A-Za-z0-9._-]{1,64}$';
|
|
41
|
+
|
|
42
|
+
function isValidMetadataKey(key: string): boolean {
|
|
43
|
+
return METADATA_KEY_PATTERN.test(key);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Reserved key carrying the fixed diagnostic: how many entries this call
|
|
47
|
+
* dropped. A count only — never which keys, never why, never the values. */
|
|
48
|
+
export const DROPPED_COUNT_KEY = 'droppedMetadataCount';
|
|
49
|
+
|
|
50
|
+
/** Upper bound on catalog size. A hostile or accidental infinite iterable
|
|
51
|
+
* must not be able to hang the JS thread inside a logging config call. */
|
|
52
|
+
export const MAX_CATALOG_SIZE = 4096;
|
|
53
|
+
|
|
54
|
+
declare const publicBrand: unique symbol;
|
|
55
|
+
declare const privateBrand: unique symbol;
|
|
56
|
+
|
|
57
|
+
/** A value explicitly marked safe to render. Opaque: the payload lives in a
|
|
58
|
+
* module-private WeakMap, never on the object. */
|
|
59
|
+
export interface PublicValue {
|
|
60
|
+
readonly [publicBrand]: true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A value explicitly marked sensitive. Renders as `<private>` outside dev. */
|
|
64
|
+
export interface PrivateValue {
|
|
65
|
+
readonly [privateBrand]: true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Membership in these maps is the ONLY proof that an object is a marker.
|
|
69
|
+
// A foreign lookalike — same shape, same toString — fails naturally because
|
|
70
|
+
// it was never registered here, and no code outside this module can register
|
|
71
|
+
// one. WeakMaps also keep the payload unreachable: there is no enumerable
|
|
72
|
+
// field to spread, stringify, or inspect.
|
|
73
|
+
const publicPayloads = new WeakMap<object, LogPrimitive>();
|
|
74
|
+
const privatePayloads = new WeakMap<object, LogPrimitive>();
|
|
75
|
+
const invalidMarkers = new WeakSet<object>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Exactly the primitives the pipeline can render. Objects, arrays, proxies,
|
|
79
|
+
* functions, symbols, null/undefined, NaN and the infinities are all
|
|
80
|
+
* rejected — an object payload is the classic way PHI slips into a log.
|
|
81
|
+
*/
|
|
82
|
+
function isLogPrimitive(value: unknown): value is LogPrimitive {
|
|
83
|
+
const kind = typeof value;
|
|
84
|
+
return (
|
|
85
|
+
kind === 'string' ||
|
|
86
|
+
kind === 'boolean' ||
|
|
87
|
+
(kind === 'number' && Number.isFinite(value))
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createMarker(): object {
|
|
92
|
+
const marker = {};
|
|
93
|
+
const hide = (key: PropertyKey, value: unknown): void => {
|
|
94
|
+
Object.defineProperty(marker, key, {
|
|
95
|
+
value,
|
|
96
|
+
enumerable: false,
|
|
97
|
+
writable: false,
|
|
98
|
+
configurable: false,
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const placeholder = (): string => MARKER_PLACEHOLDER;
|
|
102
|
+
// Non-enumerable, so `{...marker}` and `Object.keys(marker)` see nothing.
|
|
103
|
+
hide('toString', placeholder);
|
|
104
|
+
hide('toJSON', placeholder);
|
|
105
|
+
hide(Symbol.toPrimitive, placeholder);
|
|
106
|
+
hide(Symbol.for('nodejs.util.inspect.custom'), placeholder);
|
|
107
|
+
hide(Symbol.toStringTag, 'RedactedLogValue');
|
|
108
|
+
return Object.freeze(marker);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mark a value safe to render even under a `'private'` privacy default.
|
|
113
|
+
*
|
|
114
|
+
* The payload is validated here AND again at emit: `pub(patient as any)`
|
|
115
|
+
* must not smuggle an object past the primitive check. An invalid payload
|
|
116
|
+
* does not throw — logging never crashes the caller — it produces a marker
|
|
117
|
+
* that is dropped at emit and counted in the fixed diagnostic.
|
|
118
|
+
*/
|
|
119
|
+
export function pub(value: LogPrimitive): PublicValue {
|
|
120
|
+
const marker = createMarker();
|
|
121
|
+
if (isLogPrimitive(value)) {
|
|
122
|
+
publicPayloads.set(marker, value);
|
|
123
|
+
} else {
|
|
124
|
+
invalidMarkers.add(marker);
|
|
125
|
+
}
|
|
126
|
+
return marker as PublicValue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Mark a value sensitive: renders as `<private>` outside dev builds, even
|
|
130
|
+
* under a `'public'` privacy default. Same fail-closed validation as `pub`. */
|
|
131
|
+
export function priv(value: LogPrimitive): PrivateValue {
|
|
132
|
+
const marker = createMarker();
|
|
133
|
+
if (isLogPrimitive(value)) {
|
|
134
|
+
privatePayloads.set(marker, value);
|
|
135
|
+
} else {
|
|
136
|
+
invalidMarkers.add(marker);
|
|
137
|
+
}
|
|
138
|
+
return marker as PrivateValue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Stands in for a value that could not be read when a snapshot was taken —
|
|
143
|
+
* a getter that threw while a scope was being constructed.
|
|
144
|
+
*
|
|
145
|
+
* It is registered as an invalid marker, so redaction drops it and counts
|
|
146
|
+
* it like any other rejected value. Without it the key would vanish at
|
|
147
|
+
* snapshot time and never appear in `droppedMetadataCount`, which is the
|
|
148
|
+
* one thing a silent drop must never do. It carries no payload.
|
|
149
|
+
*/
|
|
150
|
+
export const UNREADABLE_VALUE: LogValue = (() => {
|
|
151
|
+
const marker = createMarker();
|
|
152
|
+
invalidMarkers.add(marker);
|
|
153
|
+
return marker as LogValue;
|
|
154
|
+
})();
|
|
155
|
+
|
|
156
|
+
type MarkerInspection =
|
|
157
|
+
| { readonly kind: 'public'; readonly payload: LogPrimitive }
|
|
158
|
+
| { readonly kind: 'private'; readonly payload: LogPrimitive }
|
|
159
|
+
| { readonly kind: 'invalid' };
|
|
160
|
+
|
|
161
|
+
/** Module-private by design — see the note at the top of this file. */
|
|
162
|
+
function inspectMarker(value: unknown): MarkerInspection | null {
|
|
163
|
+
if (value === null || typeof value !== 'object') return null;
|
|
164
|
+
const candidate = value as object;
|
|
165
|
+
|
|
166
|
+
if (publicPayloads.has(candidate)) {
|
|
167
|
+
const payload = publicPayloads.get(candidate);
|
|
168
|
+
return isLogPrimitive(payload)
|
|
169
|
+
? { kind: 'public', payload }
|
|
170
|
+
: { kind: 'invalid' };
|
|
171
|
+
}
|
|
172
|
+
if (privatePayloads.has(candidate)) {
|
|
173
|
+
const payload = privatePayloads.get(candidate);
|
|
174
|
+
return isLogPrimitive(payload)
|
|
175
|
+
? { kind: 'private', payload }
|
|
176
|
+
: { kind: 'invalid' };
|
|
177
|
+
}
|
|
178
|
+
if (invalidMarkers.has(candidate)) return { kind: 'invalid' };
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The reveal branch exists only in dev builds. Metro substitutes a literal
|
|
183
|
+
// for `__DEV__`, so in a release bundle the whole block — sentinel string
|
|
184
|
+
// included — is dead code and gets stripped. M7's CI job greps the release
|
|
185
|
+
// bundle for the sentinel to prove it.
|
|
186
|
+
let revealBranchSentinel = '';
|
|
187
|
+
|
|
188
|
+
/** True when private values may render as themselves: dev builds only.
|
|
189
|
+
* There is deliberately no runtime switch to turn this on in production. */
|
|
190
|
+
function privateRevealAllowed(): boolean {
|
|
191
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
192
|
+
revealBranchSentinel = '__NITRO_LOGGER_PRIVATE_REVEAL__';
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** @internal Test/CI hook — non-empty only once the dev reveal branch ran.
|
|
199
|
+
* Returns a build-time marker, never a payload. */
|
|
200
|
+
export function __revealBranchSentinel(): string {
|
|
201
|
+
return revealBranchSentinel;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Configuration ───────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
export type PrivacyDefault = 'public' | 'private';
|
|
207
|
+
|
|
208
|
+
export interface PrivacySettings {
|
|
209
|
+
/** `'private'` redacts bare values outside dev; `'public'` renders them. */
|
|
210
|
+
readonly privacyDefault: PrivacyDefault;
|
|
211
|
+
/** Kill switch: redact every value regardless of marker or build type. */
|
|
212
|
+
readonly redactAll: boolean;
|
|
213
|
+
/** Approved metadata keys. Undefined means "no catalog configured". */
|
|
214
|
+
readonly keyCatalog: ReadonlySet<string> | undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Coerce a caller-supplied privacy default. TypeScript's union is not a
|
|
219
|
+
* runtime guarantee — JS callers, JSON config, and `any` all reach here — so
|
|
220
|
+
* anything that is not exactly `'public'` or `'private'` resolves to
|
|
221
|
+
* `'private'`. Ambiguity must never resolve toward disclosure.
|
|
222
|
+
*/
|
|
223
|
+
export function normalizePrivacyDefault(value: unknown): PrivacyDefault {
|
|
224
|
+
return value === 'public' ? 'public' : 'private';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build a catalog from caller-supplied input without trusting it. A
|
|
229
|
+
* non-iterable, an iterator that throws mid-way, a non-string entry, or an
|
|
230
|
+
* iterable longer than {@link MAX_CATALOG_SIZE} all yield an empty set: the
|
|
231
|
+
* caller's tighten-only intent is honored in the safest possible direction,
|
|
232
|
+
* and a runaway iterable cannot hang the JS thread.
|
|
233
|
+
*/
|
|
234
|
+
export function buildCatalog(keys: unknown): Set<string> {
|
|
235
|
+
const result = new Set<string>();
|
|
236
|
+
if (keys === null || typeof keys !== 'object') return result;
|
|
237
|
+
try {
|
|
238
|
+
// Bound on entries CONSUMED, not on set size: an iterator that yields
|
|
239
|
+
// the same valid key forever never grows the set, so a size-based bound
|
|
240
|
+
// would spin the JS thread indefinitely.
|
|
241
|
+
let consumed = 0;
|
|
242
|
+
for (const key of keys as Iterable<unknown>) {
|
|
243
|
+
if (++consumed > MAX_CATALOG_SIZE) return new Set();
|
|
244
|
+
if (typeof key !== 'string' || !isValidMetadataKey(key)) {
|
|
245
|
+
return new Set();
|
|
246
|
+
}
|
|
247
|
+
result.add(key);
|
|
248
|
+
}
|
|
249
|
+
} catch {
|
|
250
|
+
return new Set();
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Whether an unconfigured catalog blocks everything.
|
|
257
|
+
*
|
|
258
|
+
* Under a `'private'` default — the telehealth/strict profile — the catalog
|
|
259
|
+
* is mandatory and fail-closed: rejecting computed keys is not enough, since
|
|
260
|
+
* a literal key like `patient123` is still PHI. With no catalog configured,
|
|
261
|
+
* no key is approved and all metadata drops. That is deliberate; it forces
|
|
262
|
+
* the app to declare its keys rather than failing open.
|
|
263
|
+
*/
|
|
264
|
+
function catalogRequired(settings: PrivacySettings): boolean {
|
|
265
|
+
return settings.privacyDefault === 'private';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── Redaction ───────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
type Resolution =
|
|
271
|
+
| { readonly kind: 'value'; readonly value: LogPrimitive }
|
|
272
|
+
| { readonly kind: 'drop' };
|
|
273
|
+
|
|
274
|
+
const DROP: Resolution = { kind: 'drop' };
|
|
275
|
+
|
|
276
|
+
/** Record which source owns each of its keys, without reading any value. */
|
|
277
|
+
function claimKeys(
|
|
278
|
+
owner: Map<string, Record<string, unknown>>,
|
|
279
|
+
source: LogMetadata | undefined
|
|
280
|
+
): void {
|
|
281
|
+
if (!source) return;
|
|
282
|
+
let keys: string[];
|
|
283
|
+
try {
|
|
284
|
+
keys = Object.keys(source);
|
|
285
|
+
} catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
for (const key of keys) {
|
|
289
|
+
owner.set(key, source as Record<string, unknown>);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveValue(raw: unknown, settings: PrivacySettings): Resolution {
|
|
294
|
+
let visibility: PrivacyDefault;
|
|
295
|
+
let payload: LogPrimitive;
|
|
296
|
+
|
|
297
|
+
const marker = inspectMarker(raw);
|
|
298
|
+
if (marker !== null) {
|
|
299
|
+
// A marker whose payload failed validation at construction or here —
|
|
300
|
+
// `pub(someObject as any)` — fails closed rather than rendering.
|
|
301
|
+
if (marker.kind === 'invalid') return DROP;
|
|
302
|
+
visibility = marker.kind;
|
|
303
|
+
payload = marker.payload;
|
|
304
|
+
} else if (isLogPrimitive(raw)) {
|
|
305
|
+
visibility = settings.privacyDefault;
|
|
306
|
+
payload = raw;
|
|
307
|
+
} else {
|
|
308
|
+
// Objects, arrays, proxies, functions, symbols, null, NaN, Infinity.
|
|
309
|
+
return DROP;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (settings.redactAll) return { kind: 'value', value: PRIVATE_PLACEHOLDER };
|
|
313
|
+
if (visibility === 'public') return { kind: 'value', value: payload };
|
|
314
|
+
return {
|
|
315
|
+
kind: 'value',
|
|
316
|
+
value: privateRevealAllowed() ? payload : PRIVATE_PLACEHOLDER,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Resolve caller metadata into what destinations may see.
|
|
322
|
+
*
|
|
323
|
+
* Takes metadata as ordered sources — a scope's defaults followed by the
|
|
324
|
+
* call site's — rather than a pre-merged object. Merging eagerly would read
|
|
325
|
+
* every value before any key was checked, which is exactly what this
|
|
326
|
+
* function exists to avoid: precedence is settled per key first, then the
|
|
327
|
+
* winning key is validated, and only a key that survives has its value read.
|
|
328
|
+
* A getter behind a malformed, reserved, or unapproved key therefore never
|
|
329
|
+
* runs, and neither does a getter the call site overrode.
|
|
330
|
+
*
|
|
331
|
+
* Every rejection — bad key, throwing getter, unsupported value —
|
|
332
|
+
* increments one count. The count is all that is reported: the diagnostic
|
|
333
|
+
* itself must not become the leak.
|
|
334
|
+
*
|
|
335
|
+
* The result is a frozen null-prototype object. A source whose keys cannot
|
|
336
|
+
* be enumerated at all (a Proxy with a throwing `ownKeys` trap) contributes
|
|
337
|
+
* nothing, since there is no key that could be safely counted or described.
|
|
338
|
+
*/
|
|
339
|
+
export function redactMetadata(
|
|
340
|
+
scopeMetadata: LogMetadata | undefined,
|
|
341
|
+
callSiteMetadata: LogMetadata | undefined,
|
|
342
|
+
settings: PrivacySettings
|
|
343
|
+
): RedactedMetadata | undefined {
|
|
344
|
+
// Two explicit positional sources rather than a collection. A container
|
|
345
|
+
// in the signature would itself be caller-controlled — a hostile array
|
|
346
|
+
// could throw from Symbol.iterator, iterate forever, or be a revoked
|
|
347
|
+
// Proxy — and guarding it is strictly more work than not having one.
|
|
348
|
+
const owner = new Map<string, Record<string, unknown>>();
|
|
349
|
+
claimKeys(owner, scopeMetadata);
|
|
350
|
+
// Second wins on collision: the call site overrides the scope's default.
|
|
351
|
+
claimKeys(owner, callSiteMetadata);
|
|
352
|
+
if (owner.size === 0) return undefined;
|
|
353
|
+
|
|
354
|
+
const catalog = settings.keyCatalog;
|
|
355
|
+
const requireCatalog = catalogRequired(settings);
|
|
356
|
+
|
|
357
|
+
const result: Record<string, LogPrimitive> = Object.create(null);
|
|
358
|
+
let dropped = 0;
|
|
359
|
+
|
|
360
|
+
for (const [key, source] of owner) {
|
|
361
|
+
if (key === DROPPED_COUNT_KEY || !isValidMetadataKey(key)) {
|
|
362
|
+
dropped += 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (catalog !== undefined ? !catalog.has(key) : requireCatalog) {
|
|
366
|
+
dropped += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Only now — after the key has earned it — is the value touched.
|
|
371
|
+
let raw: unknown;
|
|
372
|
+
try {
|
|
373
|
+
raw = source[key];
|
|
374
|
+
} catch {
|
|
375
|
+
dropped += 1;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const resolution = resolveValue(raw, settings);
|
|
380
|
+
if (resolution.kind === 'drop') {
|
|
381
|
+
dropped += 1;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
result[key] = resolution.value;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (dropped > 0) result[DROPPED_COUNT_KEY] = dropped;
|
|
388
|
+
return Object.freeze(result);
|
|
389
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rotation and retention policy for one log file.
|
|
5
|
+
*
|
|
6
|
+
* All numeric fields are validated in TypeScript (finite safe integers,
|
|
7
|
+
* positive sizes, bounded counts) AND re-clamped natively before integer
|
|
8
|
+
* conversion — a NaN/Infinity/negative must never trap a Swift/Kotlin cast.
|
|
9
|
+
*/
|
|
10
|
+
export interface RotationConfig {
|
|
11
|
+
/** Soft threshold; overshoot bounded by one batch. */
|
|
12
|
+
maxFileSizeBytes: number;
|
|
13
|
+
/** Archives to retain by count; 0 keeps none. */
|
|
14
|
+
maxArchivedFilesCount: number;
|
|
15
|
+
/** Rotate the current file once this old, regardless of size. */
|
|
16
|
+
maxFileAgeSeconds?: number;
|
|
17
|
+
/** Gzip archives as they rotate out. */
|
|
18
|
+
compressArchives: boolean;
|
|
19
|
+
/** Delete archives older than this even if under the count cap. */
|
|
20
|
+
maxArchiveAgeSeconds?: number;
|
|
21
|
+
/** Bound on current file + all archives combined. */
|
|
22
|
+
maxTotalLogBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Why an appendBatch was not accepted. */
|
|
26
|
+
export type RejectReason = 'full' | 'staleGeneration' | 'closed' | 'failed';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Snapshot of sink health. MUST be served from atomics / a short non-I/O
|
|
30
|
+
* lock — never from the writer queue — so a paused Batcher can poll it while
|
|
31
|
+
* the writer is stalled on disk.
|
|
32
|
+
*
|
|
33
|
+
* Loss fields are per-handle observed counters (monotonic writer totals with
|
|
34
|
+
* per-handle cursors underneath); observing a loss here does NOT acknowledge
|
|
35
|
+
* it — acknowledgement happens only after the consolidated loss notice has
|
|
36
|
+
* durably flushed.
|
|
37
|
+
*/
|
|
38
|
+
export interface SinkStatus {
|
|
39
|
+
queuedBytes: number;
|
|
40
|
+
lostBytes: number;
|
|
41
|
+
lostEntries: number;
|
|
42
|
+
/** Payload-free degradation bitmask: rotation|gzip|prune|sidecar|protection. */
|
|
43
|
+
degraded: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AppendResult {
|
|
47
|
+
accepted: boolean;
|
|
48
|
+
/** Present only when accepted is false. */
|
|
49
|
+
rejectReason?: RejectReason;
|
|
50
|
+
queuedBytes: number;
|
|
51
|
+
lostBytes: number;
|
|
52
|
+
lostEntries: number;
|
|
53
|
+
degraded: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface FlushOutcome {
|
|
57
|
+
/** True when every previously accepted byte reached storage and fsync. */
|
|
58
|
+
durable: boolean;
|
|
59
|
+
timedOut: boolean;
|
|
60
|
+
pendingBytes: number;
|
|
61
|
+
queuedBytes: number;
|
|
62
|
+
lostBytes: number;
|
|
63
|
+
lostEntries: number;
|
|
64
|
+
degraded: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ClearOutcome {
|
|
68
|
+
deletedCount: number;
|
|
69
|
+
failedPaths: string[];
|
|
70
|
+
/** False when any artifact survived or the deadline elapsed. */
|
|
71
|
+
durable: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Whether the writer came back with a usable file afterwards.
|
|
74
|
+
*
|
|
75
|
+
* Separate from `durable` because they are separate facts and the caller
|
|
76
|
+
* needs both. A purge can delete everything it was asked to — `durable` —
|
|
77
|
+
* and still fail to reopen, on a directory that has become unwritable or a
|
|
78
|
+
* volume that filled. Resuming on `durable` alone hands records to a writer
|
|
79
|
+
* with nowhere to put them: every one is accepted, then rejected as
|
|
80
|
+
* `staleGeneration`, then dropped.
|
|
81
|
+
*
|
|
82
|
+
* Resume only when both are true. `durable && !rebound` means the deletion
|
|
83
|
+
* is genuinely complete — which is what a compliance caller asked — but the
|
|
84
|
+
* destination must stay fenced until an explicit retry gets a live file back.
|
|
85
|
+
*/
|
|
86
|
+
rebound: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Dumb native file sink. All intelligence (batching, backpressure, drop
|
|
91
|
+
* accounting, formatting, redaction) lives in TypeScript; this object only
|
|
92
|
+
* appends pre-batched text, rotates, compresses, and reports counters.
|
|
93
|
+
*
|
|
94
|
+
* One JS FileDestination = one FileSink handle; handles for the same
|
|
95
|
+
* canonical path share one process-global writer (registry, refcounted,
|
|
96
|
+
* generation-fenced). Batch = atomic loss unit.
|
|
97
|
+
*/
|
|
98
|
+
export interface FileSink extends HybridObject<{
|
|
99
|
+
ios: 'swift';
|
|
100
|
+
android: 'kotlin';
|
|
101
|
+
}> {
|
|
102
|
+
readonly defaultLogDirectory: string;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Throws on open failure OR on config conflict with an existing writer.
|
|
106
|
+
*
|
|
107
|
+
* `lineFramed` declares that every batch handed to `appendBatch` contains
|
|
108
|
+
* whole records terminated by `\n`, with no raw newline inside a record.
|
|
109
|
+
* Only then may the startup scan trim an incomplete trailing record: with
|
|
110
|
+
* opaque multi-line text a writer cannot tell a torn record from an
|
|
111
|
+
* intentional one, and trimming would destroy good data. Absent — the
|
|
112
|
+
* default for a custom formatter that does not declare `framing: 'line'` —
|
|
113
|
+
* the file is left exactly as the crash left it and recovery is reduced.
|
|
114
|
+
*/
|
|
115
|
+
open(path: string, rotation?: RotationConfig, lineFramed?: boolean): void;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Enqueue only — never performs I/O inline. Atomic accept/reject BEFORE
|
|
119
|
+
* enqueue against the hard shared payload cap; accepted bytes stay
|
|
120
|
+
* reserved until terminal write completion. entryCount travels with the
|
|
121
|
+
* queued item for exact loss accounting.
|
|
122
|
+
*/
|
|
123
|
+
appendBatch(batch: string, entryCount: number): AppendResult;
|
|
124
|
+
|
|
125
|
+
/** Non-enqueuing health probe; safe to poll while the writer is stalled. */
|
|
126
|
+
getStatus(): SinkStatus;
|
|
127
|
+
|
|
128
|
+
/** Wall-clock-bounded synchronous drain + fsync. */
|
|
129
|
+
flush(deadlineMs: number): FlushOutcome;
|
|
130
|
+
|
|
131
|
+
/** Deadline-aware disposal; a hung write cannot block forever. */
|
|
132
|
+
close(deadlineMs: number): FlushOutcome;
|
|
133
|
+
|
|
134
|
+
/** Active file + archives, newest first. */
|
|
135
|
+
getLogFilePaths(): string[];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Registry-serialized purge of the COMPLETE artifact set (current file,
|
|
139
|
+
* sidecar, archives, gzip temporaries, staging/recovery files). Bumps the
|
|
140
|
+
* writer generation; the invoking handle rebinds only after durable
|
|
141
|
+
* success. Reached via the Logger-layer purge flow, never called ad hoc.
|
|
142
|
+
*/
|
|
143
|
+
clearLogs(deadlineMs: number): ClearOutcome;
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Routes pre-formatted lines to the platform's native log stream so JS logs
|
|
5
|
+
* interleave with native ones — os_log on iOS (Console.app / Xcode), logcat
|
|
6
|
+
* on Android.
|
|
7
|
+
*
|
|
8
|
+
* Levels cross as numeric codes 0–5 (verbose…todo) and map natively:
|
|
9
|
+
* verbose/debug → .debug, info → .info, warning → .default, error → .error,
|
|
10
|
+
* todo → .fault (iOS); Log.println priorities on Android.
|
|
11
|
+
*
|
|
12
|
+
* Batched like the file sink: one logBatch call per Batcher drain, parallel
|
|
13
|
+
* primitive arrays, no per-entry struct marshaling.
|
|
14
|
+
*/
|
|
15
|
+
export interface NativeConsoleSink extends HybridObject<{
|
|
16
|
+
ios: 'swift';
|
|
17
|
+
android: 'kotlin';
|
|
18
|
+
}> {
|
|
19
|
+
install(subsystem: string, category: string): void;
|
|
20
|
+
logBatch(levels: number[], messages: string[]): void;
|
|
21
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { PrivateValue, PublicValue } from './privacy';
|
|
2
|
+
|
|
3
|
+
export type { PrivateValue, PublicValue };
|
|
4
|
+
|
|
5
|
+
/** Severity levels, least to most severe. `todo` marks incomplete work and
|
|
6
|
+
* sorts highest so it always surfaces (SwiftLogger parity). */
|
|
7
|
+
export type LogLevel =
|
|
8
|
+
'verbose' | 'debug' | 'info' | 'warning' | 'error' | 'todo';
|
|
9
|
+
|
|
10
|
+
/** Messages accept a thunk so hot-path callers can defer interpolation —
|
|
11
|
+
* the `@autoclosure` equivalent. The thunk is NOT invoked when the entry is
|
|
12
|
+
* filtered out or no destination is eligible. */
|
|
13
|
+
export type LazyMessage = string | (() => string);
|
|
14
|
+
|
|
15
|
+
/** The only value shapes the pipeline can render. */
|
|
16
|
+
export type LogPrimitive = string | number | boolean;
|
|
17
|
+
|
|
18
|
+
/** Metadata values: a bare primitive, whose visibility follows the privacy
|
|
19
|
+
* default, or one explicitly marked with `pub()` / `priv()`. */
|
|
20
|
+
export type LogValue = LogPrimitive | PublicValue | PrivateValue;
|
|
21
|
+
|
|
22
|
+
export type LogMetadata = Record<string, LogValue>;
|
|
23
|
+
|
|
24
|
+
/** Metadata after redaction — what formatters and destinations see. Privacy
|
|
25
|
+
* markers are unrepresentable here by construction: every value has already
|
|
26
|
+
* been resolved to a primitive or replaced by a placeholder. */
|
|
27
|
+
export type RedactedMetadata = Readonly<Record<string, LogPrimitive>>;
|
|
28
|
+
|
|
29
|
+
/** A single log event, post-redaction. Readonly and frozen at runtime: one
|
|
30
|
+
* entry fans out to every destination, so no destination may mutate what its
|
|
31
|
+
* siblings receive. No file/function/line: JS has no `#fileID`, and Hermes
|
|
32
|
+
* stack parsing is unreliable — subsystems are the filtering mechanism
|
|
33
|
+
* instead. */
|
|
34
|
+
export interface LogEntry {
|
|
35
|
+
/** Epoch milliseconds, not a `Date`: `Object.freeze` does not protect a
|
|
36
|
+
* Date's internal slots, so a destination could `setTime()` the entry its
|
|
37
|
+
* siblings are about to receive. A number is immutable by construction and
|
|
38
|
+
* crosses the native bridge unchanged. */
|
|
39
|
+
readonly timestamp: number;
|
|
40
|
+
readonly level: LogLevel;
|
|
41
|
+
readonly message: string;
|
|
42
|
+
readonly metadata?: RedactedMetadata;
|
|
43
|
+
readonly correlation?: string;
|
|
44
|
+
readonly subsystem?: string;
|
|
45
|
+
}
|
package/src/utf8.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTF-8 byte length, without allocating an encoder.
|
|
3
|
+
*
|
|
4
|
+
* `TextEncoder` is not guaranteed on every Hermes build this package targets,
|
|
5
|
+
* and counting is cheaper than encoding when the answer is all that is
|
|
6
|
+
* wanted. Every byte budget in this package — the sink's payload cap, the
|
|
7
|
+
* batcher's pending cap, a formatter's truncation limit — is a UTF-8 budget,
|
|
8
|
+
* because that is what actually reaches the file.
|
|
9
|
+
*
|
|
10
|
+
* An unpaired surrogate counts as 3, matching every standard encoder: it has
|
|
11
|
+
* no UTF-8 form of its own, so encoders substitute U+FFFD, which is three
|
|
12
|
+
* bytes.
|
|
13
|
+
*/
|
|
14
|
+
export function utf8Length(text: string): number {
|
|
15
|
+
let bytes = 0;
|
|
16
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
17
|
+
const code = text.charCodeAt(i);
|
|
18
|
+
if (code < 0x80) {
|
|
19
|
+
bytes += 1;
|
|
20
|
+
} else if (code < 0x800) {
|
|
21
|
+
bytes += 2;
|
|
22
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
23
|
+
const next = text.charCodeAt(i + 1);
|
|
24
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
25
|
+
bytes += 4;
|
|
26
|
+
i += 1;
|
|
27
|
+
} else {
|
|
28
|
+
// Lone high surrogate: encodes as U+FFFD.
|
|
29
|
+
bytes += 3;
|
|
30
|
+
}
|
|
31
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
32
|
+
// Lone low surrogate: likewise.
|
|
33
|
+
bytes += 3;
|
|
34
|
+
} else {
|
|
35
|
+
bytes += 3;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|