@opengeni/contracts 0.19.0 → 0.19.4
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/index.d.ts +207 -12
- package/dist/index.js +341 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +136 -0
- package/src/secret-redaction.ts +364 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -478,8 +478,10 @@ export type ErrorCode = z.infer<typeof ErrorCode>;
|
|
|
478
478
|
|
|
479
479
|
export const ErrorEnvelope = z.object({
|
|
480
480
|
error: z.object({
|
|
481
|
+
status: z.number().int().min(400).max(599),
|
|
481
482
|
code: ErrorCode,
|
|
482
483
|
message: z.string(),
|
|
484
|
+
retryable: z.boolean(),
|
|
483
485
|
requestId: z.string().optional(),
|
|
484
486
|
details: z.record(z.string(), z.unknown()).optional(),
|
|
485
487
|
}),
|
|
@@ -2176,11 +2178,22 @@ export type ConnectionCredentialsPort = {
|
|
|
2176
2178
|
|
|
2177
2179
|
export type GitHubInstallationSummary = {
|
|
2178
2180
|
installationId: number;
|
|
2181
|
+
accountId: number;
|
|
2179
2182
|
accountLogin: string | null;
|
|
2180
2183
|
accountType: string | null;
|
|
2181
2184
|
suspended: boolean;
|
|
2182
2185
|
};
|
|
2183
2186
|
|
|
2187
|
+
export type GitHubInstallationAuthorityKind = "personal_owner" | "organization_owner";
|
|
2188
|
+
|
|
2189
|
+
export interface GitHubInstallationBindingProof {
|
|
2190
|
+
actorId: number;
|
|
2191
|
+
actorLogin: string;
|
|
2192
|
+
authorityKind: GitHubInstallationAuthorityKind;
|
|
2193
|
+
installation: GitHubInstallationSummary;
|
|
2194
|
+
repositories: GitHubRepository[];
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2184
2197
|
export type GitHubRepositoryPermissions = {
|
|
2185
2198
|
admin: boolean;
|
|
2186
2199
|
maintain: boolean;
|
|
@@ -2198,6 +2211,18 @@ export type GitHubUserInstallationAccess = GitHubInstallationSummary & {
|
|
|
2198
2211
|
};
|
|
2199
2212
|
|
|
2200
2213
|
export type GitHubAppApiPort = {
|
|
2214
|
+
/**
|
|
2215
|
+
* Exchange one fresh GitHub user-authorization code and prove current
|
|
2216
|
+
* installation authority. Implementations must accept only exact personal
|
|
2217
|
+
* ownership or active organization ownership; installation visibility,
|
|
2218
|
+
* repository permission bits, and App Manager metadata are not authority.
|
|
2219
|
+
* Organization ownership must be revalidated after repository discovery,
|
|
2220
|
+
* immediately before returning the proof used by the durable bind.
|
|
2221
|
+
*/
|
|
2222
|
+
authorizeInstallationBinding?: (input: {
|
|
2223
|
+
code: string;
|
|
2224
|
+
installationId: number;
|
|
2225
|
+
}) => Promise<GitHubInstallationBindingProof>;
|
|
2201
2226
|
authorizeUser?: (input: { code: string }) => Promise<GitHubUserInstallationAccess[]>;
|
|
2202
2227
|
verifyInstallationAccessForUser?: (input: {
|
|
2203
2228
|
code: string;
|
|
@@ -2491,6 +2516,37 @@ export type KnowledgeSourceKind = z.infer<typeof KnowledgeSourceKind>;
|
|
|
2491
2516
|
export const DocumentSearchMode = z.enum(["hybrid", "vector", "keyword"]);
|
|
2492
2517
|
export type DocumentSearchMode = z.infer<typeof DocumentSearchMode>;
|
|
2493
2518
|
|
|
2519
|
+
// 'workspace' documents are readable by anyone with workspace access;
|
|
2520
|
+
// 'private' documents are readable only by the grant subject that created them.
|
|
2521
|
+
export const DocumentVisibility = z.enum(["workspace", "private"]);
|
|
2522
|
+
export type DocumentVisibility = z.infer<typeof DocumentVisibility>;
|
|
2523
|
+
|
|
2524
|
+
// Knowledge-drop auto-curation lifecycle. 'none' = ordinary caller-described add
|
|
2525
|
+
// (never auto-curated). 'pending' = dropped, curation runs during indexing.
|
|
2526
|
+
// 'suggested' = curated but the base move was NOT applied (low confidence or
|
|
2527
|
+
// conflict) — the suggestion lives in Document.curation. 'auto_filed' = curated
|
|
2528
|
+
// and moved into the suggested base. 'failed' = curation errored (fail-soft;
|
|
2529
|
+
// the document still indexes and stays searchable).
|
|
2530
|
+
export const DocumentCurationStatus = z.enum([
|
|
2531
|
+
"none",
|
|
2532
|
+
"pending",
|
|
2533
|
+
"suggested",
|
|
2534
|
+
"auto_filed",
|
|
2535
|
+
"failed",
|
|
2536
|
+
]);
|
|
2537
|
+
export type DocumentCurationStatus = z.infer<typeof DocumentCurationStatus>;
|
|
2538
|
+
|
|
2539
|
+
// Curator audit blob persisted on the document.
|
|
2540
|
+
export const DocumentCuration = z.object({
|
|
2541
|
+
suggestedBaseId: z.string().uuid().nullable(),
|
|
2542
|
+
suggestedBaseName: z.string().nullable(),
|
|
2543
|
+
confidence: z.number().min(0).max(1),
|
|
2544
|
+
reason: z.string().nullable(),
|
|
2545
|
+
originalTitle: z.string().nullable(),
|
|
2546
|
+
model: z.string().nullable(),
|
|
2547
|
+
});
|
|
2548
|
+
export type DocumentCuration = z.infer<typeof DocumentCuration>;
|
|
2549
|
+
|
|
2494
2550
|
export const DocumentBase = z.object({
|
|
2495
2551
|
id: z.string().uuid(),
|
|
2496
2552
|
workspaceId: z.string().uuid(),
|
|
@@ -2520,6 +2576,13 @@ export const Document = z.object({
|
|
|
2520
2576
|
sourceUpdatedAt: z.string().nullable(),
|
|
2521
2577
|
sourceVersion: z.string().nullable(),
|
|
2522
2578
|
aclTags: z.array(z.string()),
|
|
2579
|
+
visibility: DocumentVisibility,
|
|
2580
|
+
createdBy: z.string().nullable(),
|
|
2581
|
+
agentAccess: z.boolean(),
|
|
2582
|
+
summary: z.string().nullable(),
|
|
2583
|
+
topics: z.array(z.string()),
|
|
2584
|
+
curationStatus: DocumentCurationStatus,
|
|
2585
|
+
curation: DocumentCuration.nullable(),
|
|
2523
2586
|
createdAt: z.string(),
|
|
2524
2587
|
updatedAt: z.string(),
|
|
2525
2588
|
});
|
|
@@ -2569,9 +2632,37 @@ export const AddDocumentRequest = z.object({
|
|
|
2569
2632
|
sourceUpdatedAt: z.string().datetime({ offset: true }).optional(),
|
|
2570
2633
|
sourceVersion: z.string().min(1).optional(),
|
|
2571
2634
|
aclTags: z.array(z.string().min(1)).optional(),
|
|
2635
|
+
visibility: DocumentVisibility.optional(),
|
|
2636
|
+
agentAccess: z.boolean().optional(),
|
|
2572
2637
|
});
|
|
2573
2638
|
export type AddDocumentRequest = z.infer<typeof AddDocumentRequest>;
|
|
2574
2639
|
|
|
2640
|
+
// A knowledge drop: raw text or an already-uploaded file, with no required
|
|
2641
|
+
// metadata. The server files it into the workspace Default base. When a
|
|
2642
|
+
// curation provider is enabled, it may name, summarize, categorize, and
|
|
2643
|
+
// (confidence permitting) move the document; provider=none leaves caller
|
|
2644
|
+
// metadata and Default placement unchanged.
|
|
2645
|
+
export const CreateKnowledgeDropRequest = z
|
|
2646
|
+
.object({
|
|
2647
|
+
text: z.string().min(1).max(2_000_000).optional(),
|
|
2648
|
+
fileId: z.string().uuid().optional(),
|
|
2649
|
+
filename: z.string().min(1).optional(),
|
|
2650
|
+
title: z.string().min(1).optional(),
|
|
2651
|
+
visibility: DocumentVisibility.optional(),
|
|
2652
|
+
agentAccess: z.boolean().optional(),
|
|
2653
|
+
})
|
|
2654
|
+
.refine((value) => (value.text === undefined) !== (value.fileId === undefined), {
|
|
2655
|
+
message: "provide exactly one of text or fileId",
|
|
2656
|
+
});
|
|
2657
|
+
export type CreateKnowledgeDropRequest = z.infer<typeof CreateKnowledgeDropRequest>;
|
|
2658
|
+
|
|
2659
|
+
// Move a document (and its indexed chunks) to another base. With no explicit
|
|
2660
|
+
// targetBaseId, applies the document's stored curation suggestion.
|
|
2661
|
+
export const MoveDocumentRequest = z.object({
|
|
2662
|
+
targetBaseId: z.string().uuid().optional(),
|
|
2663
|
+
});
|
|
2664
|
+
export type MoveDocumentRequest = z.infer<typeof MoveDocumentRequest>;
|
|
2665
|
+
|
|
2575
2666
|
export const DocumentSearchRequest = z.object({
|
|
2576
2667
|
query: z.string().min(1),
|
|
2577
2668
|
baseIds: z.array(z.string().uuid()).optional(),
|
|
@@ -3134,6 +3225,29 @@ export const UpdateSessionRequest = z.object({
|
|
|
3134
3225
|
});
|
|
3135
3226
|
export type UpdateSessionRequest = z.infer<typeof UpdateSessionRequest>;
|
|
3136
3227
|
|
|
3228
|
+
/**
|
|
3229
|
+
* Replace an existing session's durable tool policy, or explicitly opt back in
|
|
3230
|
+
* to the current workspace defaults. The mode-less explicit shape is retained
|
|
3231
|
+
* for compatibility with clients released before workspace-default adoption
|
|
3232
|
+
* was supported.
|
|
3233
|
+
*/
|
|
3234
|
+
export const UpdateSessionToolPolicyRequest = z.union([
|
|
3235
|
+
z
|
|
3236
|
+
.object({
|
|
3237
|
+
mode: z.literal("workspace_default"),
|
|
3238
|
+
expectedVersion: z.number().int().positive(),
|
|
3239
|
+
})
|
|
3240
|
+
.strict(),
|
|
3241
|
+
z
|
|
3242
|
+
.object({
|
|
3243
|
+
mode: z.literal("explicit").optional(),
|
|
3244
|
+
tools: z.array(ToolRef).max(64),
|
|
3245
|
+
expectedVersion: z.number().int().positive(),
|
|
3246
|
+
})
|
|
3247
|
+
.strict(),
|
|
3248
|
+
]);
|
|
3249
|
+
export type UpdateSessionToolPolicyRequest = z.infer<typeof UpdateSessionToolPolicyRequest>;
|
|
3250
|
+
|
|
3137
3251
|
/**
|
|
3138
3252
|
* A member's personal pin preference for a session. `expectedVersion` is
|
|
3139
3253
|
* optional: ordinary pin/unpin actions are idempotent last-write-wins, while a
|
|
@@ -3275,6 +3389,7 @@ export const SessionAuthorizationOperation = z.enum([
|
|
|
3275
3389
|
"session.human_input.write",
|
|
3276
3390
|
"session.title.write",
|
|
3277
3391
|
"session.mcp.approval_policy.write",
|
|
3392
|
+
"session.tool_policy.write",
|
|
3278
3393
|
"session.goal.read",
|
|
3279
3394
|
"session.goal.write",
|
|
3280
3395
|
"session.child.create",
|
|
@@ -3569,6 +3684,8 @@ export const NewSessionDraft = z.object({
|
|
|
3569
3684
|
text: z.string(),
|
|
3570
3685
|
resources: z.array(ResourceRef),
|
|
3571
3686
|
tools: z.array(ToolRef),
|
|
3687
|
+
/** False means the workspace-default MCP policy is still inherited. */
|
|
3688
|
+
toolsProvided: z.boolean().default(false),
|
|
3572
3689
|
model: z.string().min(1),
|
|
3573
3690
|
reasoningEffort: ReasoningEffort,
|
|
3574
3691
|
options: NewSessionDraftOptions,
|
|
@@ -3580,6 +3697,7 @@ export const SaveNewSessionDraftRequest = NewSessionDraft.pick({
|
|
|
3580
3697
|
text: true,
|
|
3581
3698
|
resources: true,
|
|
3582
3699
|
tools: true,
|
|
3700
|
+
toolsProvided: true,
|
|
3583
3701
|
model: true,
|
|
3584
3702
|
reasoningEffort: true,
|
|
3585
3703
|
options: true,
|
|
@@ -4853,6 +4971,10 @@ export const Session = z.object({
|
|
|
4853
4971
|
// Origin of the persisted tool allow-list. Optional for rolling client
|
|
4854
4972
|
// compatibility; current servers emit it and legacy rows map to `legacy`.
|
|
4855
4973
|
toolPolicy: SessionToolPolicy.optional(),
|
|
4974
|
+
// Optimistic-concurrency fence for durable policy mutations. Optional for
|
|
4975
|
+
// older clients/fixtures; current servers always emit the authoritative
|
|
4976
|
+
// value.
|
|
4977
|
+
toolPolicyVersion: z.number().int().positive().optional(),
|
|
4856
4978
|
// Secret-safe current resolution, computed at an API/read or execution
|
|
4857
4979
|
// boundary from IDs only. Optional because internal DB readers need not load
|
|
4858
4980
|
// the workspace runtime registry.
|
|
@@ -5103,6 +5225,7 @@ export const SessionEventType = z.enum([
|
|
|
5103
5225
|
"terminal.pty.exited", // PTY session ended (exitCode/reason)
|
|
5104
5226
|
"session.title_set",
|
|
5105
5227
|
"session.mcp.approval_policy.updated",
|
|
5228
|
+
"session.tool_policy.updated",
|
|
5106
5229
|
// Multi-account Codex (P1): the account a session's turn runs on changed
|
|
5107
5230
|
// (manual switch in P1; failover/rotation in P3 reuse the same event). Drives
|
|
5108
5231
|
// the in-session "Running on:" indicator's live flip.
|
|
@@ -5268,6 +5391,7 @@ export const SESSION_EVENT_SEMANTIC_CLASS_TYPES = {
|
|
|
5268
5391
|
"session.queue.changed",
|
|
5269
5392
|
"session.queue.prompt.cancelled",
|
|
5270
5393
|
"session.mcp.approval_policy.updated",
|
|
5394
|
+
"session.tool_policy.updated",
|
|
5271
5395
|
],
|
|
5272
5396
|
terminal: [
|
|
5273
5397
|
"turn.completed",
|
|
@@ -7468,10 +7592,18 @@ export type GitHubRepository = z.infer<typeof GitHubRepository>;
|
|
|
7468
7592
|
export const GitHubRepositoryScope = z.enum(["all", "selected"]);
|
|
7469
7593
|
export type GitHubRepositoryScope = z.infer<typeof GitHubRepositoryScope>;
|
|
7470
7594
|
|
|
7595
|
+
export const GitHubBindingStatus = z.enum(["disabled", "unbound", "bound"]);
|
|
7596
|
+
export type GitHubBindingStatus = z.infer<typeof GitHubBindingStatus>;
|
|
7597
|
+
|
|
7598
|
+
export const GitHubInstallationLifecycle = z.enum(["active", "suspended", "deleted", "unverified"]);
|
|
7599
|
+
export type GitHubInstallationLifecycle = z.infer<typeof GitHubInstallationLifecycle>;
|
|
7600
|
+
|
|
7471
7601
|
export const GitHubInstallationBinding = z.object({
|
|
7472
7602
|
installationId: z.number().int().positive(),
|
|
7603
|
+
githubAccountId: z.number().int().positive().nullable(),
|
|
7473
7604
|
accountLogin: z.string().nullable(),
|
|
7474
7605
|
accountType: z.string().nullable(),
|
|
7606
|
+
lifecycle: GitHubInstallationLifecycle,
|
|
7475
7607
|
repositoryScope: GitHubRepositoryScope,
|
|
7476
7608
|
repositoryCount: z.number().int().nonnegative(),
|
|
7477
7609
|
createdAt: z.string(),
|
|
@@ -7481,6 +7613,7 @@ export type GitHubInstallationBinding = z.infer<typeof GitHubInstallationBinding
|
|
|
7481
7613
|
|
|
7482
7614
|
export const GitHubAppInfo = z.object({
|
|
7483
7615
|
configured: z.boolean(),
|
|
7616
|
+
status: GitHubBindingStatus,
|
|
7484
7617
|
appId: z.string().nullable(),
|
|
7485
7618
|
clientId: z.string().nullable(),
|
|
7486
7619
|
appSlug: z.string().nullable(),
|
|
@@ -8473,6 +8606,8 @@ export type WorkspaceModelCatalogResponse = z.infer<typeof WorkspaceModelCatalog
|
|
|
8473
8606
|
*/
|
|
8474
8607
|
export const OPENGENI_API_CONTRACT_REVISION = "2026-07-turn-instructions-v1" as const;
|
|
8475
8608
|
export const OPENGENI_API_CONTRACT_HEADER = "x-opengeni-api-contract" as const;
|
|
8609
|
+
/** Bounded request/response identifier shared by browser, ingress, and API diagnostics. */
|
|
8610
|
+
export const OPENGENI_CORRELATION_HEADER = "x-opengeni-correlation-id" as const;
|
|
8476
8611
|
|
|
8477
8612
|
export const ClientConfig = /* @__PURE__ */ defineModelContractSchema(() =>
|
|
8478
8613
|
z.object({
|
|
@@ -8594,3 +8729,4 @@ export function evaluateWorkspaceModelPolicy(
|
|
|
8594
8729
|
}
|
|
8595
8730
|
|
|
8596
8731
|
export * from "./codex-fleet-policy";
|
|
8732
|
+
export * from "./secret-redaction";
|
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
}
|