agentcert 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/sandbox.js +67 -2
- 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 +5 -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,18 @@ 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
|
+
|
|
85
97
|
Review/export helpers:
|
|
86
98
|
|
|
87
99
|
```bash
|
package/dist/sandbox.js
CHANGED
|
@@ -4,6 +4,7 @@ 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,8 @@ 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));
|
|
19
22
|
throw new Error(`Unknown sandbox command ${JSON.stringify(action)}. Run \`npx agentcert sandbox --help\`.`);
|
|
20
23
|
}
|
|
21
24
|
export async function initializeSandboxAdapter(args) {
|
|
@@ -62,6 +65,44 @@ export async function pushSandboxCertification(args, runtimeOverride) {
|
|
|
62
65
|
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
63
66
|
return certified;
|
|
64
67
|
}
|
|
68
|
+
export async function runStripeSandboxReadOnly(args, runtimeOverride) {
|
|
69
|
+
if (args.includes("--stripe-key") || args.includes("--restricted-key")) {
|
|
70
|
+
throw new Error("Stripe credentials are accepted only through STRIPE_RESTRICTED_TEST_KEY, never a CLI flag.");
|
|
71
|
+
}
|
|
72
|
+
const paymentIntentId = flag(args, "--payment-intent");
|
|
73
|
+
if (!paymentIntentId)
|
|
74
|
+
throw new Error("Stripe sandbox read requires --payment-intent <pi_...>.");
|
|
75
|
+
const restrictedApiKey = process.env.STRIPE_RESTRICTED_TEST_KEY?.trim();
|
|
76
|
+
if (!restrictedApiKey) {
|
|
77
|
+
throw new Error("Set STRIPE_RESTRICTED_TEST_KEY to a read-only Stripe rk_test_ restricted key.");
|
|
78
|
+
}
|
|
79
|
+
const runtime = runtimeOverride ?? await loadSandboxRuntime();
|
|
80
|
+
if (!runtime.runStripeSandboxReadOnlyCertification) {
|
|
81
|
+
throw new Error("AgentCert Stripe sandbox runtime is missing. Reinstall the agentcert package and retry.");
|
|
82
|
+
}
|
|
83
|
+
const report = await runtime.runStripeSandboxReadOnlyCertification({ restrictedApiKey, paymentIntentId });
|
|
84
|
+
const reportPath = resolve(flag(args, "--out") ?? DEFAULT_STRIPE_REPORT_PATH);
|
|
85
|
+
await mkdir(dirname(reportPath), { recursive: true });
|
|
86
|
+
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
87
|
+
renderVendorReadResult(report, reportPath);
|
|
88
|
+
if (args.includes("--push")) {
|
|
89
|
+
const connection = await resolveConnection({
|
|
90
|
+
name: flag(args, "--connection"),
|
|
91
|
+
server: flag(args, "--server"),
|
|
92
|
+
projectId: flag(args, "--project"),
|
|
93
|
+
apiKey: flag(args, "--api-key"),
|
|
94
|
+
});
|
|
95
|
+
const uploaded = await runtime.uploadSandboxCertificationReport(report, {
|
|
96
|
+
baseUrl: connection.server,
|
|
97
|
+
projectId: connection.projectId,
|
|
98
|
+
apiKey: connection.apiKey,
|
|
99
|
+
externalId: flag(args, "--external-id"),
|
|
100
|
+
});
|
|
101
|
+
process.stdout.write(`Hosted sandbox run: ${String(uploaded.run.id ?? "created")}\n`);
|
|
102
|
+
process.stdout.write(`Hosted evidence: ${String(uploaded.evidence.id ?? "created")}\n`);
|
|
103
|
+
}
|
|
104
|
+
return { exitCode: report.verdict.passed ? 0 : 1, reportPath, report };
|
|
105
|
+
}
|
|
65
106
|
export async function loadSandboxAdapter(adapterPath) {
|
|
66
107
|
let module;
|
|
67
108
|
try {
|
|
@@ -96,16 +137,27 @@ Runs the deterministic sandbox adapter conformance suite locally.
|
|
|
96
137
|
|
|
97
138
|
Runs certification, writes the local report, and uploads it through the saved AgentCert connection.
|
|
98
139
|
Use \`agentcert connect\` first, or pass --server, --project, and --api-key.
|
|
140
|
+
`;
|
|
141
|
+
if (action === "stripe-readonly")
|
|
142
|
+
return `Usage:
|
|
143
|
+
STRIPE_RESTRICTED_TEST_KEY=rk_test_... agentcert sandbox stripe-readonly --payment-intent pi_...
|
|
144
|
+
agentcert sandbox stripe-readonly --payment-intent pi_... --push
|
|
145
|
+
|
|
146
|
+
Reads one Stripe sandbox PaymentIntent through a fixed GET/resource allowlist,
|
|
147
|
+
writes a redacted evidence report, and optionally uploads it to AgentCert Hosted.
|
|
148
|
+
The Stripe key is read only from STRIPE_RESTRICTED_TEST_KEY.
|
|
99
149
|
`;
|
|
100
150
|
return `Usage:
|
|
101
151
|
agentcert sandbox init [--adapter agentcert.sandbox.mjs]
|
|
102
152
|
agentcert sandbox certify --adapter ./my-sandbox-adapter.js
|
|
103
153
|
agentcert sandbox push --adapter ./my-sandbox-adapter.js
|
|
154
|
+
agentcert sandbox stripe-readonly --payment-intent pi_... [--push]
|
|
104
155
|
|
|
105
156
|
Commands:
|
|
106
157
|
init Write one dependency-free adapter template
|
|
107
158
|
certify Run deterministic local conformance checks
|
|
108
159
|
push Certify and upload the report to AgentCert Hosted
|
|
160
|
+
stripe-readonly Run one bounded Stripe sandbox read and write evidence
|
|
109
161
|
|
|
110
162
|
Common options:
|
|
111
163
|
--adapter <path> Adapter module (default: agentcert.sandbox.mjs)
|
|
@@ -117,9 +169,14 @@ Common options:
|
|
|
117
169
|
async function loadSandboxRuntime() {
|
|
118
170
|
const adapterKitUrl = new URL("./vendor/onegent-runtime/sandbox-adapter-kit.js", import.meta.url);
|
|
119
171
|
const hostedUrl = new URL("./vendor/onegent-runtime/sandbox-hosted.js", import.meta.url);
|
|
172
|
+
const stripeUrl = new URL("./vendor/onegent-runtime/stripe-test-readonly.js", import.meta.url);
|
|
120
173
|
try {
|
|
121
|
-
const [adapterKit, hosted] = await Promise.all([
|
|
122
|
-
|
|
174
|
+
const [adapterKit, hosted, stripe] = await Promise.all([
|
|
175
|
+
import(adapterKitUrl.href),
|
|
176
|
+
import(hostedUrl.href),
|
|
177
|
+
import(stripeUrl.href),
|
|
178
|
+
]);
|
|
179
|
+
return { ...adapterKit, ...hosted, ...stripe };
|
|
123
180
|
}
|
|
124
181
|
catch (error) {
|
|
125
182
|
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
@@ -165,6 +222,14 @@ function renderCertificationResult(report, reportPath) {
|
|
|
165
222
|
}
|
|
166
223
|
process.stdout.write(`Report: ${reportPath}\n`);
|
|
167
224
|
}
|
|
225
|
+
function renderVendorReadResult(report, reportPath) {
|
|
226
|
+
process.stdout.write(`${report.verdict.passed ? "PASS" : "FAIL"} ${report.verdict.score}/100 Stripe sandbox bounded read\n`);
|
|
227
|
+
for (const check of report.checks) {
|
|
228
|
+
process.stdout.write(`- ${check.status === "passed" ? "PASS" : "FAIL"} ${check.id}: ${check.message}\n`);
|
|
229
|
+
}
|
|
230
|
+
process.stdout.write(`Requests audited: ${report.audit.length}\n`);
|
|
231
|
+
process.stdout.write(`Report: ${reportPath}\n`);
|
|
232
|
+
}
|
|
168
233
|
function sandboxAdapterTemplate() {
|
|
169
234
|
return `// Synthetic local state only. Do not import production credentials or call live systems here.
|
|
170
235
|
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",
|
|
@@ -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
|
+
}
|