telemetry-collector-core 0.0.0-stage → 0.0.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/dist/base32.d.ts +15 -0
- package/dist/base32.d.ts.map +1 -0
- package/dist/base32.js +66 -0
- package/dist/base32.js.map +1 -0
- package/dist/fingerprint.d.ts +41 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +151 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +80 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +212 -0
- package/dist/keys.js.map +1 -0
- package/dist/normalize.d.ts +41 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +264 -0
- package/dist/normalize.js.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +89 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +122 -0
- package/dist/usage.js.map +1 -0
- package/package.json +28 -4
- package/README.md +0 -3
package/dist/base32.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crockford base32, lowercase, unpadded.
|
|
3
|
+
*
|
|
4
|
+
* Chosen over hex (shorter) and over standard RFC 4648 base32 (which includes the
|
|
5
|
+
* `i`/`l`/`o`/`u` characters — ambiguous when read aloud or retyped, and `u` is the
|
|
6
|
+
* one that lets a random token spell something unfortunate). Decoding is
|
|
7
|
+
* case-insensitive and folds the classic confusables, so a key transcribed by hand
|
|
8
|
+
* with `1` for `l` or `0` for `O` still resolves.
|
|
9
|
+
*/
|
|
10
|
+
/** Number of base32 characters needed to hold `byteLength` bytes. */
|
|
11
|
+
export declare function base32Length(byteLength: number): number;
|
|
12
|
+
export declare function base32Encode(bytes: Uint8Array): string;
|
|
13
|
+
/** Returns `null` on any character outside the alphabet, rather than throwing. */
|
|
14
|
+
export declare function base32Decode(text: string): Uint8Array | null;
|
|
15
|
+
//# sourceMappingURL=base32.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base32.d.ts","sourceRoot":"","sources":["../src/base32.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqBH,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAiBtD;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAiB5D"}
|
package/dist/base32.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crockford base32, lowercase, unpadded.
|
|
3
|
+
*
|
|
4
|
+
* Chosen over hex (shorter) and over standard RFC 4648 base32 (which includes the
|
|
5
|
+
* `i`/`l`/`o`/`u` characters — ambiguous when read aloud or retyped, and `u` is the
|
|
6
|
+
* one that lets a random token spell something unfortunate). Decoding is
|
|
7
|
+
* case-insensitive and folds the classic confusables, so a key transcribed by hand
|
|
8
|
+
* with `1` for `l` or `0` for `O` still resolves.
|
|
9
|
+
*/
|
|
10
|
+
const ALPHABET = '0123456789abcdefghjkmnpqrstvwxyz';
|
|
11
|
+
const DECODE = {};
|
|
12
|
+
for (let i = 0; i < ALPHABET.length; i++) {
|
|
13
|
+
DECODE[ALPHABET[i]] = i;
|
|
14
|
+
DECODE[ALPHABET[i].toUpperCase()] = i;
|
|
15
|
+
}
|
|
16
|
+
// Confusable folding, per the Crockford spec.
|
|
17
|
+
for (const [from, to] of [
|
|
18
|
+
['i', '1'],
|
|
19
|
+
['I', '1'],
|
|
20
|
+
['l', '1'],
|
|
21
|
+
['L', '1'],
|
|
22
|
+
['o', '0'],
|
|
23
|
+
['O', '0'],
|
|
24
|
+
]) {
|
|
25
|
+
DECODE[from] = DECODE[to];
|
|
26
|
+
}
|
|
27
|
+
/** Number of base32 characters needed to hold `byteLength` bytes. */
|
|
28
|
+
export function base32Length(byteLength) {
|
|
29
|
+
return Math.ceil((byteLength * 8) / 5);
|
|
30
|
+
}
|
|
31
|
+
export function base32Encode(bytes) {
|
|
32
|
+
let out = '';
|
|
33
|
+
let bits = 0;
|
|
34
|
+
let value = 0;
|
|
35
|
+
for (const byte of bytes) {
|
|
36
|
+
value = (value << 8) | byte;
|
|
37
|
+
bits += 8;
|
|
38
|
+
while (bits >= 5) {
|
|
39
|
+
out += ALPHABET[(value >>> (bits - 5)) & 31];
|
|
40
|
+
bits -= 5;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Left-align the remainder into the final character rather than dropping it.
|
|
44
|
+
if (bits > 0)
|
|
45
|
+
out += ALPHABET[(value << (5 - bits)) & 31];
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
/** Returns `null` on any character outside the alphabet, rather than throwing. */
|
|
49
|
+
export function base32Decode(text) {
|
|
50
|
+
const out = [];
|
|
51
|
+
let bits = 0;
|
|
52
|
+
let value = 0;
|
|
53
|
+
for (const char of text) {
|
|
54
|
+
const digit = DECODE[char];
|
|
55
|
+
if (digit === undefined)
|
|
56
|
+
return null;
|
|
57
|
+
value = (value << 5) | digit;
|
|
58
|
+
bits += 5;
|
|
59
|
+
if (bits >= 8) {
|
|
60
|
+
out.push((value >>> (bits - 8)) & 0xff);
|
|
61
|
+
bits -= 8;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return new Uint8Array(out);
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=base32.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base32.js","sourceRoot":"","sources":["../src/base32.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,MAAM,MAAM,GAA2B,EAAE,CAAC;AAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;IACzC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC;AACD,8CAA8C;AAC9C,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI;IACvB,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;IACV,CAAC,GAAG,EAAE,GAAG,CAAC;CACF,EAAE,CAAC;IACX,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,EAAE,CAAE,CAAC;AAC7B,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,YAAY,CAAC,UAAkB;IAC7C,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAiB;IAC5C,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC;QAC5B,IAAI,IAAI,CAAC,CAAC;QACV,OAAO,IAAI,IAAI,CAAC,EAAE,CAAC;YACjB,GAAG,IAAI,QAAQ,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;YAC7C,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IACD,6EAA6E;IAC7E,IAAI,IAAI,GAAG,CAAC;QAAE,GAAG,IAAI,QAAQ,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAE1D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QACrC,KAAK,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC;QAC7B,IAAI,IAAI,CAAC,CAAC;QACV,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;YACd,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;YACxC,IAAI,IAAI,CAAC,CAAC;QACZ,CAAC;IACH,CAAC;IAED,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grouping.
|
|
3
|
+
*
|
|
4
|
+
* Coalescing is what makes this service cheap: 10,000 copies of one crash must cost
|
|
5
|
+
* one row update, not 10,000 inserts. The fingerprint is the grouping key.
|
|
6
|
+
*
|
|
7
|
+
* Two deliberate choices:
|
|
8
|
+
*
|
|
9
|
+
* - **The channel and release are NOT part of the fingerprint.** An issue is meant to
|
|
10
|
+
* span versions, so the admin view can say "this started in 1.4.2 and is still
|
|
11
|
+
* happening in 1.4.4". `first_channel` / `last_channel` carry that instead.
|
|
12
|
+
* - **Line and column numbers are NOT part of the fingerprint.** They shift on every
|
|
13
|
+
* unrelated edit to the file, which would fragment one long-lived issue into a new
|
|
14
|
+
* one per build.
|
|
15
|
+
*
|
|
16
|
+
* Normalization is deliberately moderate. Over-normalizing silently merges unrelated
|
|
17
|
+
* bugs (lossy, and very hard to notice); under-normalizing shatters one bug into
|
|
18
|
+
* thousands (noisy, but obvious). When the default is wrong, a reporter can send an
|
|
19
|
+
* explicit `fingerprint` array, which wins outright.
|
|
20
|
+
*/
|
|
21
|
+
import type { StoredEvent } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Collapses the parts of a message that vary per occurrence — ids, addresses,
|
|
24
|
+
* counters — while leaving the parts that identify the bug.
|
|
25
|
+
*
|
|
26
|
+
* Quoted substrings are intentionally left alone: in `Cannot read property 'foo' of
|
|
27
|
+
* undefined`, the `'foo'` usually *is* the distinguishing detail.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeMessage(message: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The material the fingerprint hashes. Returned separately from the hash so tests
|
|
32
|
+
* (and the admin UI's "why is this grouped here?" view) can show the reasoning.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fingerprintComponents(event: StoredEvent, appId: string): string[];
|
|
35
|
+
export declare function computeFingerprint(event: StoredEvent, appId: string, seed: string): Promise<string>;
|
|
36
|
+
/** Honors an explicit `fingerprint` override from the reporter. */
|
|
37
|
+
export declare function resolveFingerprint(event: StoredEvent, appId: string, seed: string, override?: string[]): Promise<string>;
|
|
38
|
+
export declare function titleFor(event: StoredEvent): string;
|
|
39
|
+
/** The frame a human should look at first: the innermost in-app frame. */
|
|
40
|
+
export declare function culpritFor(event: StoredEvent): string | null;
|
|
41
|
+
//# sourceMappingURL=fingerprint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAA0C,WAAW,EAAE,MAAM,YAAY,CAAC;AAatF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIxD;AA6CD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmBjF;AAUD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGzG;AAED,mEAAmE;AACnE,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,WAAW,EAClB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,OAAO,CAAC,MAAM,CAAC,CAKjB;AAMD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAQnD;AAED,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAQ5D"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grouping.
|
|
3
|
+
*
|
|
4
|
+
* Coalescing is what makes this service cheap: 10,000 copies of one crash must cost
|
|
5
|
+
* one row update, not 10,000 inserts. The fingerprint is the grouping key.
|
|
6
|
+
*
|
|
7
|
+
* Two deliberate choices:
|
|
8
|
+
*
|
|
9
|
+
* - **The channel and release are NOT part of the fingerprint.** An issue is meant to
|
|
10
|
+
* span versions, so the admin view can say "this started in 1.4.2 and is still
|
|
11
|
+
* happening in 1.4.4". `first_channel` / `last_channel` carry that instead.
|
|
12
|
+
* - **Line and column numbers are NOT part of the fingerprint.** They shift on every
|
|
13
|
+
* unrelated edit to the file, which would fragment one long-lived issue into a new
|
|
14
|
+
* one per build.
|
|
15
|
+
*
|
|
16
|
+
* Normalization is deliberately moderate. Over-normalizing silently merges unrelated
|
|
17
|
+
* bugs (lossy, and very hard to notice); under-normalizing shatters one bug into
|
|
18
|
+
* thousands (noisy, but obvious). When the default is wrong, a reporter can send an
|
|
19
|
+
* explicit `fingerprint` array, which wins outright.
|
|
20
|
+
*/
|
|
21
|
+
import { sha256Hex } from './keys.js';
|
|
22
|
+
/** How many innermost frames contribute to the fingerprint. */
|
|
23
|
+
const FINGERPRINT_FRAMES = 5;
|
|
24
|
+
const NORMALIZERS = [
|
|
25
|
+
[/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<uuid>'],
|
|
26
|
+
[/\bhttps?:\/\/[^\s'"`)]+/gi, '<url>'],
|
|
27
|
+
[/\b0x[0-9a-f]+\b/gi, '<hex>'],
|
|
28
|
+
[/\b[0-9a-f]{8,}\b/gi, '<hex>'],
|
|
29
|
+
[/\b\d+(?:\.\d+)*\b/g, '<n>'],
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Collapses the parts of a message that vary per occurrence — ids, addresses,
|
|
33
|
+
* counters — while leaving the parts that identify the bug.
|
|
34
|
+
*
|
|
35
|
+
* Quoted substrings are intentionally left alone: in `Cannot read property 'foo' of
|
|
36
|
+
* undefined`, the `'foo'` usually *is* the distinguishing detail.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeMessage(message) {
|
|
39
|
+
let out = message.trim().slice(0, 1000);
|
|
40
|
+
for (const [pattern, replacement] of NORMALIZERS)
|
|
41
|
+
out = out.replace(pattern, replacement);
|
|
42
|
+
return out.replace(/\s+/g, ' ');
|
|
43
|
+
}
|
|
44
|
+
function basename(path) {
|
|
45
|
+
const cut = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
|
46
|
+
return cut === -1 ? path : path.slice(cut + 1);
|
|
47
|
+
}
|
|
48
|
+
/** Strips query strings and cache-busting hashes so `app.4f3a1b.js` groups with `app.9cd02e.js`. */
|
|
49
|
+
function normalizeFilename(filename) {
|
|
50
|
+
const name = basename(filename.split('?')[0].split('#')[0]);
|
|
51
|
+
return name.replace(/[.-][0-9a-f]{6,}(?=\.[a-z]+$)/i, '');
|
|
52
|
+
}
|
|
53
|
+
function frameIdentity(frame) {
|
|
54
|
+
const where = frame.module ?? (frame.filename ? normalizeFilename(frame.filename) : '?');
|
|
55
|
+
return `${where}:${frame.function ?? '?'}`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Frames follow Sentry's ordering convention: **oldest first, innermost last**. The
|
|
59
|
+
* innermost frames are the ones that identify the bug, so we take from the end.
|
|
60
|
+
*
|
|
61
|
+
* (Browser `Error.stack` is the opposite order — innermost first. SDKs are
|
|
62
|
+
* responsible for reversing before handing an event over. Getting this backwards
|
|
63
|
+
* silently produces fingerprints built from `main()`, which group everything
|
|
64
|
+
* together, so it is worth being explicit.)
|
|
65
|
+
*/
|
|
66
|
+
function significantFrames(frames) {
|
|
67
|
+
const inApp = frames.filter(frame => frame.in_app !== false);
|
|
68
|
+
const pool = inApp.length > 0 ? inApp : frames;
|
|
69
|
+
return pool.slice(-FINGERPRINT_FRAMES);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The exception a human (and the fingerprint) should key on.
|
|
73
|
+
*
|
|
74
|
+
* Sentry orders `exception.values` oldest-first, so the last entry is the error
|
|
75
|
+
* actually thrown and the earlier ones are the causes it wrapped. Keying on the
|
|
76
|
+
* thrown error keeps two unrelated bugs that both wrap, say, an `ECONNRESET` from
|
|
77
|
+
* collapsing into one issue.
|
|
78
|
+
*/
|
|
79
|
+
function primaryException(exception) {
|
|
80
|
+
return exception && exception.length > 0 ? exception[exception.length - 1] : undefined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The material the fingerprint hashes. Returned separately from the hash so tests
|
|
84
|
+
* (and the admin UI's "why is this grouped here?" view) can show the reasoning.
|
|
85
|
+
*/
|
|
86
|
+
export function fingerprintComponents(event, appId) {
|
|
87
|
+
const parts = [`app:${appId}`, `kind:${event.kind}`];
|
|
88
|
+
const exception = primaryException(event.exception?.values);
|
|
89
|
+
const frames = exception?.stacktrace?.frames ?? [];
|
|
90
|
+
if (exception) {
|
|
91
|
+
parts.push(`type:${exception.type ?? 'Error'}`);
|
|
92
|
+
if (frames.length > 0) {
|
|
93
|
+
for (const frame of significantFrames(frames))
|
|
94
|
+
parts.push(`frame:${frameIdentity(frame)}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// No stack to group on, so the message has to carry it.
|
|
98
|
+
parts.push(`message:${normalizeMessage(exception.value ?? '')}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
parts.push(`message:${normalizeMessage(event.message ?? '')}`);
|
|
103
|
+
}
|
|
104
|
+
return parts;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Feedback never coalesces — two people reporting "the button is broken" are two
|
|
108
|
+
* things to read, not one thing seen twice. `seed` makes each submission distinct.
|
|
109
|
+
*/
|
|
110
|
+
function isUngrouped(kind) {
|
|
111
|
+
return kind === 'feedback';
|
|
112
|
+
}
|
|
113
|
+
export async function computeFingerprint(event, appId, seed) {
|
|
114
|
+
if (isUngrouped(event.kind))
|
|
115
|
+
return sha256Hex(`app:${appId}\nkind:feedback\nseed:${seed}`);
|
|
116
|
+
return sha256Hex(fingerprintComponents(event, appId).join('\n'));
|
|
117
|
+
}
|
|
118
|
+
/** Honors an explicit `fingerprint` override from the reporter. */
|
|
119
|
+
export async function resolveFingerprint(event, appId, seed, override) {
|
|
120
|
+
if (override && override.length > 0) {
|
|
121
|
+
return sha256Hex([`app:${appId}`, ...override.map(part => `custom:${part}`)].join('\n'));
|
|
122
|
+
}
|
|
123
|
+
return computeFingerprint(event, appId, seed);
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Human-facing labels
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
export function titleFor(event) {
|
|
129
|
+
const exception = primaryException(event.exception?.values);
|
|
130
|
+
if (exception) {
|
|
131
|
+
const type = exception.type ?? 'Error';
|
|
132
|
+
const value = exception.value?.trim();
|
|
133
|
+
return truncate(value ? `${type}: ${value}` : type, 200);
|
|
134
|
+
}
|
|
135
|
+
return truncate(event.message?.trim() || '(no message)', 200);
|
|
136
|
+
}
|
|
137
|
+
/** The frame a human should look at first: the innermost in-app frame. */
|
|
138
|
+
export function culpritFor(event) {
|
|
139
|
+
const frames = primaryException(event.exception?.values)?.stacktrace?.frames ?? [];
|
|
140
|
+
const chosen = significantFrames(frames).at(-1);
|
|
141
|
+
if (!chosen)
|
|
142
|
+
return null;
|
|
143
|
+
const where = chosen.module ?? (chosen.filename ? normalizeFilename(chosen.filename) : null);
|
|
144
|
+
if (!where)
|
|
145
|
+
return chosen.function ?? null;
|
|
146
|
+
return chosen.function ? `${chosen.function} (${where})` : where;
|
|
147
|
+
}
|
|
148
|
+
function truncate(value, max) {
|
|
149
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=fingerprint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../src/fingerprint.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials.
|
|
3
|
+
*
|
|
4
|
+
* Three distinct kinds of key material, deliberately not derived from one another:
|
|
5
|
+
*
|
|
6
|
+
* 1. **App secret** (`ecs_…`) — 32 random bytes, held by the developer and by CI.
|
|
7
|
+
* Never shipped to a client. Everything else about an app hangs off this.
|
|
8
|
+
* 2. **Ingest key** (`ek1.app.channel.mac`) — HMAC-derived from the app secret.
|
|
9
|
+
* **Public by design**: it is baked into shipped client bundles, so it identifies
|
|
10
|
+
* and routes, it does not authenticate. Its value is that it is *derivable
|
|
11
|
+
* offline* — a build computes its own key with no API call, and the server
|
|
12
|
+
* verifies it with no prior registration, so a new version provisions itself on
|
|
13
|
+
* its first report.
|
|
14
|
+
* 3. **Read token** (`ert_…`) — 32 random bytes, independent of the app secret,
|
|
15
|
+
* stored server-side only as a SHA-256 hash.
|
|
16
|
+
*
|
|
17
|
+
* Why (3) is not derived from (1): the two have opposite lifecycle requirements. An
|
|
18
|
+
* ingest key is compiled into binaries already in users' hands and is effectively
|
|
19
|
+
* unrotatable; a read token lives in agent and CI environments, which is where
|
|
20
|
+
* credentials actually leak from, and must be cheap to revoke. Deriving both from one
|
|
21
|
+
* secret would mean revoking a leaked agent token forces re-keying every deployed
|
|
22
|
+
* app instance.
|
|
23
|
+
*/
|
|
24
|
+
export declare const INGEST_KEY_PREFIX = "ek1";
|
|
25
|
+
export declare const APP_SECRET_PREFIX = "ecs_";
|
|
26
|
+
export declare const READ_TOKEN_PREFIX = "ert_";
|
|
27
|
+
/** Default tolerance for attestation timestamp skew. */
|
|
28
|
+
export declare const SIGNATURE_MAX_SKEW_SECONDS = 300;
|
|
29
|
+
export declare function isValidAppId(value: string): boolean;
|
|
30
|
+
export declare function isValidChannel(value: string): boolean;
|
|
31
|
+
export declare function generateAppSecret(): string;
|
|
32
|
+
export declare function generateReadToken(): string;
|
|
33
|
+
/** Decodes an `ecs_…` secret to raw bytes. Returns `null` if malformed. */
|
|
34
|
+
export declare function parseAppSecret(secret: string): Uint8Array | null;
|
|
35
|
+
export declare function sha256Hex(value: string): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Constant-time string comparison. Compares every character of the longer input so
|
|
38
|
+
* the loop count does not reveal where the first difference is; the length check is
|
|
39
|
+
* folded into the result rather than short-circuiting.
|
|
40
|
+
*/
|
|
41
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|
|
42
|
+
export interface ParsedIngestKey {
|
|
43
|
+
appId: string;
|
|
44
|
+
channel: string;
|
|
45
|
+
mac: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `ek1.<appId>.<channel>.<mac>` — readable on purpose. Someone who finds this string
|
|
49
|
+
* in an app bundle should be able to tell at a glance that it is a routing
|
|
50
|
+
* identifier and not a leaked credential.
|
|
51
|
+
*
|
|
52
|
+
* Parsed from both ends because the channel is the only field allowed to contain
|
|
53
|
+
* `.`: the MAC is a fixed width at the tail, the app id runs to the first `.`, and
|
|
54
|
+
* whatever is between them is the channel.
|
|
55
|
+
*/
|
|
56
|
+
export declare function parseIngestKey(key: string): ParsedIngestKey | null;
|
|
57
|
+
export declare function deriveIngestKey(appSecret: string | Uint8Array, appId: string, channel: string): Promise<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Verifies a presented ingest key against the app's secret.
|
|
60
|
+
*
|
|
61
|
+
* A `true` result does **not** mean the caller is trusted — the key is public. It
|
|
62
|
+
* means the key is well-formed and really was minted from this app's secret, which
|
|
63
|
+
* is what lets the server accept a never-before-seen channel without any prior
|
|
64
|
+
* registration while still refusing channels invented by a stranger.
|
|
65
|
+
*/
|
|
66
|
+
export declare function verifyIngestKey(appSecret: string | Uint8Array, key: string): Promise<ParsedIngestKey | null>;
|
|
67
|
+
/** Read tokens are stored only as this digest, so a database leak yields nothing usable. */
|
|
68
|
+
export declare function hashReadToken(token: string): Promise<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Reporters that can legitimately hold the app secret — server-side apps, CLIs, CI
|
|
71
|
+
* jobs, dev machines — may sign their reports. Signed reports are stored with
|
|
72
|
+
* `attested = 1`, which is what makes a world-open ingest endpoint comfortable to
|
|
73
|
+
* live with: triage can filter to reports that provably came from us.
|
|
74
|
+
*
|
|
75
|
+
* The timestamp is inside the signed material so a captured request cannot be
|
|
76
|
+
* replayed indefinitely.
|
|
77
|
+
*/
|
|
78
|
+
export declare function signReport(appSecret: string | Uint8Array, timestamp: number, body: string): Promise<string>;
|
|
79
|
+
export declare function verifyReportSignature(appSecret: string | Uint8Array, timestamp: number, body: string, signature: string, nowSeconds?: number, maxSkewSeconds?: number): Promise<boolean>;
|
|
80
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AACvC,eAAO,MAAM,iBAAiB,SAAS,CAAC;AACxC,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAQxC,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAiB9C,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAYD,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAKhE;AAeD,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG9D;AAQD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAO7D;AAeD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAsBlE;AAED,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQrH;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAMlH;AAMD,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5D;AAMD;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMjH;AAED,wBAAsB,qBAAqB,CACzC,SAAS,EAAE,MAAM,GAAG,UAAU,EAC9B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,UAAU,GAAE,MAAsC,EAClD,cAAc,GAAE,MAAmC,GAClD,OAAO,CAAC,OAAO,CAAC,CAMlB"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials.
|
|
3
|
+
*
|
|
4
|
+
* Three distinct kinds of key material, deliberately not derived from one another:
|
|
5
|
+
*
|
|
6
|
+
* 1. **App secret** (`ecs_…`) — 32 random bytes, held by the developer and by CI.
|
|
7
|
+
* Never shipped to a client. Everything else about an app hangs off this.
|
|
8
|
+
* 2. **Ingest key** (`ek1.app.channel.mac`) — HMAC-derived from the app secret.
|
|
9
|
+
* **Public by design**: it is baked into shipped client bundles, so it identifies
|
|
10
|
+
* and routes, it does not authenticate. Its value is that it is *derivable
|
|
11
|
+
* offline* — a build computes its own key with no API call, and the server
|
|
12
|
+
* verifies it with no prior registration, so a new version provisions itself on
|
|
13
|
+
* its first report.
|
|
14
|
+
* 3. **Read token** (`ert_…`) — 32 random bytes, independent of the app secret,
|
|
15
|
+
* stored server-side only as a SHA-256 hash.
|
|
16
|
+
*
|
|
17
|
+
* Why (3) is not derived from (1): the two have opposite lifecycle requirements. An
|
|
18
|
+
* ingest key is compiled into binaries already in users' hands and is effectively
|
|
19
|
+
* unrotatable; a read token lives in agent and CI environments, which is where
|
|
20
|
+
* credentials actually leak from, and must be cheap to revoke. Deriving both from one
|
|
21
|
+
* secret would mean revoking a leaked agent token forces re-keying every deployed
|
|
22
|
+
* app instance.
|
|
23
|
+
*/
|
|
24
|
+
import { base32Decode, base32Encode, base32Length } from './base32.js';
|
|
25
|
+
export const INGEST_KEY_PREFIX = 'ek1';
|
|
26
|
+
export const APP_SECRET_PREFIX = 'ecs_';
|
|
27
|
+
export const READ_TOKEN_PREFIX = 'ert_';
|
|
28
|
+
const SECRET_BYTES = 32;
|
|
29
|
+
const TOKEN_BYTES = 32;
|
|
30
|
+
/** 128 bits is ample for a public, unguessable identifier. */
|
|
31
|
+
const MAC_BYTES = 16;
|
|
32
|
+
const MAC_CHARS = base32Length(MAC_BYTES);
|
|
33
|
+
/** Default tolerance for attestation timestamp skew. */
|
|
34
|
+
export const SIGNATURE_MAX_SKEW_SECONDS = 300;
|
|
35
|
+
const encoder = new TextEncoder();
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Identifier validation
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* App ids may not contain `.` — that is what makes an ingest key unambiguously
|
|
41
|
+
* parseable even though channels (`1.4.2`) routinely do.
|
|
42
|
+
*/
|
|
43
|
+
const APP_ID_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
44
|
+
/** Wide enough for semver, git SHAs, `pr-812`, `staging`, `nightly+2026-09-21`. */
|
|
45
|
+
const CHANNEL_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
46
|
+
export function isValidAppId(value) {
|
|
47
|
+
return APP_ID_RE.test(value);
|
|
48
|
+
}
|
|
49
|
+
export function isValidChannel(value) {
|
|
50
|
+
return CHANNEL_RE.test(value);
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Random material
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
function randomBytes(length) {
|
|
56
|
+
const bytes = new Uint8Array(length);
|
|
57
|
+
crypto.getRandomValues(bytes);
|
|
58
|
+
return bytes;
|
|
59
|
+
}
|
|
60
|
+
export function generateAppSecret() {
|
|
61
|
+
return APP_SECRET_PREFIX + base32Encode(randomBytes(SECRET_BYTES));
|
|
62
|
+
}
|
|
63
|
+
export function generateReadToken() {
|
|
64
|
+
return READ_TOKEN_PREFIX + base32Encode(randomBytes(TOKEN_BYTES));
|
|
65
|
+
}
|
|
66
|
+
/** Decodes an `ecs_…` secret to raw bytes. Returns `null` if malformed. */
|
|
67
|
+
export function parseAppSecret(secret) {
|
|
68
|
+
if (!secret.startsWith(APP_SECRET_PREFIX))
|
|
69
|
+
return null;
|
|
70
|
+
const bytes = base32Decode(secret.slice(APP_SECRET_PREFIX.length));
|
|
71
|
+
if (!bytes || bytes.length < SECRET_BYTES)
|
|
72
|
+
return null;
|
|
73
|
+
return bytes.slice(0, SECRET_BYTES);
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// HMAC
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
async function hmac(key, message) {
|
|
79
|
+
// Copied into a fresh buffer so the type is `Uint8Array<ArrayBuffer>` rather than
|
|
80
|
+
// `<ArrayBufferLike>`; `importKey` will not accept a possibly-shared buffer.
|
|
81
|
+
const material = new Uint8Array(key);
|
|
82
|
+
const cryptoKey = await crypto.subtle.importKey('raw', material, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
83
|
+
const signature = await crypto.subtle.sign('HMAC', cryptoKey, encoder.encode(message));
|
|
84
|
+
return new Uint8Array(signature);
|
|
85
|
+
}
|
|
86
|
+
export async function sha256Hex(value) {
|
|
87
|
+
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(value));
|
|
88
|
+
return toHex(new Uint8Array(digest));
|
|
89
|
+
}
|
|
90
|
+
function toHex(bytes) {
|
|
91
|
+
let out = '';
|
|
92
|
+
for (const byte of bytes)
|
|
93
|
+
out += byte.toString(16).padStart(2, '0');
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Constant-time string comparison. Compares every character of the longer input so
|
|
98
|
+
* the loop count does not reveal where the first difference is; the length check is
|
|
99
|
+
* folded into the result rather than short-circuiting.
|
|
100
|
+
*/
|
|
101
|
+
export function timingSafeEqual(a, b) {
|
|
102
|
+
const length = Math.max(a.length, b.length);
|
|
103
|
+
let diff = a.length ^ b.length;
|
|
104
|
+
for (let i = 0; i < length; i++) {
|
|
105
|
+
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
106
|
+
}
|
|
107
|
+
return diff === 0;
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Ingest keys
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* Domain-separated HMAC context. The `\n` separators are unambiguous because
|
|
114
|
+
* `isValidAppId` and `isValidChannel` both exclude newlines, and the version tag
|
|
115
|
+
* means a future key format cannot collide with this one.
|
|
116
|
+
*/
|
|
117
|
+
function ingestContext(appId, channel) {
|
|
118
|
+
return `telemetry-collector.ingest.v1\n${appId}\n${channel}`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* `ek1.<appId>.<channel>.<mac>` — readable on purpose. Someone who finds this string
|
|
122
|
+
* in an app bundle should be able to tell at a glance that it is a routing
|
|
123
|
+
* identifier and not a leaked credential.
|
|
124
|
+
*
|
|
125
|
+
* Parsed from both ends because the channel is the only field allowed to contain
|
|
126
|
+
* `.`: the MAC is a fixed width at the tail, the app id runs to the first `.`, and
|
|
127
|
+
* whatever is between them is the channel.
|
|
128
|
+
*/
|
|
129
|
+
export function parseIngestKey(key) {
|
|
130
|
+
const prefix = `${INGEST_KEY_PREFIX}.`;
|
|
131
|
+
if (!key.startsWith(prefix))
|
|
132
|
+
return null;
|
|
133
|
+
const rest = key.slice(prefix.length);
|
|
134
|
+
// Need at least `a.b.` before a full-width MAC.
|
|
135
|
+
if (rest.length < MAC_CHARS + 4)
|
|
136
|
+
return null;
|
|
137
|
+
if (rest[rest.length - MAC_CHARS - 1] !== '.')
|
|
138
|
+
return null;
|
|
139
|
+
const mac = rest.slice(-MAC_CHARS);
|
|
140
|
+
const head = rest.slice(0, -(MAC_CHARS + 1));
|
|
141
|
+
const split = head.indexOf('.');
|
|
142
|
+
if (split <= 0 || split === head.length - 1)
|
|
143
|
+
return null;
|
|
144
|
+
const appId = head.slice(0, split);
|
|
145
|
+
const channel = head.slice(split + 1);
|
|
146
|
+
if (!isValidAppId(appId) || !isValidChannel(channel))
|
|
147
|
+
return null;
|
|
148
|
+
if (base32Decode(mac) === null)
|
|
149
|
+
return null;
|
|
150
|
+
return { appId, channel, mac };
|
|
151
|
+
}
|
|
152
|
+
export async function deriveIngestKey(appSecret, appId, channel) {
|
|
153
|
+
const secret = typeof appSecret === 'string' ? parseAppSecret(appSecret) : appSecret;
|
|
154
|
+
if (!secret)
|
|
155
|
+
throw new Error('invalid app secret');
|
|
156
|
+
if (!isValidAppId(appId))
|
|
157
|
+
throw new Error(`invalid app id: ${JSON.stringify(appId)}`);
|
|
158
|
+
if (!isValidChannel(channel))
|
|
159
|
+
throw new Error(`invalid channel: ${JSON.stringify(channel)}`);
|
|
160
|
+
const mac = await hmac(secret, ingestContext(appId, channel));
|
|
161
|
+
return `${INGEST_KEY_PREFIX}.${appId}.${channel}.${base32Encode(mac.slice(0, MAC_BYTES))}`;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Verifies a presented ingest key against the app's secret.
|
|
165
|
+
*
|
|
166
|
+
* A `true` result does **not** mean the caller is trusted — the key is public. It
|
|
167
|
+
* means the key is well-formed and really was minted from this app's secret, which
|
|
168
|
+
* is what lets the server accept a never-before-seen channel without any prior
|
|
169
|
+
* registration while still refusing channels invented by a stranger.
|
|
170
|
+
*/
|
|
171
|
+
export async function verifyIngestKey(appSecret, key) {
|
|
172
|
+
const parsed = parseIngestKey(key);
|
|
173
|
+
if (!parsed)
|
|
174
|
+
return null;
|
|
175
|
+
const expected = await deriveIngestKey(appSecret, parsed.appId, parsed.channel);
|
|
176
|
+
return timingSafeEqual(expected, key) ? parsed : null;
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Read tokens
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
/** Read tokens are stored only as this digest, so a database leak yields nothing usable. */
|
|
182
|
+
export function hashReadToken(token) {
|
|
183
|
+
return sha256Hex(`telemetry-collector.read-token.v1\n${token}`);
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Optional report attestation
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
/**
|
|
189
|
+
* Reporters that can legitimately hold the app secret — server-side apps, CLIs, CI
|
|
190
|
+
* jobs, dev machines — may sign their reports. Signed reports are stored with
|
|
191
|
+
* `attested = 1`, which is what makes a world-open ingest endpoint comfortable to
|
|
192
|
+
* live with: triage can filter to reports that provably came from us.
|
|
193
|
+
*
|
|
194
|
+
* The timestamp is inside the signed material so a captured request cannot be
|
|
195
|
+
* replayed indefinitely.
|
|
196
|
+
*/
|
|
197
|
+
export async function signReport(appSecret, timestamp, body) {
|
|
198
|
+
const secret = typeof appSecret === 'string' ? parseAppSecret(appSecret) : appSecret;
|
|
199
|
+
if (!secret)
|
|
200
|
+
throw new Error('invalid app secret');
|
|
201
|
+
const mac = await hmac(secret, `telemetry-collector.report.v1\n${timestamp}\n${body}`);
|
|
202
|
+
return `v1=${toHex(mac)}`;
|
|
203
|
+
}
|
|
204
|
+
export async function verifyReportSignature(appSecret, timestamp, body, signature, nowSeconds = Math.floor(Date.now() / 1000), maxSkewSeconds = SIGNATURE_MAX_SKEW_SECONDS) {
|
|
205
|
+
if (!Number.isFinite(timestamp))
|
|
206
|
+
return false;
|
|
207
|
+
if (Math.abs(nowSeconds - timestamp) > maxSkewSeconds)
|
|
208
|
+
return false;
|
|
209
|
+
const expected = await signReport(appSecret, timestamp, body);
|
|
210
|
+
return timingSafeEqual(expected, signature);
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
|