@opengeni/contracts 0.38.3 → 0.40.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.
@@ -1,40 +0,0 @@
1
- export type SecretForRedaction = {
2
- name: string;
3
- value: string;
4
- };
5
- /**
6
- * Returns true only for fields whose value is itself credential material.
7
- * Container fields such as `headers` and URL fields are intentionally not
8
- * included: their nested/value sanitizers retain useful names, hosts, paths,
9
- * and non-sensitive query parameters.
10
- */
11
- export declare function isSensitiveFieldName(name: string): boolean;
12
- /**
13
- * Return true only for header names whose values are credential material.
14
- * Ordinary protocol metadata (`content-type`, `accept`, `user-agent`, and
15
- * pagination/signature headers outside this allowlist) must remain intact.
16
- */
17
- export declare function isCredentialHeaderName(name: string): boolean;
18
- /**
19
- * Redact only exact known-secret provenance from a structured object key.
20
- * Generic field/header heuristics intentionally do not run here: a key is
21
- * metadata unless the caller has proved that its bytes are secret material.
22
- */
23
- export declare function redactSensitiveKey(key: string, knownSecrets?: readonly SecretForRedaction[]): string;
24
- /**
25
- * Redact known secret provenance and common credential-bearing text shapes.
26
- * This is deliberately a conservative safety boundary, not a promise of
27
- * general-purpose DLP. It never includes a matched value in a marker or error.
28
- */
29
- export declare function redactSensitiveText(text: string, knownSecrets?: readonly SecretForRedaction[]): string;
30
- /** Deeply redact plain structured data while retaining its diagnostic shape. */
31
- export declare function redactSensitiveData<T>(value: T, knownSecrets?: readonly SecretForRedaction[]): T;
32
- /** Build the worker-friendly single-argument redactor used at turn boundaries. */
33
- export declare function createSecretRedactor(knownSecrets: readonly SecretForRedaction[]): (value: unknown) => unknown;
34
- /**
35
- * Redact a serialized JSON checkpoint without requiring it to be valid JSON.
36
- * Valid JSON retains structure; malformed/opaque text still receives text
37
- * classification and exact-known-value replacement.
38
- */
39
- export declare function redactSerializedJson(serialized: string, knownSecrets?: readonly SecretForRedaction[]): string;
40
- export declare function identityRedactor<T>(value: T): T;
@@ -1,364 +0,0 @@
1
- const MIN_REDACTABLE_VALUE_LENGTH = 6;
2
- const REDACTED = "[redacted]";
3
- const MAX_REDACTION_DEPTH = 64;
4
- const CYCLE_MARKER = "[OpenGeni omitted cyclic value during secret redaction]";
5
- const DEPTH_MARKER = "[OpenGeni omitted value beyond secret-redaction depth]";
6
-
7
- export type SecretForRedaction = {
8
- name: string;
9
- value: string;
10
- };
11
-
12
- type PreparedSecret = {
13
- marker: string;
14
- value: string;
15
- };
16
-
17
- const SENSITIVE_FIELD_NAMES = new Set([
18
- "authorization",
19
- "proxyauthorization",
20
- "cookie",
21
- "setcookie",
22
- "accesstoken",
23
- "refreshtoken",
24
- "idtoken",
25
- "apikey",
26
- "secret",
27
- "clientsecret",
28
- "password",
29
- "passwd",
30
- "privatekey",
31
- "credential",
32
- "credentials",
33
- "credentialencrypted",
34
- "encryptedcredential",
35
- "headersencrypted",
36
- "encryptedpkceverifier",
37
- "codeverifier",
38
- "signingkey",
39
- ]);
40
-
41
- const CREDENTIAL_HEADER_PATTERNS = [
42
- /^(?:proxy-)?authorization$/i,
43
- /^(?:set-)?cookie$/i,
44
- /^(?:x[-_])?api[-_]?key$/i,
45
- /^(?:x[-_])?(?:access|refresh|id)[-_]?token$/i,
46
- /^(?:x[-_])?(?:auth|session)[-_]?(?:token|key|secret)$/i,
47
- /^(?:x[-_])?(?:client|app|consumer)[-_]?secret$/i,
48
- /^x-opengeni-access-key$/i,
49
- ] as const;
50
-
51
- const SECRET_KEY_SOURCE =
52
- "(?:proxy[-_ ]?authorization|authorization|set[-_ ]?cookie|cookie|access[-_ ]?token|refresh[-_ ]?token|id[-_ ]?token|api[-_ ]?key|client[-_ ]?secret|secret|password|passwd|private[-_ ]?key|credential(?:s|[-_ ]?encrypted)?|encrypted[-_ ]?credential|encrypted[-_ ]?pkce[-_ ]?verifier|code[-_ ]?verifier|signing[-_ ]?key)";
53
- const UNQUOTED_SECRET_KEY_SOURCE =
54
- "(?:access[-_ ]?token|refresh[-_ ]?token|id[-_ ]?token|api[-_ ]?key|client[-_ ]?secret|secret|password|passwd|private[-_ ]?key|credential(?:s|[-_ ]?encrypted)?|encrypted[-_ ]?credential|encrypted[-_ ]?pkce[-_ ]?verifier|code[-_ ]?verifier|signing[-_ ]?key)";
55
-
56
- const AUTHORIZATION_HEADER_PATTERN =
57
- /(\b(?:proxy-)?authorization[^\S\r\n]*:[^\S\r\n]*)([^\r\n'"`]+)/gi;
58
- const COOKIE_HEADER_PATTERN = /(\b(?:set-cookie|cookie)\s*:\s*)([^\r\n'"`]+)/gi;
59
- const API_KEY_HEADER_PATTERN = /(\b(?:x[-_])?api[-_]?key[^\S\r\n]*:[^\S\r\n]*)([^\r\n'"`]+)/gi;
60
- const CURL_USER_PATTERN = /((?:^|\s)(?:-u|--user)(?:=|\s+))(?:("[^"]*")|('[^']*')|([^\s]+))/gm;
61
- const URL_USERINFO_PATTERN = /(https?:\/\/)[^\s/@]+@/gi;
62
- const SIGNED_QUERY_PATTERN = new RegExp(
63
- `([?&](?:sig|signature|x-amz-signature|x-amz-credential|x-amz-security-token|x-goog-signature|x-goog-credential|access_token|refresh_token|token)=)([^&#\\s'"<>]+)`,
64
- "gi",
65
- );
66
- const QUOTED_SECRET_ASSIGNMENT_PATTERN = new RegExp(
67
- `((?:["']${SECRET_KEY_SOURCE}["']|\\b${SECRET_KEY_SOURCE})\\s*[:=]\\s*)(["'])(.*?)\\2`,
68
- "gi",
69
- );
70
- const UNQUOTED_SECRET_ASSIGNMENT_PATTERN = new RegExp(
71
- `((?:\\b${UNQUOTED_SECRET_KEY_SOURCE})\\s*[:=]\\s*)([^\\s,;}&]+)`,
72
- "gi",
73
- );
74
- const SECRET_ENV_ASSIGNMENT_PATTERN =
75
- /((?:^|[\s;])(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY|CREDENTIAL|AUTHORIZATION|COOKIE)[A-Za-z0-9_]*\s*=\s*)(?:("[^"]*")|('[^']*')|([^\s;]+))/gim;
76
-
77
- const PROVIDER_TOKEN_PATTERNS = [
78
- /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
79
- /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
80
- /\bglpat-[A-Za-z0-9_-]{20,}\b/g,
81
- /\bsk-[A-Za-z0-9_-]{20,}\b/g,
82
- /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
83
- /\bAIza[0-9A-Za-z_-]{30,}\b/g,
84
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
85
- /\bogd_[A-Za-z0-9._~-]{10,}\b/g,
86
- /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}\b/g,
87
- ] as const;
88
-
89
- /**
90
- * Returns true only for fields whose value is itself credential material.
91
- * Container fields such as `headers` and URL fields are intentionally not
92
- * included: their nested/value sanitizers retain useful names, hosts, paths,
93
- * and non-sensitive query parameters.
94
- */
95
- export function isSensitiveFieldName(name: string): boolean {
96
- return SENSITIVE_FIELD_NAMES.has(normalizeFieldName(name));
97
- }
98
-
99
- /**
100
- * Return true only for header names whose values are credential material.
101
- * Ordinary protocol metadata (`content-type`, `accept`, `user-agent`, and
102
- * pagination/signature headers outside this allowlist) must remain intact.
103
- */
104
- export function isCredentialHeaderName(name: string): boolean {
105
- return CREDENTIAL_HEADER_PATTERNS.some((pattern) => pattern.test(name));
106
- }
107
-
108
- /**
109
- * Redact only exact known-secret provenance from a structured object key.
110
- * Generic field/header heuristics intentionally do not run here: a key is
111
- * metadata unless the caller has proved that its bytes are secret material.
112
- */
113
- export function redactSensitiveKey(
114
- key: string,
115
- knownSecrets: readonly SecretForRedaction[] = [],
116
- ): string {
117
- return replacePreparedSecrets(key, prepareSecrets(knownSecrets));
118
- }
119
-
120
- /**
121
- * Redact known secret provenance and common credential-bearing text shapes.
122
- * This is deliberately a conservative safety boundary, not a promise of
123
- * general-purpose DLP. It never includes a matched value in a marker or error.
124
- */
125
- export function redactSensitiveText(
126
- text: string,
127
- knownSecrets: readonly SecretForRedaction[] = [],
128
- ): string {
129
- let redacted = replacePreparedSecrets(text, prepareSecrets(knownSecrets));
130
-
131
- redacted = redacted.replace(
132
- AUTHORIZATION_HEADER_PATTERN,
133
- (match, prefix: string, rawValue: string) => {
134
- const value = rawValue.trimEnd();
135
- const trailingWhitespace = rawValue.slice(value.length);
136
- const schemeMatch = value.match(/^([A-Za-z][A-Za-z0-9_-]*)(\s+)(.+)$/);
137
- if (schemeMatch) {
138
- const scheme = schemeMatch[1];
139
- const whitespace = schemeMatch[2];
140
- const credential = schemeMatch[3];
141
- if (scheme && whitespace && credential) {
142
- return isRedactionMarker(credential.trim())
143
- ? match
144
- : `${prefix}${scheme}${whitespace}${REDACTED}${trailingWhitespace}`;
145
- }
146
- }
147
- return isRedactionMarker(value) ? match : `${prefix}${REDACTED}${trailingWhitespace}`;
148
- },
149
- );
150
- redacted = redacted.replace(COOKIE_HEADER_PATTERN, `$1${REDACTED}`);
151
- redacted = redacted.replace(API_KEY_HEADER_PATTERN, `$1${REDACTED}`);
152
- redacted = redacted.replace(CURL_USER_PATTERN, (_match, prefix: string) => {
153
- return `${prefix}${REDACTED}`;
154
- });
155
- redacted = redacted.replace(URL_USERINFO_PATTERN, `$1${REDACTED}@`);
156
- redacted = redacted.replace(SIGNED_QUERY_PATTERN, `$1${REDACTED}`);
157
- redacted = redacted.replace(
158
- QUOTED_SECRET_ASSIGNMENT_PATTERN,
159
- (match, prefix: string, quote: string, value: string) =>
160
- isRedactionMarker(value) ? match : `${prefix}${quote}${REDACTED}${quote}`,
161
- );
162
- redacted = redacted.replace(
163
- UNQUOTED_SECRET_ASSIGNMENT_PATTERN,
164
- (match, prefix: string, value: string) =>
165
- isRedactionMarker(value) ? match : `${prefix}${REDACTED}`,
166
- );
167
- redacted = redacted.replace(
168
- SECRET_ENV_ASSIGNMENT_PATTERN,
169
- (
170
- match,
171
- prefix: string,
172
- doubleQuoted: string | undefined,
173
- singleQuoted: string | undefined,
174
- bare: string | undefined,
175
- ) => {
176
- const value = doubleQuoted ?? singleQuoted ?? bare ?? "";
177
- return isRedactionMarker(stripMatchingQuotes(value)) ? match : `${prefix}${REDACTED}`;
178
- },
179
- );
180
- for (const pattern of PROVIDER_TOKEN_PATTERNS) {
181
- redacted = redacted.replace(pattern, REDACTED);
182
- }
183
- return redacted;
184
- }
185
-
186
- /** Deeply redact plain structured data while retaining its diagnostic shape. */
187
- export function redactSensitiveData<T>(
188
- value: T,
189
- knownSecrets: readonly SecretForRedaction[] = [],
190
- ): T {
191
- return redactSensitiveDataDeep(value, knownSecrets, new WeakSet<object>(), 0);
192
- }
193
-
194
- /** Build the worker-friendly single-argument redactor used at turn boundaries. */
195
- export function createSecretRedactor(
196
- knownSecrets: readonly SecretForRedaction[],
197
- ): (value: unknown) => unknown {
198
- const prepared = prepareSecrets(knownSecrets).map(({ marker, value }) => ({
199
- name: marker.slice("[redacted:".length, -1),
200
- value,
201
- }));
202
- return (value: unknown) => redactSensitiveData(value, prepared);
203
- }
204
-
205
- /**
206
- * Redact a serialized JSON checkpoint without requiring it to be valid JSON.
207
- * Valid JSON retains structure; malformed/opaque text still receives text
208
- * classification and exact-known-value replacement.
209
- */
210
- export function redactSerializedJson(
211
- serialized: string,
212
- knownSecrets: readonly SecretForRedaction[] = [],
213
- ): string {
214
- try {
215
- return JSON.stringify(redactSensitiveData(JSON.parse(serialized), knownSecrets));
216
- } catch {
217
- return redactSensitiveText(serialized, knownSecrets);
218
- }
219
- }
220
-
221
- export function identityRedactor<T>(value: T): T {
222
- return value;
223
- }
224
-
225
- function redactSensitiveDataDeep<T>(
226
- value: T,
227
- knownSecrets: readonly SecretForRedaction[],
228
- seen: WeakSet<object>,
229
- depth: number,
230
- ): T {
231
- if (typeof value === "string") {
232
- return redactSensitiveText(value, knownSecrets) as T;
233
- }
234
- if (!value || typeof value !== "object" || value instanceof Date) {
235
- return value;
236
- }
237
- if (depth >= MAX_REDACTION_DEPTH) {
238
- return DEPTH_MARKER as T;
239
- }
240
- if (seen.has(value)) {
241
- return CYCLE_MARKER as T;
242
- }
243
- seen.add(value);
244
- try {
245
- if (Array.isArray(value)) {
246
- return value.map((item) => redactSensitiveDataDeep(item, knownSecrets, seen, depth + 1)) as T;
247
- }
248
- if (!isPlainObject(value)) {
249
- return value;
250
- }
251
- const usedKeys = new Set<string>();
252
- return Object.fromEntries(
253
- Object.entries(value).map(([key, child]) => {
254
- const safeKey = nextUniqueKey(redactSensitiveKey(key, knownSecrets), usedKeys);
255
- if (isSensitiveFieldName(key)) {
256
- return [safeKey, REDACTED] as const;
257
- }
258
- if (normalizeFieldName(key) === "headers") {
259
- return [safeKey, redactHeaderMap(child, knownSecrets, seen, depth + 1)] as const;
260
- }
261
- return [safeKey, redactSensitiveDataDeep(child, knownSecrets, seen, depth + 1)] as const;
262
- }),
263
- ) as T;
264
- } finally {
265
- seen.delete(value);
266
- }
267
- }
268
-
269
- function redactHeaderMap(
270
- value: unknown,
271
- knownSecrets: readonly SecretForRedaction[],
272
- seen: WeakSet<object>,
273
- depth: number,
274
- ): unknown {
275
- if (!isPlainObject(value)) {
276
- return redactSensitiveDataDeep(value, knownSecrets, seen, depth);
277
- }
278
- if (depth >= MAX_REDACTION_DEPTH) return DEPTH_MARKER;
279
- if (seen.has(value)) return CYCLE_MARKER;
280
- seen.add(value);
281
- try {
282
- const usedKeys = new Set<string>();
283
- return Object.fromEntries(
284
- Object.entries(value).map(([key, child]) => {
285
- const safeKey = nextUniqueKey(redactSensitiveKey(key, knownSecrets), usedKeys);
286
- return [
287
- safeKey,
288
- isCredentialHeaderName(key)
289
- ? REDACTED
290
- : redactSensitiveDataDeep(child, knownSecrets, seen, depth + 1),
291
- ];
292
- }),
293
- );
294
- } finally {
295
- seen.delete(value);
296
- }
297
- }
298
-
299
- function prepareSecrets(knownSecrets: readonly SecretForRedaction[]): PreparedSecret[] {
300
- const unique = new Map<string, string>();
301
- for (const secret of knownSecrets) {
302
- if (secret.value.length < MIN_REDACTABLE_VALUE_LENGTH || unique.has(secret.value)) {
303
- continue;
304
- }
305
- unique.set(secret.value, `[redacted:${safeSecretName(secret.name)}]`);
306
- }
307
- return [...unique]
308
- .map(([value, marker]) => ({ marker, value }))
309
- .sort((a, b) => b.value.length - a.value.length || a.marker.localeCompare(b.marker));
310
- }
311
-
312
- function replacePreparedSecrets(text: string, prepared: readonly PreparedSecret[]): string {
313
- let redacted = text;
314
- for (const secret of prepared) {
315
- if (redacted.includes(secret.value)) {
316
- redacted = redacted.split(secret.value).join(secret.marker);
317
- }
318
- }
319
- return redacted;
320
- }
321
-
322
- function nextUniqueKey(base: string, usedKeys: Set<string>): string {
323
- let candidate = base;
324
- let suffix = 2;
325
- while (usedKeys.has(candidate)) {
326
- candidate = `${base}#${suffix}`;
327
- suffix += 1;
328
- }
329
- usedKeys.add(candidate);
330
- return candidate;
331
- }
332
-
333
- function safeSecretName(name: string): string {
334
- const safe = name
335
- .toUpperCase()
336
- .replace(/[^A-Z0-9_]+/g, "_")
337
- .replace(/^_+|_+$/g, "");
338
- return safe.slice(0, 64) || "KNOWN_SECRET";
339
- }
340
-
341
- function normalizeFieldName(name: string): string {
342
- return name.toLowerCase().replace(/[-_\s]/g, "");
343
- }
344
-
345
- function isPlainObject(value: unknown): value is Record<string, unknown> {
346
- if (!value || typeof value !== "object") return false;
347
- const prototype = Object.getPrototypeOf(value);
348
- return prototype === Object.prototype || prototype === null;
349
- }
350
-
351
- function isRedactionMarker(value: string): boolean {
352
- return /^\[redacted(?::[A-Z0-9_]{1,64})?\]$/.test(value);
353
- }
354
-
355
- function stripMatchingQuotes(value: string): string {
356
- if (
357
- value.length >= 2 &&
358
- ((value.startsWith('"') && value.endsWith('"')) ||
359
- (value.startsWith("'") && value.endsWith("'")))
360
- ) {
361
- return value.slice(1, -1);
362
- }
363
- return value;
364
- }