@a2a-exposed/control-core 0.8.26 → 0.8.28
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 +12 -33
- package/dist/index.js +320 -0
- package/dist/types/control/src/core/index.d.ts +1 -0
- package/dist/types/control/src/core/redact.d.ts +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,50 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/a2a-exposed) [](https://github.com/telegraphic-dev/a2a-exposed/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
a2a-exposed gives an AI agent a public [A2A](https://a2a-protocol.org) address. A Cloudflare Worker serves the agent card and the JSON-RPC endpoint
|
|
5
|
+
a2a-exposed gives an AI agent a public [A2A](https://a2a-protocol.org) address. A Cloudflare Worker serves the agent card and the JSON-RPC endpoint, and inbound messages become tasks. Webhooks wake your agent, or the agent polls. The owner pages are `/` and `/device`.
|
|
6
6
|
|
|
7
7
|
## Who it's for
|
|
8
8
|
|
|
9
9
|
Agents that can call out over HTTP and cannot run a public server: hosted assistants, coding agents, routines, and chat bots. Peers discover the card at `https://<host>/.well-known/agent-card.json`, send `SendMessage` with a per-peer token, and read the reply with `GetTask` (or a push notification, when the peer registered one).
|
|
10
10
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
No Cloudflare account, or no preference? Start with a [hosted agent](docs/getting-started.md#hosted-inbox) at `https://<name>.a2a.exposed`. It is in open beta with a Free plan; see [pricing](https://a2a.exposed/pricing).
|
|
14
|
-
|
|
15
|
-
Claude Code cloud sessions use the [hosted connector](docs/mcp.md#hosted-connector), with no skill install or CLI. Skip project installs in shared cloud checkouts; optional skills use `npx -y skills add telegraphic-dev/a2a-exposed -g`.
|
|
16
|
-
|
|
17
|
-
Self-hosting requires Node 22.18+. The skills are installed into the project the agent works in. They do not install the CLI. Every command runs through npx.
|
|
18
|
-
|
|
19
|
-
1. **Add the skills to the project**
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npx -y skills add telegraphic-dev/a2a-exposed
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
2. **Deploy an A2A endpoint** in your Cloudflare account. [Self-hosting](docs/self-hosting.md) covers the login: `cf auth login`, a named profile (`--cf-profile`), an API token, and `--account-id` when that token can see more than one account.
|
|
26
|
-
|
|
27
|
-
Before running `init` or another Cloudflare change, the agent shows you the account name/id from `npx cf auth whoami` and the plan: Worker name, D1 database, hostname and any workers.dev registration or custom-domain DNS record, and owner/wake secrets by purpose without values. It waits for your explicit yes; a login is not consent. This also applies to `deploy`, `tunnel create`, façade tunnel/Access steps, and `uninstall`. See the [consent checklist](skills/a2a-exposed-setup/references/deploy.md#before-cloudflare-changes). Ask before anything billable or DNS changes on a zone that already serves something.
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
npx -y a2a-exposed@latest init --hostname agent.example.com --agent-name "My Agent"
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Omit `--hostname` to serve on `*.workers.dev`. If you already have a wake URL, put `WAKE_WEBHOOK_URL` and `WAKE_WEBHOOK_KEY` in the environment before `init`. Never pass secrets on the command line.
|
|
11
|
+
## Hosted
|
|
34
12
|
|
|
35
|
-
|
|
13
|
+
No Cloudflare account is required. [a2a.exposed](https://a2a.exposed/hosted) hosts the A2A endpoint and is open to everyone. It is in open beta with a Free plan; see [pricing](https://a2a.exposed/pricing). Sign in at https://a2a.exposed/app and create an agent at `https://<name>.a2a.exposed`.
|
|
36
14
|
|
|
37
|
-
|
|
38
|
-
npx -y a2a-exposed@latest status
|
|
39
|
-
npx -y a2a-exposed@latest pair set-password --web
|
|
40
|
-
```
|
|
15
|
+
When the CLI cannot run, connect https://a2a.exposed/mcp. Sign-in is the owner identity. The owner token stays on the A2A endpoint. Claude Code cloud sessions use the [hosted connector](docs/mcp.md#hosted-connector). The steps are in [Getting started](docs/getting-started.md#hosted-inbox).
|
|
41
16
|
|
|
42
|
-
|
|
17
|
+
## Self-host
|
|
43
18
|
|
|
44
|
-
|
|
19
|
+
Self-hosting needs Node 22.18+. Commands run through npx. Confirm the Cloudflare account and the plan before `init` changes it. The consent checklist is in the [setup skill](skills/a2a-exposed-setup/references/deploy.md#before-cloudflare-changes). [Self-hosting](docs/self-hosting.md) covers login, API tokens, and `--account-id`.
|
|
45
20
|
|
|
46
|
-
|
|
21
|
+
```bash
|
|
22
|
+
npx -y skills add telegraphic-dev/a2a-exposed
|
|
23
|
+
npx -y a2a-exposed@latest init --hostname agent.example.com --agent-name "My Agent"
|
|
24
|
+
npx -y a2a-exposed@latest status
|
|
25
|
+
```
|
|
47
26
|
|
|
48
|
-
|
|
27
|
+
Omit `--hostname` to serve on `*.workers.dev`. If you already have a wake URL, put `WAKE_WEBHOOK_URL` and `WAKE_WEBHOOK_KEY` in the environment before `init`. Never pass secrets on the command line. Pairing and the approval password are in [Getting started](docs/getting-started.md).
|
|
49
28
|
|
|
50
29
|
## Docs
|
|
51
30
|
|
package/dist/index.js
CHANGED
|
@@ -3182,6 +3182,324 @@ var CONTROL_MIGRATIONS = [
|
|
|
3182
3182
|
{ name: "0014_cloudflare_inbox_agent.sql", sql: cloudflare_inbox_agent_default },
|
|
3183
3183
|
{ name: "0015_cloudflare_grant_access.sql", sql: cloudflare_grant_access_default }
|
|
3184
3184
|
];
|
|
3185
|
+
|
|
3186
|
+
// src/core/redact.ts
|
|
3187
|
+
var REDACTED = "[redacted]";
|
|
3188
|
+
var TOKEN_RE = /(?:a2aow_|s2a_|a2amcpr_|a2amcp_|a2aot_)[A-Za-z0-9_-]{20,}/g;
|
|
3189
|
+
var USERINFO_RE = /([a-z][a-z0-9+.-]*:\/\/)[^/?\s]*@/gi;
|
|
3190
|
+
var BEARER_RE = /(bearer\s+)(\S{8,})/gi;
|
|
3191
|
+
var BASIC_RE = /(basic\s+)(\S+)/gi;
|
|
3192
|
+
var PEM_RE = /-----BEGIN (?:[A-Z0-9]+ )?PRIVATE KEY-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )?PRIVATE KEY-----|$)/g;
|
|
3193
|
+
var API_KEY_RE = /(?:(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|\bAKIA[0-9A-Z]{16}\b|\bsk_(?:live|test)_[A-Za-z0-9]{10,}|\brk_(?:live|test)_[A-Za-z0-9]{10,}|\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}|\bxox[baprs]-[A-Za-z0-9-]{10,}|\bAIza[0-9A-Za-z_-]{35}\b)/g;
|
|
3194
|
+
var JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g;
|
|
3195
|
+
var AUTH_RE = /\b((?:proxy-)?authorization)(\s*[:=]\s*)([^\n]+)/gi;
|
|
3196
|
+
var QUERY_RE = /(^|[?&#\s])([^?&#\s="'<>]+)(\s*=\s*)("(?:\\.|[^"\\])*(?:"|$)|'(?:\\.|[^'\\])*(?:'|$)|(?:\\(?!["'])|[^&#\s"'<>\\])*)/g;
|
|
3197
|
+
var ASSIGN_RE = /\b([A-Za-z_][A-Za-z0-9_]*)(\s*=\s*)/g;
|
|
3198
|
+
var SECRET_EXACT = /* @__PURE__ */ new Set([
|
|
3199
|
+
"authorization",
|
|
3200
|
+
"proxyauthorization",
|
|
3201
|
+
"cookie",
|
|
3202
|
+
"setcookie",
|
|
3203
|
+
"password",
|
|
3204
|
+
"passwd",
|
|
3205
|
+
"secret",
|
|
3206
|
+
"token",
|
|
3207
|
+
"apikey",
|
|
3208
|
+
"accesstoken",
|
|
3209
|
+
"refreshtoken",
|
|
3210
|
+
"idtoken",
|
|
3211
|
+
"clientsecret",
|
|
3212
|
+
"privatekey",
|
|
3213
|
+
"hmacsecret",
|
|
3214
|
+
"credential",
|
|
3215
|
+
"credentials",
|
|
3216
|
+
"bearer",
|
|
3217
|
+
"webhookkey",
|
|
3218
|
+
"accessclientid",
|
|
3219
|
+
"accessclientsecret"
|
|
3220
|
+
]);
|
|
3221
|
+
var SECRET_SUFFIX = [
|
|
3222
|
+
"authorization",
|
|
3223
|
+
"token",
|
|
3224
|
+
"secret",
|
|
3225
|
+
"password",
|
|
3226
|
+
"passwd",
|
|
3227
|
+
"credential",
|
|
3228
|
+
"credentials",
|
|
3229
|
+
"apikey",
|
|
3230
|
+
"clientsecret",
|
|
3231
|
+
"privatekey",
|
|
3232
|
+
"webhookkey",
|
|
3233
|
+
"accessclientid",
|
|
3234
|
+
"accessclientsecret",
|
|
3235
|
+
"cookie"
|
|
3236
|
+
];
|
|
3237
|
+
function parameterName(name) {
|
|
3238
|
+
return name.replace(/\+/g, " ").replace(/(?:%[0-9a-f]{2})+/gi, (encoded) => {
|
|
3239
|
+
try {
|
|
3240
|
+
return decodeURIComponent(encoded);
|
|
3241
|
+
} catch {
|
|
3242
|
+
return encoded.replace(/%([0-7][0-9a-f])/gi, (_all, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
function compactName(name) {
|
|
3247
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3248
|
+
}
|
|
3249
|
+
function secretName(name) {
|
|
3250
|
+
const compact = compactName(name);
|
|
3251
|
+
if (!compact || compact === "key" || compact === "name" || compact === "value" || compact === "id") return false;
|
|
3252
|
+
if (SECRET_EXACT.has(compact)) return true;
|
|
3253
|
+
return SECRET_SUFFIX.some((suffix) => compact.length > suffix.length && compact.endsWith(suffix));
|
|
3254
|
+
}
|
|
3255
|
+
function replaceAll(value, pattern, replacement) {
|
|
3256
|
+
pattern.lastIndex = 0;
|
|
3257
|
+
return value.replace(pattern, replacement);
|
|
3258
|
+
}
|
|
3259
|
+
function quotedValueEnd(text, start) {
|
|
3260
|
+
const quote = text[start];
|
|
3261
|
+
if (quote !== '"' && quote !== "'") return -1;
|
|
3262
|
+
for (let i = start + 1; i < text.length; i += 1) {
|
|
3263
|
+
if (text[i] === "\\") {
|
|
3264
|
+
i += 1;
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
if (text[i] === quote) return i + 1;
|
|
3268
|
+
}
|
|
3269
|
+
return text.length;
|
|
3270
|
+
}
|
|
3271
|
+
function unquotedValueEnd(text, start) {
|
|
3272
|
+
let i = start;
|
|
3273
|
+
while (i < text.length && !/\s/.test(text[i])) i += 1;
|
|
3274
|
+
return i;
|
|
3275
|
+
}
|
|
3276
|
+
function redactAssignments(text) {
|
|
3277
|
+
const pattern = new RegExp(ASSIGN_RE.source, "g");
|
|
3278
|
+
const parameters = Array.from(text.matchAll(new RegExp(QUERY_RE.source, "g")));
|
|
3279
|
+
let parameter = 0;
|
|
3280
|
+
let out = "";
|
|
3281
|
+
let last = 0;
|
|
3282
|
+
let match;
|
|
3283
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
3284
|
+
while (parameter < parameters.length && parameters[parameter].index + parameters[parameter][0].length <= match.index) parameter += 1;
|
|
3285
|
+
if (parameter < parameters.length && parameters[parameter].index <= match.index) continue;
|
|
3286
|
+
const name = match[1] ?? "";
|
|
3287
|
+
if (!secretName(name)) continue;
|
|
3288
|
+
const sep = match[2] ?? "=";
|
|
3289
|
+
const valueStart = match.index + match[0].length;
|
|
3290
|
+
const quoted = quotedValueEnd(text, valueStart);
|
|
3291
|
+
const valueEnd = quoted >= 0 ? quoted : unquotedValueEnd(text, valueStart);
|
|
3292
|
+
out += text.slice(last, match.index);
|
|
3293
|
+
out += secretName(name) && valueEnd > valueStart ? `${name}${sep}${REDACTED}` : text.slice(match.index, valueEnd);
|
|
3294
|
+
last = valueEnd;
|
|
3295
|
+
pattern.lastIndex = valueEnd;
|
|
3296
|
+
}
|
|
3297
|
+
return out + text.slice(last);
|
|
3298
|
+
}
|
|
3299
|
+
function jsonValueEnd(text, start) {
|
|
3300
|
+
if (text[start] === '"') return quotedValueEnd(text, start);
|
|
3301
|
+
if (text[start] === "[" || text[start] === "{") {
|
|
3302
|
+
const closers = [text[start] === "[" ? "]" : "}"];
|
|
3303
|
+
for (let i = start + 1; i < text.length; i += 1) {
|
|
3304
|
+
const char = text[i];
|
|
3305
|
+
if (char === '"') i = quotedValueEnd(text, i) - 1;
|
|
3306
|
+
else if (char === "[" || char === "{") closers.push(char === "[" ? "]" : "}");
|
|
3307
|
+
else if (char === closers[closers.length - 1]) {
|
|
3308
|
+
closers.pop();
|
|
3309
|
+
if (!closers.length) return i + 1;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return text.length;
|
|
3313
|
+
}
|
|
3314
|
+
let end = start;
|
|
3315
|
+
while (end < text.length && !/[\s,}\]]/.test(text[end])) end += 1;
|
|
3316
|
+
return end;
|
|
3317
|
+
}
|
|
3318
|
+
function redactJsonFields(text, depth, cache) {
|
|
3319
|
+
let out = "";
|
|
3320
|
+
let last = 0;
|
|
3321
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
3322
|
+
if (text[i] !== '"') continue;
|
|
3323
|
+
const keyEnd = quotedValueEnd(text, i);
|
|
3324
|
+
let cursor = keyEnd;
|
|
3325
|
+
while (/\s/.test(text[cursor] ?? "") && cursor < text.length) cursor += 1;
|
|
3326
|
+
if (text[cursor] === ":") {
|
|
3327
|
+
let name;
|
|
3328
|
+
try {
|
|
3329
|
+
name = JSON.parse(text.slice(i, keyEnd));
|
|
3330
|
+
} catch {
|
|
3331
|
+
i = keyEnd - 1;
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3334
|
+
if (secretName(name)) {
|
|
3335
|
+
cursor += 1;
|
|
3336
|
+
while (cursor < text.length && /\s/.test(text[cursor])) cursor += 1;
|
|
3337
|
+
const end = jsonValueEnd(text, cursor);
|
|
3338
|
+
if (end > cursor) {
|
|
3339
|
+
out += text.slice(last, cursor) + JSON.stringify(REDACTED);
|
|
3340
|
+
last = end;
|
|
3341
|
+
i = end - 1;
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
if (text[cursor] !== ":") {
|
|
3347
|
+
try {
|
|
3348
|
+
const decoded = JSON.parse(text.slice(i, keyEnd));
|
|
3349
|
+
const cleaned = redactNested(decoded, depth + 1, cache);
|
|
3350
|
+
if (cleaned !== decoded) {
|
|
3351
|
+
out += text.slice(last, i) + JSON.stringify(cleaned);
|
|
3352
|
+
last = keyEnd;
|
|
3353
|
+
}
|
|
3354
|
+
} catch {
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
i = keyEnd - 1;
|
|
3358
|
+
}
|
|
3359
|
+
return out + text.slice(last);
|
|
3360
|
+
}
|
|
3361
|
+
function redactNested(value, depth, cache = /* @__PURE__ */ new Map()) {
|
|
3362
|
+
try {
|
|
3363
|
+
if (depth >= 64) return REDACTED;
|
|
3364
|
+
const key = depth + ":" + value;
|
|
3365
|
+
const cached = cache.get(key);
|
|
3366
|
+
if (cached !== void 0) return cached;
|
|
3367
|
+
const text = redactText(value, depth, cache);
|
|
3368
|
+
const decoded = parameterName(text);
|
|
3369
|
+
let result = text;
|
|
3370
|
+
if (decoded !== text) {
|
|
3371
|
+
const cleaned = redactNested(decoded, depth + 1, cache);
|
|
3372
|
+
if (cleaned !== decoded) result = encodeURIComponent(cleaned);
|
|
3373
|
+
}
|
|
3374
|
+
cache.set(key, result);
|
|
3375
|
+
return result;
|
|
3376
|
+
} catch {
|
|
3377
|
+
return REDACTED;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function redactText(value, depth, cache) {
|
|
3381
|
+
let text = redactJsonFields(value, depth, cache);
|
|
3382
|
+
text = replaceAll(text, PEM_RE, REDACTED);
|
|
3383
|
+
text = replaceAll(text, JWT_RE, REDACTED);
|
|
3384
|
+
text = replaceAll(text, API_KEY_RE, REDACTED);
|
|
3385
|
+
text = replaceAll(text, TOKEN_RE, REDACTED);
|
|
3386
|
+
text = replaceAll(text, BEARER_RE, `$1${REDACTED}`);
|
|
3387
|
+
text = replaceAll(text, BASIC_RE, `$1${REDACTED}`);
|
|
3388
|
+
text = replaceAll(text, AUTH_RE, (_all, name, sep, rest) => {
|
|
3389
|
+
if (rest.trim().length < 8 && !/bearer|basic/i.test(rest)) return `${name}${sep}${rest}`;
|
|
3390
|
+
return `${name}${sep}${REDACTED}`;
|
|
3391
|
+
});
|
|
3392
|
+
text = replaceAll(text, USERINFO_RE, `$1${REDACTED}@`);
|
|
3393
|
+
const cleanParameter = (all, sep, name, assignment, item) => {
|
|
3394
|
+
let decoded = name;
|
|
3395
|
+
while (true) {
|
|
3396
|
+
const next = parameterName(decoded);
|
|
3397
|
+
if (next === decoded) break;
|
|
3398
|
+
decoded = next;
|
|
3399
|
+
}
|
|
3400
|
+
let setup = false;
|
|
3401
|
+
if (decoded.trim().toLowerCase() === "t") {
|
|
3402
|
+
let candidate = item.replace(/^["']|["']$/g, "");
|
|
3403
|
+
while (true) {
|
|
3404
|
+
const next = parameterName(candidate);
|
|
3405
|
+
if (next === candidate) break;
|
|
3406
|
+
candidate = next;
|
|
3407
|
+
}
|
|
3408
|
+
setup = /^[A-Za-z0-9_-]{16,}/.test(candidate);
|
|
3409
|
+
}
|
|
3410
|
+
if (!item) return all;
|
|
3411
|
+
if (!secretName(decoded) && !setup) {
|
|
3412
|
+
const quote = item[0];
|
|
3413
|
+
const wrapped = (quote === '"' || quote === "'") && item.endsWith(quote) && item.length > 1;
|
|
3414
|
+
const cleaned = wrapped ? quote + redactNested(item.slice(1, -1), depth + 1, cache) + quote : redactNested(item, depth + 1, cache);
|
|
3415
|
+
return `${sep}${name}${assignment}${cleaned}`;
|
|
3416
|
+
}
|
|
3417
|
+
return `${sep}${name}${assignment}${REDACTED}`;
|
|
3418
|
+
};
|
|
3419
|
+
const strings = [];
|
|
3420
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
3421
|
+
if (text[i] !== '"') continue;
|
|
3422
|
+
const end = quotedValueEnd(text, i);
|
|
3423
|
+
try {
|
|
3424
|
+
JSON.parse(text.slice(i, end));
|
|
3425
|
+
strings.push([i, end]);
|
|
3426
|
+
} catch {
|
|
3427
|
+
}
|
|
3428
|
+
i = end - 1;
|
|
3429
|
+
}
|
|
3430
|
+
const parameters = new RegExp(QUERY_RE.source, "g");
|
|
3431
|
+
let out = "";
|
|
3432
|
+
let last = 0;
|
|
3433
|
+
let match;
|
|
3434
|
+
while ((match = parameters.exec(text)) !== null) {
|
|
3435
|
+
if (strings.some(([start, end2]) => match.index >= start && match.index < end2)) continue;
|
|
3436
|
+
const valueStart = match.index + match[1].length + match[2].length + match[3].length;
|
|
3437
|
+
const quoted = quotedValueEnd(text, valueStart);
|
|
3438
|
+
let end = quoted >= 0 ? quoted : valueStart;
|
|
3439
|
+
while (end < text.length && !/[&#\s<>]/.test(text[end])) {
|
|
3440
|
+
if ((text[end] === '"' || text[end] === "'") && (quoted >= 0 || text[end - 1] === "=")) end = quotedValueEnd(text, end);
|
|
3441
|
+
else end += 1;
|
|
3442
|
+
}
|
|
3443
|
+
match[4] = text.slice(valueStart, end);
|
|
3444
|
+
match[0] = text.slice(match.index, end);
|
|
3445
|
+
parameters.lastIndex = end;
|
|
3446
|
+
const cleaned = cleanParameter(match[0], match[1], match[2], match[3], match[4]);
|
|
3447
|
+
if (cleaned === match[0]) {
|
|
3448
|
+
parameters.lastIndex = match.index + match[1].length + match[2].length + match[3].length;
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
out += text.slice(last, match.index) + cleaned;
|
|
3452
|
+
last = parameters.lastIndex;
|
|
3453
|
+
}
|
|
3454
|
+
text = out + text.slice(last);
|
|
3455
|
+
text = redactAssignments(text);
|
|
3456
|
+
return text;
|
|
3457
|
+
}
|
|
3458
|
+
function own(record, name) {
|
|
3459
|
+
for (const key of Object.keys(record)) {
|
|
3460
|
+
if (key.toLowerCase() === name) return { key, value: record[key] };
|
|
3461
|
+
}
|
|
3462
|
+
return null;
|
|
3463
|
+
}
|
|
3464
|
+
function pairLabel(record) {
|
|
3465
|
+
const name = own(record, "name");
|
|
3466
|
+
const key = own(record, "key");
|
|
3467
|
+
if (name && typeof name.value === "string" && secretName(name.value)) return name.value;
|
|
3468
|
+
if (key && typeof key.value === "string" && secretName(key.value)) return key.value;
|
|
3469
|
+
return "";
|
|
3470
|
+
}
|
|
3471
|
+
function walk(value, seen) {
|
|
3472
|
+
if (typeof value === "string") return redactNested(value, 0);
|
|
3473
|
+
if (!value || typeof value !== "object") return value;
|
|
3474
|
+
if (seen.has(value)) return REDACTED;
|
|
3475
|
+
seen.add(value);
|
|
3476
|
+
if (Array.isArray(value)) {
|
|
3477
|
+
if (value.length === 2 && typeof value[0] === "string" && secretName(value[0])) return [value[0], REDACTED];
|
|
3478
|
+
return value.map((item) => walk(item, seen));
|
|
3479
|
+
}
|
|
3480
|
+
const proto = Object.getPrototypeOf(value);
|
|
3481
|
+
if (proto !== Object.prototype && proto !== null) return value;
|
|
3482
|
+
const record = value;
|
|
3483
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
3484
|
+
const valueField = own(record, "value");
|
|
3485
|
+
if (valueField && pairLabel(record)) hidden.add(valueField.key);
|
|
3486
|
+
const out = {};
|
|
3487
|
+
for (const [key, item] of Object.entries(record)) {
|
|
3488
|
+
if (hidden.has(key) || secretName(key)) {
|
|
3489
|
+
out[key] = REDACTED;
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
3492
|
+
out[key] = walk(item, seen);
|
|
3493
|
+
}
|
|
3494
|
+
return out;
|
|
3495
|
+
}
|
|
3496
|
+
function sanitized(value) {
|
|
3497
|
+
return walk(value, /* @__PURE__ */ new WeakSet());
|
|
3498
|
+
}
|
|
3499
|
+
function redact(value) {
|
|
3500
|
+
if (typeof value === "string") return redactNested(value, 0);
|
|
3501
|
+
return sanitized(value);
|
|
3502
|
+
}
|
|
3185
3503
|
export {
|
|
3186
3504
|
ACCOUNT_SETTINGS_READ,
|
|
3187
3505
|
CONTROL_MIGRATIONS,
|
|
@@ -3278,6 +3596,7 @@ export {
|
|
|
3278
3596
|
readCloudflareInbox,
|
|
3279
3597
|
readCloudflareInboxById,
|
|
3280
3598
|
readInboxById,
|
|
3599
|
+
redact,
|
|
3281
3600
|
refreshCloudflareGrant,
|
|
3282
3601
|
refreshTokenAfterResponse,
|
|
3283
3602
|
registerWorkersDevSubdomain,
|
|
@@ -3292,6 +3611,7 @@ export {
|
|
|
3292
3611
|
runInboxStep,
|
|
3293
3612
|
runRemovalStep,
|
|
3294
3613
|
sanitizeCloudflareText,
|
|
3614
|
+
sanitized,
|
|
3295
3615
|
saveCloudflareGrant,
|
|
3296
3616
|
saveWakeSettings,
|
|
3297
3617
|
sealCloudflareRefresh,
|
|
@@ -32,3 +32,4 @@ export type { ApprovalOidcInput, InboxMigration, InboxWizard, ProvisionFields, W
|
|
|
32
32
|
export { INBOX_MIGRATIONS } from "../provision/inbox-migrations.js";
|
|
33
33
|
export { CONTROL_MIGRATIONS, CONTROL_MIGRATIONS_DIR } from "./migrations.js";
|
|
34
34
|
export type { ControlMigration } from "./migrations.js";
|
|
35
|
+
export { redact, sanitized } from "./redact.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy `value` with secrets removed.
|
|
3
|
+
* A `{ name, value }` or `{ key, value }` pair, and a `[name, value]` pair, lose `value`
|
|
4
|
+
* when that name is a secret. A pair whose name is an ordinary field stays.
|
|
5
|
+
*/
|
|
6
|
+
export declare function sanitized<T>(value: T): T;
|
|
7
|
+
/** Remove secrets from text. An object is copied with {@link sanitized}. */
|
|
8
|
+
export declare function redact(value: string): string;
|
|
9
|
+
export declare function redact(value: unknown): unknown;
|