@bli-cockpit/telemetry-core 0.1.22 → 0.1.24
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/evidence-upload.d.ts +20 -4
- package/dist/ingest-dto.d.ts +15 -3
- package/dist/privacy.d.ts +5 -1
- package/dist/secret-guards.d.ts +46 -1
- package/dist/secret-guards.js +140 -4
- package/dist/source-adapter.d.ts +10 -2
- package/dist/upload-failure-class.d.ts +35 -0
- package/dist/upload-failure-class.js +67 -4
- package/package.json +1 -1
|
@@ -28,7 +28,10 @@ export declare const RawEvidenceLegacyUploadResponseSchema: z.ZodObject<{
|
|
|
28
28
|
byte_size: z.ZodNumber;
|
|
29
29
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
30
30
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
31
|
-
status: z.
|
|
31
|
+
status: z.ZodEnum<{
|
|
32
|
+
sanitized: "sanitized";
|
|
33
|
+
scanned_clean: "scanned_clean";
|
|
34
|
+
}>;
|
|
32
35
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
33
36
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
34
37
|
local_collector: "local_collector";
|
|
@@ -51,6 +54,7 @@ export declare const RawEvidenceLegacyUploadResponseSchema: z.ZodObject<{
|
|
|
51
54
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
52
55
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
53
56
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
54
58
|
}, z.core.$strict>>;
|
|
55
59
|
}, z.core.$strict>>;
|
|
56
60
|
}, z.core.$strict>;
|
|
@@ -90,7 +94,10 @@ export declare const RawEvidenceUploadBeginObjectSchema: z.ZodObject<{
|
|
|
90
94
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
91
95
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
92
96
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
93
|
-
status: z.
|
|
97
|
+
status: z.ZodEnum<{
|
|
98
|
+
sanitized: "sanitized";
|
|
99
|
+
scanned_clean: "scanned_clean";
|
|
100
|
+
}>;
|
|
94
101
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
95
102
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
96
103
|
local_collector: "local_collector";
|
|
@@ -113,6 +120,7 @@ export declare const RawEvidenceUploadBeginObjectSchema: z.ZodObject<{
|
|
|
113
120
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
114
121
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
115
122
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
123
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
116
124
|
}, z.core.$strict>>;
|
|
117
125
|
}, z.core.$strict>;
|
|
118
126
|
chunk_size_bytes: z.ZodNumber;
|
|
@@ -186,7 +194,10 @@ export declare const RawEvidenceUploadBeginRequestSchema: z.ZodObject<{
|
|
|
186
194
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
187
195
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
188
196
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
189
|
-
status: z.
|
|
197
|
+
status: z.ZodEnum<{
|
|
198
|
+
sanitized: "sanitized";
|
|
199
|
+
scanned_clean: "scanned_clean";
|
|
200
|
+
}>;
|
|
190
201
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
191
202
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
192
203
|
local_collector: "local_collector";
|
|
@@ -209,6 +220,7 @@ export declare const RawEvidenceUploadBeginRequestSchema: z.ZodObject<{
|
|
|
209
220
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
210
221
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
211
222
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
223
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
212
224
|
}, z.core.$strict>>;
|
|
213
225
|
}, z.core.$strict>;
|
|
214
226
|
chunk_size_bytes: z.ZodNumber;
|
|
@@ -351,7 +363,10 @@ export declare const RawEvidenceUploadCommitResponseSchema: z.ZodObject<{
|
|
|
351
363
|
chunk_count: z.ZodNumber;
|
|
352
364
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
353
365
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
354
|
-
status: z.
|
|
366
|
+
status: z.ZodEnum<{
|
|
367
|
+
sanitized: "sanitized";
|
|
368
|
+
scanned_clean: "scanned_clean";
|
|
369
|
+
}>;
|
|
355
370
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
356
371
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
357
372
|
local_collector: "local_collector";
|
|
@@ -374,6 +389,7 @@ export declare const RawEvidenceUploadCommitResponseSchema: z.ZodObject<{
|
|
|
374
389
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
375
390
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
376
391
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
392
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
377
393
|
}, z.core.$strict>>;
|
|
378
394
|
}, z.core.$strict>;
|
|
379
395
|
export type RawEvidenceUploadCommitResponse = z.infer<typeof RawEvidenceUploadCommitResponseSchema>;
|
package/dist/ingest-dto.d.ts
CHANGED
|
@@ -290,7 +290,10 @@ export declare const TelemetryIngestEventDtoSchema: z.ZodObject<{
|
|
|
290
290
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
291
291
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
292
292
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
293
|
-
status: z.
|
|
293
|
+
status: z.ZodEnum<{
|
|
294
|
+
sanitized: "sanitized";
|
|
295
|
+
scanned_clean: "scanned_clean";
|
|
296
|
+
}>;
|
|
294
297
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
295
298
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
296
299
|
local_collector: "local_collector";
|
|
@@ -313,6 +316,7 @@ export declare const TelemetryIngestEventDtoSchema: z.ZodObject<{
|
|
|
313
316
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
314
317
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
315
318
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
319
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
316
320
|
}, z.core.$strict>>;
|
|
317
321
|
}, z.core.$strict>>>;
|
|
318
322
|
otel: z.ZodOptional<z.ZodObject<{
|
|
@@ -763,7 +767,10 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
|
|
|
763
767
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
764
768
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
765
769
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
766
|
-
status: z.
|
|
770
|
+
status: z.ZodEnum<{
|
|
771
|
+
sanitized: "sanitized";
|
|
772
|
+
scanned_clean: "scanned_clean";
|
|
773
|
+
}>;
|
|
767
774
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
768
775
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
769
776
|
local_collector: "local_collector";
|
|
@@ -786,6 +793,7 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
|
|
|
786
793
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
787
794
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
788
795
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
796
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
789
797
|
}, z.core.$strict>>;
|
|
790
798
|
}, z.core.$strict>>>;
|
|
791
799
|
otel: z.ZodOptional<z.ZodObject<{
|
|
@@ -913,7 +921,10 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
|
|
|
913
921
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
914
922
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
915
923
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
916
|
-
status: z.
|
|
924
|
+
status: z.ZodEnum<{
|
|
925
|
+
sanitized: "sanitized";
|
|
926
|
+
scanned_clean: "scanned_clean";
|
|
927
|
+
}>;
|
|
917
928
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
918
929
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
919
930
|
local_collector: "local_collector";
|
|
@@ -936,6 +947,7 @@ export declare const TelemetryIngestEnvelopeSchema: z.ZodObject<{
|
|
|
936
947
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
937
948
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
938
949
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
950
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
939
951
|
}, z.core.$strict>>;
|
|
940
952
|
}, z.core.$strict>>>;
|
|
941
953
|
}, z.core.$strict>>>;
|
package/dist/privacy.d.ts
CHANGED
|
@@ -68,7 +68,10 @@ export declare const RawEvidencePointerSchema: z.ZodObject<{
|
|
|
68
68
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
69
69
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
70
70
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
71
|
-
status: z.
|
|
71
|
+
status: z.ZodEnum<{
|
|
72
|
+
sanitized: "sanitized";
|
|
73
|
+
scanned_clean: "scanned_clean";
|
|
74
|
+
}>;
|
|
72
75
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
73
76
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
74
77
|
local_collector: "local_collector";
|
|
@@ -91,6 +94,7 @@ export declare const RawEvidencePointerSchema: z.ZodObject<{
|
|
|
91
94
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
92
95
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
93
96
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
94
98
|
}, z.core.$strict>>;
|
|
95
99
|
}, z.core.$strict>;
|
|
96
100
|
export type RawEvidencePointer = z.infer<typeof RawEvidencePointerSchema>;
|
package/dist/secret-guards.d.ts
CHANGED
|
@@ -43,9 +43,27 @@ export declare const RawEvidenceRedactionRangeSchema: z.ZodObject<{
|
|
|
43
43
|
rule_id: z.ZodString;
|
|
44
44
|
}, z.core.$strict>;
|
|
45
45
|
export type RawEvidenceRedactionRange = z.infer<typeof RawEvidenceRedactionRangeSchema>;
|
|
46
|
+
/**
|
|
47
|
+
* The verdict of a secret scan — NOT "did the bytes change" (BLI-3277).
|
|
48
|
+
*
|
|
49
|
+
* `sanitized` means the scan matched and the uploaded bytes differ from what was
|
|
50
|
+
* read. `scanned_clean` means the same scan ran over the same bytes and matched
|
|
51
|
+
* nothing, so the uploaded bytes are the original ones. The second value exists
|
|
52
|
+
* because every reader downstream asks "was this scanned?", and for 70% of
|
|
53
|
+
* transcripts — the ones with no secret in them — the honest answer used to be
|
|
54
|
+
* an absent record, which reads identically to "nobody ever looked".
|
|
55
|
+
*/
|
|
56
|
+
export declare const RawEvidenceRedactionStatusSchema: z.ZodEnum<{
|
|
57
|
+
sanitized: "sanitized";
|
|
58
|
+
scanned_clean: "scanned_clean";
|
|
59
|
+
}>;
|
|
60
|
+
export type RawEvidenceRedactionStatus = z.infer<typeof RawEvidenceRedactionStatusSchema>;
|
|
46
61
|
export declare const RawEvidenceRedactionMetadataSchema: z.ZodObject<{
|
|
47
62
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
48
|
-
status: z.
|
|
63
|
+
status: z.ZodEnum<{
|
|
64
|
+
sanitized: "sanitized";
|
|
65
|
+
scanned_clean: "scanned_clean";
|
|
66
|
+
}>;
|
|
49
67
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
50
68
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
51
69
|
local_collector: "local_collector";
|
|
@@ -68,8 +86,35 @@ export declare const RawEvidenceRedactionMetadataSchema: z.ZodObject<{
|
|
|
68
86
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
69
87
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
70
88
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
89
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
71
90
|
}, z.core.$strict>;
|
|
72
91
|
export type RawEvidenceRedactionMetadata = z.infer<typeof RawEvidenceRedactionMetadataSchema>;
|
|
92
|
+
/**
|
|
93
|
+
* The receipt for a file the scan cleared (BLI-3277, moved here by BLI-3280).
|
|
94
|
+
*
|
|
95
|
+
* Same schema, same `mode` — the deterministic ruleset is what ran — with the
|
|
96
|
+
* verdict `scanned_clean` and zero of everything else. The content fields are
|
|
97
|
+
* the point: an evidence ref is only readable downstream when its redaction
|
|
98
|
+
* record hashes the bytes that are actually in the bucket, and for a clean file
|
|
99
|
+
* those are the original bytes, so both halves carry the same digest and size.
|
|
100
|
+
*
|
|
101
|
+
* It lives in telemetry-core rather than in the collector because two callers
|
|
102
|
+
* now build this exact record and they must never drift: the collector writes
|
|
103
|
+
* it at upload time, and the BLI-3280 operator backfill writes it onto refs
|
|
104
|
+
* that were stored before the receipt existed. Both must satisfy the same
|
|
105
|
+
* `sanitized_content_hash_sha256 === content_hash_sha256` /
|
|
106
|
+
* `sanitized_byte_size === byte_size` equality the digest reader checks, and a
|
|
107
|
+
* second copy of this function is a second chance to get that wrong.
|
|
108
|
+
*
|
|
109
|
+
* `appliedBy` is who ran the scan, and it is not decoration: a backfill passes
|
|
110
|
+
* `legacy_upload` so a reader can tell "the collector scanned this before it
|
|
111
|
+
* uploaded" apart from "an operator scanned the stored bytes afterwards".
|
|
112
|
+
*/
|
|
113
|
+
export declare function scannedCleanRedactionMetadata(bytes: Uint8Array, options?: {
|
|
114
|
+
appliedBy?: RawEvidenceRedactionSource;
|
|
115
|
+
/** ISO-8601 with offset. Omitted entirely when absent (BLI-3290). */
|
|
116
|
+
appliedAt?: string;
|
|
117
|
+
}): RawEvidenceRedactionMetadata;
|
|
73
118
|
export interface SecretRedactionResult {
|
|
74
119
|
redacted: boolean;
|
|
75
120
|
text: string;
|
package/dist/secret-guards.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { z } from "zod";
|
|
3
|
+
import { IsoDateTimeSchema } from "./common.js";
|
|
2
4
|
/**
|
|
3
5
|
* Secret-like content + file-name guards shared by the local collector
|
|
4
6
|
* (attribution, collection) and the dashboard commit-time guard. Keeping a
|
|
@@ -153,22 +155,156 @@ export const RawEvidenceRedactionRangeSchema = z
|
|
|
153
155
|
});
|
|
154
156
|
}
|
|
155
157
|
});
|
|
158
|
+
/**
|
|
159
|
+
* The verdict of a secret scan — NOT "did the bytes change" (BLI-3277).
|
|
160
|
+
*
|
|
161
|
+
* `sanitized` means the scan matched and the uploaded bytes differ from what was
|
|
162
|
+
* read. `scanned_clean` means the same scan ran over the same bytes and matched
|
|
163
|
+
* nothing, so the uploaded bytes are the original ones. The second value exists
|
|
164
|
+
* because every reader downstream asks "was this scanned?", and for 70% of
|
|
165
|
+
* transcripts — the ones with no secret in them — the honest answer used to be
|
|
166
|
+
* an absent record, which reads identically to "nobody ever looked".
|
|
167
|
+
*/
|
|
168
|
+
export const RawEvidenceRedactionStatusSchema = z.enum([
|
|
169
|
+
"sanitized",
|
|
170
|
+
"scanned_clean",
|
|
171
|
+
]);
|
|
156
172
|
export const RawEvidenceRedactionMetadataSchema = z
|
|
157
173
|
.object({
|
|
158
174
|
schema_version: z.literal("raw-evidence-redaction.v1"),
|
|
159
|
-
status:
|
|
175
|
+
status: RawEvidenceRedactionStatusSchema,
|
|
160
176
|
mode: z.literal("deterministic_text_replacement"),
|
|
161
177
|
applied_by: z.array(RawEvidenceRedactionSourceSchema).min(1),
|
|
162
|
-
rule_counts: z.array(RawEvidenceRedactionRuleCountSchema)
|
|
163
|
-
secret_like_match_count: z.number().int().
|
|
178
|
+
rule_counts: z.array(RawEvidenceRedactionRuleCountSchema),
|
|
179
|
+
secret_like_match_count: z.number().int().nonnegative(),
|
|
164
180
|
redacted_fields: z.array(z.string().trim().min(1).max(160)).default([]),
|
|
165
181
|
redacted_ranges: z.array(RawEvidenceRedactionRangeSchema).max(200).default([]),
|
|
166
182
|
original_content_hash_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
167
183
|
sanitized_content_hash_sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
168
184
|
original_byte_size: z.number().int().nonnegative().optional(),
|
|
169
185
|
sanitized_byte_size: z.number().int().nonnegative().optional(),
|
|
186
|
+
/**
|
|
187
|
+
* When the scan ran, ISO-8601 with an offset (BLI-3290).
|
|
188
|
+
*
|
|
189
|
+
* Optional, and it stays optional: every receipt written before this field
|
|
190
|
+
* existed is still valid, and the collector does not stamp it — at upload
|
|
191
|
+
* time the ref's own `received_at` already answers "when". It exists for
|
|
192
|
+
* receipts written over bytes that were stored long ago, where the ref's
|
|
193
|
+
* timestamps describe the ORIGINAL upload and nothing in the record would
|
|
194
|
+
* otherwise say when an operator re-scanned or re-masked the object.
|
|
195
|
+
*/
|
|
196
|
+
applied_at: IsoDateTimeSchema.optional(),
|
|
170
197
|
})
|
|
171
|
-
.strict()
|
|
198
|
+
.strict()
|
|
199
|
+
// The per-status invariants the old `z.literal("sanitized")` + `.min(1)` +
|
|
200
|
+
// `.positive()` shape enforced structurally. They still hold for a sanitized
|
|
201
|
+
// record; a clean one has to prove the opposite — no rule fired, no range was
|
|
202
|
+
// replaced, no field was masked, and the bytes came out the way they went in.
|
|
203
|
+
.superRefine((metadata, context) => {
|
|
204
|
+
if (metadata.status === "sanitized") {
|
|
205
|
+
if (metadata.rule_counts.length === 0) {
|
|
206
|
+
context.addIssue({
|
|
207
|
+
code: "custom",
|
|
208
|
+
message: "a sanitized redaction must name at least one rule",
|
|
209
|
+
path: ["rule_counts"],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (metadata.secret_like_match_count < 1) {
|
|
213
|
+
context.addIssue({
|
|
214
|
+
code: "custom",
|
|
215
|
+
message: "a sanitized redaction must count at least one match",
|
|
216
|
+
path: ["secret_like_match_count"],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (metadata.rule_counts.length > 0) {
|
|
222
|
+
context.addIssue({
|
|
223
|
+
code: "custom",
|
|
224
|
+
message: "a clean scan cannot name a rule that fired",
|
|
225
|
+
path: ["rule_counts"],
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (metadata.secret_like_match_count !== 0) {
|
|
229
|
+
context.addIssue({
|
|
230
|
+
code: "custom",
|
|
231
|
+
message: "a clean scan cannot count a match",
|
|
232
|
+
path: ["secret_like_match_count"],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (metadata.redacted_ranges.length > 0) {
|
|
236
|
+
context.addIssue({
|
|
237
|
+
code: "custom",
|
|
238
|
+
message: "a clean scan cannot redact a range",
|
|
239
|
+
path: ["redacted_ranges"],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (metadata.redacted_fields.length > 0) {
|
|
243
|
+
context.addIssue({
|
|
244
|
+
code: "custom",
|
|
245
|
+
message: "a clean scan cannot redact a field",
|
|
246
|
+
path: ["redacted_fields"],
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (metadata.original_content_hash_sha256 !== undefined &&
|
|
250
|
+
metadata.sanitized_content_hash_sha256 !== undefined &&
|
|
251
|
+
metadata.original_content_hash_sha256 !==
|
|
252
|
+
metadata.sanitized_content_hash_sha256) {
|
|
253
|
+
context.addIssue({
|
|
254
|
+
code: "custom",
|
|
255
|
+
message: "a clean scan cannot change the content hash",
|
|
256
|
+
path: ["sanitized_content_hash_sha256"],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
if (metadata.original_byte_size !== undefined &&
|
|
260
|
+
metadata.sanitized_byte_size !== undefined &&
|
|
261
|
+
metadata.original_byte_size !== metadata.sanitized_byte_size) {
|
|
262
|
+
context.addIssue({
|
|
263
|
+
code: "custom",
|
|
264
|
+
message: "a clean scan cannot change the byte size",
|
|
265
|
+
path: ["sanitized_byte_size"],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
/**
|
|
270
|
+
* The receipt for a file the scan cleared (BLI-3277, moved here by BLI-3280).
|
|
271
|
+
*
|
|
272
|
+
* Same schema, same `mode` — the deterministic ruleset is what ran — with the
|
|
273
|
+
* verdict `scanned_clean` and zero of everything else. The content fields are
|
|
274
|
+
* the point: an evidence ref is only readable downstream when its redaction
|
|
275
|
+
* record hashes the bytes that are actually in the bucket, and for a clean file
|
|
276
|
+
* those are the original bytes, so both halves carry the same digest and size.
|
|
277
|
+
*
|
|
278
|
+
* It lives in telemetry-core rather than in the collector because two callers
|
|
279
|
+
* now build this exact record and they must never drift: the collector writes
|
|
280
|
+
* it at upload time, and the BLI-3280 operator backfill writes it onto refs
|
|
281
|
+
* that were stored before the receipt existed. Both must satisfy the same
|
|
282
|
+
* `sanitized_content_hash_sha256 === content_hash_sha256` /
|
|
283
|
+
* `sanitized_byte_size === byte_size` equality the digest reader checks, and a
|
|
284
|
+
* second copy of this function is a second chance to get that wrong.
|
|
285
|
+
*
|
|
286
|
+
* `appliedBy` is who ran the scan, and it is not decoration: a backfill passes
|
|
287
|
+
* `legacy_upload` so a reader can tell "the collector scanned this before it
|
|
288
|
+
* uploaded" apart from "an operator scanned the stored bytes afterwards".
|
|
289
|
+
*/
|
|
290
|
+
export function scannedCleanRedactionMetadata(bytes, options = {}) {
|
|
291
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
292
|
+
return {
|
|
293
|
+
schema_version: "raw-evidence-redaction.v1",
|
|
294
|
+
status: "scanned_clean",
|
|
295
|
+
mode: "deterministic_text_replacement",
|
|
296
|
+
applied_by: [options.appliedBy ?? "local_collector"],
|
|
297
|
+
rule_counts: [],
|
|
298
|
+
secret_like_match_count: 0,
|
|
299
|
+
redacted_fields: [],
|
|
300
|
+
redacted_ranges: [],
|
|
301
|
+
original_content_hash_sha256: digest,
|
|
302
|
+
sanitized_content_hash_sha256: digest,
|
|
303
|
+
original_byte_size: bytes.byteLength,
|
|
304
|
+
sanitized_byte_size: bytes.byteLength,
|
|
305
|
+
...(options.appliedAt === undefined ? {} : { applied_at: options.appliedAt }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
172
308
|
// Same fence as the detector (BLI-3116) — the two must agree or a prefixed
|
|
173
309
|
// name would be detected and then left unredacted, which costs whole sessions
|
|
174
310
|
// (see findSecretRedactionMatches).
|
package/dist/source-adapter.d.ts
CHANGED
|
@@ -76,7 +76,10 @@ export declare const SourceScanEventSummarySchema: z.ZodObject<{
|
|
|
76
76
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
77
77
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
78
78
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
79
|
-
status: z.
|
|
79
|
+
status: z.ZodEnum<{
|
|
80
|
+
sanitized: "sanitized";
|
|
81
|
+
scanned_clean: "scanned_clean";
|
|
82
|
+
}>;
|
|
80
83
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
81
84
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
82
85
|
local_collector: "local_collector";
|
|
@@ -99,6 +102,7 @@ export declare const SourceScanEventSummarySchema: z.ZodObject<{
|
|
|
99
102
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
100
103
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
101
104
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
105
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
102
106
|
}, z.core.$strict>>;
|
|
103
107
|
}, z.core.$strict>>>;
|
|
104
108
|
}, z.core.$strict>;
|
|
@@ -190,7 +194,10 @@ export declare const SourceScanResultSchema: z.ZodObject<{
|
|
|
190
194
|
redacted_summary: z.ZodOptional<z.ZodString>;
|
|
191
195
|
redaction: z.ZodOptional<z.ZodObject<{
|
|
192
196
|
schema_version: z.ZodLiteral<"raw-evidence-redaction.v1">;
|
|
193
|
-
status: z.
|
|
197
|
+
status: z.ZodEnum<{
|
|
198
|
+
sanitized: "sanitized";
|
|
199
|
+
scanned_clean: "scanned_clean";
|
|
200
|
+
}>;
|
|
194
201
|
mode: z.ZodLiteral<"deterministic_text_replacement">;
|
|
195
202
|
applied_by: z.ZodArray<z.ZodEnum<{
|
|
196
203
|
local_collector: "local_collector";
|
|
@@ -213,6 +220,7 @@ export declare const SourceScanResultSchema: z.ZodObject<{
|
|
|
213
220
|
sanitized_content_hash_sha256: z.ZodOptional<z.ZodString>;
|
|
214
221
|
original_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
215
222
|
sanitized_byte_size: z.ZodOptional<z.ZodNumber>;
|
|
223
|
+
applied_at: z.ZodOptional<z.ZodString>;
|
|
216
224
|
}, z.core.$strict>>;
|
|
217
225
|
}, z.core.$strict>>>;
|
|
218
226
|
}, z.core.$strict>>>;
|
|
@@ -66,6 +66,41 @@ export declare const COMMIT_CRASHED_PLATFORM = "commit_crashed_platform";
|
|
|
66
66
|
* eligible sync offers the same bytes again.
|
|
67
67
|
*/
|
|
68
68
|
export declare const DELIVERY_BACKOFF_HOLDING = "delivery_backoff_holding";
|
|
69
|
+
/**
|
|
70
|
+
* Raw evidence was never offered because the session's attribution state is not
|
|
71
|
+
* one the upload policy accepts (BLI-3272).
|
|
72
|
+
*
|
|
73
|
+
* `isRawEvidenceUploadableAttributionState` refuses a session whose attribution
|
|
74
|
+
* produced neither a repository identity nor an approved-root synthetic
|
|
75
|
+
* workspace. That refusal is a real, explainable decision — but until this label
|
|
76
|
+
* existed the report builders answered it by spreading `{}`, so the row reached
|
|
77
|
+
* `ambient_codex_sessions` with `upload_state = NULL` AND `upload_reason = NULL`.
|
|
78
|
+
* 1,256 sessions sat in that hole on 2026-08-29, 197 of them first seen inside a
|
|
79
|
+
* week from current 0.2.30 machines, every one `unattributed` /
|
|
80
|
+
* `cwd_outside_scanned_worktrees`. A withheld session that says nothing is the
|
|
81
|
+
* exact failure the session-first contract forbids, and the migration comment
|
|
82
|
+
* claiming "every write path now sets it" was false for all of them.
|
|
83
|
+
*
|
|
84
|
+
* Written with the deciding attribution reason appended after a colon
|
|
85
|
+
* (`not_uploadable_attribution_state:cwd_outside_scanned_worktrees`) so the
|
|
86
|
+
* upload half of the row explains itself without a join;
|
|
87
|
+
* `baseUploadFailureReason` strips that qualifier for classification.
|
|
88
|
+
*
|
|
89
|
+
* `deterministic` for the same reason as the bare attribution labels below: the
|
|
90
|
+
* scan completed and produced no target, so repeating it changes nothing.
|
|
91
|
+
* Widening the operator's approved roots can change it — a human decision, not a
|
|
92
|
+
* retry.
|
|
93
|
+
*/
|
|
94
|
+
export declare const NOT_UPLOADABLE_ATTRIBUTION_STATE = "not_uploadable_attribution_state";
|
|
95
|
+
/**
|
|
96
|
+
* Compose the withheld-by-attribution reason for one session.
|
|
97
|
+
*
|
|
98
|
+
* The detail is the attribution reason that decided it. Anything outside the
|
|
99
|
+
* safe-label charset — colons included, so the stem always ends at the first one
|
|
100
|
+
* — collapses to `_`, and an empty or unusable detail degrades to the bare stem
|
|
101
|
+
* rather than inventing one.
|
|
102
|
+
*/
|
|
103
|
+
export declare function notUploadableAttributionStateReason(attributionReason: string | null | undefined): string;
|
|
69
104
|
/**
|
|
70
105
|
* The listed reason a composed label is a variant of, or the label unchanged.
|
|
71
106
|
*
|
|
@@ -72,6 +72,53 @@ export const COMMIT_CRASHED_PLATFORM = "commit_crashed_platform";
|
|
|
72
72
|
* eligible sync offers the same bytes again.
|
|
73
73
|
*/
|
|
74
74
|
export const DELIVERY_BACKOFF_HOLDING = "delivery_backoff_holding";
|
|
75
|
+
/**
|
|
76
|
+
* Raw evidence was never offered because the session's attribution state is not
|
|
77
|
+
* one the upload policy accepts (BLI-3272).
|
|
78
|
+
*
|
|
79
|
+
* `isRawEvidenceUploadableAttributionState` refuses a session whose attribution
|
|
80
|
+
* produced neither a repository identity nor an approved-root synthetic
|
|
81
|
+
* workspace. That refusal is a real, explainable decision — but until this label
|
|
82
|
+
* existed the report builders answered it by spreading `{}`, so the row reached
|
|
83
|
+
* `ambient_codex_sessions` with `upload_state = NULL` AND `upload_reason = NULL`.
|
|
84
|
+
* 1,256 sessions sat in that hole on 2026-08-29, 197 of them first seen inside a
|
|
85
|
+
* week from current 0.2.30 machines, every one `unattributed` /
|
|
86
|
+
* `cwd_outside_scanned_worktrees`. A withheld session that says nothing is the
|
|
87
|
+
* exact failure the session-first contract forbids, and the migration comment
|
|
88
|
+
* claiming "every write path now sets it" was false for all of them.
|
|
89
|
+
*
|
|
90
|
+
* Written with the deciding attribution reason appended after a colon
|
|
91
|
+
* (`not_uploadable_attribution_state:cwd_outside_scanned_worktrees`) so the
|
|
92
|
+
* upload half of the row explains itself without a join;
|
|
93
|
+
* `baseUploadFailureReason` strips that qualifier for classification.
|
|
94
|
+
*
|
|
95
|
+
* `deterministic` for the same reason as the bare attribution labels below: the
|
|
96
|
+
* scan completed and produced no target, so repeating it changes nothing.
|
|
97
|
+
* Widening the operator's approved roots can change it — a human decision, not a
|
|
98
|
+
* retry.
|
|
99
|
+
*/
|
|
100
|
+
export const NOT_UPLOADABLE_ATTRIBUTION_STATE = "not_uploadable_attribution_state";
|
|
101
|
+
/** Longest label the ambient envelope's `SafeLabelSchema` accepts. */
|
|
102
|
+
const MAX_UPLOAD_REASON_LENGTH = 120;
|
|
103
|
+
/**
|
|
104
|
+
* Compose the withheld-by-attribution reason for one session.
|
|
105
|
+
*
|
|
106
|
+
* The detail is the attribution reason that decided it. Anything outside the
|
|
107
|
+
* safe-label charset — colons included, so the stem always ends at the first one
|
|
108
|
+
* — collapses to `_`, and an empty or unusable detail degrades to the bare stem
|
|
109
|
+
* rather than inventing one.
|
|
110
|
+
*/
|
|
111
|
+
export function notUploadableAttributionStateReason(attributionReason) {
|
|
112
|
+
const detail = (attributionReason ?? "")
|
|
113
|
+
.trim()
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.replace(/[^a-z0-9_.-]+/gu, "_")
|
|
116
|
+
.replace(/^_+|_+$/gu, "");
|
|
117
|
+
if (!detail)
|
|
118
|
+
return NOT_UPLOADABLE_ATTRIBUTION_STATE;
|
|
119
|
+
const budget = MAX_UPLOAD_REASON_LENGTH - NOT_UPLOADABLE_ATTRIBUTION_STATE.length - 1;
|
|
120
|
+
return `${NOT_UPLOADABLE_ATTRIBUTION_STATE}:${detail.slice(0, budget)}`;
|
|
121
|
+
}
|
|
75
122
|
const UPLOAD_FAILURE_CLASSES = {
|
|
76
123
|
// Budgets and locks: the file was fine and the run simply ran out of room or
|
|
77
124
|
// was beaten to it. The next pass has room.
|
|
@@ -117,6 +164,10 @@ const UPLOAD_FAILURE_CLASSES = {
|
|
|
117
164
|
cwd_not_a_repo: "deterministic",
|
|
118
165
|
no_repo_signals: "deterministic",
|
|
119
166
|
multiple_worktrees_close_scores: "deterministic",
|
|
167
|
+
// The upload policy declined the session's attribution state outright
|
|
168
|
+
// (BLI-3272). Same shape of answer as the four above — the scan finished and
|
|
169
|
+
// there is nowhere to put the object — so the same class.
|
|
170
|
+
[NOT_UPLOADABLE_ATTRIBUTION_STATE]: "deterministic",
|
|
120
171
|
// Working as designed. Never retry these — a retry here is an attempt to get
|
|
121
172
|
// a redaction guard to change its mind.
|
|
122
173
|
secret_like_content_guard: "withheld_by_policy",
|
|
@@ -137,10 +188,22 @@ const HTTP_STATUS_SUFFIX = /_http_\d{3}$/u;
|
|
|
137
188
|
* still classifies as `unknown`, exactly as before.
|
|
138
189
|
*/
|
|
139
190
|
export function baseUploadFailureReason(reason) {
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
return reason;
|
|
143
|
-
|
|
191
|
+
const httpStem = reason.replace(HTTP_STATUS_SUFFIX, "");
|
|
192
|
+
if (httpStem !== reason) {
|
|
193
|
+
return Object.hasOwn(UPLOAD_FAILURE_CLASSES, httpStem) ? httpStem : reason;
|
|
194
|
+
}
|
|
195
|
+
// Same contract for the `:detail` qualifier the withheld-by-attribution label
|
|
196
|
+
// carries (BLI-3272): the colon is only a separator when what precedes it is
|
|
197
|
+
// already a classified reason, so nothing an unlisted label happens to
|
|
198
|
+
// contain can smuggle it into a class.
|
|
199
|
+
const separator = reason.indexOf(":");
|
|
200
|
+
if (separator > 0) {
|
|
201
|
+
const qualifierStem = reason.slice(0, separator);
|
|
202
|
+
if (Object.hasOwn(UPLOAD_FAILURE_CLASSES, qualifierStem)) {
|
|
203
|
+
return qualifierStem;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return reason;
|
|
144
207
|
}
|
|
145
208
|
/** Classify a persisted upload reason. Unlisted reasons stay `unknown`. */
|
|
146
209
|
export function classifyUploadFailure(reason) {
|