@aexhq/sdk 0.13.6
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/LICENSE +201 -0
- package/README.md +160 -0
- package/dist/_contracts/connection-ticket.d.ts +21 -0
- package/dist/_contracts/connection-ticket.js +49 -0
- package/dist/_contracts/event-envelope.d.ts +276 -0
- package/dist/_contracts/event-envelope.js +324 -0
- package/dist/_contracts/event-stream-client.d.ts +47 -0
- package/dist/_contracts/event-stream-client.js +141 -0
- package/dist/_contracts/http.d.ts +35 -0
- package/dist/_contracts/http.js +114 -0
- package/dist/_contracts/index.d.ts +28 -0
- package/dist/_contracts/index.js +29 -0
- package/dist/_contracts/managed-key.d.ts +74 -0
- package/dist/_contracts/managed-key.js +110 -0
- package/dist/_contracts/operations.d.ts +237 -0
- package/dist/_contracts/operations.js +632 -0
- package/dist/_contracts/provider-support.d.ts +220 -0
- package/dist/_contracts/provider-support.js +90 -0
- package/dist/_contracts/proxy-protocol.d.ts +257 -0
- package/dist/_contracts/proxy-protocol.js +234 -0
- package/dist/_contracts/proxy-validation.d.ts +19 -0
- package/dist/_contracts/proxy-validation.js +51 -0
- package/dist/_contracts/run-artifacts.d.ts +47 -0
- package/dist/_contracts/run-artifacts.js +101 -0
- package/dist/_contracts/run-config.d.ts +304 -0
- package/dist/_contracts/run-config.js +659 -0
- package/dist/_contracts/run-cost.d.ts +125 -0
- package/dist/_contracts/run-cost.js +616 -0
- package/dist/_contracts/run-custody.d.ts +226 -0
- package/dist/_contracts/run-custody.js +465 -0
- package/dist/_contracts/run-record.d.ts +127 -0
- package/dist/_contracts/run-record.js +177 -0
- package/dist/_contracts/run-retention.d.ts +213 -0
- package/dist/_contracts/run-retention.js +484 -0
- package/dist/_contracts/run-unit.d.ts +194 -0
- package/dist/_contracts/run-unit.js +215 -0
- package/dist/_contracts/runner-event.d.ts +114 -0
- package/dist/_contracts/runner-event.js +187 -0
- package/dist/_contracts/runtime-manifest.d.ts +106 -0
- package/dist/_contracts/runtime-manifest.js +98 -0
- package/dist/_contracts/runtime-security-profile.d.ts +27 -0
- package/dist/_contracts/runtime-security-profile.js +82 -0
- package/dist/_contracts/runtime-sizes.d.ts +144 -0
- package/dist/_contracts/runtime-sizes.js +136 -0
- package/dist/_contracts/runtime-types.d.ts +212 -0
- package/dist/_contracts/runtime-types.js +2 -0
- package/dist/_contracts/sdk-errors.d.ts +34 -0
- package/dist/_contracts/sdk-errors.js +52 -0
- package/dist/_contracts/sdk-secrets.d.ts +31 -0
- package/dist/_contracts/sdk-secrets.js +220 -0
- package/dist/_contracts/side-effect-audit.d.ts +129 -0
- package/dist/_contracts/side-effect-audit.js +494 -0
- package/dist/_contracts/sse.d.ts +74 -0
- package/dist/_contracts/sse.js +0 -0
- package/dist/_contracts/stable.d.ts +26 -0
- package/dist/_contracts/stable.js +44 -0
- package/dist/_contracts/status.d.ts +19 -0
- package/dist/_contracts/status.js +61 -0
- package/dist/_contracts/submission.d.ts +383 -0
- package/dist/_contracts/submission.js +1380 -0
- package/dist/agents-md.d.ts +46 -0
- package/dist/agents-md.js +83 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/asset-upload.d.ts +66 -0
- package/dist/asset-upload.js +168 -0
- package/dist/asset-upload.js.map +1 -0
- package/dist/bundle.d.ts +33 -0
- package/dist/bundle.js +89 -0
- package/dist/bundle.js.map +1 -0
- package/dist/cli.mjs +4140 -0
- package/dist/cli.mjs.sha256 +1 -0
- package/dist/client.d.ts +460 -0
- package/dist/client.js +857 -0
- package/dist/client.js.map +1 -0
- package/dist/fetch-archive.d.ts +16 -0
- package/dist/fetch-archive.js +170 -0
- package/dist/fetch-archive.js.map +1 -0
- package/dist/file.d.ts +57 -0
- package/dist/file.js +153 -0
- package/dist/file.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +84 -0
- package/dist/mcp-server.js +114 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/node-fs.d.ts +12 -0
- package/dist/node-fs.js +44 -0
- package/dist/node-fs.js.map +1 -0
- package/dist/proxy-endpoint.d.ts +131 -0
- package/dist/proxy-endpoint.js +147 -0
- package/dist/proxy-endpoint.js.map +1 -0
- package/dist/skill.d.ts +117 -0
- package/dist/skill.js +169 -0
- package/dist/skill.js.map +1 -0
- package/dist/version.d.ts +9 -0
- package/dist/version.js +10 -0
- package/dist/version.js.map +1 -0
- package/docs/cleanup.md +38 -0
- package/docs/credentials.md +153 -0
- package/docs/events.md +76 -0
- package/docs/mcp.md +47 -0
- package/docs/outputs.md +157 -0
- package/docs/product-boundaries.md +57 -0
- package/docs/provider-runtime-capabilities.md +103 -0
- package/docs/quickstart.md +110 -0
- package/docs/release.md +99 -0
- package/docs/run-config.md +53 -0
- package/docs/run-record.md +39 -0
- package/docs/skills.md +139 -0
- package/docs/testing.md +29 -0
- package/package.json +47 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loose record describing a run as the dashboard BFF returns it. Concrete
|
|
3
|
+
* dashboard-managed fields appear in the index signature; the SDK and CLI
|
|
4
|
+
* may surface them without strong typing per-field.
|
|
5
|
+
*
|
|
6
|
+
* `runtimeManifest` is derived from the validated submission + the chosen
|
|
7
|
+
* provider on every read — see `runtime-manifest.ts`. It is `undefined`
|
|
8
|
+
* on responses from BFFs that predate Phase 2 of the runtime-environment
|
|
9
|
+
* rollout; SDK consumers MUST treat it as best-effort and not panic on
|
|
10
|
+
* its absence.
|
|
11
|
+
*/
|
|
12
|
+
export interface Run {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly status: string;
|
|
15
|
+
readonly workspaceId?: string;
|
|
16
|
+
readonly createdAt?: string;
|
|
17
|
+
readonly updatedAt?: string;
|
|
18
|
+
readonly terminalAt?: string | null;
|
|
19
|
+
readonly errorMessage?: string | null;
|
|
20
|
+
readonly usage?: UsageSummary;
|
|
21
|
+
readonly costTelemetry?: import("./run-cost.js").RunCostTelemetry;
|
|
22
|
+
readonly runtimeManifest?: import("./runtime-manifest.js").RuntimeManifest;
|
|
23
|
+
readonly [key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface UsageSummary {
|
|
26
|
+
readonly inputTokens?: number;
|
|
27
|
+
readonly outputTokens?: number;
|
|
28
|
+
readonly cacheReadInputTokens?: number;
|
|
29
|
+
readonly cacheCreationInputTokens?: number;
|
|
30
|
+
readonly totalTokens?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A run event as recorded by the dashboard. Includes the `type` field
|
|
34
|
+
* that the `is*Event` type guards narrow on plus any provider payload.
|
|
35
|
+
*
|
|
36
|
+
* The unified-stream discriminators (`channel`, `source`, `sourceSeq`,
|
|
37
|
+
* `emittedAt`, `receivedAt`, `level`) carry through from the coordinator
|
|
38
|
+
* envelope (see `event-envelope.ts` —
|
|
39
|
+
* {@link import("./event-envelope.js").AexEvent}) so SDK/CLI consumers can
|
|
40
|
+
* split/filter the one stream by channel or source. All are OPTIONAL: archived
|
|
41
|
+
* events from before the unification (and any producer that omits an ordering
|
|
42
|
+
* attribute) lack them, an absent `channel` means `"event"` (see `channelOf`),
|
|
43
|
+
* and `level` is present only on `log`-channel records.
|
|
44
|
+
*/
|
|
45
|
+
export interface RunEvent {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly type: string;
|
|
48
|
+
readonly runId?: string;
|
|
49
|
+
readonly recordedAt?: string;
|
|
50
|
+
/** Which sub-stream this record rides — `"event"` (typed) or `"log"`. Absent ⇒ `"event"`. */
|
|
51
|
+
readonly channel?: import("./event-envelope.js").AexEventChannel;
|
|
52
|
+
/** Coarse origin classifier (agent/worker/runtime/mcp/aex/workflow/machine). */
|
|
53
|
+
readonly source?: import("./event-envelope.js").AexEventSource;
|
|
54
|
+
/** Per-source monotonic counter assigned at the source (carried, not re-ordered). */
|
|
55
|
+
readonly sourceSeq?: number;
|
|
56
|
+
/** Source wall-clock ms at emit (carried for a best-effort client time view). */
|
|
57
|
+
readonly emittedAt?: number;
|
|
58
|
+
/** The DO's authoritative receive-time (wall-clock ms) stamped at ingest, companion to `seq`. */
|
|
59
|
+
readonly receivedAt?: number;
|
|
60
|
+
/** Log severity, first-class on a `channel: "log"` record ("info" | "warn" | "error"). */
|
|
61
|
+
readonly level?: import("./event-envelope.js").AexLogLevel;
|
|
62
|
+
readonly [key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Provider-emitted event payload. Provider-specific fields are passed through
|
|
66
|
+
* structurally so historical events and managed-runner events can be displayed
|
|
67
|
+
* and downloaded without each client needing provider-specific schemas.
|
|
68
|
+
*/
|
|
69
|
+
export interface ProviderEvent {
|
|
70
|
+
readonly type: string;
|
|
71
|
+
readonly id?: string | undefined;
|
|
72
|
+
readonly created_at?: string | undefined;
|
|
73
|
+
readonly [key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One captured output file as the dashboard reports it. Use
|
|
77
|
+
* `createOutputLink` to get a short-lived signed URL for download.
|
|
78
|
+
*/
|
|
79
|
+
export interface Output {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly filename?: string;
|
|
82
|
+
readonly sizeBytes?: number;
|
|
83
|
+
readonly contentType?: string;
|
|
84
|
+
readonly createdAt?: string;
|
|
85
|
+
readonly [key: string]: unknown;
|
|
86
|
+
}
|
|
87
|
+
export type OutputFilePathMatch = "exact" | "suffix";
|
|
88
|
+
export interface OutputFilePathSelector {
|
|
89
|
+
readonly path: string;
|
|
90
|
+
readonly match?: OutputFilePathMatch;
|
|
91
|
+
}
|
|
92
|
+
export interface OutputFileIdSelector {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
}
|
|
95
|
+
export type OutputFileSelector = Output | OutputFileIdSelector | OutputFilePathSelector;
|
|
96
|
+
export interface OutputFileDownload {
|
|
97
|
+
readonly output: Output;
|
|
98
|
+
readonly bytes: Uint8Array;
|
|
99
|
+
}
|
|
100
|
+
export interface SignedOutputLink {
|
|
101
|
+
readonly url: string;
|
|
102
|
+
readonly expiresAt?: string;
|
|
103
|
+
readonly [key: string]: unknown;
|
|
104
|
+
}
|
|
105
|
+
export interface WhoAmI {
|
|
106
|
+
readonly principalType: "api_token" | "user";
|
|
107
|
+
readonly workspaceId?: string;
|
|
108
|
+
readonly tokenId?: string;
|
|
109
|
+
readonly tokenName?: string | null;
|
|
110
|
+
readonly scopes?: readonly string[];
|
|
111
|
+
/**
|
|
112
|
+
* Workspace-level caps the BFF will enforce on subsequent calls.
|
|
113
|
+
* Surfaced so consumers (e.g. broll's app-side admission gate) can
|
|
114
|
+
* decide whether to keep their own gate or rely on platform headers.
|
|
115
|
+
* All fields optional — older BFFs may omit. Numbers are concrete
|
|
116
|
+
* snapshots at the time of the `whoami` call.
|
|
117
|
+
*/
|
|
118
|
+
readonly caps?: {
|
|
119
|
+
/** Token-bucket cap on POST /api/runs per minute, per workspace. */
|
|
120
|
+
readonly runSubmitPerMinute?: number;
|
|
121
|
+
/** Hard cap on concurrent non-terminal runs the workspace may hold. */
|
|
122
|
+
readonly maxConcurrentRuns?: number;
|
|
123
|
+
/** Storage cap (bytes) on captured output objects, workspace-wide. */
|
|
124
|
+
readonly storageCapBytes?: number;
|
|
125
|
+
/** Current captured-output usage in bytes. */
|
|
126
|
+
readonly storageUsedBytes?: number;
|
|
127
|
+
/**
|
|
128
|
+
* Wall-clock ceiling on a single run before forced termination.
|
|
129
|
+
* `null` means no aex-imposed cap, but this is **not unlimited
|
|
130
|
+
* overall**: the managed runner, infrastructure, or upstream provider may
|
|
131
|
+
* still impose a ceiling, and a run that exceeds it terminates regardless.
|
|
132
|
+
*/
|
|
133
|
+
readonly maxRunDurationMs?: number | null;
|
|
134
|
+
};
|
|
135
|
+
readonly [key: string]: unknown;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Workspace skill bundle as the dashboard BFF returns it. Mirrors a row
|
|
139
|
+
* of `skill_bundles` joined with its computed manifest. `state` is the
|
|
140
|
+
* upload lifecycle (`pending` -> `ready`); only `ready` rows are
|
|
141
|
+
* referenceable from a run. `deletedAt` is the soft-delete tombstone
|
|
142
|
+
* (`null` for live bundles).
|
|
143
|
+
*
|
|
144
|
+
* See the public architecture notes and server-side persistence schema for
|
|
145
|
+
* the authoritative shape.
|
|
146
|
+
*/
|
|
147
|
+
export interface Skill {
|
|
148
|
+
readonly id: string;
|
|
149
|
+
readonly workspaceId?: string;
|
|
150
|
+
readonly name: string;
|
|
151
|
+
readonly state: "pending" | "ready";
|
|
152
|
+
readonly hash?: string | null;
|
|
153
|
+
readonly sizeBytes?: number | null;
|
|
154
|
+
readonly fileCount?: number | null;
|
|
155
|
+
readonly manifest?: ReadonlyArray<{
|
|
156
|
+
readonly path: string;
|
|
157
|
+
readonly size: number;
|
|
158
|
+
readonly mode: number;
|
|
159
|
+
}>;
|
|
160
|
+
readonly createdAt?: string;
|
|
161
|
+
readonly updatedAt?: string;
|
|
162
|
+
readonly finalizedAt?: string | null;
|
|
163
|
+
readonly deletedAt?: string | null;
|
|
164
|
+
readonly [key: string]: unknown;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Wire-level record for a workspace AgentsMd file as returned by the BFF.
|
|
168
|
+
* Mirrors `PublicWorkspaceFile` from the dashboard service layer.
|
|
169
|
+
*/
|
|
170
|
+
export interface AgentsMdRecord {
|
|
171
|
+
readonly id: string;
|
|
172
|
+
readonly kind?: "agentsmd";
|
|
173
|
+
readonly name: string;
|
|
174
|
+
readonly state: "pending" | "ready";
|
|
175
|
+
readonly hash?: string | null;
|
|
176
|
+
readonly sizeBytes?: number | null;
|
|
177
|
+
readonly fileCount?: number | null;
|
|
178
|
+
readonly manifest?: ReadonlyArray<{
|
|
179
|
+
readonly path: string;
|
|
180
|
+
readonly size: number;
|
|
181
|
+
readonly mode: number;
|
|
182
|
+
}>;
|
|
183
|
+
readonly createdAt?: string;
|
|
184
|
+
readonly updatedAt?: string;
|
|
185
|
+
readonly finalizedAt?: string | null;
|
|
186
|
+
readonly deletedAt?: string | null;
|
|
187
|
+
readonly [key: string]: unknown;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Wire-level record for a workspace File as returned by the BFF.
|
|
191
|
+
* Mirrors `PublicWorkspaceFile` from the dashboard service layer
|
|
192
|
+
* with kind='file' and `f_*` ids.
|
|
193
|
+
*/
|
|
194
|
+
export interface FileRecord {
|
|
195
|
+
readonly id: string;
|
|
196
|
+
readonly kind?: "file";
|
|
197
|
+
readonly name: string;
|
|
198
|
+
readonly state: "pending" | "ready";
|
|
199
|
+
readonly hash?: string | null;
|
|
200
|
+
readonly sizeBytes?: number | null;
|
|
201
|
+
readonly fileCount?: number | null;
|
|
202
|
+
readonly manifest?: ReadonlyArray<{
|
|
203
|
+
readonly path: string;
|
|
204
|
+
readonly size: number;
|
|
205
|
+
readonly mode: number;
|
|
206
|
+
}>;
|
|
207
|
+
readonly createdAt?: string;
|
|
208
|
+
readonly updatedAt?: string;
|
|
209
|
+
readonly finalizedAt?: string | null;
|
|
210
|
+
readonly deletedAt?: string | null;
|
|
211
|
+
readonly [key: string]: unknown;
|
|
212
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type AexErrorCode = "RUN_CONFIG_INVALID" | "CREDENTIAL_INVALID" | "PROVIDER_ERROR" | "RUN_STATE_ERROR" | "CLEANUP_ERROR" | "RUNTIME_UNSUPPORTED" | "API_ERROR";
|
|
2
|
+
export declare class AexError extends Error {
|
|
3
|
+
readonly code: AexErrorCode;
|
|
4
|
+
readonly details?: unknown;
|
|
5
|
+
constructor(code: AexErrorCode, message: string, details?: unknown);
|
|
6
|
+
}
|
|
7
|
+
export declare class RunConfigValidationError extends AexError {
|
|
8
|
+
constructor(message: string, details?: unknown);
|
|
9
|
+
}
|
|
10
|
+
export declare class CredentialValidationError extends AexError {
|
|
11
|
+
constructor(message: string, details?: unknown);
|
|
12
|
+
}
|
|
13
|
+
export declare class ProviderError extends AexError {
|
|
14
|
+
readonly status: number | undefined;
|
|
15
|
+
constructor(message: string, options?: {
|
|
16
|
+
status?: number;
|
|
17
|
+
details?: unknown;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export declare class RunStateError extends AexError {
|
|
21
|
+
constructor(message: string, details?: unknown);
|
|
22
|
+
}
|
|
23
|
+
export declare class CleanupError extends AexError {
|
|
24
|
+
constructor(message: string, details?: unknown);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Thrown by SDK and CLI operations when the dashboard BFF returns a non-2xx
|
|
28
|
+
* response. Carries the HTTP status and parsed body for the caller to inspect.
|
|
29
|
+
*/
|
|
30
|
+
export declare class AexApiError extends AexError {
|
|
31
|
+
readonly status: number;
|
|
32
|
+
readonly body: unknown;
|
|
33
|
+
constructor(status: number, message: string, body: unknown);
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { redactSecrets } from "./sdk-secrets.js";
|
|
2
|
+
export class AexError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
details;
|
|
5
|
+
constructor(code, message, details) {
|
|
6
|
+
super(redactSecrets(message));
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.details = details === undefined ? undefined : redactSecrets(details);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class RunConfigValidationError extends AexError {
|
|
13
|
+
constructor(message, details) {
|
|
14
|
+
super("RUN_CONFIG_INVALID", message, details);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class CredentialValidationError extends AexError {
|
|
18
|
+
constructor(message, details) {
|
|
19
|
+
super("CREDENTIAL_INVALID", message, details);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ProviderError extends AexError {
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, options = {}) {
|
|
25
|
+
super("PROVIDER_ERROR", message, options.details);
|
|
26
|
+
this.status = options.status;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export class RunStateError extends AexError {
|
|
30
|
+
constructor(message, details) {
|
|
31
|
+
super("RUN_STATE_ERROR", message, details);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class CleanupError extends AexError {
|
|
35
|
+
constructor(message, details) {
|
|
36
|
+
super("CLEANUP_ERROR", message, details);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Thrown by SDK and CLI operations when the dashboard BFF returns a non-2xx
|
|
41
|
+
* response. Carries the HTTP status and parsed body for the caller to inspect.
|
|
42
|
+
*/
|
|
43
|
+
export class AexApiError extends AexError {
|
|
44
|
+
status;
|
|
45
|
+
body;
|
|
46
|
+
constructor(status, message, body) {
|
|
47
|
+
super("API_ERROR", message, body);
|
|
48
|
+
this.status = status;
|
|
49
|
+
this.body = redactSecrets(body);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=sdk-errors.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
export declare class SecretString {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(value: string, label?: string);
|
|
5
|
+
unwrap(): string;
|
|
6
|
+
toString(): string;
|
|
7
|
+
toJSON(): string;
|
|
8
|
+
}
|
|
9
|
+
export declare function redactSecrets<T>(value: T): T;
|
|
10
|
+
/**
|
|
11
|
+
* Redact every secret-shaped run in `input`.
|
|
12
|
+
*
|
|
13
|
+
* `known` is an OPTIONAL belt-and-suspenders set of literal values to also mask
|
|
14
|
+
* (e.g. creds the caller happens to have loaded). Correctness does NOT depend on
|
|
15
|
+
* it — every shape above is caught with or without seeding — but masking known
|
|
16
|
+
* values first removes any residual substring of a value the shapes split.
|
|
17
|
+
*/
|
|
18
|
+
export declare function redactString(input: string, known?: Iterable<string>): string;
|
|
19
|
+
export declare function containsSecretLikeValue(input: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* A line-buffered `Transform` that redacts secrets BEFORE bytes touch disk or
|
|
22
|
+
* console. Pipe a child process's stdout/stderr through it
|
|
23
|
+
* (`child.stdout.pipe(createRedactingStream()).pipe(process.stdout)`) so a
|
|
24
|
+
* secret can never be written and "cleaned up after"; the secret you didn't
|
|
25
|
+
* recognise is the one already on disk.
|
|
26
|
+
*
|
|
27
|
+
* Buffers by line so a secret split across a chunk boundary is still redacted:
|
|
28
|
+
* a partial trailing line is held until the next chunk (or `flush`) completes
|
|
29
|
+
* it. `known` is forwarded to `redactString` for optional value seeding.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createRedactingStream(known?: Iterable<string>): Transform;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* Value-AGNOSTIC secret patterns: each matches a SHAPE, not a known value.
|
|
4
|
+
* Correctness must not depend on seeding the redactor with the literal
|
|
5
|
+
* secret — the two real leaks this project hit were a database password that
|
|
6
|
+
* survived a naive `sed` mask (as a substring) and an Anthropic key emitted by
|
|
7
|
+
* `wrangler workflows describe` that the harness NEVER loaded. A value-seeded
|
|
8
|
+
* redactor is blind to both; these patterns catch them by form.
|
|
9
|
+
*
|
|
10
|
+
* Ordering matters: structured shapes (connection strings, auth headers, JWT)
|
|
11
|
+
* come BEFORE the generic key/entropy catch-alls so the more descriptive
|
|
12
|
+
* `[REDACTED …]` label wins on overlapping matches.
|
|
13
|
+
*/
|
|
14
|
+
const SECRET_PATTERNS = [
|
|
15
|
+
// postgres / postgresql connection strings — redact the whole URI so the
|
|
16
|
+
// embedded password can never survive as a substring (the `sed`-mask leak).
|
|
17
|
+
/\bpostgres(?:ql)?:\/\/[^\s"']+/gi,
|
|
18
|
+
// Authorization: Bearer <token> — redact the credential, keep the header name
|
|
19
|
+
// so logs stay legible.
|
|
20
|
+
/\b(Authorization\s*:\s*Bearer)\s+[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
21
|
+
// Anthropic + OpenAI-style prefixed keys.
|
|
22
|
+
/sk-ant-[A-Za-z0-9_-]{16,}/g,
|
|
23
|
+
/sk-[A-Za-z0-9_-]{20,}/g,
|
|
24
|
+
// aex workspace / proxy tokens: apt_… / ant_….
|
|
25
|
+
/\b(?:apt|ant)_[A-Za-z0-9_-]{16,}/g,
|
|
26
|
+
// Slack tokens.
|
|
27
|
+
/xox[pbar]-[A-Za-z0-9-]{10,}/g,
|
|
28
|
+
// AWS access key id + secret access key shapes.
|
|
29
|
+
/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
30
|
+
/\baws_secret_access_key["'\s:=]+[A-Za-z0-9/+=]{40}/gi,
|
|
31
|
+
// JWT-shaped: header.payload.signature, each base64url, header starts `eyJ`.
|
|
32
|
+
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
33
|
+
// Keyword-introduced secret assignments (bearer/token/api_key/…).
|
|
34
|
+
/(?:bearer|token|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)["'\s:=]+[A-Za-z0-9_\-./+=]{12,}/gi
|
|
35
|
+
];
|
|
36
|
+
const REDACTED = "[REDACTED]";
|
|
37
|
+
/**
|
|
38
|
+
* Generic high-entropy token catch-all, applied AFTER the named patterns.
|
|
39
|
+
* Catches the secret nobody recognised: any long base64url/hex-ish run that is
|
|
40
|
+
* BOTH high-entropy AND character-class-diverse.
|
|
41
|
+
*
|
|
42
|
+
* The candidate run deliberately EXCLUDES `_`: secrets are contiguous opaque
|
|
43
|
+
* runs, whereas `SCREAMING_SNAKE_CASE` env names and `snake_case` identifiers
|
|
44
|
+
* are `_`-separated words — excluding `_` breaks those into sub-24 fragments so
|
|
45
|
+
* they are never eaten (the `CF_CONFORMANCE_PROBE_URL` false positive). The
|
|
46
|
+
* class-diversity gate (≥2 of lower/upper/digit) then keeps the remaining
|
|
47
|
+
* single-class runs (all-lowercase module paths like `internal/modules/esm`)
|
|
48
|
+
* while still catching mixed-case/alnum secret blobs. Entropy alone cannot
|
|
49
|
+
* separate these — `the_quick…` (4.16) out-scores a real key prefix.
|
|
50
|
+
*/
|
|
51
|
+
const HIGH_ENTROPY_CANDIDATE = /[A-Za-z0-9+/=-]{24,}/g;
|
|
52
|
+
const ENTROPY_BITS_PER_CHAR = 3.0;
|
|
53
|
+
const MIN_CHAR_CLASSES = 2;
|
|
54
|
+
/**
|
|
55
|
+
* A mixed-case run with no digit and < this length is treated as a benign
|
|
56
|
+
* identifier, not a secret. Real opaque secrets are alnum-mixed (carry a
|
|
57
|
+
* digit) or very long; digit-free camelCase identifiers like
|
|
58
|
+
* `asyncRunEntryPointWithESMLoader` / `createDurableObjectNamespace` (which
|
|
59
|
+
* appear in stack traces the diagnostic bundle captures) are 24–39 chars and
|
|
60
|
+
* digit-free — eating them would gut debuggability. The length escape hatch
|
|
61
|
+
* still catches the rare long digit-free secret.
|
|
62
|
+
*/
|
|
63
|
+
const HIGH_ENTROPY_NO_DIGIT_MIN_LEN = 40;
|
|
64
|
+
export class SecretString {
|
|
65
|
+
#value;
|
|
66
|
+
constructor(value, label = "secret") {
|
|
67
|
+
if (!value) {
|
|
68
|
+
throw new Error(`${label} is required`);
|
|
69
|
+
}
|
|
70
|
+
this.#value = value;
|
|
71
|
+
}
|
|
72
|
+
unwrap() {
|
|
73
|
+
return this.#value;
|
|
74
|
+
}
|
|
75
|
+
toString() {
|
|
76
|
+
return REDACTED;
|
|
77
|
+
}
|
|
78
|
+
toJSON() {
|
|
79
|
+
return REDACTED;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function redactSecrets(value) {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
return redactString(value);
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value.map((item) => redactSecrets(item));
|
|
88
|
+
}
|
|
89
|
+
if (value && typeof value === "object") {
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [key, item] of Object.entries(value)) {
|
|
92
|
+
if (isSecretKey(key)) {
|
|
93
|
+
out[key] = REDACTED;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
out[key] = redactSecrets(item);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Redact every secret-shaped run in `input`.
|
|
105
|
+
*
|
|
106
|
+
* `known` is an OPTIONAL belt-and-suspenders set of literal values to also mask
|
|
107
|
+
* (e.g. creds the caller happens to have loaded). Correctness does NOT depend on
|
|
108
|
+
* it — every shape above is caught with or without seeding — but masking known
|
|
109
|
+
* values first removes any residual substring of a value the shapes split.
|
|
110
|
+
*/
|
|
111
|
+
export function redactString(input, known = []) {
|
|
112
|
+
let out = input;
|
|
113
|
+
for (const value of known) {
|
|
114
|
+
if (value && value.length >= 4) {
|
|
115
|
+
out = out.split(value).join(REDACTED);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
out = SECRET_PATTERNS.reduce((current, pattern) => current.replace(pattern, (match, captured) =>
|
|
119
|
+
// Patterns with a captured prefix (Authorization header) keep the
|
|
120
|
+
// prefix; the rest replace the whole match.
|
|
121
|
+
typeof captured === "string" ? `${captured} ${REDACTED}` : REDACTED), out);
|
|
122
|
+
return out.replace(HIGH_ENTROPY_CANDIDATE, (match) => looksHighEntropySecret(match) ? REDACTED : match);
|
|
123
|
+
}
|
|
124
|
+
export function containsSecretLikeValue(input) {
|
|
125
|
+
if (SECRET_PATTERNS.some((pattern) => {
|
|
126
|
+
pattern.lastIndex = 0;
|
|
127
|
+
return pattern.test(input);
|
|
128
|
+
})) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
HIGH_ENTROPY_CANDIDATE.lastIndex = 0;
|
|
132
|
+
let candidate;
|
|
133
|
+
while ((candidate = HIGH_ENTROPY_CANDIDATE.exec(input)) !== null) {
|
|
134
|
+
if (looksHighEntropySecret(candidate[0])) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A line-buffered `Transform` that redacts secrets BEFORE bytes touch disk or
|
|
142
|
+
* console. Pipe a child process's stdout/stderr through it
|
|
143
|
+
* (`child.stdout.pipe(createRedactingStream()).pipe(process.stdout)`) so a
|
|
144
|
+
* secret can never be written and "cleaned up after"; the secret you didn't
|
|
145
|
+
* recognise is the one already on disk.
|
|
146
|
+
*
|
|
147
|
+
* Buffers by line so a secret split across a chunk boundary is still redacted:
|
|
148
|
+
* a partial trailing line is held until the next chunk (or `flush`) completes
|
|
149
|
+
* it. `known` is forwarded to `redactString` for optional value seeding.
|
|
150
|
+
*/
|
|
151
|
+
export function createRedactingStream(known = []) {
|
|
152
|
+
const knownValues = [...known];
|
|
153
|
+
let carry = "";
|
|
154
|
+
return new Transform({
|
|
155
|
+
transform(chunk, _encoding, callback) {
|
|
156
|
+
carry += chunk.toString();
|
|
157
|
+
const lastNewline = carry.lastIndexOf("\n");
|
|
158
|
+
if (lastNewline === -1) {
|
|
159
|
+
callback();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const ready = carry.slice(0, lastNewline + 1);
|
|
163
|
+
carry = carry.slice(lastNewline + 1);
|
|
164
|
+
callback(null, redactString(ready, knownValues));
|
|
165
|
+
},
|
|
166
|
+
flush(callback) {
|
|
167
|
+
if (carry === "") {
|
|
168
|
+
callback();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const tail = carry;
|
|
172
|
+
carry = "";
|
|
173
|
+
callback(null, redactString(tail, knownValues));
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function isSecretKey(key) {
|
|
178
|
+
return /(?:api[_-]?key|authorization|token|secret|password|credential)/i.test(key);
|
|
179
|
+
}
|
|
180
|
+
/** A candidate run is a high-entropy secret if it is both information-dense
|
|
181
|
+
* AND mixes character classes (the property that separates opaque secret blobs
|
|
182
|
+
* from single-class identifiers/paths). */
|
|
183
|
+
function looksHighEntropySecret(value) {
|
|
184
|
+
if (charClassCount(value) < MIN_CHAR_CLASSES)
|
|
185
|
+
return false;
|
|
186
|
+
if (shannonEntropyBits(value) < ENTROPY_BITS_PER_CHAR)
|
|
187
|
+
return false;
|
|
188
|
+
// Require a digit OR extreme length so digit-free mixed-case identifiers
|
|
189
|
+
// (stack-trace frames, API symbol names) are not destroyed in diagnostic
|
|
190
|
+
// output, while real secret shapes (alnum-mixed, or very long) still match.
|
|
191
|
+
return /[0-9]/.test(value) || value.length >= HIGH_ENTROPY_NO_DIGIT_MIN_LEN;
|
|
192
|
+
}
|
|
193
|
+
/** How many of {lowercase, uppercase, digit} appear in `value` (0–3). */
|
|
194
|
+
function charClassCount(value) {
|
|
195
|
+
let count = 0;
|
|
196
|
+
if (/[a-z]/.test(value))
|
|
197
|
+
count++;
|
|
198
|
+
if (/[A-Z]/.test(value))
|
|
199
|
+
count++;
|
|
200
|
+
if (/[0-9]/.test(value))
|
|
201
|
+
count++;
|
|
202
|
+
return count;
|
|
203
|
+
}
|
|
204
|
+
/** Shannon entropy in bits/char — the per-character information density. */
|
|
205
|
+
function shannonEntropyBits(value) {
|
|
206
|
+
if (value.length === 0) {
|
|
207
|
+
return 0;
|
|
208
|
+
}
|
|
209
|
+
const counts = new Map();
|
|
210
|
+
for (const char of value) {
|
|
211
|
+
counts.set(char, (counts.get(char) ?? 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
let bits = 0;
|
|
214
|
+
for (const count of counts.values()) {
|
|
215
|
+
const p = count / value.length;
|
|
216
|
+
bits -= p * Math.log2(p);
|
|
217
|
+
}
|
|
218
|
+
return bits;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=sdk-secrets.js.map
|