@app-vitals/agent-lib 0.2.1 → 0.4.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.
- package/admin-types.ts +296 -3
- package/package.json +2 -1
- package/redact.ts +70 -0
- package/redact.unit.test.ts +87 -0
- package/sentry.ts +5 -3
package/admin-types.ts
CHANGED
|
@@ -145,7 +145,9 @@ export interface paths {
|
|
|
145
145
|
[name: string]: unknown;
|
|
146
146
|
};
|
|
147
147
|
content: {
|
|
148
|
-
"application/json":
|
|
148
|
+
"application/json":
|
|
149
|
+
| components["schemas"]["ProvisionAgentResult"]
|
|
150
|
+
| components["schemas"]["ProvisionSkippedResult"];
|
|
149
151
|
};
|
|
150
152
|
};
|
|
151
153
|
/** @description Forbidden */
|
|
@@ -1432,6 +1434,134 @@ export interface paths {
|
|
|
1432
1434
|
patch?: never;
|
|
1433
1435
|
trace?: never;
|
|
1434
1436
|
};
|
|
1437
|
+
"/agents/{id}/crons/{cronId}/runs/{runId}/transcript": {
|
|
1438
|
+
parameters: {
|
|
1439
|
+
query?: never;
|
|
1440
|
+
header?: never;
|
|
1441
|
+
path?: never;
|
|
1442
|
+
cookie?: never;
|
|
1443
|
+
};
|
|
1444
|
+
/**
|
|
1445
|
+
* Get a cron run transcript
|
|
1446
|
+
* @description Returns the stored redacted transcript. `*`, `platform.read` and `platform.ops` callers only: an agent key is 403 even for its own runs. Every read writes an AgentTranscriptRead audit row before the response. 404 when SHIPWRIGHT_TRANSCRIPT_API_ENABLED is not `enabled` or no transcript exists.
|
|
1447
|
+
*/
|
|
1448
|
+
get: {
|
|
1449
|
+
parameters: {
|
|
1450
|
+
query?: never;
|
|
1451
|
+
header?: never;
|
|
1452
|
+
path: {
|
|
1453
|
+
id: string;
|
|
1454
|
+
cronId: string;
|
|
1455
|
+
runId: string;
|
|
1456
|
+
};
|
|
1457
|
+
cookie?: never;
|
|
1458
|
+
};
|
|
1459
|
+
requestBody?: never;
|
|
1460
|
+
responses: {
|
|
1461
|
+
/** @description The transcript */
|
|
1462
|
+
200: {
|
|
1463
|
+
headers: {
|
|
1464
|
+
[name: string]: unknown;
|
|
1465
|
+
};
|
|
1466
|
+
content: {
|
|
1467
|
+
"application/json": components["schemas"]["CronRunTranscript"];
|
|
1468
|
+
};
|
|
1469
|
+
};
|
|
1470
|
+
/** @description Forbidden */
|
|
1471
|
+
403: {
|
|
1472
|
+
headers: {
|
|
1473
|
+
[name: string]: unknown;
|
|
1474
|
+
};
|
|
1475
|
+
content: {
|
|
1476
|
+
"application/json": components["schemas"]["Error"];
|
|
1477
|
+
};
|
|
1478
|
+
};
|
|
1479
|
+
/** @description No transcript, or transcript API disabled */
|
|
1480
|
+
404: {
|
|
1481
|
+
headers: {
|
|
1482
|
+
[name: string]: unknown;
|
|
1483
|
+
};
|
|
1484
|
+
content: {
|
|
1485
|
+
"application/json": components["schemas"]["Error"];
|
|
1486
|
+
};
|
|
1487
|
+
};
|
|
1488
|
+
};
|
|
1489
|
+
};
|
|
1490
|
+
/**
|
|
1491
|
+
* Upload a cron run transcript
|
|
1492
|
+
* @description Stores (upserts) the runtime's already-redacted session transcript for a cron run, then keeps only the agent's newest 200 transcripts. Callers: the agent's own key, or `*`. `body` over 1 MiB (UTF-8 bytes) is a 413. 404 unless SHIPWRIGHT_TRANSCRIPT_API_ENABLED is `enabled`, or when the run is not owned by the cron/agent.
|
|
1493
|
+
*/
|
|
1494
|
+
put: {
|
|
1495
|
+
parameters: {
|
|
1496
|
+
query?: never;
|
|
1497
|
+
header?: never;
|
|
1498
|
+
path: {
|
|
1499
|
+
id: string;
|
|
1500
|
+
cronId: string;
|
|
1501
|
+
runId: string;
|
|
1502
|
+
};
|
|
1503
|
+
cookie?: never;
|
|
1504
|
+
};
|
|
1505
|
+
requestBody?: {
|
|
1506
|
+
content: {
|
|
1507
|
+
"application/json": components["schemas"]["UploadCronRunTranscriptBody"];
|
|
1508
|
+
};
|
|
1509
|
+
};
|
|
1510
|
+
responses: {
|
|
1511
|
+
/** @description Transcript stored */
|
|
1512
|
+
200: {
|
|
1513
|
+
headers: {
|
|
1514
|
+
[name: string]: unknown;
|
|
1515
|
+
};
|
|
1516
|
+
content: {
|
|
1517
|
+
"application/json": components["schemas"]["CronRunTranscriptUploadResult"];
|
|
1518
|
+
};
|
|
1519
|
+
};
|
|
1520
|
+
/** @description Bad request */
|
|
1521
|
+
400: {
|
|
1522
|
+
headers: {
|
|
1523
|
+
[name: string]: unknown;
|
|
1524
|
+
};
|
|
1525
|
+
content: {
|
|
1526
|
+
"application/json": components["schemas"]["Error"];
|
|
1527
|
+
};
|
|
1528
|
+
};
|
|
1529
|
+
/** @description Caller not allowed */
|
|
1530
|
+
403: {
|
|
1531
|
+
headers: {
|
|
1532
|
+
[name: string]: unknown;
|
|
1533
|
+
};
|
|
1534
|
+
content: {
|
|
1535
|
+
"application/json": components["schemas"]["Error"];
|
|
1536
|
+
};
|
|
1537
|
+
};
|
|
1538
|
+
/** @description Run not found, or transcript API disabled */
|
|
1539
|
+
404: {
|
|
1540
|
+
headers: {
|
|
1541
|
+
[name: string]: unknown;
|
|
1542
|
+
};
|
|
1543
|
+
content: {
|
|
1544
|
+
"application/json": components["schemas"]["Error"];
|
|
1545
|
+
};
|
|
1546
|
+
};
|
|
1547
|
+
/** @description Transcript body over 1 MiB */
|
|
1548
|
+
413: {
|
|
1549
|
+
headers: {
|
|
1550
|
+
[name: string]: unknown;
|
|
1551
|
+
};
|
|
1552
|
+
content: {
|
|
1553
|
+
"application/json": components["schemas"]["Error"];
|
|
1554
|
+
};
|
|
1555
|
+
};
|
|
1556
|
+
};
|
|
1557
|
+
};
|
|
1558
|
+
post?: never;
|
|
1559
|
+
delete?: never;
|
|
1560
|
+
options?: never;
|
|
1561
|
+
head?: never;
|
|
1562
|
+
patch?: never;
|
|
1563
|
+
trace?: never;
|
|
1564
|
+
};
|
|
1435
1565
|
"/agents/{id}/plugins": {
|
|
1436
1566
|
parameters: {
|
|
1437
1567
|
query?: never;
|
|
@@ -1653,7 +1783,13 @@ export interface paths {
|
|
|
1653
1783
|
header?: never;
|
|
1654
1784
|
path: {
|
|
1655
1785
|
id: string;
|
|
1656
|
-
phase:
|
|
1786
|
+
phase:
|
|
1787
|
+
| "prd"
|
|
1788
|
+
| "plan-session"
|
|
1789
|
+
| "review"
|
|
1790
|
+
| "patch"
|
|
1791
|
+
| "deploy"
|
|
1792
|
+
| "dev-task";
|
|
1657
1793
|
};
|
|
1658
1794
|
cookie?: never;
|
|
1659
1795
|
};
|
|
@@ -2743,6 +2879,84 @@ export interface paths {
|
|
|
2743
2879
|
patch?: never;
|
|
2744
2880
|
trace?: never;
|
|
2745
2881
|
};
|
|
2882
|
+
"/agents/{id}/crons/{cronId}/run-now": {
|
|
2883
|
+
parameters: {
|
|
2884
|
+
query?: never;
|
|
2885
|
+
header?: never;
|
|
2886
|
+
path?: never;
|
|
2887
|
+
cookie?: never;
|
|
2888
|
+
};
|
|
2889
|
+
get?: never;
|
|
2890
|
+
put?: never;
|
|
2891
|
+
/**
|
|
2892
|
+
* Request an immediate run of a cron job
|
|
2893
|
+
* @description Asks the agent's runtime to run the cron job once, now, with the job's own stored prompt. Takes no request body (any body is 400). Stamps `runRequestedAt` on the job and writes an AgentCronRunRequest audit row before returning 202; the runtime honours a fresh request (under 15 minutes old) once on its next cron sync. Callers: `*`, `platform.ops`, or the agent's own key. 404 unless SHIPWRIGHT_CRON_RUN_NOW_ENABLED is `enabled`; 409 when the trial-expiry lockdown disabled the cron.
|
|
2894
|
+
*/
|
|
2895
|
+
post: {
|
|
2896
|
+
parameters: {
|
|
2897
|
+
query?: never;
|
|
2898
|
+
header?: never;
|
|
2899
|
+
path: {
|
|
2900
|
+
id: string;
|
|
2901
|
+
cronId: string;
|
|
2902
|
+
};
|
|
2903
|
+
cookie?: never;
|
|
2904
|
+
};
|
|
2905
|
+
requestBody?: never;
|
|
2906
|
+
responses: {
|
|
2907
|
+
/** @description Run requested */
|
|
2908
|
+
202: {
|
|
2909
|
+
headers: {
|
|
2910
|
+
[name: string]: unknown;
|
|
2911
|
+
};
|
|
2912
|
+
content: {
|
|
2913
|
+
"application/json": components["schemas"]["CronRunNowResponse"];
|
|
2914
|
+
};
|
|
2915
|
+
};
|
|
2916
|
+
/** @description A request body was sent */
|
|
2917
|
+
400: {
|
|
2918
|
+
headers: {
|
|
2919
|
+
[name: string]: unknown;
|
|
2920
|
+
};
|
|
2921
|
+
content: {
|
|
2922
|
+
"application/json": components["schemas"]["Error"];
|
|
2923
|
+
};
|
|
2924
|
+
};
|
|
2925
|
+
/** @description Caller not allowed */
|
|
2926
|
+
403: {
|
|
2927
|
+
headers: {
|
|
2928
|
+
[name: string]: unknown;
|
|
2929
|
+
};
|
|
2930
|
+
content: {
|
|
2931
|
+
"application/json": components["schemas"]["Error"];
|
|
2932
|
+
};
|
|
2933
|
+
};
|
|
2934
|
+
/** @description Cron not found, or cron run-now disabled */
|
|
2935
|
+
404: {
|
|
2936
|
+
headers: {
|
|
2937
|
+
[name: string]: unknown;
|
|
2938
|
+
};
|
|
2939
|
+
content: {
|
|
2940
|
+
"application/json": components["schemas"]["Error"];
|
|
2941
|
+
};
|
|
2942
|
+
};
|
|
2943
|
+
/** @description Cron disabled by lockdown */
|
|
2944
|
+
409: {
|
|
2945
|
+
headers: {
|
|
2946
|
+
[name: string]: unknown;
|
|
2947
|
+
};
|
|
2948
|
+
content: {
|
|
2949
|
+
"application/json": components["schemas"]["Error"];
|
|
2950
|
+
};
|
|
2951
|
+
};
|
|
2952
|
+
};
|
|
2953
|
+
};
|
|
2954
|
+
delete?: never;
|
|
2955
|
+
options?: never;
|
|
2956
|
+
head?: never;
|
|
2957
|
+
patch?: never;
|
|
2958
|
+
trace?: never;
|
|
2959
|
+
};
|
|
2746
2960
|
"/agents/{id}/ask": {
|
|
2747
2961
|
parameters: {
|
|
2748
2962
|
query?: never;
|
|
@@ -3131,6 +3345,13 @@ export interface components {
|
|
|
3131
3345
|
CronWrapper: {
|
|
3132
3346
|
cron: components["schemas"]["AgentCronJob"];
|
|
3133
3347
|
};
|
|
3348
|
+
CronRunNowResponse: {
|
|
3349
|
+
/**
|
|
3350
|
+
* Format: date-time
|
|
3351
|
+
* @example 2026-10-26T12:00:00.000Z
|
|
3352
|
+
*/
|
|
3353
|
+
requestedAt: string;
|
|
3354
|
+
};
|
|
3134
3355
|
AgentCronJob: {
|
|
3135
3356
|
/** @example clx1234567890 */
|
|
3136
3357
|
id: string;
|
|
@@ -3156,6 +3377,12 @@ export interface components {
|
|
|
3156
3377
|
system: boolean;
|
|
3157
3378
|
/** @example null */
|
|
3158
3379
|
parentCronId: string | null;
|
|
3380
|
+
/**
|
|
3381
|
+
* Format: date-time
|
|
3382
|
+
* @description Set by POST .../run-now; the runtime honours it once while fresh (15 minutes) and run creation with commandVariant run-now clears it.
|
|
3383
|
+
* @example null
|
|
3384
|
+
*/
|
|
3385
|
+
runRequestedAt: string | null;
|
|
3159
3386
|
/**
|
|
3160
3387
|
* Format: date-time
|
|
3161
3388
|
* @example 2026-01-01T00:00:00.000Z
|
|
@@ -3565,6 +3792,53 @@ export interface components {
|
|
|
3565
3792
|
TokensWrapper: {
|
|
3566
3793
|
tokens: components["schemas"]["AgentToken"][];
|
|
3567
3794
|
};
|
|
3795
|
+
CronRunTranscriptUploadResult: {
|
|
3796
|
+
/** @example clx1111111111 */
|
|
3797
|
+
runId: string;
|
|
3798
|
+
/** @example 2048 */
|
|
3799
|
+
bytes: number;
|
|
3800
|
+
/** @example false */
|
|
3801
|
+
truncated: boolean;
|
|
3802
|
+
/** @example 0 */
|
|
3803
|
+
redactions: number;
|
|
3804
|
+
};
|
|
3805
|
+
UploadCronRunTranscriptBody: {
|
|
3806
|
+
/**
|
|
3807
|
+
* @description Claude session id the transcript was read from.
|
|
3808
|
+
* @example 7c1c8d1e-0000-4000-8000-000000000000
|
|
3809
|
+
*/
|
|
3810
|
+
sessionId?: string;
|
|
3811
|
+
/** @description The already-redacted transcript (JSONL text). Over 1 MiB (UTF-8 bytes) is a 413. Never logged. */
|
|
3812
|
+
body: string;
|
|
3813
|
+
/**
|
|
3814
|
+
* @description True when the runtime dropped earlier lines to fit.
|
|
3815
|
+
* @example false
|
|
3816
|
+
*/
|
|
3817
|
+
truncated: boolean;
|
|
3818
|
+
/**
|
|
3819
|
+
* @description How many secrets the runtime redacted.
|
|
3820
|
+
* @example 0
|
|
3821
|
+
*/
|
|
3822
|
+
redactions: number;
|
|
3823
|
+
};
|
|
3824
|
+
CronRunTranscript: {
|
|
3825
|
+
/** @example clx1111111111 */
|
|
3826
|
+
runId: string;
|
|
3827
|
+
/** @example clx0987654321 */
|
|
3828
|
+
cronId: string;
|
|
3829
|
+
/** @example null */
|
|
3830
|
+
sessionId: string | null;
|
|
3831
|
+
/** @example 2048 */
|
|
3832
|
+
bytes: number;
|
|
3833
|
+
/** @example false */
|
|
3834
|
+
truncated: boolean;
|
|
3835
|
+
/** @example 0 */
|
|
3836
|
+
redactions: number;
|
|
3837
|
+
/** @example 2026-10-26T12:00:00.000Z */
|
|
3838
|
+
createdAt: string;
|
|
3839
|
+
/** @description Redacted JSONL transcript. */
|
|
3840
|
+
body: string;
|
|
3841
|
+
};
|
|
3568
3842
|
PluginWrapper: {
|
|
3569
3843
|
plugin: components["schemas"]["AgentPlugin"];
|
|
3570
3844
|
};
|
|
@@ -3615,7 +3889,13 @@ export interface components {
|
|
|
3615
3889
|
* @example review
|
|
3616
3890
|
* @enum {string}
|
|
3617
3891
|
*/
|
|
3618
|
-
phase:
|
|
3892
|
+
phase:
|
|
3893
|
+
| "prd"
|
|
3894
|
+
| "plan-session"
|
|
3895
|
+
| "review"
|
|
3896
|
+
| "patch"
|
|
3897
|
+
| "deploy"
|
|
3898
|
+
| "dev-task";
|
|
3619
3899
|
/** @example shipwright:code-reviewer */
|
|
3620
3900
|
subagentType: string | null;
|
|
3621
3901
|
/**
|
|
@@ -3968,6 +4248,7 @@ export interface components {
|
|
|
3968
4248
|
phaseMethodology: {
|
|
3969
4249
|
[key: string]: string | null;
|
|
3970
4250
|
};
|
|
4251
|
+
features?: components["schemas"]["AgentConfigFeatures"];
|
|
3971
4252
|
};
|
|
3972
4253
|
AgentConfigPlugin: {
|
|
3973
4254
|
/** @example shipwright */
|
|
@@ -3975,6 +4256,18 @@ export interface components {
|
|
|
3975
4256
|
/** @example shipwright */
|
|
3976
4257
|
plugin: string;
|
|
3977
4258
|
};
|
|
4259
|
+
AgentConfigFeatures: {
|
|
4260
|
+
/**
|
|
4261
|
+
* @description True when SHIPWRIGHT_TRANSCRIPT_API_ENABLED is on: the runtime uploads a redacted transcript after each cron run.
|
|
4262
|
+
* @example false
|
|
4263
|
+
*/
|
|
4264
|
+
transcriptUpload: boolean;
|
|
4265
|
+
/**
|
|
4266
|
+
* @description Reserved for the repo-access check (COS-44.1).
|
|
4267
|
+
* @example false
|
|
4268
|
+
*/
|
|
4269
|
+
repoAccessCheck: boolean;
|
|
4270
|
+
};
|
|
3978
4271
|
RuntimeError: {
|
|
3979
4272
|
/** @example Not found */
|
|
3980
4273
|
error: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@app-vitals/agent-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/app-vitals/agent.git",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"./pool-observability": "./pool-observability.ts",
|
|
27
27
|
"./pricing": "./pricing.ts",
|
|
28
28
|
"./progress-phases": "./progress-phases.ts",
|
|
29
|
+
"./redact": "./redact.ts",
|
|
29
30
|
"./request-context": "./request-context.ts",
|
|
30
31
|
"./secret-env-vars": "./secret-env-vars.ts",
|
|
31
32
|
"./default-account": "./default-account.ts",
|
package/redact.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/redact.ts
|
|
3
|
+
* Secret redaction for cron-run transcripts (COS-43.1). Lives in lib/ so the
|
|
4
|
+
* runtime (which redacts before upload) and admin can both import it.
|
|
5
|
+
*
|
|
6
|
+
* Two passes, both returning the redacted text plus how many substitutions
|
|
7
|
+
* were made:
|
|
8
|
+
* - redactValues: the live values of SECRET_ENV_VARS (the same scrub the
|
|
9
|
+
* Sentry hooks use, lib/sentry.ts redactSecrets).
|
|
10
|
+
* - redactShapes: well-known credential shapes (GitHub/Anthropic/OpenAI/
|
|
11
|
+
* Slack/AWS tokens, PEM private keys, bearer tokens, JWTs).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { liveSecretValues, redactSecrets } from "./sentry.ts";
|
|
15
|
+
|
|
16
|
+
export interface RedactResult {
|
|
17
|
+
text: string;
|
|
18
|
+
count: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
22
|
+
|
|
23
|
+
export const REDACTED = "[REDACTED]";
|
|
24
|
+
|
|
25
|
+
const SHAPES: readonly RegExp[] = [
|
|
26
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
27
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
28
|
+
/ghp_[A-Za-z0-9]{20,}/g,
|
|
29
|
+
/sk-ant-[A-Za-z0-9_-]{20,}/g,
|
|
30
|
+
// Not preceded by an alphanumeric, so "task-<slug>" is not an OpenAI key.
|
|
31
|
+
/(?<![A-Za-z0-9])sk-[A-Za-z0-9]{20,}/g,
|
|
32
|
+
/xox[abprs]-[A-Za-z0-9-]{10,}/g,
|
|
33
|
+
/AKIA[A-Z0-9]{16}/g,
|
|
34
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
35
|
+
/Bearer [A-Za-z0-9._-]{16,}/g,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** Replaces the live values of SECRET_ENV_VARS in `env` (default process.env). */
|
|
39
|
+
export function redactValues(
|
|
40
|
+
text: string,
|
|
41
|
+
env: Env = process.env,
|
|
42
|
+
): RedactResult {
|
|
43
|
+
const secrets = liveSecretValues(env);
|
|
44
|
+
let count = 0;
|
|
45
|
+
for (const secret of secrets) {
|
|
46
|
+
count += text.split(secret).length - 1;
|
|
47
|
+
}
|
|
48
|
+
if (count === 0) return { text, count: 0 };
|
|
49
|
+
return { text: redactSecrets(text, secrets, new WeakSet(), 0), count };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Replaces well-known credential shapes. */
|
|
53
|
+
export function redactShapes(text: string): RedactResult {
|
|
54
|
+
let count = 0;
|
|
55
|
+
let out = text;
|
|
56
|
+
for (const re of SHAPES) {
|
|
57
|
+
out = out.replace(re, () => {
|
|
58
|
+
count++;
|
|
59
|
+
return REDACTED;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return { text: out, count };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** redactValues then redactShapes; counts are summed. */
|
|
66
|
+
export function redact(text: string, env: Env = process.env): RedactResult {
|
|
67
|
+
const values = redactValues(text, env);
|
|
68
|
+
const shapes = redactShapes(values.text);
|
|
69
|
+
return { text: shapes.text, count: values.count + shapes.count };
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/redact.unit.test.ts
|
|
3
|
+
* Transcript redaction (COS-43.1): live secret values + token shapes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import { redact, redactShapes, redactValues } from "./redact.ts";
|
|
8
|
+
|
|
9
|
+
const PLANTED: Record<string, string> = {
|
|
10
|
+
"GitHub classic PAT": `ghp_${"A1b2C3d4E5".repeat(3)}`,
|
|
11
|
+
"GitHub fine-grained PAT": `github_pat_${"a1B2c3D4_".repeat(4)}`,
|
|
12
|
+
"Anthropic key": `sk-ant-api03-${"Zy9-x8_W7".repeat(4)}`,
|
|
13
|
+
"OpenAI-style key": `sk-${"Qw3Er4Ty5U".repeat(3)}`,
|
|
14
|
+
"Slack token": "xoxb-1234567890-abcdefghij",
|
|
15
|
+
"AWS access key": "AKIAIOSFODNN7EXAMPLE",
|
|
16
|
+
"PEM private key": [
|
|
17
|
+
"-----BEGIN RSA PRIVATE KEY-----",
|
|
18
|
+
"MIIEowIBAAKCAQEAxyz",
|
|
19
|
+
"abcdef==",
|
|
20
|
+
"-----END RSA PRIVATE KEY-----",
|
|
21
|
+
].join("\n"),
|
|
22
|
+
"Bearer token": "Bearer abcdefghijklmnop1234.-_",
|
|
23
|
+
JWT: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dBjftJeZ4CVPmB92K27uhbUJU1p1r",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("redactShapes", () => {
|
|
27
|
+
for (const [name, token] of Object.entries(PLANTED)) {
|
|
28
|
+
it(`redacts a ${name} and counts it`, () => {
|
|
29
|
+
const out = redactShapes(`before ${token} after`);
|
|
30
|
+
expect(out.text).not.toContain(token);
|
|
31
|
+
expect(out.text).toContain("before ");
|
|
32
|
+
expect(out.text).toContain(" after");
|
|
33
|
+
expect(out.count).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
it("redacts the PEM body, not just the header", () => {
|
|
38
|
+
const out = redactShapes(PLANTED["PEM private key"] as string);
|
|
39
|
+
expect(out.text).not.toContain("MIIEowIBAAKCAQEAxyz");
|
|
40
|
+
expect(out.text).not.toContain("END RSA PRIVATE KEY");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("counts every occurrence", () => {
|
|
44
|
+
const t = PLANTED["GitHub classic PAT"] as string;
|
|
45
|
+
const out = redactShapes(`${t} and ${t}\n${PLANTED["AWS access key"]}`);
|
|
46
|
+
expect(out.count).toBe(3);
|
|
47
|
+
expect(out.text).not.toContain(t);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not flag a sha256 hex digest or ordinary prose", () => {
|
|
51
|
+
const sha = `${"a".repeat(8)}${"0123456789abcdef".repeat(3)}deadbeef`;
|
|
52
|
+
const text = `commit ${sha} skipped the task-store; sk-short is fine`;
|
|
53
|
+
const out = redactShapes(text);
|
|
54
|
+
expect(out.text).toBe(text);
|
|
55
|
+
expect(out.count).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("redactValues", () => {
|
|
60
|
+
it("replaces live SECRET_ENV_VARS values and counts them", () => {
|
|
61
|
+
const env = { GH_TOKEN: "super-secret-gh-value", PATH: "/usr/bin" };
|
|
62
|
+
const out = redactValues(
|
|
63
|
+
"token=super-secret-gh-value again super-secret-gh-value /usr/bin",
|
|
64
|
+
env,
|
|
65
|
+
);
|
|
66
|
+
expect(out.text).not.toContain("super-secret-gh-value");
|
|
67
|
+
expect(out.text).toContain("/usr/bin");
|
|
68
|
+
expect(out.count).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is a no-op with no live secrets", () => {
|
|
72
|
+
expect(redactValues("hello", {})).toEqual({ text: "hello", count: 0 });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("redact", () => {
|
|
77
|
+
it("applies values then shapes and sums the counts", () => {
|
|
78
|
+
const env = { SLACK_BOT_TOKEN: "plain-live-value-123" };
|
|
79
|
+
const out = redact(
|
|
80
|
+
`a plain-live-value-123 b ${PLANTED["GitHub classic PAT"]}`,
|
|
81
|
+
env,
|
|
82
|
+
);
|
|
83
|
+
expect(out.count).toBe(2);
|
|
84
|
+
expect(out.text).not.toContain("plain-live-value-123");
|
|
85
|
+
expect(out.text).not.toContain("ghp_");
|
|
86
|
+
});
|
|
87
|
+
});
|
package/sentry.ts
CHANGED
|
@@ -69,13 +69,15 @@ export interface ErrorCapturingClient {
|
|
|
69
69
|
const MAX_SCRUB_DEPTH = 20;
|
|
70
70
|
|
|
71
71
|
/** Recomputed per call (not cached at module load) so tests can mutate env freely between assertions. */
|
|
72
|
-
function liveSecretValues(
|
|
73
|
-
|
|
72
|
+
export function liveSecretValues(
|
|
73
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
74
|
+
): string[] {
|
|
75
|
+
return SECRET_ENV_VARS.map((key) => env[key]).filter(
|
|
74
76
|
(value): value is string => !!value,
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
function redactSecrets<T>(
|
|
80
|
+
export function redactSecrets<T>(
|
|
79
81
|
value: T,
|
|
80
82
|
secrets: string[],
|
|
81
83
|
seen: WeakSet<object>,
|