agentcert 0.3.0 → 0.5.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/README.md +29 -0
- package/dist/sandbox.js +165 -3
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +3 -2
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -1
- package/dist/vendor/onegent-runtime/sandbox-hosted.js +24 -1
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,35 @@ certifications return a non-zero exit status. The generated template is
|
|
|
82
82
|
synthetic and network-denied; it must not contain production credentials or
|
|
83
83
|
connect to live systems.
|
|
84
84
|
|
|
85
|
+
Run one bounded read against an existing Stripe sandbox PaymentIntent:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
STRIPE_RESTRICTED_TEST_KEY="rk_test_..." npx agentcert sandbox stripe-readonly --payment-intent pi_...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Add `--push` to retain the redacted report in AgentCert Hosted. The command
|
|
92
|
+
allows only `https://api.stripe.com`, `GET`, and allowlisted PaymentIntent
|
|
93
|
+
routes, with a 5-second timeout and 10-request-per-minute process-local cap.
|
|
94
|
+
The Stripe key is environment-only and is never written to output or evidence.
|
|
95
|
+
See the [Bounded Vendor Sandbox Egress guide](https://github.com/Kakarottoooo/agentcert/blob/main/docs/bounded-vendor-sandbox-egress.md).
|
|
96
|
+
|
|
97
|
+
Release maintainers can run the protected, manual
|
|
98
|
+
`Real Stripe sandbox acceptance` GitHub workflow. It performs the bounded read,
|
|
99
|
+
independently scans the generated report before upload, validates it again at
|
|
100
|
+
the CLI boundary, uploads the v0.4 evidence, and compares it with prior
|
|
101
|
+
protected runs. See the [real vendor acceptance guide](https://github.com/Kakarottoooo/agentcert/blob/main/docs/real-vendor-acceptance.md).
|
|
102
|
+
|
|
103
|
+
The workflow's upload step is also available as a narrow command for an
|
|
104
|
+
already-generated report:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx agentcert sandbox upload-report --report .agentcert/vendor-sandbox/current-report.json --external-id vendor-acceptance:stripe:<run>:<attempt>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This command accepts only AgentCert sandbox conformance and vendor-egress
|
|
111
|
+
contracts and rejects reports containing credential-shaped values or forbidden
|
|
112
|
+
sensitive fields.
|
|
113
|
+
|
|
85
114
|
Review/export helpers:
|
|
86
115
|
|
|
87
116
|
```bash
|
package/dist/sandbox.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { resolveConnection } from "./credentials.js";
|
|
5
5
|
const DEFAULT_ADAPTER_PATH = "agentcert.sandbox.mjs";
|
|
6
6
|
const DEFAULT_REPORT_PATH = ".agentcert/sandbox/sandbox-adapter-conformance.json";
|
|
7
|
+
const DEFAULT_STRIPE_REPORT_PATH = ".agentcert/sandbox/stripe-readonly-report.json";
|
|
7
8
|
export async function runSandboxCommand(args) {
|
|
8
9
|
const action = args[0] ?? "help";
|
|
9
10
|
if (action === "help" || args.includes("--help") || args.includes("-h")) {
|
|
@@ -16,6 +17,10 @@ export async function runSandboxCommand(args) {
|
|
|
16
17
|
return certifySandboxAdapter(args.slice(1));
|
|
17
18
|
if (action === "push")
|
|
18
19
|
return pushSandboxCertification(args.slice(1));
|
|
20
|
+
if (action === "stripe-readonly")
|
|
21
|
+
return runStripeSandboxReadOnly(args.slice(1));
|
|
22
|
+
if (action === "upload-report")
|
|
23
|
+
return uploadSandboxReport(args.slice(1));
|
|
19
24
|
throw new Error(`Unknown sandbox command ${JSON.stringify(action)}. Run \`npx agentcert sandbox --help\`.`);
|
|
20
25
|
}
|
|
21
26
|
export async function initializeSandboxAdapter(args) {
|
|
@@ -62,6 +67,97 @@ export async function pushSandboxCertification(args, runtimeOverride) {
|
|
|
62
67
|
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
63
68
|
return certified;
|
|
64
69
|
}
|
|
70
|
+
export async function runStripeSandboxReadOnly(args, runtimeOverride) {
|
|
71
|
+
if (args.includes("--stripe-key") || args.includes("--restricted-key")) {
|
|
72
|
+
throw new Error("Stripe credentials are accepted only through STRIPE_RESTRICTED_TEST_KEY, never a CLI flag.");
|
|
73
|
+
}
|
|
74
|
+
const paymentIntentId = flag(args, "--payment-intent");
|
|
75
|
+
if (!paymentIntentId)
|
|
76
|
+
throw new Error("Stripe sandbox read requires --payment-intent <pi_...>.");
|
|
77
|
+
const restrictedApiKey = process.env.STRIPE_RESTRICTED_TEST_KEY?.trim();
|
|
78
|
+
if (!restrictedApiKey) {
|
|
79
|
+
throw new Error("Set STRIPE_RESTRICTED_TEST_KEY to a read-only Stripe rk_test_ restricted key.");
|
|
80
|
+
}
|
|
81
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
82
|
+
if (!runtime.runStripeSandboxReadOnlyCertification) {
|
|
83
|
+
throw new Error("AgentCert Stripe sandbox runtime is missing. Reinstall the agentcert package and retry.");
|
|
84
|
+
}
|
|
85
|
+
const report = await runtime.runStripeSandboxReadOnlyCertification({ restrictedApiKey, paymentIntentId });
|
|
86
|
+
const reportPath = resolve(flag(args, "--out") ?? DEFAULT_STRIPE_REPORT_PATH);
|
|
87
|
+
await mkdir(dirname(reportPath), { recursive: true });
|
|
88
|
+
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
89
|
+
renderVendorReadResult(report, reportPath);
|
|
90
|
+
if (args.includes("--push")) {
|
|
91
|
+
const connection = await resolveConnection({
|
|
92
|
+
name: flag(args, "--connection"),
|
|
93
|
+
server: flag(args, "--server"),
|
|
94
|
+
projectId: flag(args, "--project"),
|
|
95
|
+
apiKey: flag(args, "--api-key"),
|
|
96
|
+
});
|
|
97
|
+
const uploaded = await runtime.uploadSandboxCertificationReport(report, {
|
|
98
|
+
baseUrl: connection.server,
|
|
99
|
+
projectId: connection.projectId,
|
|
100
|
+
apiKey: connection.apiKey,
|
|
101
|
+
externalId: flag(args, "--external-id"),
|
|
102
|
+
});
|
|
103
|
+
process.stdout.write(`Hosted sandbox run: ${String(uploaded.run.id ?? "created")}\n`);
|
|
104
|
+
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
105
|
+
}
|
|
106
|
+
return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
|
|
107
|
+
}
|
|
108
|
+
export async function uploadSandboxReport(args, runtimeOverride) {
|
|
109
|
+
const requestedPath = flag(args, "--report");
|
|
110
|
+
if (!requestedPath)
|
|
111
|
+
throw new Error("Sandbox report upload requires --report <report.json>.");
|
|
112
|
+
const reportPath = resolve(requestedPath);
|
|
113
|
+
const report = parseUploadableSandboxReport(await readFile(reportPath, "utf8"));
|
|
114
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
115
|
+
const connection = await resolveConnection({
|
|
116
|
+
name: flag(args, "--connection"),
|
|
117
|
+
server: flag(args, "--server"),
|
|
118
|
+
projectId: flag(args, "--project"),
|
|
119
|
+
apiKey: flag(args, "--api-key"),
|
|
120
|
+
});
|
|
121
|
+
const uploaded = await runtime.uploadSandboxCertificationReport(report, {
|
|
122
|
+
baseUrl: connection.server,
|
|
123
|
+
projectId: connection.projectId,
|
|
124
|
+
apiKey: connection.apiKey,
|
|
125
|
+
externalId: flag(args, "--external-id"),
|
|
126
|
+
});
|
|
127
|
+
process.stdout.write(`Uploaded validated sandbox report: ${reportPath}\n`);
|
|
128
|
+
process.stdout.write(`Hosted sandbox run: ${String(uploaded.run.id ?? "created")}\n`);
|
|
129
|
+
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
130
|
+
return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
|
|
131
|
+
}
|
|
132
|
+
export function parseUploadableSandboxReport(rawText) {
|
|
133
|
+
let value;
|
|
134
|
+
try {
|
|
135
|
+
value = JSON.parse(rawText);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new Error("Sandbox report is not valid JSON.");
|
|
139
|
+
}
|
|
140
|
+
if (!isRecord(value))
|
|
141
|
+
throw new Error("Sandbox report must be a JSON object.");
|
|
142
|
+
const supported = (value.kind === "agentcert.sandbox_adapter_conformance"
|
|
143
|
+
&& value.schemaVersion === "agentcert.sandbox_adapter_conformance.v0.2")
|
|
144
|
+
|| (value.kind === "agentcert.sandbox_vendor_egress"
|
|
145
|
+
&& value.schemaVersion === "agentcert.sandbox_vendor_egress.v0.4");
|
|
146
|
+
if (!supported)
|
|
147
|
+
throw new Error("Sandbox report kind or schema version is not supported for upload.");
|
|
148
|
+
if (typeof value.implementation !== "string" || !value.implementation.trim()
|
|
149
|
+
|| typeof value.generatedAt !== "string" || !Number.isFinite(Date.parse(value.generatedAt))
|
|
150
|
+
|| !isRecord(value.verdict) || typeof value.verdict.passed !== "boolean"
|
|
151
|
+
|| typeof value.verdict.score !== "number" || value.verdict.score < 0 || value.verdict.score > 100
|
|
152
|
+
|| !Array.isArray(value.checks)) {
|
|
153
|
+
throw new Error("Sandbox report is missing required conformance fields.");
|
|
154
|
+
}
|
|
155
|
+
const findings = sensitiveReportFindings(value, rawText);
|
|
156
|
+
if (findings.length > 0) {
|
|
157
|
+
throw new Error(`Sandbox report contains forbidden credential material: ${findings.join(", ")}.`);
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
65
161
|
export async function loadSandboxAdapter(adapterPath) {
|
|
66
162
|
let module;
|
|
67
163
|
try {
|
|
@@ -96,16 +192,37 @@ Runs the deterministic sandbox adapter conformance suite locally.
|
|
|
96
192
|
|
|
97
193
|
Runs certification, writes the local report, and uploads it through the saved AgentCert connection.
|
|
98
194
|
Use \`agentcert connect\` first, or pass --server, --project, and --api-key.
|
|
195
|
+
`;
|
|
196
|
+
if (action === "stripe-readonly")
|
|
197
|
+
return `Usage:
|
|
198
|
+
STRIPE_RESTRICTED_TEST_KEY=rk_test_... agentcert sandbox stripe-readonly --payment-intent pi_...
|
|
199
|
+
agentcert sandbox stripe-readonly --payment-intent pi_... --push
|
|
200
|
+
|
|
201
|
+
Reads one Stripe sandbox PaymentIntent through a fixed GET/resource allowlist,
|
|
202
|
+
writes a redacted evidence report, and optionally uploads it to AgentCert Hosted.
|
|
203
|
+
The Stripe key is read only from STRIPE_RESTRICTED_TEST_KEY.
|
|
204
|
+
`;
|
|
205
|
+
if (action === "upload-report")
|
|
206
|
+
return `Usage:
|
|
207
|
+
agentcert sandbox upload-report --report .agentcert/vendor-sandbox/current-report.json --external-id <id>
|
|
208
|
+
|
|
209
|
+
Validates a recognized AgentCert sandbox report, rejects credential material,
|
|
210
|
+
and uploads it through the saved AgentCert connection. This command never
|
|
211
|
+
executes a vendor request.
|
|
99
212
|
`;
|
|
100
213
|
return `Usage:
|
|
101
214
|
agentcert sandbox init [--adapter agentcert.sandbox.mjs]
|
|
102
215
|
agentcert sandbox certify --adapter ./my-sandbox-adapter.js
|
|
103
216
|
agentcert sandbox push --adapter ./my-sandbox-adapter.js
|
|
217
|
+
agentcert sandbox stripe-readonly --payment-intent pi_... [--push]
|
|
218
|
+
agentcert sandbox upload-report --report <report.json>
|
|
104
219
|
|
|
105
220
|
Commands:
|
|
106
221
|
init Write one dependency-free adapter template
|
|
107
222
|
certify Run deterministic local conformance checks
|
|
108
223
|
push Certify and upload the report to AgentCert Hosted
|
|
224
|
+
stripe-readonly Run one bounded Stripe sandbox read and write evidence
|
|
225
|
+
upload-report Validate and upload an existing sandbox report
|
|
109
226
|
|
|
110
227
|
Common options:
|
|
111
228
|
--adapter <path> Adapter module (default: agentcert.sandbox.mjs)
|
|
@@ -117,9 +234,14 @@ Common options:
|
|
|
117
234
|
async function loadSandboxRuntime() {
|
|
118
235
|
const adapterKitUrl = new URL("./vendor/onegent-runtime/sandbox-adapter-kit.js", import.meta.url);
|
|
119
236
|
const hostedUrl = new URL("./vendor/onegent-runtime/sandbox-hosted.js", import.meta.url);
|
|
237
|
+
const stripeUrl = new URL("./vendor/onegent-runtime/stripe-test-readonly.js", import.meta.url);
|
|
120
238
|
try {
|
|
121
|
-
const [adapterKit, hosted] = await Promise.all([
|
|
122
|
-
|
|
239
|
+
const [adapterKit, hosted, stripe] = await Promise.all([
|
|
240
|
+
import(adapterKitUrl.href),
|
|
241
|
+
import(hostedUrl.href),
|
|
242
|
+
import(stripeUrl.href),
|
|
243
|
+
]);
|
|
244
|
+
return { ...adapterKit, ...hosted, ...stripe };
|
|
123
245
|
}
|
|
124
246
|
catch (error) {
|
|
125
247
|
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
@@ -142,6 +264,38 @@ function isSandboxSystem(value) {
|
|
|
142
264
|
&& ["createTenant", "deleteTenant", "resetTenant", "seedTenant", "hasTenant", "snapshotTenant", "adapterForTenant"]
|
|
143
265
|
.every((method) => typeof system[method] === "function");
|
|
144
266
|
}
|
|
267
|
+
function sensitiveReportFindings(value, rawText) {
|
|
268
|
+
const findings = new Set();
|
|
269
|
+
const forbiddenFields = new Set(["authorization", "headers", "apikey", "restrictedapikey", "secretkey", "clientsecret", "metadata", "rawresponse", "responsebody"]);
|
|
270
|
+
const inspect = (entry) => {
|
|
271
|
+
if (Array.isArray(entry)) {
|
|
272
|
+
entry.forEach(inspect);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!isRecord(entry))
|
|
276
|
+
return;
|
|
277
|
+
for (const [key, nested] of Object.entries(entry)) {
|
|
278
|
+
if (forbiddenFields.has(key.replace(/[^a-z]/gi, "").toLowerCase()))
|
|
279
|
+
findings.add(`field:${key}`);
|
|
280
|
+
inspect(nested);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
inspect(value);
|
|
284
|
+
if (/\b(?:rk|sk)_(?:test|live)_[A-Za-z0-9_]{8,}\b/.test(rawText))
|
|
285
|
+
findings.add("stripe-key");
|
|
286
|
+
if (/\bac_live_[A-Za-z0-9_-]{8,}\b/.test(rawText))
|
|
287
|
+
findings.add("agentcert-key");
|
|
288
|
+
if (/\bBearer\s+\S+/i.test(rawText))
|
|
289
|
+
findings.add("authorization-value");
|
|
290
|
+
for (const secret of [process.env.STRIPE_RESTRICTED_TEST_KEY, process.env.AGENTCERT_API_KEY]) {
|
|
291
|
+
if (secret && secret.length >= 8 && rawText.includes(secret))
|
|
292
|
+
findings.add("exact-secret");
|
|
293
|
+
}
|
|
294
|
+
return [...findings].sort();
|
|
295
|
+
}
|
|
296
|
+
function isRecord(value) {
|
|
297
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
298
|
+
}
|
|
145
299
|
function flag(args, name) {
|
|
146
300
|
const index = args.indexOf(name);
|
|
147
301
|
return index >= 0 ? args[index + 1] : undefined;
|
|
@@ -165,6 +319,14 @@ function renderCertificationResult(report, reportPath) {
|
|
|
165
319
|
}
|
|
166
320
|
process.stdout.write(`Report: ${reportPath}\n`);
|
|
167
321
|
}
|
|
322
|
+
function renderVendorReadResult(report, reportPath) {
|
|
323
|
+
process.stdout.write(`${report.verdict.passed ? "PASS" : "FAIL"} ${report.verdict.score}/100 Stripe sandbox bounded read\n`);
|
|
324
|
+
for (const check of report.checks) {
|
|
325
|
+
process.stdout.write(`- ${check.status === "passed" ? "PASS" : "FAIL"} ${check.id}: ${check.message}\n`);
|
|
326
|
+
}
|
|
327
|
+
process.stdout.write(`Requests audited: ${report.audit.length}\n`);
|
|
328
|
+
process.stdout.write(`Report: ${reportPath}\n`);
|
|
329
|
+
}
|
|
168
330
|
function sandboxAdapterTemplate() {
|
|
169
331
|
return `// Synthetic local state only. Do not import production credentials or call live systems here.
|
|
170
332
|
const tenants = new Map();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { SandboxAdapterConformanceReport } from "./sandbox-adapter-kit.js";
|
|
2
2
|
import type { SandboxCertificationReport } from "./sandbox-harness.js";
|
|
3
|
+
import type { StripeSandboxReadOnlyReport } from "./stripe-test-readonly.js";
|
|
3
4
|
export declare const SANDBOX_EVIDENCE_BUNDLE_SCHEMA_VERSION: "agentcert.evidence.v0.1";
|
|
4
|
-
export type HostedSandboxReport = SandboxCertificationReport | SandboxAdapterConformanceReport;
|
|
5
|
+
export type HostedSandboxReport = SandboxCertificationReport | SandboxAdapterConformanceReport | StripeSandboxReadOnlyReport;
|
|
5
6
|
export interface SandboxHostedUploadOptions {
|
|
6
7
|
baseUrl: string;
|
|
7
8
|
projectId: string;
|
|
@@ -48,7 +49,7 @@ export interface SandboxCertificationEvidenceBundle {
|
|
|
48
49
|
artifacts: Record<string, string>;
|
|
49
50
|
evidence: Array<{
|
|
50
51
|
id: string;
|
|
51
|
-
kind: "sandbox_adapter_conformance" | "sandbox_certification";
|
|
52
|
+
kind: "sandbox_adapter_conformance" | "sandbox_certification" | "sandbox_vendor_egress";
|
|
52
53
|
severity: "info" | "high";
|
|
53
54
|
message: string;
|
|
54
55
|
source: "onegent-runtime";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-hosted.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/sandbox-hosted.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAC;AAChF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"sandbox-hosted.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/sandbox-hosted.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,7 +5,11 @@ export function createSandboxCertificationEvidenceBundle(report) {
|
|
|
5
5
|
const runId = `sandbox-${reportDigest}`;
|
|
6
6
|
const evidence = [{
|
|
7
7
|
id: `${report.kind}:${reportDigest.slice(0, 16)}`,
|
|
8
|
-
kind: report.kind === "agentcert.sandbox_adapter_conformance"
|
|
8
|
+
kind: report.kind === "agentcert.sandbox_adapter_conformance"
|
|
9
|
+
? "sandbox_adapter_conformance"
|
|
10
|
+
: report.kind === "agentcert.sandbox_vendor_egress"
|
|
11
|
+
? "sandbox_vendor_egress"
|
|
12
|
+
: "sandbox_certification",
|
|
9
13
|
severity: report.verdict.passed ? "info" : "high",
|
|
10
14
|
message: `${report.kind} ${report.verdict.passed ? "passed" : "failed"} (${report.verdict.score}/100).`,
|
|
11
15
|
source: "onegent-runtime",
|
|
@@ -64,6 +68,15 @@ export async function uploadSandboxCertificationReport(report, options) {
|
|
|
64
68
|
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
65
69
|
const operationId = `sandbox-upload-${digest.slice(0, 32)}`;
|
|
66
70
|
const externalId = options.externalId?.trim() || `${report.kind}:${report.implementation}:${report.generatedAt}`;
|
|
71
|
+
const vendorMetadata = report.kind === "agentcert.sandbox_vendor_egress"
|
|
72
|
+
? {
|
|
73
|
+
vendor: report.vendor,
|
|
74
|
+
environment: report.environment,
|
|
75
|
+
policySha256: createHash("sha256").update(canonicalJson(report.policy)).digest("hex"),
|
|
76
|
+
requestDurationMs: report.audit.find((entry) => entry.outcome === "allowed")?.durationMs,
|
|
77
|
+
acceptanceType: externalId.startsWith("vendor-acceptance:stripe:") ? "real_vendor_sandbox" : "vendor_sandbox",
|
|
78
|
+
}
|
|
79
|
+
: {};
|
|
67
80
|
const run = await jsonRequest(requestFetch, `${projectUrl}/runs`, apiKey, {
|
|
68
81
|
method: "POST",
|
|
69
82
|
headers: { "idempotency-key": `${operationId}:run` },
|
|
@@ -77,6 +90,7 @@ export async function uploadSandboxCertificationReport(report, options) {
|
|
|
77
90
|
evidenceType: report.kind,
|
|
78
91
|
implementation: report.implementation,
|
|
79
92
|
sandboxOnly: true,
|
|
93
|
+
...vendorMetadata,
|
|
80
94
|
},
|
|
81
95
|
}),
|
|
82
96
|
});
|
|
@@ -127,3 +141,12 @@ function required(value, name) {
|
|
|
127
141
|
throw new Error(`Sandbox hosted upload ${name} is required.`);
|
|
128
142
|
return normalized;
|
|
129
143
|
}
|
|
144
|
+
function canonicalJson(value) {
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
147
|
+
if (value && typeof value === "object") {
|
|
148
|
+
const record = value;
|
|
149
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
150
|
+
}
|
|
151
|
+
return JSON.stringify(value);
|
|
152
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type VendorSandboxRequestAudit } from "./vendor-sandbox-egress.js";
|
|
2
|
+
export declare const STRIPE_TEST_API_ORIGIN: "https://api.stripe.com";
|
|
3
|
+
export declare const STRIPE_SANDBOX_TIMEOUT_MS = 5000;
|
|
4
|
+
export declare const STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE = 10;
|
|
5
|
+
export declare const STRIPE_PAYMENT_INTENT_RETRIEVE: "stripe.payment_intent.retrieve";
|
|
6
|
+
export declare const STRIPE_PAYMENT_INTENT_LIST: "stripe.payment_intent.list";
|
|
7
|
+
export interface StripePaymentIntentSnapshot {
|
|
8
|
+
id: string;
|
|
9
|
+
object: "payment_intent";
|
|
10
|
+
livemode: false;
|
|
11
|
+
amount?: number;
|
|
12
|
+
amountCapturable?: number;
|
|
13
|
+
amountReceived?: number;
|
|
14
|
+
currency?: string;
|
|
15
|
+
status?: string;
|
|
16
|
+
created?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface StripeTestModeReadOnlyAdapter {
|
|
19
|
+
readonly name: "stripe-test-mode-readonly";
|
|
20
|
+
readonly safety: {
|
|
21
|
+
readonly mode: "sandbox";
|
|
22
|
+
readonly access: "read-only";
|
|
23
|
+
readonly vendor: "stripe";
|
|
24
|
+
readonly allowedOrigins: readonly [typeof STRIPE_TEST_API_ORIGIN];
|
|
25
|
+
readonly allowedMethods: readonly ["GET"];
|
|
26
|
+
readonly allowedResources: readonly [typeof STRIPE_PAYMENT_INTENT_RETRIEVE, typeof STRIPE_PAYMENT_INTENT_LIST];
|
|
27
|
+
readonly timeoutMs: number;
|
|
28
|
+
readonly maxRequestsPerMinute: number;
|
|
29
|
+
};
|
|
30
|
+
retrievePaymentIntent(id: string): Promise<StripePaymentIntentSnapshot>;
|
|
31
|
+
listPaymentIntents(limit?: number): Promise<StripePaymentIntentSnapshot[]>;
|
|
32
|
+
getRequestAudit(): VendorSandboxRequestAudit[];
|
|
33
|
+
}
|
|
34
|
+
export interface StripeTestModeReadOnlyOptions {
|
|
35
|
+
restrictedApiKey: string;
|
|
36
|
+
fetch?: typeof fetch;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
maxRequestsPerMinute?: number;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
}
|
|
41
|
+
export interface StripeSandboxReadOnlyCheck {
|
|
42
|
+
id: string;
|
|
43
|
+
status: "passed" | "failed";
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
export interface StripeSandboxReadOnlyReport {
|
|
47
|
+
schemaVersion: "agentcert.sandbox_vendor_egress.v0.4";
|
|
48
|
+
kind: "agentcert.sandbox_vendor_egress";
|
|
49
|
+
implementation: "stripe-payment-intent-readonly";
|
|
50
|
+
vendor: "stripe";
|
|
51
|
+
environment: "sandbox";
|
|
52
|
+
generatedAt: string;
|
|
53
|
+
verdict: {
|
|
54
|
+
passed: boolean;
|
|
55
|
+
score: number;
|
|
56
|
+
};
|
|
57
|
+
summary: {
|
|
58
|
+
passed: number;
|
|
59
|
+
failed: number;
|
|
60
|
+
total: number;
|
|
61
|
+
};
|
|
62
|
+
checks: StripeSandboxReadOnlyCheck[];
|
|
63
|
+
policy: {
|
|
64
|
+
allowedOrigins: readonly [typeof STRIPE_TEST_API_ORIGIN];
|
|
65
|
+
allowedMethods: readonly ["GET"];
|
|
66
|
+
allowedResources: readonly [typeof STRIPE_PAYMENT_INTENT_RETRIEVE, typeof STRIPE_PAYMENT_INTENT_LIST];
|
|
67
|
+
timeoutMs: number;
|
|
68
|
+
maxRequestsPerMinute: number;
|
|
69
|
+
};
|
|
70
|
+
audit: VendorSandboxRequestAudit[];
|
|
71
|
+
observation?: StripePaymentIntentSnapshot;
|
|
72
|
+
disclaimer: string;
|
|
73
|
+
}
|
|
74
|
+
export interface RunStripeSandboxReadOnlyOptions extends StripeTestModeReadOnlyOptions {
|
|
75
|
+
paymentIntentId: string;
|
|
76
|
+
}
|
|
77
|
+
export declare function createStripeTestModeReadOnlyAdapter(options: StripeTestModeReadOnlyOptions): StripeTestModeReadOnlyAdapter;
|
|
78
|
+
export declare function runStripeSandboxReadOnlyCertification(options: RunStripeSandboxReadOnlyOptions): Promise<StripeSandboxReadOnlyReport>;
|
|
79
|
+
//# sourceMappingURL=stripe-test-readonly.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stripe-test-readonly.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/stripe-test-readonly.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createBoundedVendorSandboxEgress, VendorSandboxEgressError, } from "./vendor-sandbox-egress.js";
|
|
2
|
+
export const STRIPE_TEST_API_ORIGIN = "https://api.stripe.com";
|
|
3
|
+
export const STRIPE_SANDBOX_TIMEOUT_MS = 5_000;
|
|
4
|
+
export const STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE = 10;
|
|
5
|
+
export const STRIPE_PAYMENT_INTENT_RETRIEVE = "stripe.payment_intent.retrieve";
|
|
6
|
+
export const STRIPE_PAYMENT_INTENT_LIST = "stripe.payment_intent.list";
|
|
7
|
+
const STRIPE_RESOURCES = Object.freeze([
|
|
8
|
+
Object.freeze({
|
|
9
|
+
id: STRIPE_PAYMENT_INTENT_RETRIEVE,
|
|
10
|
+
method: "GET",
|
|
11
|
+
pathPattern: /^\/v1\/payment_intents\/pi_[A-Za-z0-9]{8,128}$/,
|
|
12
|
+
}),
|
|
13
|
+
Object.freeze({
|
|
14
|
+
id: STRIPE_PAYMENT_INTENT_LIST,
|
|
15
|
+
method: "GET",
|
|
16
|
+
pathPattern: /^\/v1\/payment_intents\?limit=(?:[1-9]|10)$/,
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
export function createStripeTestModeReadOnlyAdapter(options) {
|
|
20
|
+
const apiKey = options.restrictedApiKey.trim();
|
|
21
|
+
if (!/^rk_test_[A-Za-z0-9_]{8,}$/.test(apiKey)) {
|
|
22
|
+
throw new Error("Stripe read-only reference adapter requires an rk_test_ restricted test-mode key.");
|
|
23
|
+
}
|
|
24
|
+
const timeoutMs = options.timeoutMs ?? STRIPE_SANDBOX_TIMEOUT_MS;
|
|
25
|
+
const maxRequestsPerMinute = options.maxRequestsPerMinute ?? STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE;
|
|
26
|
+
const egress = createBoundedVendorSandboxEgress({
|
|
27
|
+
policy: {
|
|
28
|
+
vendor: "stripe",
|
|
29
|
+
allowedOrigin: STRIPE_TEST_API_ORIGIN,
|
|
30
|
+
resources: STRIPE_RESOURCES,
|
|
31
|
+
timeoutMs,
|
|
32
|
+
maxRequestsPerMinute,
|
|
33
|
+
},
|
|
34
|
+
fetch: options.fetch,
|
|
35
|
+
now: options.now,
|
|
36
|
+
});
|
|
37
|
+
const get = (resource, path) => egress.requestJson({
|
|
38
|
+
resource,
|
|
39
|
+
method: "GET",
|
|
40
|
+
path,
|
|
41
|
+
headers: {
|
|
42
|
+
authorization: `Bearer ${apiKey}`,
|
|
43
|
+
accept: "application/json",
|
|
44
|
+
"user-agent": "agentcert-onegent-runtime/stripe-sandbox-readonly-v0.4",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
name: "stripe-test-mode-readonly",
|
|
49
|
+
safety: Object.freeze({
|
|
50
|
+
mode: "sandbox",
|
|
51
|
+
access: "read-only",
|
|
52
|
+
vendor: "stripe",
|
|
53
|
+
allowedOrigins: Object.freeze([STRIPE_TEST_API_ORIGIN]),
|
|
54
|
+
allowedMethods: Object.freeze(["GET"]),
|
|
55
|
+
allowedResources: Object.freeze([STRIPE_PAYMENT_INTENT_RETRIEVE, STRIPE_PAYMENT_INTENT_LIST]),
|
|
56
|
+
timeoutMs,
|
|
57
|
+
maxRequestsPerMinute,
|
|
58
|
+
}),
|
|
59
|
+
retrievePaymentIntent: async (id) => {
|
|
60
|
+
if (!/^pi_[A-Za-z0-9]{8,128}$/.test(id))
|
|
61
|
+
throw new Error("Stripe PaymentIntent ID is invalid.");
|
|
62
|
+
return paymentIntentSnapshot(await get(STRIPE_PAYMENT_INTENT_RETRIEVE, `/v1/payment_intents/${encodeURIComponent(id)}`));
|
|
63
|
+
},
|
|
64
|
+
listPaymentIntents: async (limit = 10) => {
|
|
65
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10)
|
|
66
|
+
throw new Error("Stripe read-only list limit must be an integer from 1 to 10.");
|
|
67
|
+
const payload = await get(STRIPE_PAYMENT_INTENT_LIST, `/v1/payment_intents?limit=${limit}`);
|
|
68
|
+
if (!isRecord(payload) || payload.object !== "list" || !Array.isArray(payload.data)) {
|
|
69
|
+
throw new Error("Stripe returned an invalid PaymentIntent list response.");
|
|
70
|
+
}
|
|
71
|
+
return payload.data.map(paymentIntentSnapshot);
|
|
72
|
+
},
|
|
73
|
+
getRequestAudit: () => egress.getAuditLog(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export async function runStripeSandboxReadOnlyCertification(options) {
|
|
77
|
+
const generatedAt = new Date((options.now ?? Date.now)()).toISOString();
|
|
78
|
+
const timeoutMs = options.timeoutMs ?? STRIPE_SANDBOX_TIMEOUT_MS;
|
|
79
|
+
const maxRequestsPerMinute = options.maxRequestsPerMinute ?? STRIPE_SANDBOX_MAX_REQUESTS_PER_MINUTE;
|
|
80
|
+
const checks = [];
|
|
81
|
+
let adapter;
|
|
82
|
+
let observation;
|
|
83
|
+
try {
|
|
84
|
+
adapter = createStripeTestModeReadOnlyAdapter(options);
|
|
85
|
+
checks.push(passed("restricted-test-key", "Credential format is restricted to Stripe rk_test_ keys and is not retained."), passed("fixed-egress-policy", "Egress is fixed to Stripe HTTPS, GET, and allowlisted PaymentIntent resources."));
|
|
86
|
+
observation = await adapter.retrievePaymentIntent(options.paymentIntentId);
|
|
87
|
+
checks.push(passed("bounded-read", "The allowlisted PaymentIntent read completed within the configured bounds."), passed("test-mode-response", "Stripe explicitly returned livemode=false."), passed("evidence-redaction", "The evidence snapshot excludes credentials, client_secret, metadata, and raw response bytes."));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (!adapter) {
|
|
91
|
+
checks.push(failed("restricted-test-key", "A valid Stripe rk_test_ restricted test-mode key was not available."), failed("fixed-egress-policy", "The request was not attempted because credential validation failed."));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
checks.push(failed("bounded-read", safeStripeFailure(error)));
|
|
95
|
+
}
|
|
96
|
+
checks.push(failed("test-mode-response", "No validated Stripe test-mode observation was produced."), failed("evidence-redaction", "No successful redacted observation was produced."));
|
|
97
|
+
}
|
|
98
|
+
const passedCount = checks.filter((check) => check.status === "passed").length;
|
|
99
|
+
const report = {
|
|
100
|
+
schemaVersion: "agentcert.sandbox_vendor_egress.v0.4",
|
|
101
|
+
kind: "agentcert.sandbox_vendor_egress",
|
|
102
|
+
implementation: "stripe-payment-intent-readonly",
|
|
103
|
+
vendor: "stripe",
|
|
104
|
+
environment: "sandbox",
|
|
105
|
+
generatedAt,
|
|
106
|
+
verdict: { passed: passedCount === checks.length, score: Math.round((passedCount / checks.length) * 100) },
|
|
107
|
+
summary: { passed: passedCount, failed: checks.length - passedCount, total: checks.length },
|
|
108
|
+
checks,
|
|
109
|
+
policy: {
|
|
110
|
+
allowedOrigins: [STRIPE_TEST_API_ORIGIN],
|
|
111
|
+
allowedMethods: ["GET"],
|
|
112
|
+
allowedResources: [STRIPE_PAYMENT_INTENT_RETRIEVE, STRIPE_PAYMENT_INTENT_LIST],
|
|
113
|
+
timeoutMs,
|
|
114
|
+
maxRequestsPerMinute,
|
|
115
|
+
},
|
|
116
|
+
audit: adapter?.getRequestAudit() ?? [],
|
|
117
|
+
...(observation ? { observation } : {}),
|
|
118
|
+
disclaimer: "Stripe sandbox read-only evidence proves this bounded test-mode observation only. It does not authorize writes, attest Stripe controls, or certify production behavior.",
|
|
119
|
+
};
|
|
120
|
+
return structuredClone(report);
|
|
121
|
+
}
|
|
122
|
+
function paymentIntentSnapshot(value) {
|
|
123
|
+
if (!isRecord(value) || value.object !== "payment_intent" || typeof value.id !== "string") {
|
|
124
|
+
throw new Error("Stripe returned an invalid PaymentIntent response.");
|
|
125
|
+
}
|
|
126
|
+
if (value.livemode !== false)
|
|
127
|
+
throw new Error("Stripe read-only adapter refuses responses that are not explicitly test mode.");
|
|
128
|
+
return {
|
|
129
|
+
id: value.id,
|
|
130
|
+
object: "payment_intent",
|
|
131
|
+
livemode: false,
|
|
132
|
+
...numberField(value, "amount", "amount"),
|
|
133
|
+
...numberField(value, "amount_capturable", "amountCapturable"),
|
|
134
|
+
...numberField(value, "amount_received", "amountReceived"),
|
|
135
|
+
...stringField(value, "currency", "currency"),
|
|
136
|
+
...stringField(value, "status", "status"),
|
|
137
|
+
...numberField(value, "created", "created"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function safeStripeFailure(error) {
|
|
141
|
+
if (error instanceof VendorSandboxEgressError)
|
|
142
|
+
return `Stripe sandbox read failed (${error.code}).`;
|
|
143
|
+
if (error instanceof Error && error.message.includes("not explicitly test mode"))
|
|
144
|
+
return "Stripe response was not explicitly test mode.";
|
|
145
|
+
if (error instanceof Error && error.message.includes("invalid PaymentIntent"))
|
|
146
|
+
return "Stripe returned an invalid PaymentIntent response.";
|
|
147
|
+
return "Stripe sandbox read failed before a validated observation was produced.";
|
|
148
|
+
}
|
|
149
|
+
function passed(id, message) {
|
|
150
|
+
return { id, status: "passed", message };
|
|
151
|
+
}
|
|
152
|
+
function failed(id, message) {
|
|
153
|
+
return { id, status: "failed", message };
|
|
154
|
+
}
|
|
155
|
+
function numberField(value, source, target) {
|
|
156
|
+
return typeof value[source] === "number" && Number.isFinite(value[source]) ? { [target]: value[source] } : {};
|
|
157
|
+
}
|
|
158
|
+
function stringField(value, source, target) {
|
|
159
|
+
return typeof value[source] === "string" ? { [target]: value[source] } : {};
|
|
160
|
+
}
|
|
161
|
+
function isRecord(value) {
|
|
162
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
163
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type VendorSandboxHttpMethod = "GET";
|
|
2
|
+
export interface VendorSandboxResourceRule {
|
|
3
|
+
id: string;
|
|
4
|
+
method: VendorSandboxHttpMethod;
|
|
5
|
+
pathPattern: RegExp;
|
|
6
|
+
}
|
|
7
|
+
export interface VendorSandboxEgressPolicy {
|
|
8
|
+
vendor: string;
|
|
9
|
+
allowedOrigin: string;
|
|
10
|
+
resources: readonly VendorSandboxResourceRule[];
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
maxRequestsPerMinute: number;
|
|
13
|
+
}
|
|
14
|
+
export type VendorSandboxAuditOutcome = "allowed" | "denied" | "rate_limited" | "timeout" | "http_error" | "failed";
|
|
15
|
+
export interface VendorSandboxRequestAudit {
|
|
16
|
+
requestId: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
vendor: string;
|
|
19
|
+
resource: string;
|
|
20
|
+
method: VendorSandboxHttpMethod;
|
|
21
|
+
origin: string;
|
|
22
|
+
outcome: VendorSandboxAuditOutcome;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
status?: number;
|
|
25
|
+
errorCode?: VendorSandboxEgressErrorCode;
|
|
26
|
+
}
|
|
27
|
+
export type VendorSandboxEgressErrorCode = "ORIGIN_DENIED" | "METHOD_DENIED" | "RESOURCE_DENIED" | "RATE_LIMITED" | "TIMEOUT" | "HTTP_ERROR" | "REQUEST_FAILED";
|
|
28
|
+
export declare class VendorSandboxEgressError extends Error {
|
|
29
|
+
readonly code: VendorSandboxEgressErrorCode;
|
|
30
|
+
readonly status?: number | undefined;
|
|
31
|
+
constructor(code: VendorSandboxEgressErrorCode, message: string, status?: number | undefined);
|
|
32
|
+
}
|
|
33
|
+
export interface VendorSandboxEgressRequest {
|
|
34
|
+
resource: string;
|
|
35
|
+
method: VendorSandboxHttpMethod;
|
|
36
|
+
path: string;
|
|
37
|
+
headers?: HeadersInit;
|
|
38
|
+
}
|
|
39
|
+
export interface BoundedVendorSandboxEgress {
|
|
40
|
+
requestJson(request: VendorSandboxEgressRequest): Promise<unknown>;
|
|
41
|
+
getAuditLog(): VendorSandboxRequestAudit[];
|
|
42
|
+
}
|
|
43
|
+
export interface BoundedVendorSandboxEgressOptions {
|
|
44
|
+
policy: VendorSandboxEgressPolicy;
|
|
45
|
+
fetch?: typeof fetch;
|
|
46
|
+
now?: () => number;
|
|
47
|
+
}
|
|
48
|
+
export declare function createBoundedVendorSandboxEgress(options: BoundedVendorSandboxEgressOptions): BoundedVendorSandboxEgress;
|
|
49
|
+
//# sourceMappingURL=vendor-sandbox-egress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vendor-sandbox-egress.d.ts","sourceRoot":"","sources":["../../../../onegent-runtime/src/vendor-sandbox-egress.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG,KAAK,CAAC;AAE5C,MAAM,WAAW,yBAAyB;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,uBAAuB,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,yBAAyB,GACjC,SAAS,GACT,QAAQ,GACR,cAAc,GACd,SAAS,GACT,YAAY,GACZ,QAAQ,CAAC;AAEb,MAAM,WAAW,yBAAyB;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,yBAAyB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,4BAA4B,CAAC;CAC1C;AAED,MAAM,MAAM,4BAA4B,GACpC,eAAe,GACf,eAAe,GACf,iBAAiB,GACjB,cAAc,GACd,SAAS,GACT,YAAY,GACZ,gBAAgB,CAAC;AAErB,qBAAa,wBAAyB,SAAQ,KAAK;aAE/B,IAAI,EAAE,4BAA4B;aAElC,MAAM,CAAC,EAAE,MAAM;gBAFf,IAAI,EAAE,4BAA4B,EAClD,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA;CAKlC;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,uBAAuB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,WAAW,IAAI,yBAAyB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,iCAAiC;IAChD,MAAM,EAAE,yBAAyB,CAAC;IAClC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAID,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CA+G5B"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export class VendorSandboxEgressError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
constructor(code, message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.name = "VendorSandboxEgressError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const MAX_AUDIT_ENTRIES = 100;
|
|
12
|
+
export function createBoundedVendorSandboxEgress(options) {
|
|
13
|
+
const policy = validatePolicy(options.policy);
|
|
14
|
+
const requestFetch = options.fetch ?? fetch;
|
|
15
|
+
const now = options.now ?? Date.now;
|
|
16
|
+
const audit = [];
|
|
17
|
+
let requestSequence = 0;
|
|
18
|
+
let windowStartedAt = now();
|
|
19
|
+
let requestsInWindow = 0;
|
|
20
|
+
const record = (entry) => {
|
|
21
|
+
audit.push(Object.freeze({ ...entry }));
|
|
22
|
+
if (audit.length > MAX_AUDIT_ENTRIES)
|
|
23
|
+
audit.splice(0, audit.length - MAX_AUDIT_ENTRIES);
|
|
24
|
+
};
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
requestJson: async (request) => {
|
|
27
|
+
const startedAt = now();
|
|
28
|
+
const requestId = `${policy.vendor}-${++requestSequence}`;
|
|
29
|
+
const method = request.method;
|
|
30
|
+
const url = safeUrl(request.path, policy.allowedOrigin);
|
|
31
|
+
const baseAudit = {
|
|
32
|
+
requestId,
|
|
33
|
+
timestamp: new Date(startedAt).toISOString(),
|
|
34
|
+
vendor: policy.vendor,
|
|
35
|
+
resource: request.resource,
|
|
36
|
+
method,
|
|
37
|
+
origin: url?.origin ?? policy.allowedOrigin,
|
|
38
|
+
};
|
|
39
|
+
const deny = (error) => {
|
|
40
|
+
record({ ...baseAudit, outcome: "denied", durationMs: elapsed(now, startedAt), errorCode: error.code });
|
|
41
|
+
throw error;
|
|
42
|
+
};
|
|
43
|
+
if (!url || url.origin !== policy.allowedOrigin || url.protocol !== "https:" || url.username || url.password || url.hash) {
|
|
44
|
+
return deny(new VendorSandboxEgressError("ORIGIN_DENIED", "Vendor sandbox request origin is not allowlisted."));
|
|
45
|
+
}
|
|
46
|
+
const resource = policy.resources.find((entry) => entry.id === request.resource);
|
|
47
|
+
if (!resource) {
|
|
48
|
+
return deny(new VendorSandboxEgressError("RESOURCE_DENIED", "Vendor sandbox resource is not allowlisted."));
|
|
49
|
+
}
|
|
50
|
+
if (method !== resource.method) {
|
|
51
|
+
return deny(new VendorSandboxEgressError("METHOD_DENIED", "Vendor sandbox HTTP method is not allowlisted for this resource."));
|
|
52
|
+
}
|
|
53
|
+
resource.pathPattern.lastIndex = 0;
|
|
54
|
+
if (!resource.pathPattern.test(`${url.pathname}${url.search}`)) {
|
|
55
|
+
return deny(new VendorSandboxEgressError("RESOURCE_DENIED", "Vendor sandbox path is not allowlisted for this resource."));
|
|
56
|
+
}
|
|
57
|
+
const current = now();
|
|
58
|
+
if (current - windowStartedAt >= 60_000) {
|
|
59
|
+
windowStartedAt = current;
|
|
60
|
+
requestsInWindow = 0;
|
|
61
|
+
}
|
|
62
|
+
if (requestsInWindow >= policy.maxRequestsPerMinute) {
|
|
63
|
+
const error = new VendorSandboxEgressError("RATE_LIMITED", "Vendor sandbox request limit exceeded. Retry after the current one-minute window.");
|
|
64
|
+
record({ ...baseAudit, outcome: "rate_limited", durationMs: elapsed(now, startedAt), errorCode: error.code });
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
requestsInWindow += 1;
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
let timedOut = false;
|
|
70
|
+
let timeout;
|
|
71
|
+
const operation = (async () => {
|
|
72
|
+
const response = await requestFetch(url, {
|
|
73
|
+
method,
|
|
74
|
+
headers: request.headers,
|
|
75
|
+
redirect: "error",
|
|
76
|
+
signal: controller.signal,
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new VendorSandboxEgressError("HTTP_ERROR", `Vendor sandbox returned HTTP ${response.status}.`, response.status);
|
|
80
|
+
}
|
|
81
|
+
return { payload: await response.json(), status: response.status };
|
|
82
|
+
})();
|
|
83
|
+
const timeoutOperation = new Promise((_, reject) => {
|
|
84
|
+
timeout = setTimeout(() => {
|
|
85
|
+
timedOut = true;
|
|
86
|
+
controller.abort();
|
|
87
|
+
reject(new VendorSandboxEgressError("TIMEOUT", `Vendor sandbox request exceeded ${policy.timeoutMs} ms.`));
|
|
88
|
+
}, policy.timeoutMs);
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const result = await Promise.race([operation, timeoutOperation]);
|
|
92
|
+
record({ ...baseAudit, outcome: "allowed", durationMs: elapsed(now, startedAt), status: result.status });
|
|
93
|
+
return result.payload;
|
|
94
|
+
}
|
|
95
|
+
catch (cause) {
|
|
96
|
+
const error = cause instanceof VendorSandboxEgressError
|
|
97
|
+
? cause
|
|
98
|
+
: timedOut
|
|
99
|
+
? new VendorSandboxEgressError("TIMEOUT", `Vendor sandbox request exceeded ${policy.timeoutMs} ms.`)
|
|
100
|
+
: new VendorSandboxEgressError("REQUEST_FAILED", "Vendor sandbox request failed.");
|
|
101
|
+
record({
|
|
102
|
+
...baseAudit,
|
|
103
|
+
outcome: error.code === "TIMEOUT" ? "timeout" : error.code === "HTTP_ERROR" ? "http_error" : "failed",
|
|
104
|
+
durationMs: elapsed(now, startedAt),
|
|
105
|
+
status: error.status,
|
|
106
|
+
errorCode: error.code,
|
|
107
|
+
});
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (timeout)
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
getAuditLog: () => audit.map((entry) => ({ ...entry })),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function validatePolicy(policy) {
|
|
119
|
+
const origin = new URL(policy.allowedOrigin);
|
|
120
|
+
if (origin.protocol !== "https:" || origin.origin !== policy.allowedOrigin || origin.pathname !== "/" || origin.search || origin.hash) {
|
|
121
|
+
throw new Error("Vendor sandbox allowedOrigin must be an exact HTTPS origin without a path.");
|
|
122
|
+
}
|
|
123
|
+
if (!policy.vendor.trim())
|
|
124
|
+
throw new Error("Vendor sandbox policy vendor is required.");
|
|
125
|
+
if (!Number.isSafeInteger(policy.timeoutMs) || policy.timeoutMs < 100 || policy.timeoutMs > 30_000) {
|
|
126
|
+
throw new Error("Vendor sandbox timeoutMs must be an integer from 100 to 30000.");
|
|
127
|
+
}
|
|
128
|
+
if (!Number.isSafeInteger(policy.maxRequestsPerMinute) || policy.maxRequestsPerMinute < 1 || policy.maxRequestsPerMinute > 60) {
|
|
129
|
+
throw new Error("Vendor sandbox maxRequestsPerMinute must be an integer from 1 to 60.");
|
|
130
|
+
}
|
|
131
|
+
if (policy.resources.length === 0 || new Set(policy.resources.map((entry) => entry.id)).size !== policy.resources.length) {
|
|
132
|
+
throw new Error("Vendor sandbox policy requires unique resource rules.");
|
|
133
|
+
}
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
...policy,
|
|
136
|
+
resources: Object.freeze(policy.resources.map((entry) => Object.freeze({
|
|
137
|
+
...entry,
|
|
138
|
+
pathPattern: new RegExp(entry.pathPattern.source, entry.pathPattern.flags),
|
|
139
|
+
}))),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function safeUrl(path, origin) {
|
|
143
|
+
try {
|
|
144
|
+
return new URL(path, `${origin}/`);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function elapsed(now, startedAt) {
|
|
151
|
+
return Math.max(0, Math.round(now() - startedAt));
|
|
152
|
+
}
|