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.
@@ -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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"}
@@ -0,0 +1,7 @@
1
+ export * from './base32.js';
2
+ export * from './keys.js';
3
+ export * from './types.js';
4
+ export * from './fingerprint.js';
5
+ export * from './normalize.js';
6
+ export * from './usage.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -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,7 @@
1
+ export * from './base32.js';
2
+ export * from './keys.js';
3
+ export * from './types.js';
4
+ export * from './fingerprint.js';
5
+ export * from './normalize.js';
6
+ export * from './usage.js';
7
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}