@onesi/genesis 0.1.13 → 0.2.1
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/README.md +1 -1
- package/dist/auto.d.ts +1010 -0
- package/dist/auto.js +125 -0
- package/dist/beacons.d.ts +42 -0
- package/dist/beacons.js +71 -0
- package/dist/blinds-owner-enrollment.d.ts +121 -0
- package/dist/blinds-owner-enrollment.js +23 -2
- package/dist/boxes.d.ts +1074 -0
- package/dist/boxes.js +338 -0
- package/dist/client.d.ts +3 -19947
- package/dist/client.js +127 -18
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +38 -3
- package/dist/flares.d.ts +32 -0
- package/dist/flares.js +5 -0
- package/dist/fleets.d.ts +32 -0
- package/dist/fleets.js +59 -6
- package/dist/flocks.d.ts +334 -1
- package/dist/flocks.js +85 -0
- package/dist/foundry.d.ts +429 -0
- package/dist/foundry.js +135 -0
- package/dist/garden.d.ts +754 -0
- package/dist/garden.js +76 -0
- package/dist/gateway.d.ts +365 -0
- package/dist/gateway.js +82 -0
- package/dist/gears.d.ts +121 -2
- package/dist/gears.js +24 -2
- package/dist/generated/operations.d.ts +3 -19948
- package/dist/generated/operations.js +64214 -13423
- package/dist/generated/schema.d.ts +85708 -17364
- package/dist/genesis.d.ts +9041 -1250
- package/dist/genesis.js +92 -25
- package/dist/genesis.openapi.json +133172 -31267
- package/dist/henry.d.ts +202 -1
- package/dist/henry.js +223 -0
- package/dist/hives-cells.d.ts +8 -3
- package/dist/hives-cells.js +17 -4
- package/dist/hives-environment.d.ts +9 -0
- package/dist/hives-environment.js +22 -0
- package/dist/hives-node.js +4 -1
- package/dist/hives-operations.d.ts +121 -3
- package/dist/hives-operations.js +2 -0
- package/dist/hives-platform.d.ts +149 -0
- package/dist/hives-platform.js +183 -0
- package/dist/hives-resources-transport.d.ts +7 -1
- package/dist/hives-resources-transport.js +10 -2
- package/dist/hives-sse.d.ts +24 -0
- package/dist/hives-sse.js +111 -0
- package/dist/hives-topics.d.ts +4 -3
- package/dist/hives-topics.js +9 -4
- package/dist/iam-directory.d.ts +8 -0
- package/dist/iam-directory.js +23 -0
- package/dist/iam.d.ts +397 -0
- package/dist/iam.js +92 -0
- package/dist/idempotency.d.ts +3 -0
- package/dist/idempotency.js +27 -0
- package/dist/index.d.ts +22 -6
- package/dist/index.js +19 -5
- package/dist/instances.d.ts +177 -2
- package/dist/instances.js +37 -5
- package/dist/minds-agent.d.ts +28 -0
- package/dist/minds-agent.js +53 -10
- package/dist/minds-engine.d.ts +394 -0
- package/dist/minds-engine.js +555 -0
- package/dist/minds.d.ts +109 -1
- package/dist/minds.js +49 -14
- package/dist/orgs.d.ts +405 -0
- package/dist/orgs.js +63 -0
- package/dist/redact.d.ts +37 -0
- package/dist/redact.js +330 -0
- package/dist/runner.d.ts +4 -0
- package/dist/runner.js +1 -0
- package/dist/scopes.d.ts +316 -0
- package/dist/scopes.js +40 -0
- package/dist/taps.d.ts +118 -0
- package/dist/taps.js +45 -0
- package/dist/traces.d.ts +574 -0
- package/dist/traces.js +97 -0
- package/dist/types.d.ts +12 -1
- package/package.json +1 -1
package/dist/redact.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction for service error bodies and messages before the CLI or MCP prints them. The design is
|
|
3
|
+
* conservative: when a key or value might be a secret, it is replaced with `[redacted]`. Field names are
|
|
4
|
+
* matched after normalisation (camelCase split, `-`/`.`/space to `_`, lower-cased), so `x-api-key`,
|
|
5
|
+
* `privateKey`, `Set-Cookie` and `client_secret` are all the same kind of name.
|
|
6
|
+
*/
|
|
7
|
+
export const REDACTED = '[redacted]';
|
|
8
|
+
/** Bounds of the printed body: serialized bytes, per string, per array/object, nesting and the oversize preview. */
|
|
9
|
+
export const ERROR_BODY_LIMITS = { bytes: 16384, string: 2048, items: 100, depth: 8, preview: 4096 };
|
|
10
|
+
/** Words that make any field secret wherever they appear in its name. */
|
|
11
|
+
const SECRET_WORDS = new Set(['token', 'tokens', 'secret', 'secrets', 'password', 'passwords', 'passwd', 'pwd', 'passphrase', 'passcode',
|
|
12
|
+
'credential', 'credentials', 'authorization', 'auth', 'cookie', 'cookies', 'ciphertext', 'sealed', 'plaintext', 'session', 'sessions',
|
|
13
|
+
'dpop', 'signature', 'signatures', 'sig', 'otp', 'totp', 'private', 'apikey', 'jwt', 'jwe', 'bearer', 'privkey', 'mnemonic', 'seed',
|
|
14
|
+
// Card security codes and PINs, also inside compound names (`card_cvc`, `cvv2`, `pin_code`).
|
|
15
|
+
'cvc', 'cvv', 'cvc2', 'cvv2', 'csc', 'pin', 'pincode', 'securitycode']);
|
|
16
|
+
/** Whole names that are secret on their own (CLI `scrub` list, sealed envelopes). */
|
|
17
|
+
const SECRET_NAMES = new Set(['envelope', 'enc', 'ct', 'key_material', 'pin', 'cvv', 'cvc', 'code_verifier', 'pass', 'pw', 'hmac', 'sid',
|
|
18
|
+
'jsessionid', 'phpsessid', 'pan', 'card_number', 'cardnumber', 'cc_number', 'ccnumber', 'credit_card_number', 'primary_account_number',
|
|
19
|
+
'account_number', 'iban', 'ssn', 'social_security_number', 'routing_number', 'routingnumber', 'aba_routing_number', 'bank_account',
|
|
20
|
+
'bank_account_number', 'sort_code']);
|
|
21
|
+
/** Names written as one lower- or upper-case word (`accesstoken`, `CLIENTSECRET`): the secret compounds, matched at the end of the name. */
|
|
22
|
+
const GLUED_SECRET = /(?:token|secret|password|passwd|apikey|privatekey|secretkey|accesskey|signingkey|encryptionkey|masterkey|clientkey|codeverifier|(?:recovery|backup)codes?|sessionid|sessid|securitycode|cardcode|pincode|cvc2?|cvv2?)$/;
|
|
23
|
+
/** `<word>_key(s)` is secret unless the word names a non-secret key. */
|
|
24
|
+
const PUBLIC_KEY_WORDS = new Set(['idempotency', 'public', 'object', 'cache', 'sort', 'partition', 'primary', 'foreign', 'route', 'lookup',
|
|
25
|
+
'dedupe', 'dedup', 'group', 'message', 'storage', 'bucket', 'file', 'path', 'map', 'kv', 'row', 'column', 'index', 'unique', 'composite', 'hash']);
|
|
26
|
+
/** `<word>_code` that is a one-time secret rather than an error code. */
|
|
27
|
+
const SECRET_CODE_WORDS = new Set(['security', 'card', 'login', 'auth', 'authorization', 'verification', 'device', 'user', 'otp', 'recovery', 'backup', 'pairing',
|
|
28
|
+
'join', 'invite', 'enrollment', 'activation', 'oauth', 'mfa', 'sms', 'email', 'pasted', 'one_time', 'confirmation', 'reset']);
|
|
29
|
+
/** Last words that make a name metadata about a secret, not the secret (`api_key_id`, `token_type`). Never for sessions or cookies. */
|
|
30
|
+
const METADATA_WORDS = new Set(['id', 'ids', 'at', 'kind', 'type', 'types', 'count', 'status', 'state', 'scope', 'scopes', 'name', 'names',
|
|
31
|
+
'label', 'hint', 'version', 'provider', 'source', 'format', 'mode', 'required', 'expires', 'expiry', 'ttl', 'algorithm', 'alg', 'length', 'policy', 'url',
|
|
32
|
+
'check', 'checks']);
|
|
33
|
+
const words = (key) => {
|
|
34
|
+
const plain = key.toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
35
|
+
const split = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2').toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
36
|
+
return [...new Set([plain, split])].map(k => k.split('_').filter(Boolean));
|
|
37
|
+
};
|
|
38
|
+
function secretWords(w, value) {
|
|
39
|
+
const joined = w.join('_');
|
|
40
|
+
if (SECRET_NAMES.has(joined))
|
|
41
|
+
return true;
|
|
42
|
+
const last = w[w.length - 1] ?? '';
|
|
43
|
+
// Usage counters (`input_tokens: 12`) are numbers, not tokens.
|
|
44
|
+
if (last === 'tokens' && typeof value === 'number')
|
|
45
|
+
return false;
|
|
46
|
+
const secret = w.some((word, i) => SECRET_WORDS.has(word)
|
|
47
|
+
|| (word === 'key' || word === 'keys') && i > 0 && !PUBLIC_KEY_WORDS.has(w[i - 1])
|
|
48
|
+
|| (word === 'code' || word === 'codes') && i > 0 && (SECRET_CODE_WORDS.has(w[i - 1]) || SECRET_CODE_WORDS.has(`${w[i - 2]}_${w[i - 1]}`)));
|
|
49
|
+
if (!secret)
|
|
50
|
+
return false;
|
|
51
|
+
const persistent = w.some(word => word === 'session' || word === 'sessions' || word === 'cookie' || word === 'cookies');
|
|
52
|
+
return !(w.length > 1 && METADATA_WORDS.has(last) && !(persistent && (last === 'id' || last === 'ids')));
|
|
53
|
+
}
|
|
54
|
+
/** True when a field of this name may hold a secret. */
|
|
55
|
+
export function secretField(key, value) {
|
|
56
|
+
if (words(key).some(w => w.length > 0 && secretWords(w, value)))
|
|
57
|
+
return true;
|
|
58
|
+
// A glued name has no separators or case change to split on; numeric `inputtokens` counters stay readable.
|
|
59
|
+
const glued = key.trim();
|
|
60
|
+
return /^(?:[A-Za-z][a-z0-9]*|[A-Z0-9]+)$/.test(glued) && GLUED_SECRET.test(glued.toLowerCase()) && !(/tokens$/i.test(glued) && typeof value === 'number');
|
|
61
|
+
}
|
|
62
|
+
/** True when a run of 12 to 19 digits passes the Luhn check and could be a card number (a `1` prefix only for 15-digit UATP cards, so ms timestamps stay readable). */
|
|
63
|
+
export function cardNumber(digits) {
|
|
64
|
+
if (!/^\d{12,19}$/.test(digits) || digits[0] === '1' && digits.length !== 15)
|
|
65
|
+
return false;
|
|
66
|
+
let sum = 0;
|
|
67
|
+
for (let i = 0; i < digits.length; i++) {
|
|
68
|
+
let d = digits.charCodeAt(digits.length - 1 - i) - 48;
|
|
69
|
+
if (i % 2 === 1) {
|
|
70
|
+
d *= 2;
|
|
71
|
+
if (d > 9)
|
|
72
|
+
d -= 9;
|
|
73
|
+
}
|
|
74
|
+
sum += d;
|
|
75
|
+
}
|
|
76
|
+
return sum % 10 === 0;
|
|
77
|
+
}
|
|
78
|
+
/** Digit groups inside one run longer than this are identifiers (hashes, reservation ids), not a card with glued digits. */
|
|
79
|
+
const CARD_WINDOW_RUN = 40;
|
|
80
|
+
/**
|
|
81
|
+
* Redacts the card numbers in a run of digit groups (separated by whitespace, a dash or a dot). A card is any span
|
|
82
|
+
* of whole groups with 12 to 19 digits that passes the Luhn check, so a card beside other digits (`<PAN> 12/28`,
|
|
83
|
+
* `<PAN> 123`, `line 12 <PAN>`) is still found. A span may also end inside its last group when 2 to 4 digits are
|
|
84
|
+
* glued on (an expiry or CVC: `<PAN>1228`, `5555 5555 5555 44441228`), and every window of a 20 to 40 digit group
|
|
85
|
+
* is tried. Inside a longer group a `1` prefix is never a card, so snowflake ids and nanosecond timestamps print.
|
|
86
|
+
*/
|
|
87
|
+
function redactCardRun(run) {
|
|
88
|
+
const groups = run.match(/\d+/g) ?? [], seps = run.match(/\D+/g) ?? [];
|
|
89
|
+
// A dotted quad is an IPv4 address.
|
|
90
|
+
if (groups.length === 4 && seps.every(s => s === '.') && groups.every(g => g.length <= 3))
|
|
91
|
+
return run;
|
|
92
|
+
const hit = groups.map(() => false);
|
|
93
|
+
const glued = (d) => d[0] !== '1' && cardNumber(d);
|
|
94
|
+
for (let i = 0; i < groups.length; i++) {
|
|
95
|
+
let digits = '';
|
|
96
|
+
for (let j = i; j < groups.length && digits.length < 19; j++) {
|
|
97
|
+
const g = groups[j];
|
|
98
|
+
for (let rest = 2; rest <= 4 && rest < g.length; rest++) {
|
|
99
|
+
const d = digits + g.slice(0, g.length - rest);
|
|
100
|
+
if (d.length >= 12 && d.length <= 19 && glued(d))
|
|
101
|
+
hit.fill(true, i, j + 1);
|
|
102
|
+
}
|
|
103
|
+
digits += g;
|
|
104
|
+
if (digits.length >= 12 && digits.length <= 19 && cardNumber(digits))
|
|
105
|
+
hit.fill(true, i, j + 1);
|
|
106
|
+
}
|
|
107
|
+
const g = groups[i];
|
|
108
|
+
if (hit[i] || g.length < 20 || g.length > CARD_WINDOW_RUN)
|
|
109
|
+
continue;
|
|
110
|
+
for (let a = 0; a + 12 <= g.length && !hit[i]; a++)
|
|
111
|
+
for (let len = 12; len <= 19 && a + len <= g.length; len++)
|
|
112
|
+
if (glued(g.slice(a, a + len))) {
|
|
113
|
+
hit[i] = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!hit.includes(true))
|
|
118
|
+
return run;
|
|
119
|
+
// Consecutive redacted groups become one `[redacted]`.
|
|
120
|
+
return groups.map((g, i) => hit[i] && i > 0 && hit[i - 1] ? '' : (i ? seps[i - 1] : '') + (hit[i] ? REDACTED : g)).join('');
|
|
121
|
+
}
|
|
122
|
+
/** True when a string passes the IBAN mod-97 check. */
|
|
123
|
+
function iban(value) {
|
|
124
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(value))
|
|
125
|
+
return false;
|
|
126
|
+
const moved = value.slice(4) + value.slice(0, 4);
|
|
127
|
+
let rest = 0;
|
|
128
|
+
for (const ch of moved) {
|
|
129
|
+
const n = ch >= 'A' && ch <= 'Z' ? String(ch.charCodeAt(0) - 55) : ch;
|
|
130
|
+
for (const d of n)
|
|
131
|
+
rest = (rest * 10 + d.charCodeAt(0) - 48) % 97;
|
|
132
|
+
}
|
|
133
|
+
return rest === 1;
|
|
134
|
+
}
|
|
135
|
+
/** Card-holding fields: `card` with a scalar value, and `number` under a card-like parent (`card.number`, `payment_card.number`). */
|
|
136
|
+
const cardField = (key, value, parent) => {
|
|
137
|
+
const k = key.toLowerCase();
|
|
138
|
+
const scalar = typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint';
|
|
139
|
+
const under = (...names) => parent !== undefined && words(parent).some(w => names.some(n => w.includes(n)));
|
|
140
|
+
return scalar && (k === 'card' || k === 'cards' || k === 'number' && under('card', 'cards', 'bank')
|
|
141
|
+
|| (k === 'account' || k === 'routing') && under('bank', 'bank_account', 'ach'));
|
|
142
|
+
};
|
|
143
|
+
/** Email local parts are personal data: `jr@example.com` prints as `[redacted]@example.com`. */
|
|
144
|
+
const EMAIL = /(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,63})(?![A-Za-z0-9-])/g;
|
|
145
|
+
/** OAuth 2.0 error tokens (RFC 6749, 8628, OIDC): a `code` beside one of these is an authorization code. */
|
|
146
|
+
const OAUTH_ERRORS = new Set(['invalid_request', 'invalid_client', 'invalid_grant', 'unauthorized_client', 'unsupported_grant_type', 'invalid_scope',
|
|
147
|
+
'access_denied', 'unsupported_response_type', 'server_error', 'temporarily_unavailable', 'invalid_token', 'authorization_pending', 'slow_down',
|
|
148
|
+
'expired_token', 'interaction_required', 'login_required', 'consent_required', 'account_selection_required', 'invalid_request_uri', 'invalid_dpop_proof']);
|
|
149
|
+
/** A value that looks random (letters and digits, no separators, 16 or more characters): an authorization code, never an error code. */
|
|
150
|
+
export const randomCode = (v) => typeof v === 'string' && /^[A-Za-z0-9]{16,}$/.test(v) && /\d/.test(v) && /[A-Za-z]/.test(v);
|
|
151
|
+
/** Query and form parameter names that carry secrets beyond the secret-named fields. */
|
|
152
|
+
const SECRET_PARAMS = new Set(['code', 'key', 'sig', 'apikey', 'api_key', 'x_amz_credential', 'x_amz_security_token', 'x_amz_signature',
|
|
153
|
+
'x_goog_signature', 'x_goog_credential', 'access_token', 'refresh_token', 'id_token', 'client_secret', 'code_verifier', 'state_token']);
|
|
154
|
+
const secretParam = (name) => {
|
|
155
|
+
let decoded = name;
|
|
156
|
+
try {
|
|
157
|
+
decoded = decodeURIComponent(name);
|
|
158
|
+
}
|
|
159
|
+
catch { /* keep raw */ }
|
|
160
|
+
return SECRET_PARAMS.has(decoded.toLowerCase().replace(/[^a-z0-9]+/g, '_')) || secretField(decoded, '');
|
|
161
|
+
};
|
|
162
|
+
const SECRET_TEXT = [
|
|
163
|
+
// JSON pairs inside text that is not JSON as a whole (`upstream said {"accessToken":"..."}`).
|
|
164
|
+
[/"([^"\\\r\n]{1,128})"(\s*:\s*)"((?:[^"\\]|\\.){0,4096})"/g, (m, name, colon, value) => value && value !== REDACTED && secretField(name, value) ? `"${name}"${colon}"${REDACTED}"` : m],
|
|
165
|
+
// The same pairs with HTML-escaped quotes (`{"api_key":"..."}`).
|
|
166
|
+
[/"([^&"\r\n]{1,128})"(\s*:\s*)"((?:(?!")[\s\S]){0,4096}?)"/g, (m, name, colon, value) => value && value !== REDACTED && secretField(name, value) ? `"${name}"${colon}"${REDACTED}"` : m],
|
|
167
|
+
// The same pairs backslash-escaped any number of times (`{\"client_secret\":\"...\"}` from a `%q`-quoted upstream body).
|
|
168
|
+
[/(\\+")([^"\\\r\n]{1,128})\1(\s*:\s*)\1((?:(?!\1)[\s\S]){0,4096}?)\1/g, (m, q, name, colon, value) => value && value !== REDACTED && secretField(name, value) ? `${q}${name}${q}${colon}${q}${REDACTED}${q}` : m],
|
|
169
|
+
// Header lines (`X-Api-Key: ...` in a raw header list or a dumped request), one per line.
|
|
170
|
+
[/(^|[\r\n]|\\r\\n|\\n)([ \t]*)([A-Za-z][A-Za-z0-9_-]{0,63})([ \t]*:[ \t]*)([^\r\n]+?)(?=[ \t]*(?:,[ \t]*\\*["']|[\r\n]|\\r\\n|\\n|$))/g,
|
|
171
|
+
(m, lead, pad, name, colon, value) => value !== REDACTED && secretField(name, value) ? `${lead}${pad}${name}${colon}${REDACTED}` : m],
|
|
172
|
+
// Stripe client secrets (PaymentIntent, SetupIntent, Checkout Session, Source) and ephemeral keys.
|
|
173
|
+
[/(?<![A-Za-z0-9])(?:pi|seti|cs|src)_[A-Za-z0-9_]+?_secret_[A-Za-z0-9]+/g, REDACTED],
|
|
174
|
+
[/(?<![A-Za-z0-9])ek_(?:live|test)_[A-Za-z0-9_-]{8,}/g, REDACTED],
|
|
175
|
+
// PEM private keys (whole block, or to the end when cut).
|
|
176
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, REDACTED],
|
|
177
|
+
// URL userinfo: scheme://user:pass@host.
|
|
178
|
+
[/\b([a-z][a-z0-9+.-]{0,31}:\/\/)[^\s/?#@]+@/gi, `$1${REDACTED}@`],
|
|
179
|
+
// An Authorization / Proxy-Authorization header in text, any scheme.
|
|
180
|
+
[/\b((?:proxy[-_ ]?)?authorization["']?\s*[:=]\s*["']?)(?!\[redacted\])(?:[A-Za-z][A-Za-z0-9_-]*\s+)?[^\s"',;]{8,}/gi, `$1${REDACTED}`],
|
|
181
|
+
// Auth schemes followed by a credential.
|
|
182
|
+
[/\b(bearer|dpop)\s+[A-Za-z0-9._~+/=-]{8,}/gi, `$1 ${REDACTED}`],
|
|
183
|
+
// Other schemes when what follows looks like a credential (digits, base64 symbols or mixed case), not a word.
|
|
184
|
+
[/\b(Basic|Token|Digest|ApiKey|Key|Negotiate|NTLM|basic|token)\s+([A-Za-z0-9._~+/=-]{8,})/g, (m, scheme, value) => /[0-9+/=]/.test(value) || /[a-z]/.test(value) && /[A-Z]/.test(value) ? `${scheme} ${REDACTED}` : m],
|
|
185
|
+
// JWT (3 parts) and JWE (5 parts).
|
|
186
|
+
[/eyJ[A-Za-z0-9_-]{4,}(?:\.[A-Za-z0-9_-]*){2,4}/g, REDACTED],
|
|
187
|
+
// Known key and token forms, also when glued to a word by `_` or `-`.
|
|
188
|
+
[/(?<![A-Za-z0-9])(?:sk|rk|mek|whsec|ghp|gho|ghs|ghu|ghr|github_pat|glpat|xox[abposre]|shpat|shpss|dop_v1|sq0atp|sq0csp)[_-][A-Za-z0-9_-]{8,}/g, REDACTED],
|
|
189
|
+
[/(?<![A-Z0-9])(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA|ANVA|AIPA)[A-Z0-9]{16}(?![A-Z0-9])/g, REDACTED],
|
|
190
|
+
[/(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, REDACTED],
|
|
191
|
+
[/(?<![A-Za-z0-9])ya29\.[A-Za-z0-9_-]{16,}/g, REDACTED],
|
|
192
|
+
// IBANs: two letters, two check digits, then 11 to 30 letters and digits, optionally spaced in fours; any case.
|
|
193
|
+
// A spaced IBAN is read in groups of four; trailing groups are dropped until the check passes, so a following word stays.
|
|
194
|
+
[/(?<![A-Za-z0-9])[A-Za-z]{2}\d{2}(?:[A-Za-z0-9]{11,30}|(?: [A-Za-z0-9]{4}){2,8}(?: [A-Za-z0-9]{1,4})?)(?![A-Za-z0-9])/g, (m) => {
|
|
195
|
+
const parts = m.split(' ');
|
|
196
|
+
for (let k = parts.length; k >= 1; k--) {
|
|
197
|
+
if (iban(parts.slice(0, k).join('').toUpperCase()))
|
|
198
|
+
return [REDACTED, ...parts.slice(k)].join(' ');
|
|
199
|
+
}
|
|
200
|
+
return m;
|
|
201
|
+
}],
|
|
202
|
+
// Card numbers: Luhn-valid spans of 12 to 19 digits in a run of digit groups, also beside other digits (see redactCardRun).
|
|
203
|
+
// Order matters: this runs after the token and scheme rules, so a digit group inside a token never breaks the token's
|
|
204
|
+
// match, and before the parameter and free-text rules, which stop at a space and would leave a spaced card's tail.
|
|
205
|
+
// Groups may be separated by any whitespace (NBSP, thin and ideographic spaces, tabs, line breaks, repeated spaces),
|
|
206
|
+
// a dash or a dot. Groups and runs are bounded so a hostile run cannot make the scan quadratic; a group over 64 digits is an identifier.
|
|
207
|
+
[/(?<![0-9A-Za-z])\d{1,64}(?:(?:\s{0,8}[-.]\s{0,8}|\s{1,8})\d{1,64}){0,63}(?![0-9A-Za-z])/g, (m) => redactCardRun(m)],
|
|
208
|
+
// US social security numbers in their dashed form.
|
|
209
|
+
[/(?<![0-9A-Za-z-])\d{3}-\d{2}-\d{4}(?![0-9A-Za-z-])/g, REDACTED],
|
|
210
|
+
// Query, fragment and form parameters (`?code=`, `&X-Amz-Signature=`, a body starting `client_secret=`).
|
|
211
|
+
[/(^|[?&#;\s])([A-Za-z0-9_.~%[\]-]{1,128})=([^&#\s"']*)/g, (m, lead, name, value) => value && secretParam(name) ? `${lead}${name}=${REDACTED}` : m],
|
|
212
|
+
// `password: x`, `"api_key":"x"` and similar in free text that is not parseable JSON.
|
|
213
|
+
[/((?<![A-Za-z0-9])(?:\\*["'])?(?:password|passwd|pwd|passphrase|secret|client[_-]?secret|api[_-]?key|access[_-]?key|private[_-]?key|(?:access|refresh|id|session|auth)[_-]?token|token|cookie|set-cookie|cvc2?|cvv2?|csc|security[_ -]?code|card[_ -]?code|pin(?:[_-]?code)?)(?:\\*["'])?\s*[:=]\s*(?:\\*["'])?)(?!\[redacted\])[^\s"'\\,;&}]+/gi, `$1${REDACTED}`],
|
|
214
|
+
// Numeric JSON values of secret fields in text (`{"number":"...","cvc":123}`).
|
|
215
|
+
[/"([^"\\\r\n]{1,128})"(\s*:\s*)(-?\d[\d.]*)(?=\s*[,}\]])/g, (m, name, colon, value) => secretField(name, Number(value)) || cardField(name, value, undefined) ? `"${name}"${colon}${REDACTED}` : m],
|
|
216
|
+
// Email addresses keep their domain only.
|
|
217
|
+
[EMAIL, `${REDACTED}@$1`],
|
|
218
|
+
];
|
|
219
|
+
/**
|
|
220
|
+
* Terminal escape sequences, control characters (line breaks and tabs kept) and invisible or bidi characters. They
|
|
221
|
+
* are removed before redaction, so none can split a secret (`4242\x1b[0m4242...`) and hide it from the patterns.
|
|
222
|
+
*/
|
|
223
|
+
const INVISIBLE = /(?:\x1b\]|\x9d)[^\x07\x1b\x9c]*(?:\x07|\x1b\\|\x9c)?|(?:\x1b\[|\x9b)[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]?|\x1b[\x20-\x2f]*[\x30-\x7e]?|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\u00ad\u061c\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff\u2028\u2029]/g;
|
|
224
|
+
/** Tag characters (U+E0000 to U+E007F), invisible and astral, so matched with the `u` flag. */
|
|
225
|
+
const INVISIBLE_TAGS = /[\u{e0000}-\u{e007f}]/gu;
|
|
226
|
+
/** Redacts secret forms in free text: an error message, a raw body or a string field. Control and invisible characters are removed first. */
|
|
227
|
+
export function redactErrorText(text) {
|
|
228
|
+
return SECRET_TEXT.reduce((t, [pattern, to]) => t.replace(pattern, to), text.replace(INVISIBLE, '').replace(INVISIBLE_TAGS, ''));
|
|
229
|
+
}
|
|
230
|
+
/** A validation item's field path (`loc`, `field`, `path`, ...) names a secret field. */
|
|
231
|
+
const LOCATION_KEYS = ['k', 'headername', 'header_name', 'loc', 'location', 'field', 'fields', 'path', 'param', 'parameter', 'pointer', 'property', 'attribute', 'key', 'name', 'header'];
|
|
232
|
+
const locationSecret = (row) => Object.entries(row).some(([rawKey, v]) => {
|
|
233
|
+
// Case-insensitive: `{Name: "Authorization", Value}` is the same pair as `{name, value}`.
|
|
234
|
+
if (!LOCATION_KEYS.includes(rawKey.toLowerCase()))
|
|
235
|
+
return false;
|
|
236
|
+
const parts = (Array.isArray(v) ? v : [v]).filter((p) => typeof p === 'string').flatMap(p => p.split(/[./[\]\s]+/));
|
|
237
|
+
return parts.some(p => p !== '' && secretField(p, ''));
|
|
238
|
+
});
|
|
239
|
+
const validationShaped = (row) => ['loc', 'location', 'field', 'path', 'param', 'parameter', 'pointer', 'property', 'msg'].some(k => k in row);
|
|
240
|
+
/** Echoed values of a validation item or header pair. */
|
|
241
|
+
const ECHO_KEYS = new Set(['v', 'headervalue', 'header_value', 'value', 'values', 'val', 'content', 'input', 'inputs', 'received', 'actual', 'given', 'provided', 'ctx', 'context', 'example']);
|
|
242
|
+
/** Fields of an OAuth token/authorize answer: their `code` is an authorization code, not an error code. */
|
|
243
|
+
const OAUTH_KEYS = ['redirect_uri', 'redirect_url', 'session_state', 'code_verifier', 'code_challenge', 'access_token', 'id_token',
|
|
244
|
+
'refresh_token', 'token_type', 'iss', 'client_id', 'grant_type', 'expires_in', 'response_type', 'nonce'];
|
|
245
|
+
/** True when an object looks like an OAuth token or authorize answer (so `code` may be an authorization code). */
|
|
246
|
+
export const oauthShaped = (row) => OAUTH_KEYS.some(k => k in row);
|
|
247
|
+
/**
|
|
248
|
+
* True when the `code` of this object may be an authorization code: an OAuth-shaped object, a `code` beside an
|
|
249
|
+
* OAuth error token (`{error: "invalid_grant", code}`), or a random-looking value. `errorBody` redacts that `code`.
|
|
250
|
+
*/
|
|
251
|
+
export const authorizationCode = (row) => oauthShaped(row) || randomCode(row.code)
|
|
252
|
+
// A short random code beside an OAuth `state` (`{code: "a1b2c3d4", state}`); refusal codes do not mix digits into letters like this.
|
|
253
|
+
|| 'state' in row && typeof row.code === 'string' && /^[A-Za-z0-9-]{6,}$/.test(row.code) && /\d/.test(row.code) && /[A-Za-z]/.test(row.code)
|
|
254
|
+
|| typeof row.error === 'string' && OAUTH_ERRORS.has(row.error.toLowerCase());
|
|
255
|
+
const cut = (text) => text.length > ERROR_BODY_LIMITS.string
|
|
256
|
+
? `${text.slice(0, ERROR_BODY_LIMITS.string)}…[truncated ${text.length - ERROR_BODY_LIMITS.string} chars]` : text;
|
|
257
|
+
function redactString(value, depth) {
|
|
258
|
+
const trimmed = value.trim();
|
|
259
|
+
// JSON inside a string (a raw text body, a `detail` field): redact it field by field, then serialize again.
|
|
260
|
+
if (/^[{[]/.test(trimmed) && trimmed.length <= 262144) {
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(trimmed);
|
|
263
|
+
if (parsed !== null && typeof parsed === 'object')
|
|
264
|
+
return cut(JSON.stringify(redactValue(parsed, depth + 1)));
|
|
265
|
+
}
|
|
266
|
+
catch { /* not JSON */ }
|
|
267
|
+
}
|
|
268
|
+
return cut(redactErrorText(value));
|
|
269
|
+
}
|
|
270
|
+
/** A header name followed by its value in a string array: `["Authorization", "..."]` tuples and flat `rawHeaders` lists. */
|
|
271
|
+
const HEADER_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
|
|
272
|
+
const pairedSecret = (list, i) => i % 2 === 1 && list.length % 2 === 0 && list.every(v => typeof v === 'string')
|
|
273
|
+
&& HEADER_NAME.test(list[i - 1]) && secretField(list[i - 1], list[i]);
|
|
274
|
+
function redactValue(value, depth, parent) {
|
|
275
|
+
if (typeof value === 'string')
|
|
276
|
+
return redactString(value, depth);
|
|
277
|
+
// A card number sent as a JSON number.
|
|
278
|
+
if ((typeof value === 'number' && Number.isInteger(value) || typeof value === 'bigint') && redactCardRun(String(value)) !== String(value))
|
|
279
|
+
return REDACTED;
|
|
280
|
+
if (value === null || typeof value !== 'object')
|
|
281
|
+
return typeof value === 'bigint' || typeof value === 'function' || typeof value === 'symbol' ? String(value) : value;
|
|
282
|
+
if (depth >= ERROR_BODY_LIMITS.depth)
|
|
283
|
+
return '[truncated]';
|
|
284
|
+
if (Array.isArray(value)) {
|
|
285
|
+
const items = value.slice(0, ERROR_BODY_LIMITS.items).map((v, i) => v !== '' && pairedSecret(value, i) ? REDACTED : redactValue(v, depth + 1, parent));
|
|
286
|
+
return value.length > ERROR_BODY_LIMITS.items ? [...items, `[truncated ${value.length - ERROR_BODY_LIMITS.items} items]`] : items;
|
|
287
|
+
}
|
|
288
|
+
const row = value;
|
|
289
|
+
const entries = Object.entries(row);
|
|
290
|
+
// Header pairs ({name: "Authorization", value}) and validation echoes ({loc: [..., "api_key"], input}) whose field is secret.
|
|
291
|
+
const sensitiveLocation = locationSecret(row);
|
|
292
|
+
const validation = validationShaped(row);
|
|
293
|
+
const oauth = authorizationCode(row);
|
|
294
|
+
const out = {};
|
|
295
|
+
let hidden = 0;
|
|
296
|
+
for (const [rawKey, v] of entries.slice(0, ERROR_BODY_LIMITS.items)) {
|
|
297
|
+
// A key that is itself a secret (`{"sk_live_...": 1}`) is replaced, and so is its value.
|
|
298
|
+
const keyText = redactErrorText(rawKey);
|
|
299
|
+
const key = keyText === rawKey ? rawKey : `${REDACTED}${hidden++ ? `#${hidden}` : ''}`;
|
|
300
|
+
// null, empty and boolean values cannot carry a secret, and stay readable.
|
|
301
|
+
const empty = v === null || v === undefined || v === '' || typeof v === 'boolean';
|
|
302
|
+
const secret = keyText !== rawKey || secretField(rawKey, v)
|
|
303
|
+
|| ECHO_KEYS.has(rawKey.toLowerCase()) && (sensitiveLocation || validation && /^inputs?$|^ctx$|^context$/i.test(rawKey))
|
|
304
|
+
|| oauth && rawKey.toLowerCase() === 'code' || cardField(rawKey, v, parent);
|
|
305
|
+
out[key] = secret && !empty ? REDACTED : redactValue(v, depth + 1, rawKey);
|
|
306
|
+
}
|
|
307
|
+
if (entries.length > ERROR_BODY_LIMITS.items)
|
|
308
|
+
out['…'] = `[truncated ${entries.length - ERROR_BODY_LIMITS.items} fields]`;
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* A redacted, bounded copy of an error body: secret-named fields (after key normalisation, glued names included),
|
|
313
|
+
* header pairs (`{name, value}` in any case, `[name, value]` tuples, flat `rawHeaders`, `Name: value` lines) and
|
|
314
|
+
* validation echoes of secret fields, OAuth authorization codes, card numbers (Luhn-checked) and other payment or
|
|
315
|
+
* personal data, Stripe client secrets, secret-shaped keys and values, and JSON nested in strings, escaped or not. A result still over 16 KiB serialized becomes `{truncated, bytes, preview}` of the redacted text.
|
|
316
|
+
*/
|
|
317
|
+
export function redactErrorBody(body) {
|
|
318
|
+
if (body === undefined || body === '')
|
|
319
|
+
return undefined;
|
|
320
|
+
const safe = redactValue(body, 0);
|
|
321
|
+
let text;
|
|
322
|
+
try {
|
|
323
|
+
text = JSON.stringify(safe) ?? '';
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
const bytes = new TextEncoder().encode(text).length;
|
|
329
|
+
return bytes <= ERROR_BODY_LIMITS.bytes ? safe : { truncated: true, bytes, preview: text.slice(0, ERROR_BODY_LIMITS.preview) };
|
|
330
|
+
}
|
package/dist/runner.d.ts
CHANGED
|
@@ -63,6 +63,10 @@ export declare function runnerModule(client: GenesisClient): Readonly<{
|
|
|
63
63
|
cancel: (jobId: string, opts?: RunnerRequestOptions) => Promise<{
|
|
64
64
|
snapshot: components["schemas"]["Runner.Snapshot"];
|
|
65
65
|
}>;
|
|
66
|
+
delete: (jobId: string, opts?: RunnerRequestOptions) => Promise<{
|
|
67
|
+
jobId: components["schemas"]["Runner.SafeOpaqueRef"];
|
|
68
|
+
deleted: true;
|
|
69
|
+
}>;
|
|
66
70
|
}>;
|
|
67
71
|
}>;
|
|
68
72
|
export {};
|
package/dist/runner.js
CHANGED
|
@@ -111,6 +111,7 @@ export function runnerModule(client) {
|
|
|
111
111
|
},
|
|
112
112
|
result: (jobId, opts = {}) => client.request('runner_getJobResult', { jobId: checkedJobId(jobId) }, requestOptions(opts)),
|
|
113
113
|
cancel: (jobId, opts = {}) => client.request('runner_cancelJob', { jobId: checkedJobId(jobId) }, requestOptions(opts)),
|
|
114
|
+
delete: (jobId, opts = {}) => client.request('runner_deleteJob', { jobId: checkedJobId(jobId) }, requestOptions(opts)),
|
|
114
115
|
}),
|
|
115
116
|
});
|
|
116
117
|
}
|