ai-saas-guard 0.30.2 → 0.32.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 +10 -8
- package/dist/hosted/deployed-staging.d.ts +66 -0
- package/dist/hosted/deployed-staging.js +267 -0
- package/dist/hosted/service.js +12 -1
- package/dist/hosted/staging-harness.d.ts +65 -3
- package/dist/hosted/staging-harness.js +174 -4
- package/dist/hosted/worker.js +51 -0
- package/docs/README.zh-CN.md +9 -7
- package/docs/hosted-deployed-worker-staging.md +72 -0
- package/docs/hosted-operational-release-gate.md +9 -4
- package/docs/hosted-operations-evidence.md +5 -1
- package/docs/hosted-preimplementation-contracts.md +7 -2
- package/docs/hosted-staging-harness.md +35 -1
- package/docs/npm-publishing.md +3 -3
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -187,13 +187,13 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
187
187
|
| Area | Status |
|
|
188
188
|
| --- | --- |
|
|
189
189
|
| Public GitHub repository | Available |
|
|
190
|
-
| npm CLI | `ai-saas-guard@0.
|
|
191
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.
|
|
190
|
+
| npm CLI | `ai-saas-guard@0.32.0` |
|
|
191
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.32.0` |
|
|
192
192
|
| Outputs | Short summary, terminal, JSON, SARIF, and PR-focused markdown |
|
|
193
193
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
194
194
|
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
195
|
-
| Versioned Action tags | `v0.
|
|
196
|
-
| Current release | `0.
|
|
195
|
+
| Versioned Action tags | `v0.32.0`, `v0` |
|
|
196
|
+
| Current release | `0.32.0` adds deployed worker staging evidence: public HTTPS health validation, deployed success/failure cleanup probes, log-boundary checks, and release-gate evaluation for a Node/container read-only checkout worker candidate |
|
|
197
197
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
198
198
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
199
199
|
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
@@ -304,13 +304,15 @@ The hosted GitHub App deployment planner is documented in [docs/github-app-deplo
|
|
|
304
304
|
|
|
305
305
|
The hosted production adapter layer is documented in [docs/hosted-production-adapters.md](docs/hosted-production-adapters.md). It exports `createHostedGitHubAppJwt`, `planHostedGitHubInstallationTokenRequest`, and `planHostedProductionWorkerExecution` from `ai-saas-guard/hosted/production-adapters`. It adds RS256 GitHub App JWT generation, selected-repository installation-token request plans, separate worker and Check Run token scopes, a fixed read-only worker command, bounded timeout and output budgets, compact JSON-only output, and cleanup plans for success, failure, timeout, and cancellation. It still does not expose a public hosted service by itself.
|
|
306
306
|
|
|
307
|
-
The hosted read-only checkout worker is exported from `ai-saas-guard/hosted/worker`. It creates a temporary checkout from trusted GitHub App identity, uses a runtime installation token only through git askpass, runs the fixed `ai-saas-guard pr-risk --json` command with bounded timeout/output, converts CLI JSON into compact findings, and deletes the checkout after success or failure. It does not return source, diffs, secrets, checkout paths, PR-authored commands, or installation tokens.
|
|
307
|
+
The hosted read-only checkout worker is exported from `ai-saas-guard/hosted/worker`. It creates a temporary checkout from trusted GitHub App identity, uses a runtime installation token only through git askpass, removes askpass material before the CLI phase, rejects mutated command/checkout/token-scope plans, runs the fixed `ai-saas-guard pr-risk --json` command with bounded timeout/output, converts CLI JSON into compact findings, and deletes the checkout after success or failure. It does not return source, diffs, secrets, checkout paths, PR-authored commands, or installation tokens.
|
|
308
308
|
|
|
309
309
|
The hosted Node/container app skeleton is documented in [docs/hosted-node-container-app.md](docs/hosted-node-container-app.md). It exports `createHostedHttpApp`, `createInMemoryHostedAppPlatform`, `createHostedNodeCheckoutAppPlatform`, and `planHostedNodeContainerDeployment` from `ai-saas-guard/hosted/app`. It adds a safe `/healthz` route, signed `/github/webhook` ingress, one-job worker tick, in-memory provider adapters for tests, a concrete read-only checkout worker composition with visible timeout/output safety budgets, and deployment-plan validation for secret manager, queue, compact report store, worker sandbox, and GitHub Checks publisher references. It still does not deploy or expose a public hosted service by itself.
|
|
310
310
|
|
|
311
311
|
The hosted staging deployment planner is documented in [docs/hosted-staging-deployment.md](docs/hosted-staging-deployment.md). It exports `planHostedProviderBinding`, `planHostedStagingDeployment`, and `planHostedGitHubAppPromotion` from `ai-saas-guard/hosted/staging`. It composes real provider references, the Node/container deployment plan, hosted operational release-gate evidence, and GitHub App deployment planning so staging and production promotion stay blocked until the required queue, store, worker sandbox, Check Run publisher, logs, metrics, rollback, and incident-response references are present. It still does not call a cloud provider, create a GitHub App, or expose a public hosted service by itself.
|
|
312
312
|
|
|
313
|
-
The hosted staging harness is documented in [docs/hosted-staging-harness.md](docs/hosted-staging-harness.md). It exports `createFileBackedHostedStagingHarness` and `
|
|
313
|
+
The hosted staging harness is documented in [docs/hosted-staging-harness.md](docs/hosted-staging-harness.md). It exports `createFileBackedHostedStagingHarness`, `createHostedStagingHarnessEvidence`, `createHostedStagingReleaseEvidenceBundle`, `evaluateHostedStagingReleaseEvidenceBundle`, and `validateHostedLogBoundary` from `ai-saas-guard/hosted/staging-harness`. It runs signed webhook replay through the provider-independent hosted runtime with local file-backed queue, compact report, and Check Run adapters, verifies worker sandbox cleanup, turns success/failure cleanup probes plus log-boundary samples into release-gate evidence, and evaluates the hosted gate without cloud calls. It is a staging rehearsal tool only; it does not call cloud providers, create a GitHub App, publish live Check Runs, or expose a public hosted service.
|
|
314
|
+
|
|
315
|
+
Deployed worker staging evidence is documented in [docs/hosted-deployed-worker-staging.md](docs/hosted-deployed-worker-staging.md). It exports `createHostedDeployedWorkerStagingEvidenceBundle` and `evaluateHostedDeployedWorkerStagingReleaseGate` from `ai-saas-guard/hosted/deployed-staging`. It turns public HTTPS health, signed webhook replay, deployed worker cleanup, log-boundary samples, and external CI/scan/rollback evidence into the hosted release gate for a Node/container read-only checkout worker candidate. It does not deploy cloud resources or claim production hosted exposure.
|
|
314
316
|
|
|
315
317
|
The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-saas-guard-hosted.zr9959.workers.dev` and documented in [hosted/cloudflare-worker/README.md](hosted/cloudflare-worker/README.md). It exposes `/healthz`, `/github/app/manifest-callback`, and signed `/github/webhook` intake backed by Cloudflare KV. A private staging GitHub App, `ai-saas-guard-hosted`, is installed on `zr9959/ai-saas-guard` with selected-repository access and the first-slice permission contract. The Worker verifies signatures, stores compact pull request identity records, exchanges a scoped installation token, fetches PR file metadata from GitHub, classifies PR-risk hotspots, and publishes a bounded Check Run summary. Current deployed evidence is tracked in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md): health, signed webhook delivery, compact KV records, cleanup, and Check Run publication pass for the staging smoke. The Cloudflare Worker still does not run a full source checkout scan worker or store raw webhook payloads, PR title/body text, raw diffs, source, secrets, checkout paths, or installation tokens.
|
|
316
318
|
|
|
@@ -320,7 +322,7 @@ Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninst
|
|
|
320
322
|
|
|
321
323
|
Hosted pricing and packaging boundaries are documented in [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md). Core local scanning stays useful without an account; hosted plans may add workflow convenience, saved reports, team policy, and optional human review, but they do not gate local CLI scanning.
|
|
322
324
|
|
|
323
|
-
Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key, a worker read-only scan planner that fixes the CLI command and requires repository `contents: read`, a concrete Node read-only checkout scan runner, and a Check Run publication planner that requires repository `checks: write` and builds bounded check-only payloads from compact reports. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, a retention/deletion cleanup planner, an operational release gate evaluator, the production adapter plans needed for GitHub App auth and bounded worker execution, the Node/container app skeleton needed for real provider wiring, the staging deployment planner needed before production GitHub App promotion,
|
|
325
|
+
Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key, a worker read-only scan planner that fixes the CLI command and requires repository `contents: read`, a concrete Node read-only checkout scan runner, and a Check Run publication planner that requires repository `checks: write` and builds bounded check-only payloads from compact reports. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, a retention/deletion cleanup planner, an operational release gate evaluator, the production adapter plans needed for GitHub App auth and bounded worker execution, the Node/container app skeleton needed for real provider wiring, the staging deployment planner needed before production GitHub App promotion, the local staging harness needed to rehearse webhook replay, persistence, publication, and cleanup without cloud calls, and the deployed worker staging evidence helper needed to evaluate public HTTPS health, deployed cleanup, and log-boundary evidence without storing raw hosted data. The service runtime composes these contracts behind replaceable adapters. PR comments remain a later workflow or paid hosted feature, not part of the hosted MVP contract.
|
|
324
326
|
|
|
325
327
|
A public hosted compact report schema fixture is available at [examples/hosted-compact-report.json](examples/hosted-compact-report.json). It is synthetic and public-safe: compact evidence only, no raw source, raw diffs, secrets, webhook payload bodies, customer payloads, private URLs, or worker checkout paths.
|
|
326
328
|
|
|
@@ -360,7 +362,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
360
362
|
|
|
361
363
|
## GitHub Action
|
|
362
364
|
|
|
363
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
365
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.32.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
364
366
|
|
|
365
367
|
```yaml
|
|
366
368
|
name: ai-saas-guard
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type HostedOperationalReleaseGateDecision, type HostedOperationalReleaseGateEvidence } from "./contracts.js";
|
|
2
|
+
import type { HostedLogBoundaryValidation, HostedStagingHarnessReplayResult, HostedStagingHarnessWorkerTickResult } from "./staging-harness.js";
|
|
3
|
+
export interface HostedDeployedWorkerHealthProbe {
|
|
4
|
+
observedAt: string;
|
|
5
|
+
status: number;
|
|
6
|
+
body: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface HostedDeployedWorkerStagingEvidenceBundleInput {
|
|
9
|
+
collectedAt: string;
|
|
10
|
+
evidenceBaseUrl: string;
|
|
11
|
+
owner: string;
|
|
12
|
+
publicBaseUrl: string;
|
|
13
|
+
scannerVersion: string;
|
|
14
|
+
healthProbe: HostedDeployedWorkerHealthProbe;
|
|
15
|
+
webhookReplays: HostedStagingHarnessReplayResult[];
|
|
16
|
+
workerTicks: HostedStagingHarnessWorkerTickResult[];
|
|
17
|
+
logBoundary: HostedLogBoundaryValidation;
|
|
18
|
+
externalEvidence: HostedOperationalReleaseGateEvidence[];
|
|
19
|
+
requiredFailureReasons?: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface HostedDeployedWorkerStagingEvidenceBundle {
|
|
22
|
+
readyForReleaseGate: boolean;
|
|
23
|
+
blockedReasons: string[];
|
|
24
|
+
evidence: HostedOperationalReleaseGateEvidence[];
|
|
25
|
+
releaseGateInput: {
|
|
26
|
+
evidence: HostedOperationalReleaseGateEvidence[];
|
|
27
|
+
};
|
|
28
|
+
deployedScenarioSummary: HostedDeployedWorkerStagingScenarioSummary;
|
|
29
|
+
privacy: HostedDeployedWorkerStagingPrivacy;
|
|
30
|
+
}
|
|
31
|
+
export interface HostedDeployedWorkerStagingScenarioSummary {
|
|
32
|
+
publicIngressAccepted: boolean;
|
|
33
|
+
healthAccepted: boolean;
|
|
34
|
+
webhookReplayAccepted: boolean;
|
|
35
|
+
completedWorkerProbe: boolean;
|
|
36
|
+
failureCleanupProbe: boolean;
|
|
37
|
+
observedFailureReasons: string[];
|
|
38
|
+
allWorkerCheckoutsDeleted: boolean;
|
|
39
|
+
checkRunPublished: boolean;
|
|
40
|
+
logBoundaryAccepted: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface HostedDeployedWorkerStagingReleaseGateInput {
|
|
43
|
+
bundle: HostedDeployedWorkerStagingEvidenceBundle;
|
|
44
|
+
commitSha: string;
|
|
45
|
+
scannerVersion: string;
|
|
46
|
+
deploymentTarget: string;
|
|
47
|
+
evaluatedAt: string;
|
|
48
|
+
releaseNotes: string;
|
|
49
|
+
containerImageDigest: string;
|
|
50
|
+
maxEvidenceAgeDays?: number;
|
|
51
|
+
}
|
|
52
|
+
export interface HostedDeployedWorkerStagingPrivacy {
|
|
53
|
+
includesRawHealthResponse: false;
|
|
54
|
+
includesPublicBaseUrl: false;
|
|
55
|
+
includesRawWebhookPayload: false;
|
|
56
|
+
includesUntrustedPrText: false;
|
|
57
|
+
includesRawSource: false;
|
|
58
|
+
includesRawDiffs: false;
|
|
59
|
+
includesSecrets: false;
|
|
60
|
+
includesCustomerPayloads: false;
|
|
61
|
+
includesPrivateCheckoutPath: false;
|
|
62
|
+
includesInstallationToken: false;
|
|
63
|
+
claimsProductionHostedService: false;
|
|
64
|
+
}
|
|
65
|
+
export declare function createHostedDeployedWorkerStagingEvidenceBundle(input: HostedDeployedWorkerStagingEvidenceBundleInput): HostedDeployedWorkerStagingEvidenceBundle;
|
|
66
|
+
export declare function evaluateHostedDeployedWorkerStagingReleaseGate(input: HostedDeployedWorkerStagingReleaseGateInput): HostedOperationalReleaseGateDecision;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { evaluateHostedOperationalReleaseGate, HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS } from "./contracts.js";
|
|
2
|
+
import { HOSTED_NODE_CONTAINER_PLATFORM, HOSTED_NODE_CONTAINER_ROLES } from "./app.js";
|
|
3
|
+
export function createHostedDeployedWorkerStagingEvidenceBundle(input) {
|
|
4
|
+
const summary = deployedScenarioSummary(input);
|
|
5
|
+
const blockedReasons = deployedBlockedReasons(input, summary);
|
|
6
|
+
const externalEvidence = new Map(input.externalEvidence.map((evidence) => [evidence.id, sanitizeEvidence(evidence, input)]));
|
|
7
|
+
const evidence = HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS.map((requirement) => {
|
|
8
|
+
const generated = generatedEvidenceFor(requirement.id, input, summary, blockedReasons);
|
|
9
|
+
return generated ?? externalEvidence.get(requirement.id) ?? missingEvidence(requirement.id, input);
|
|
10
|
+
});
|
|
11
|
+
const readyForReleaseGate = blockedReasons.length === 0 && evidence.every((item) => item.status === "passed");
|
|
12
|
+
return {
|
|
13
|
+
readyForReleaseGate,
|
|
14
|
+
blockedReasons,
|
|
15
|
+
evidence,
|
|
16
|
+
releaseGateInput: { evidence },
|
|
17
|
+
deployedScenarioSummary: summary,
|
|
18
|
+
privacy: deployedPrivacy()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function evaluateHostedDeployedWorkerStagingReleaseGate(input) {
|
|
22
|
+
return evaluateHostedOperationalReleaseGate({
|
|
23
|
+
commitSha: input.commitSha,
|
|
24
|
+
scannerVersion: input.scannerVersion,
|
|
25
|
+
deploymentTarget: input.deploymentTarget,
|
|
26
|
+
evaluatedAt: input.evaluatedAt,
|
|
27
|
+
evidence: input.bundle.evidence,
|
|
28
|
+
releaseNotes: input.releaseNotes,
|
|
29
|
+
containerImageDigest: input.containerImageDigest,
|
|
30
|
+
maxEvidenceAgeDays: input.maxEvidenceAgeDays
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function deployedScenarioSummary(input) {
|
|
34
|
+
const processedWorkers = input.workerTicks.filter((tick) => tick.processed);
|
|
35
|
+
const completedWorkers = processedWorkers.filter((tick) => tick.status === "completed");
|
|
36
|
+
const observedFailureReasons = [
|
|
37
|
+
...new Set(processedWorkers.flatMap((tick) => tick.status === "failed" && tick.cleanupVerified && tick.safeFailureReason
|
|
38
|
+
? [tick.safeFailureReason]
|
|
39
|
+
: []))
|
|
40
|
+
].sort();
|
|
41
|
+
const requiredFailureReasons = input.requiredFailureReasons ?? [];
|
|
42
|
+
const failureCleanupProbe = requiredFailureReasons.length
|
|
43
|
+
? requiredFailureReasons.every((reason) => observedFailureReasons.includes(reason))
|
|
44
|
+
: processedWorkers.some((tick) => tick.status === "failed" && tick.cleanupVerified);
|
|
45
|
+
const allWorkerCheckoutsDeleted = processedWorkers.length > 0 &&
|
|
46
|
+
processedWorkers.every((tick) => tick.workerSandboxDeleted &&
|
|
47
|
+
tick.activeWorkerSandboxCount === 0 &&
|
|
48
|
+
tick.cleanupVerified);
|
|
49
|
+
return {
|
|
50
|
+
publicIngressAccepted: isSafePublicHttpsUrl(input.publicBaseUrl),
|
|
51
|
+
healthAccepted: healthProbeAccepted(input),
|
|
52
|
+
webhookReplayAccepted: input.webhookReplays.some((replay) => replay.accepted && replay.queuedWorker && replay.shouldCreateCheckRun),
|
|
53
|
+
completedWorkerProbe: completedWorkers.some((tick) => tick.checkRunPublished && tick.compactReportStored),
|
|
54
|
+
failureCleanupProbe,
|
|
55
|
+
observedFailureReasons,
|
|
56
|
+
allWorkerCheckoutsDeleted,
|
|
57
|
+
checkRunPublished: completedWorkers.some((tick) => tick.checkRunPublished),
|
|
58
|
+
logBoundaryAccepted: input.logBoundary.sampleCount > 0 && input.logBoundary.accepted
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function deployedBlockedReasons(input, summary) {
|
|
62
|
+
const reasons = [];
|
|
63
|
+
const health = healthBody(input.healthProbe.body);
|
|
64
|
+
if (!summary.publicIngressAccepted)
|
|
65
|
+
reasons.push("invalid_public_base_url");
|
|
66
|
+
if (!isSafePublicHttpsUrl(input.evidenceBaseUrl))
|
|
67
|
+
reasons.push("unsafe_evidence_base_url");
|
|
68
|
+
if (input.healthProbe.status !== 200)
|
|
69
|
+
reasons.push("health_status_unhealthy");
|
|
70
|
+
if (health.ok !== true)
|
|
71
|
+
reasons.push("health_not_ok");
|
|
72
|
+
if (health.platform !== HOSTED_NODE_CONTAINER_PLATFORM)
|
|
73
|
+
reasons.push("health_platform_mismatch");
|
|
74
|
+
if (health.scannerVersion !== input.scannerVersion) {
|
|
75
|
+
reasons.push("health_scanner_version_mismatch");
|
|
76
|
+
}
|
|
77
|
+
if (!health.roles.includes("webhook-ingress"))
|
|
78
|
+
reasons.push("health_missing_webhook_role");
|
|
79
|
+
if (!health.roles.includes("scan-worker"))
|
|
80
|
+
reasons.push("health_missing_scan_worker_role");
|
|
81
|
+
if (!privacyFlagsAreSafe(health.privacy))
|
|
82
|
+
reasons.push("health_privacy_flags_unsafe");
|
|
83
|
+
if (!summary.webhookReplayAccepted)
|
|
84
|
+
reasons.push("webhook_replay_missing");
|
|
85
|
+
if (!summary.completedWorkerProbe)
|
|
86
|
+
reasons.push("worker_success_probe_missing");
|
|
87
|
+
if (!summary.failureCleanupProbe)
|
|
88
|
+
reasons.push("worker_failure_cleanup_probe_missing");
|
|
89
|
+
if (!summary.allWorkerCheckoutsDeleted)
|
|
90
|
+
reasons.push("worker_cleanup_not_verified");
|
|
91
|
+
if (!summary.checkRunPublished)
|
|
92
|
+
reasons.push("check_run_not_published");
|
|
93
|
+
if (!summary.logBoundaryAccepted)
|
|
94
|
+
reasons.push("log_boundary_rejected");
|
|
95
|
+
return reasons;
|
|
96
|
+
}
|
|
97
|
+
function generatedEvidenceFor(id, input, summary, blockedReasons) {
|
|
98
|
+
if (blockedReasons.length > 0) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (id === "webhook_replay") {
|
|
102
|
+
return summary.publicIngressAccepted && summary.healthAccepted && summary.webhookReplayAccepted
|
|
103
|
+
? passedEvidence(id, "Deployed staging ingress accepted a signed webhook and queued check-run-only work.", input)
|
|
104
|
+
: missingEvidence(id, input);
|
|
105
|
+
}
|
|
106
|
+
if (id === "queue_worker_cleanup") {
|
|
107
|
+
return summary.completedWorkerProbe &&
|
|
108
|
+
summary.failureCleanupProbe &&
|
|
109
|
+
summary.allWorkerCheckoutsDeleted
|
|
110
|
+
? passedEvidence(id, "Deployed staging worker success and failure probes published compact checks and deleted worker checkouts.", input)
|
|
111
|
+
: missingEvidence(id, input);
|
|
112
|
+
}
|
|
113
|
+
if (id === "privacy_retention") {
|
|
114
|
+
return summary.logBoundaryAccepted && privacyFlagsAreSafe(input.logBoundary.privacy)
|
|
115
|
+
? passedEvidence(id, "Deployed staging log samples stayed within the safe metadata boundary and avoided raw payloads.", input)
|
|
116
|
+
: missingEvidence(id, input);
|
|
117
|
+
}
|
|
118
|
+
if (id === "release_cleanup") {
|
|
119
|
+
return summary.allWorkerCheckoutsDeleted
|
|
120
|
+
? passedEvidence(id, "Deployed staging release cleanup left no active worker checkout entries.", input)
|
|
121
|
+
: missingEvidence(id, input);
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
function healthProbeAccepted(input) {
|
|
126
|
+
const health = healthBody(input.healthProbe.body);
|
|
127
|
+
return (input.healthProbe.status === 200 &&
|
|
128
|
+
health.ok === true &&
|
|
129
|
+
health.platform === HOSTED_NODE_CONTAINER_PLATFORM &&
|
|
130
|
+
HOSTED_NODE_CONTAINER_ROLES.every((role) => health.roles.includes(role)) &&
|
|
131
|
+
health.scannerVersion === input.scannerVersion &&
|
|
132
|
+
privacyFlagsAreSafe(health.privacy));
|
|
133
|
+
}
|
|
134
|
+
function sanitizeEvidence(evidence, input) {
|
|
135
|
+
return {
|
|
136
|
+
id: evidence.id,
|
|
137
|
+
status: evidence.status,
|
|
138
|
+
...(evidence.collectedAt === undefined
|
|
139
|
+
? { collectedAt: input.collectedAt }
|
|
140
|
+
: { collectedAt: evidence.collectedAt }),
|
|
141
|
+
...(safeEvidenceUrl(evidence.evidenceUrl) === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { evidenceUrl: safeEvidenceUrl(evidence.evidenceUrl) }),
|
|
144
|
+
note: `External deployed-staging release-gate evidence recorded for ${evidence.id}.`,
|
|
145
|
+
...(evidence.owner === undefined ? { owner: input.owner } : { owner: evidence.owner })
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function passedEvidence(id, note, input) {
|
|
149
|
+
return {
|
|
150
|
+
id,
|
|
151
|
+
status: "passed",
|
|
152
|
+
collectedAt: input.collectedAt,
|
|
153
|
+
...(evidenceUrlFor(input, id) === undefined ? {} : { evidenceUrl: evidenceUrlFor(input, id) }),
|
|
154
|
+
note,
|
|
155
|
+
owner: input.owner
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function missingEvidence(id, input) {
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
status: "missing",
|
|
162
|
+
collectedAt: input.collectedAt,
|
|
163
|
+
note: `Missing deployed staging worker evidence for ${id}.`,
|
|
164
|
+
owner: input.owner
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function evidenceUrlFor(input, id) {
|
|
168
|
+
const baseUrl = safeEvidenceUrl(input.evidenceBaseUrl);
|
|
169
|
+
return baseUrl === undefined ? undefined : `${baseUrl}/${id}.json`;
|
|
170
|
+
}
|
|
171
|
+
function safeEvidenceUrl(value) {
|
|
172
|
+
if (!value || !isSafePublicHttpsUrl(value)) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return trimTrailingSlashes(value.trim());
|
|
176
|
+
}
|
|
177
|
+
function healthBody(value) {
|
|
178
|
+
if (!isRecord(value)) {
|
|
179
|
+
return {
|
|
180
|
+
ok: undefined,
|
|
181
|
+
platform: undefined,
|
|
182
|
+
roles: [],
|
|
183
|
+
scannerVersion: undefined,
|
|
184
|
+
privacy: undefined
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const roles = Array.isArray(value.roles)
|
|
188
|
+
? value.roles.filter((role) => typeof role === "string")
|
|
189
|
+
: [];
|
|
190
|
+
return {
|
|
191
|
+
ok: typeof value.ok === "boolean" ? value.ok : undefined,
|
|
192
|
+
platform: typeof value.platform === "string" ? value.platform : undefined,
|
|
193
|
+
roles,
|
|
194
|
+
scannerVersion: typeof value.scannerVersion === "string" ? value.scannerVersion : undefined,
|
|
195
|
+
privacy: isRecord(value.privacy) ? value.privacy : undefined
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function privacyFlagsAreSafe(value) {
|
|
199
|
+
return isRecord(value) && Object.values(value).every((flag) => flag === false);
|
|
200
|
+
}
|
|
201
|
+
function isSafePublicHttpsUrl(value) {
|
|
202
|
+
try {
|
|
203
|
+
const url = new URL(value.trim());
|
|
204
|
+
return (url.protocol === "https:" &&
|
|
205
|
+
!url.username &&
|
|
206
|
+
!url.password &&
|
|
207
|
+
!url.search &&
|
|
208
|
+
!url.hash &&
|
|
209
|
+
!isUnsafeHostedHostname(url.hostname));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function isUnsafeHostedHostname(hostname) {
|
|
216
|
+
const normalized = hostname.toLowerCase().replace(/\.$/, "");
|
|
217
|
+
return (normalized === "localhost" ||
|
|
218
|
+
normalized.endsWith(".localhost") ||
|
|
219
|
+
isUnsafeIpv4Hostname(normalized) ||
|
|
220
|
+
normalized === "::1" ||
|
|
221
|
+
normalized.startsWith("fc") ||
|
|
222
|
+
normalized.startsWith("fd") ||
|
|
223
|
+
normalized.startsWith("fe80:"));
|
|
224
|
+
}
|
|
225
|
+
function isUnsafeIpv4Hostname(hostname) {
|
|
226
|
+
const parts = hostname.split(".");
|
|
227
|
+
if (parts.length !== 4 || !parts.every((part) => /^\d+$/.test(part)))
|
|
228
|
+
return false;
|
|
229
|
+
const octets = parts.map((part) => Number(part));
|
|
230
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
const [first, second] = octets;
|
|
234
|
+
return (first === 0 ||
|
|
235
|
+
first === 10 ||
|
|
236
|
+
first === 127 ||
|
|
237
|
+
(first === 169 && second === 254) ||
|
|
238
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
239
|
+
(first === 192 && second === 168) ||
|
|
240
|
+
(first === 100 && second >= 64 && second <= 127) ||
|
|
241
|
+
first >= 224);
|
|
242
|
+
}
|
|
243
|
+
function trimTrailingSlashes(value) {
|
|
244
|
+
let end = value.length;
|
|
245
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
246
|
+
end -= 1;
|
|
247
|
+
}
|
|
248
|
+
return value.slice(0, end);
|
|
249
|
+
}
|
|
250
|
+
function isRecord(value) {
|
|
251
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
252
|
+
}
|
|
253
|
+
function deployedPrivacy() {
|
|
254
|
+
return {
|
|
255
|
+
includesRawHealthResponse: false,
|
|
256
|
+
includesPublicBaseUrl: false,
|
|
257
|
+
includesRawWebhookPayload: false,
|
|
258
|
+
includesUntrustedPrText: false,
|
|
259
|
+
includesRawSource: false,
|
|
260
|
+
includesRawDiffs: false,
|
|
261
|
+
includesSecrets: false,
|
|
262
|
+
includesCustomerPayloads: false,
|
|
263
|
+
includesPrivateCheckoutPath: false,
|
|
264
|
+
includesInstallationToken: false,
|
|
265
|
+
claimsProductionHostedService: false
|
|
266
|
+
};
|
|
267
|
+
}
|
package/dist/hosted/service.js
CHANGED
|
@@ -173,7 +173,7 @@ export function createHostedServiceRuntime(options) {
|
|
|
173
173
|
cleanup
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
-
catch {
|
|
176
|
+
catch (error) {
|
|
177
177
|
const cleanup = createHostedWorkerCheckoutCleanupPlan({
|
|
178
178
|
identity: queuedRecord.identity,
|
|
179
179
|
jobKey: queuedRecord.key,
|
|
@@ -186,6 +186,7 @@ export function createHostedServiceRuntime(options) {
|
|
|
186
186
|
processed: true,
|
|
187
187
|
status: "failed",
|
|
188
188
|
queueRecord: cloneQueueRecord(queuedRecord),
|
|
189
|
+
reason: safeScanRunnerFailureReason(error),
|
|
189
190
|
errorClass: "scan_runner_failed",
|
|
190
191
|
workerPlan: acceptedWorkerPlan,
|
|
191
192
|
cleanup
|
|
@@ -194,6 +195,16 @@ export function createHostedServiceRuntime(options) {
|
|
|
194
195
|
}
|
|
195
196
|
};
|
|
196
197
|
}
|
|
198
|
+
function safeScanRunnerFailureReason(error) {
|
|
199
|
+
if (typeof error === "object" &&
|
|
200
|
+
error !== null &&
|
|
201
|
+
"safeReason" in error &&
|
|
202
|
+
typeof error.safeReason === "string" &&
|
|
203
|
+
/^[a-z][a-z0-9_]{1,80}$/.test(error.safeReason)) {
|
|
204
|
+
return error.safeReason;
|
|
205
|
+
}
|
|
206
|
+
return "scan_runner_failed";
|
|
207
|
+
}
|
|
197
208
|
function rejectWebhookRequest(stage, reason, deliveryId) {
|
|
198
209
|
return {
|
|
199
210
|
accepted: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type HostedOperationalReleaseGateEvidence } from "./contracts.js";
|
|
2
|
-
import { type HostedServiceRuntimeOptions, type HostedServiceScanRunnerResult, type HostedServiceWebhookStage } from "./service.js";
|
|
1
|
+
import { type HostedOperationalReleaseGateDecision, type HostedOperationalReleaseGateEvidence } from "./contracts.js";
|
|
2
|
+
import { type HostedServiceRuntimeOptions, type HostedServiceScanRunnerInput, type HostedServiceScanRunnerResult, type HostedServiceWebhookStage } from "./service.js";
|
|
3
3
|
type RepositoryIdSource = HostedServiceRuntimeOptions["selectedRepositoryIdsByInstallation"];
|
|
4
4
|
export interface FileBackedHostedStagingHarnessOptions {
|
|
5
5
|
rootDir: string;
|
|
@@ -7,7 +7,7 @@ export interface FileBackedHostedStagingHarnessOptions {
|
|
|
7
7
|
scannerVersion: string;
|
|
8
8
|
selectedRepositoryIdsByInstallation: RepositoryIdSource;
|
|
9
9
|
removedRepositoryIdsByInstallation?: RepositoryIdSource;
|
|
10
|
-
scanResult: HostedServiceScanRunnerResult;
|
|
10
|
+
scanResult: HostedServiceScanRunnerResult | ((input: HostedServiceScanRunnerInput) => HostedServiceScanRunnerResult | Promise<HostedServiceScanRunnerResult>);
|
|
11
11
|
now?: () => string;
|
|
12
12
|
}
|
|
13
13
|
export interface FileBackedHostedStagingHarness {
|
|
@@ -59,6 +59,7 @@ export type HostedStagingHarnessWorkerTickResult = {
|
|
|
59
59
|
status: "failed";
|
|
60
60
|
errorClass: "worker_plan_rejected" | "check_run_publication_rejected" | "scan_runner_failed";
|
|
61
61
|
reason?: string;
|
|
62
|
+
safeFailureReason?: string;
|
|
62
63
|
workerSandboxDeleted: boolean;
|
|
63
64
|
activeWorkerSandboxCount: number;
|
|
64
65
|
cleanupVerified: boolean;
|
|
@@ -80,6 +81,67 @@ export interface HostedStagingHarnessPrivacy {
|
|
|
80
81
|
includesInstallationToken: false;
|
|
81
82
|
claimsLiveHostedService: false;
|
|
82
83
|
}
|
|
84
|
+
export type HostedLogBoundaryBlockedReason = "raw_source" | "raw_diff" | "secret_value" | "customer_payload" | "installation_token" | "checkout_path" | "private_url" | "untrusted_pr_text";
|
|
85
|
+
export interface HostedLogBoundaryForbiddenInput {
|
|
86
|
+
rawSource?: string;
|
|
87
|
+
rawDiff?: string;
|
|
88
|
+
secretValues?: string[];
|
|
89
|
+
customerPayloads?: string[];
|
|
90
|
+
installationTokens?: string[];
|
|
91
|
+
checkoutPaths?: string[];
|
|
92
|
+
privateUrls?: string[];
|
|
93
|
+
untrustedPrText?: string[];
|
|
94
|
+
}
|
|
95
|
+
export interface HostedLogBoundaryValidationInput {
|
|
96
|
+
samples: unknown[];
|
|
97
|
+
forbidden: HostedLogBoundaryForbiddenInput;
|
|
98
|
+
}
|
|
99
|
+
export interface HostedLogBoundaryValidation {
|
|
100
|
+
accepted: boolean;
|
|
101
|
+
sampleCount: number;
|
|
102
|
+
blockedReasons: HostedLogBoundaryBlockedReason[];
|
|
103
|
+
allowedFields: string[];
|
|
104
|
+
privacy: HostedStagingHarnessPrivacy;
|
|
105
|
+
}
|
|
106
|
+
export interface HostedStagingReleaseEvidenceBundleInput {
|
|
107
|
+
collectedAt: string;
|
|
108
|
+
evidenceBaseUrl: string;
|
|
109
|
+
owner: string;
|
|
110
|
+
webhookReplays: HostedStagingHarnessReplayResult[];
|
|
111
|
+
workerTicks: HostedStagingHarnessWorkerTickResult[];
|
|
112
|
+
logBoundary: HostedLogBoundaryValidation;
|
|
113
|
+
externalEvidence: HostedOperationalReleaseGateEvidence[];
|
|
114
|
+
requiredFailureReasons?: string[];
|
|
115
|
+
}
|
|
116
|
+
export interface HostedStagingReleaseEvidenceBundle {
|
|
117
|
+
readyForReleaseGate: boolean;
|
|
118
|
+
evidence: HostedOperationalReleaseGateEvidence[];
|
|
119
|
+
releaseGateInput: {
|
|
120
|
+
evidence: HostedOperationalReleaseGateEvidence[];
|
|
121
|
+
};
|
|
122
|
+
scenarioSummary: {
|
|
123
|
+
webhookReplayAccepted: boolean;
|
|
124
|
+
completedWorkerProbe: boolean;
|
|
125
|
+
failureCleanupProbe: boolean;
|
|
126
|
+
observedFailureReasons: string[];
|
|
127
|
+
allWorkerCheckoutsDeleted: boolean;
|
|
128
|
+
logBoundaryAccepted: boolean;
|
|
129
|
+
};
|
|
130
|
+
privacy: HostedStagingHarnessPrivacy;
|
|
131
|
+
}
|
|
132
|
+
export interface HostedStagingReleaseEvidenceGateInput {
|
|
133
|
+
bundle: HostedStagingReleaseEvidenceBundle;
|
|
134
|
+
commitSha: string;
|
|
135
|
+
scannerVersion: string;
|
|
136
|
+
deploymentTarget: string;
|
|
137
|
+
evaluatedAt: string;
|
|
138
|
+
releaseNotes: string;
|
|
139
|
+
containerImageDigest: string;
|
|
140
|
+
maxEvidenceAgeDays?: number;
|
|
141
|
+
}
|
|
83
142
|
export declare function createFileBackedHostedStagingHarness(options: FileBackedHostedStagingHarnessOptions): FileBackedHostedStagingHarness;
|
|
84
143
|
export declare function createHostedStagingHarnessEvidence(input: HostedStagingHarnessEvidenceInput): HostedOperationalReleaseGateEvidence[];
|
|
144
|
+
export declare function validateHostedLogBoundary(input: HostedLogBoundaryValidationInput): HostedLogBoundaryValidation;
|
|
145
|
+
export declare function createHostedStagingReleaseEvidenceBundle(input: HostedStagingReleaseEvidenceBundleInput): HostedStagingReleaseEvidenceBundle;
|
|
146
|
+
export declare function evaluateHostedStagingReleaseEvidenceBundle(input: HostedStagingReleaseEvidenceGateInput): HostedOperationalReleaseGateDecision;
|
|
85
147
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS } from "./contracts.js";
|
|
3
|
+
import { evaluateHostedOperationalReleaseGate, HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS } from "./contracts.js";
|
|
4
4
|
import { createHostedServiceRuntime } from "./service.js";
|
|
5
5
|
export function createFileBackedHostedStagingHarness(options) {
|
|
6
6
|
const paths = hostedStagingHarnessPaths(options.rootDir);
|
|
@@ -16,12 +16,16 @@ export function createFileBackedHostedStagingHarness(options) {
|
|
|
16
16
|
queue,
|
|
17
17
|
compactReportStore: reportStore,
|
|
18
18
|
checkRunPublisher,
|
|
19
|
-
scanRunner: async (
|
|
19
|
+
scanRunner: async (input) => {
|
|
20
|
+
const { queueRecord } = input;
|
|
20
21
|
const sandboxPath = join(paths.workerSandboxRoot, safeFileSegment(queueRecord.key));
|
|
21
22
|
workerSandboxPaths.add(sandboxPath);
|
|
22
23
|
await mkdir(sandboxPath, { recursive: true });
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const scanResult = typeof options.scanResult === "function"
|
|
25
|
+
? await options.scanResult(input)
|
|
26
|
+
: options.scanResult;
|
|
27
|
+
await writeFile(join(sandboxPath, "source.ts"), scanResult.rawSource ?? "", "utf8");
|
|
28
|
+
return scanResult;
|
|
25
29
|
},
|
|
26
30
|
now: options.now
|
|
27
31
|
});
|
|
@@ -77,6 +81,7 @@ export function createFileBackedHostedStagingHarness(options) {
|
|
|
77
81
|
status: "failed",
|
|
78
82
|
errorClass: result.errorClass,
|
|
79
83
|
...(result.reason === undefined ? {} : { reason: result.reason }),
|
|
84
|
+
...(result.reason === undefined ? {} : { safeFailureReason: result.reason }),
|
|
80
85
|
workerSandboxDeleted: activeWorkerSandboxCount === 0,
|
|
81
86
|
activeWorkerSandboxCount,
|
|
82
87
|
cleanupVerified: (result.cleanup?.shouldDeleteWorkerCheckout ?? true) && activeWorkerSandboxCount === 0,
|
|
@@ -95,6 +100,171 @@ export function createHostedStagingHarnessEvidence(input) {
|
|
|
95
100
|
owner: input.owner
|
|
96
101
|
}));
|
|
97
102
|
}
|
|
103
|
+
export function validateHostedLogBoundary(input) {
|
|
104
|
+
const serializedSamples = input.samples.map((sample) => JSON.stringify(sample)).join("\n");
|
|
105
|
+
const blockedReasons = new Set();
|
|
106
|
+
markIfContains(blockedReasons, serializedSamples, input.forbidden.rawSource, "raw_source");
|
|
107
|
+
markIfContains(blockedReasons, serializedSamples, input.forbidden.rawDiff, "raw_diff");
|
|
108
|
+
markIfContainsAny(blockedReasons, serializedSamples, input.forbidden.secretValues, "secret_value");
|
|
109
|
+
markIfContainsAny(blockedReasons, serializedSamples, input.forbidden.customerPayloads, "customer_payload");
|
|
110
|
+
markIfContainsAny(blockedReasons, serializedSamples, input.forbidden.installationTokens, "installation_token");
|
|
111
|
+
markIfContainsAny(blockedReasons, serializedSamples, input.forbidden.checkoutPaths, "checkout_path");
|
|
112
|
+
markIfContainsAny(blockedReasons, serializedSamples, input.forbidden.privateUrls, "private_url");
|
|
113
|
+
markIfContainsAny(blockedReasons, serializedSamples, input.forbidden.untrustedPrText, "untrusted_pr_text");
|
|
114
|
+
if (/\bgh[opsu]_[A-Za-z0-9_]{8,}\b/.test(serializedSamples)) {
|
|
115
|
+
blockedReasons.add("installation_token");
|
|
116
|
+
}
|
|
117
|
+
if (/\b(?:sk_(?:live|test)|whsec_)[A-Za-z0-9_]+\b|-----BEGIN [A-Z ]+-----/.test(serializedSamples)) {
|
|
118
|
+
blockedReasons.add("secret_value");
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
accepted: blockedReasons.size === 0,
|
|
122
|
+
sampleCount: input.samples.length,
|
|
123
|
+
blockedReasons: [...blockedReasons].sort(),
|
|
124
|
+
allowedFields: [
|
|
125
|
+
"scanKey",
|
|
126
|
+
"installationId",
|
|
127
|
+
"repositoryId",
|
|
128
|
+
"pullRequestNumber",
|
|
129
|
+
"headSha",
|
|
130
|
+
"scannerVersion",
|
|
131
|
+
"durationMs",
|
|
132
|
+
"summaryCounts",
|
|
133
|
+
"errorClass",
|
|
134
|
+
"cleanupStatus"
|
|
135
|
+
],
|
|
136
|
+
privacy: hostedStagingHarnessPrivacy()
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function createHostedStagingReleaseEvidenceBundle(input) {
|
|
140
|
+
const externalEvidence = new Map(input.externalEvidence.map((evidence) => [evidence.id, sanitizeEvidence(evidence, input)]));
|
|
141
|
+
const scenarioSummary = hostedStagingScenarioSummary(input);
|
|
142
|
+
const evidence = HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS.map((requirement) => {
|
|
143
|
+
const generated = generatedEvidenceFor(requirement.id, input, scenarioSummary);
|
|
144
|
+
return generated ?? externalEvidence.get(requirement.id) ?? missingEvidence(requirement.id, input);
|
|
145
|
+
});
|
|
146
|
+
const readyForReleaseGate = evidence.every((item) => item.status === "passed");
|
|
147
|
+
return {
|
|
148
|
+
readyForReleaseGate,
|
|
149
|
+
evidence,
|
|
150
|
+
releaseGateInput: { evidence },
|
|
151
|
+
scenarioSummary,
|
|
152
|
+
privacy: hostedStagingHarnessPrivacy()
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
export function evaluateHostedStagingReleaseEvidenceBundle(input) {
|
|
156
|
+
return evaluateHostedOperationalReleaseGate({
|
|
157
|
+
commitSha: input.commitSha,
|
|
158
|
+
scannerVersion: input.scannerVersion,
|
|
159
|
+
deploymentTarget: input.deploymentTarget,
|
|
160
|
+
evaluatedAt: input.evaluatedAt,
|
|
161
|
+
evidence: input.bundle.evidence,
|
|
162
|
+
releaseNotes: input.releaseNotes,
|
|
163
|
+
containerImageDigest: input.containerImageDigest,
|
|
164
|
+
maxEvidenceAgeDays: input.maxEvidenceAgeDays
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function hostedStagingScenarioSummary(input) {
|
|
168
|
+
const processedWorkers = input.workerTicks.filter((tick) => tick.processed);
|
|
169
|
+
const webhookReplayAccepted = input.webhookReplays.some((replay) => replay.accepted && replay.queuedWorker && replay.shouldCreateCheckRun);
|
|
170
|
+
const completedWorkerProbe = processedWorkers.some((tick) => tick.status === "completed" && tick.cleanupVerified);
|
|
171
|
+
const observedFailureReasons = [
|
|
172
|
+
...new Set(processedWorkers.flatMap((tick) => tick.status === "failed" && tick.cleanupVerified && tick.safeFailureReason
|
|
173
|
+
? [tick.safeFailureReason]
|
|
174
|
+
: []))
|
|
175
|
+
].sort();
|
|
176
|
+
const requiredFailureReasons = input.requiredFailureReasons ?? [];
|
|
177
|
+
const failureCleanupProbe = requiredFailureReasons.length
|
|
178
|
+
? requiredFailureReasons.every((reason) => observedFailureReasons.includes(reason))
|
|
179
|
+
: processedWorkers.some((tick) => tick.status === "failed" && tick.cleanupVerified);
|
|
180
|
+
const allWorkerCheckoutsDeleted = processedWorkers.length > 0 &&
|
|
181
|
+
processedWorkers.every((tick) => tick.workerSandboxDeleted &&
|
|
182
|
+
tick.activeWorkerSandboxCount === 0 &&
|
|
183
|
+
tick.cleanupVerified);
|
|
184
|
+
return {
|
|
185
|
+
webhookReplayAccepted,
|
|
186
|
+
completedWorkerProbe,
|
|
187
|
+
failureCleanupProbe,
|
|
188
|
+
observedFailureReasons,
|
|
189
|
+
allWorkerCheckoutsDeleted,
|
|
190
|
+
logBoundaryAccepted: input.logBoundary.accepted
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function generatedEvidenceFor(id, input, summary) {
|
|
194
|
+
if (id === "webhook_replay") {
|
|
195
|
+
return summary.webhookReplayAccepted
|
|
196
|
+
? passedEvidence(id, "Signed webhook replay queued a check-run-only worker from trusted fields.", input)
|
|
197
|
+
: missingEvidence(id, input);
|
|
198
|
+
}
|
|
199
|
+
if (id === "queue_worker_cleanup") {
|
|
200
|
+
return summary.completedWorkerProbe &&
|
|
201
|
+
summary.failureCleanupProbe &&
|
|
202
|
+
summary.allWorkerCheckoutsDeleted
|
|
203
|
+
? passedEvidence(id, "Success and failure worker probes deleted worker checkouts and recorded cleanup-safe status.", input)
|
|
204
|
+
: missingEvidence(id, input);
|
|
205
|
+
}
|
|
206
|
+
if (id === "privacy_retention") {
|
|
207
|
+
return input.logBoundary.sampleCount > 0 && summary.logBoundaryAccepted && privacyFlagsAreSafe(input)
|
|
208
|
+
? passedEvidence(id, "Log boundary accepted safe metadata only and compact reports avoided raw payloads.", input)
|
|
209
|
+
: missingEvidence(id, input);
|
|
210
|
+
}
|
|
211
|
+
if (id === "release_cleanup") {
|
|
212
|
+
return summary.allWorkerCheckoutsDeleted
|
|
213
|
+
? passedEvidence(id, "Release cleanup probe left no active staging worker sandbox entries.", input)
|
|
214
|
+
: missingEvidence(id, input);
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function privacyFlagsAreSafe(input) {
|
|
219
|
+
const replayPrivacySafe = input.webhookReplays.every((replay) => Object.values(replay.privacy).every((value) => value === false));
|
|
220
|
+
const workerPrivacySafe = input.workerTicks.every((tick) => Object.values(tick.privacy).every((value) => value === false));
|
|
221
|
+
const logPrivacySafe = Object.values(input.logBoundary.privacy).every((value) => value === false);
|
|
222
|
+
return replayPrivacySafe && workerPrivacySafe && logPrivacySafe;
|
|
223
|
+
}
|
|
224
|
+
function sanitizeEvidence(evidence, input) {
|
|
225
|
+
return {
|
|
226
|
+
id: evidence.id,
|
|
227
|
+
status: evidence.status,
|
|
228
|
+
...(evidence.collectedAt === undefined
|
|
229
|
+
? { collectedAt: input.collectedAt }
|
|
230
|
+
: { collectedAt: evidence.collectedAt }),
|
|
231
|
+
...(evidence.evidenceUrl === undefined ? {} : { evidenceUrl: evidence.evidenceUrl }),
|
|
232
|
+
note: `External release-gate evidence recorded for ${evidence.id}.`,
|
|
233
|
+
...(evidence.owner === undefined ? { owner: input.owner } : { owner: evidence.owner })
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function passedEvidence(id, note, input) {
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
status: "passed",
|
|
240
|
+
collectedAt: input.collectedAt,
|
|
241
|
+
evidenceUrl: evidenceUrlFor(input, id),
|
|
242
|
+
note,
|
|
243
|
+
owner: input.owner
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function missingEvidence(id, input) {
|
|
247
|
+
return {
|
|
248
|
+
id,
|
|
249
|
+
status: "missing",
|
|
250
|
+
collectedAt: input.collectedAt,
|
|
251
|
+
note: `Missing executable staging evidence for ${id}.`,
|
|
252
|
+
owner: input.owner
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function evidenceUrlFor(input, id) {
|
|
256
|
+
return `${input.evidenceBaseUrl.replace(/\/+$/, "")}/${id}.json`;
|
|
257
|
+
}
|
|
258
|
+
function markIfContains(blockedReasons, haystack, value, reason) {
|
|
259
|
+
if (value && haystack.includes(value)) {
|
|
260
|
+
blockedReasons.add(reason);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function markIfContainsAny(blockedReasons, haystack, values, reason) {
|
|
264
|
+
for (const value of values ?? []) {
|
|
265
|
+
markIfContains(blockedReasons, haystack, value, reason);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
98
268
|
function hostedStagingHarnessPaths(rootDir) {
|
|
99
269
|
const queueDir = join(rootDir, "queue");
|
|
100
270
|
const reportDir = join(rootDir, "reports");
|
package/dist/hosted/worker.js
CHANGED
|
@@ -34,6 +34,9 @@ export async function runHostedReadOnlyCheckoutScan(input, options) {
|
|
|
34
34
|
if (!plan.accepted || !plan.readOnly || !checkout || !cli || cli.writeMode !== "read_only") {
|
|
35
35
|
throw new HostedReadOnlyCheckoutScanError("invalid_worker_plan");
|
|
36
36
|
}
|
|
37
|
+
if (!isTrustedFixedReadOnlyPlan(input)) {
|
|
38
|
+
throw new HostedReadOnlyCheckoutScanError("invalid_worker_plan");
|
|
39
|
+
}
|
|
37
40
|
const repository = parseRepositoryFullName(checkout.repositoryFullName);
|
|
38
41
|
if (!repository) {
|
|
39
42
|
throw new HostedReadOnlyCheckoutScanError("invalid_repository_full_name");
|
|
@@ -76,6 +79,7 @@ export async function runHostedReadOnlyCheckoutScan(input, options) {
|
|
|
76
79
|
await runCommand(options, gitSecretEnv, commandSpec("git_fetch_head", gitCommand, ["fetch", "--no-tags", "--depth", String(fetchDepth), "origin", checkout.targetCommitSha], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
|
|
77
80
|
await runCommand(options, gitSecretEnv, commandSpec("git_fetch_base", gitCommand, ["fetch", "--no-tags", "--depth", String(fetchDepth), "origin", checkout.baseSha], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
|
|
78
81
|
await runCommand(options, gitSecretEnv, commandSpec("git_checkout", gitCommand, ["checkout", "--detach", checkout.targetCommitSha], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
|
|
82
|
+
await rm(askpassPath, { force: true });
|
|
79
83
|
const cliEnv = safeWorkerEnv(checkoutDir);
|
|
80
84
|
const cliArgs = cli.args.map((arg) => arg === "<worker-checkout>" ? checkoutDir : arg);
|
|
81
85
|
const cliResult = await runCommand(options, {}, commandSpec("cli_scan", options.cliCommand ?? cli.command, cliArgs, checkoutDir, cliEnv, timeoutMs, maxOutputBytes));
|
|
@@ -143,6 +147,53 @@ function compactScanRunnerResult(stdout) {
|
|
|
143
147
|
throw new HostedReadOnlyCheckoutScanError("invalid_cli_output");
|
|
144
148
|
}
|
|
145
149
|
}
|
|
150
|
+
function isTrustedFixedReadOnlyPlan(input) {
|
|
151
|
+
const { plan, queueRecord } = input;
|
|
152
|
+
const { checkout, cli, installationTokenScope, output } = plan;
|
|
153
|
+
const identity = queueRecord.identity;
|
|
154
|
+
if (!checkout || !cli || !installationTokenScope || !output)
|
|
155
|
+
return false;
|
|
156
|
+
const expectedCliArgs = [
|
|
157
|
+
"pr-risk",
|
|
158
|
+
"--root",
|
|
159
|
+
"<worker-checkout>",
|
|
160
|
+
"--base",
|
|
161
|
+
identity.baseSha,
|
|
162
|
+
"--json"
|
|
163
|
+
];
|
|
164
|
+
return (plan.jobKey === queueRecord.key &&
|
|
165
|
+
plan.readOnly === true &&
|
|
166
|
+
plan.shouldFetchSource === true &&
|
|
167
|
+
plan.shouldRunCli === true &&
|
|
168
|
+
plan.shouldPersistRawSource === false &&
|
|
169
|
+
plan.shouldPersistRawDiffs === false &&
|
|
170
|
+
plan.shouldCreatePrComment === false &&
|
|
171
|
+
installationTokenScope.installationId === identity.installationId &&
|
|
172
|
+
installationTokenScope.repositoryId === identity.repositoryId &&
|
|
173
|
+
installationTokenScope.permissions.contents === "read" &&
|
|
174
|
+
installationTokenScope.selectedRepositoryOnly === true &&
|
|
175
|
+
checkout.repositoryId === identity.repositoryId &&
|
|
176
|
+
checkout.repositoryFullName === identity.repositoryFullName &&
|
|
177
|
+
checkout.pullRequestNumber === identity.pullRequestNumber &&
|
|
178
|
+
checkout.baseSha === identity.baseSha &&
|
|
179
|
+
checkout.targetCommitSha === identity.headSha &&
|
|
180
|
+
checkout.directoryScope === "temporary_worker_directory" &&
|
|
181
|
+
checkout.cleanupRequired === true &&
|
|
182
|
+
checkout.returnsCheckoutPath === false &&
|
|
183
|
+
cli.command === "ai-saas-guard" &&
|
|
184
|
+
cli.workingDirectory === "<worker-checkout>" &&
|
|
185
|
+
cli.networkAccess === "disabled" &&
|
|
186
|
+
cli.writeMode === "read_only" &&
|
|
187
|
+
arraysEqual(cli.args, expectedCliArgs) &&
|
|
188
|
+
output.compactJsonOnly === true &&
|
|
189
|
+
output.persistRawSource === false &&
|
|
190
|
+
output.persistRawDiffs === false &&
|
|
191
|
+
output.persistSecrets === false &&
|
|
192
|
+
output.persistCustomerPayloads === false);
|
|
193
|
+
}
|
|
194
|
+
function arraysEqual(left, right) {
|
|
195
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
196
|
+
}
|
|
146
197
|
function compactFinding(value) {
|
|
147
198
|
if (!isRecord(value))
|
|
148
199
|
return [];
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -169,18 +169,18 @@ node dist/cli.js scan --root /path/to/your-saas
|
|
|
169
169
|
|
|
170
170
|
这个仓库是公开 GitHub 仓库。
|
|
171
171
|
|
|
172
|
-
CLI 已发布到 npm:`ai-saas-guard@0.
|
|
172
|
+
CLI 已发布到 npm:`ai-saas-guard@0.32.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.32.0`。
|
|
173
173
|
|
|
174
174
|
| 模块 | 状态 |
|
|
175
175
|
| --- | --- |
|
|
176
176
|
| 公开 GitHub 仓库 | 已可用 |
|
|
177
|
-
| npm CLI | `ai-saas-guard@0.
|
|
178
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.
|
|
177
|
+
| npm CLI | `ai-saas-guard@0.32.0` |
|
|
178
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.32.0` |
|
|
179
179
|
| 输出格式 | 短 summary、Terminal、JSON、SARIF 和 PR markdown |
|
|
180
180
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
|
|
181
181
|
| 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
|
|
182
|
-
| 当前版本 | `0.
|
|
183
|
-
| Action 标签 | `v0.
|
|
182
|
+
| 当前版本 | `0.32.0` 增加 deployed worker staging evidence:public HTTPS health validation、deployed 成功/失败 cleanup probes、log-boundary checks,以及针对 Node/container read-only checkout worker candidate 的 release-gate evaluation |
|
|
183
|
+
| Action 标签 | `v0.32.0`、`v0` |
|
|
184
184
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
185
185
|
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
186
186
|
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
|
|
@@ -349,6 +349,7 @@ GitHub Marketplace wrapper 决策见 [docs/github-marketplace-wrapper-decision.m
|
|
|
349
349
|
- [docs/hosted-node-container-app.md](hosted-node-container-app.md)
|
|
350
350
|
- [docs/hosted-staging-deployment.md](hosted-staging-deployment.md)
|
|
351
351
|
- [docs/hosted-staging-harness.md](hosted-staging-harness.md)
|
|
352
|
+
- [docs/hosted-deployed-worker-staging.md](hosted-deployed-worker-staging.md)
|
|
352
353
|
- [docs/hosted-operational-release-gate.md](hosted-operational-release-gate.md)
|
|
353
354
|
- [docs/hosted-uninstall-data-deletion.md](hosted-uninstall-data-deletion.md)
|
|
354
355
|
- [docs/hosted-pricing-packaging.md](hosted-pricing-packaging.md)
|
|
@@ -359,13 +360,14 @@ GitHub Marketplace wrapper 决策见 [docs/github-marketplace-wrapper-decision.m
|
|
|
359
360
|
- pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
|
|
360
361
|
- durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
|
|
361
362
|
- worker read-only scan planner:只用 trusted identity 规划临时 worker checkout,要求 repository `contents: read`,固定运行 `ai-saas-guard pr-risk --json`,并忽略 PR 正文里的 repo 名、token scope 或命令
|
|
362
|
-
- hosted read-only checkout worker:`ai-saas-guard/hosted/worker` 导出 `createHostedReadOnlyCheckoutScanRunner`,从 trusted GitHub App identity 创建临时 checkout,只通过 git askpass 使用 runtime installation token,运行固定 `ai-saas-guard pr-risk --json`,把 CLI JSON 转成 compact findings,并在成功或失败后删除 checkout;不会返回源码、diff、secret、checkout path、PR 里写的命令或 installation token
|
|
363
|
+
- hosted read-only checkout worker:`ai-saas-guard/hosted/worker` 导出 `createHostedReadOnlyCheckoutScanRunner`,从 trusted GitHub App identity 创建临时 checkout,只通过 git askpass 使用 runtime installation token,在 CLI 阶段前移除 askpass material,拒绝被篡改的 command/checkout/token-scope plan,运行固定 `ai-saas-guard pr-risk --json`,把 CLI JSON 转成 compact findings,并在成功或失败后删除 checkout;不会返回源码、diff、secret、checkout path、PR 里写的命令或 installation token
|
|
363
364
|
- hosted service runtime:`ai-saas-guard/hosted/service` 导出 `createHostedServiceRuntime`,把签名 webhook intake、幂等 queue upsert、read-only worker 编排、compact report 存储、Check Run 发布 adapter 和 worker cleanup 串成可测试的服务核心;它本身不部署公开 hosted 环境
|
|
364
365
|
- GitHub App deployment planner:`ai-saas-guard/hosted/github-app` 导出 `planHostedGitHubAppDeployment`,生成 first slice 最小权限 manifest,并在 release gate、公开 HTTPS URL、container digest、secret 引用、原始 secret 输入、permission 或 event 不安全时阻止创建
|
|
365
366
|
- Hosted production adapter layer:`ai-saas-guard/hosted/production-adapters` 导出 `createHostedGitHubAppJwt`、`planHostedGitHubInstallationTokenRequest` 和 `planHostedProductionWorkerExecution`,用于 GitHub App RS256 JWT、selected-repository installation token 请求规划、worker/check-run 分离 token scope、固定只读 worker 命令、timeout/output 预算、compact JSON-only 输出,以及 success/failure/timeout/cancellation 的 cleanup 规划;它本身仍然不部署公开 hosted 服务
|
|
366
367
|
- Hosted Node/container app skeleton:`ai-saas-guard/hosted/app` 导出 `createHostedHttpApp`、`createInMemoryHostedAppPlatform`、`createHostedNodeCheckoutAppPlatform` 和 `planHostedNodeContainerDeployment`,提供安全 `/healthz`、签名 `/github/webhook` ingress、单 job worker tick、测试用 in-memory provider adapters、真实 read-only checkout worker 组合入口、可见 timeout/output 安全预算,以及 secret manager、queue、compact report store、worker sandbox、GitHub Checks publisher 的部署引用校验;它本身仍然不部署或暴露公开 hosted 服务
|
|
367
368
|
- Hosted staging deployment planner:`ai-saas-guard/hosted/staging` 导出 `planHostedProviderBinding`、`planHostedStagingDeployment` 和 `planHostedGitHubAppPromotion`,把真实 provider 引用、Node/container deployment plan、hosted operational release-gate evidence 和 GitHub App deployment planning 组合起来;缺少 queue、store、worker sandbox、Check Run publisher、logs、metrics、rollback 或 incident-response 引用时,会阻止 staging exposure 和 production promotion;它本身仍然不会调用云平台、创建 GitHub App 或暴露公开 hosted 服务
|
|
368
|
-
- Hosted staging harness:`ai-saas-guard/hosted/staging-harness` 导出 `createFileBackedHostedStagingHarness` 和 `
|
|
369
|
+
- Hosted staging harness:`ai-saas-guard/hosted/staging-harness` 导出 `createFileBackedHostedStagingHarness`、`createHostedStagingHarnessEvidence`、`createHostedStagingReleaseEvidenceBundle`、`evaluateHostedStagingReleaseEvidenceBundle` 和 `validateHostedLogBoundary`,可以在本地用 file-backed queue、compact report、Check Run request 和 worker sandbox 跑通签名 webhook replay、worker tick 和 cleanup 校验,把 success/failure cleanup probes 与 log-boundary samples 转成 release-gate evidence,并直接执行 hosted release gate 判断;它只是 staging 演练工具,不会调用云平台、创建 GitHub App、写真实 Check Run 或暴露公开 hosted 服务
|
|
370
|
+
- Deployed worker staging evidence:`ai-saas-guard/hosted/deployed-staging` 导出 `createHostedDeployedWorkerStagingEvidenceBundle` 和 `evaluateHostedDeployedWorkerStagingReleaseGate`,把 public HTTPS health、signed webhook replay、deployed worker cleanup、log-boundary samples 以及外部 CI/scan/rollback evidence 转成 hosted release gate evidence;它不会部署云资源,也不会宣称 production hosted exposure
|
|
369
371
|
- Cloudflare hosted ingress:`hosted/cloudflare-worker` 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`,提供 `/healthz`、`/github/app/manifest-callback` 和签名 `/github/webhook` intake;Worker 已具备 compact pull request identity、file/category risk signal 和 Check Run metadata 路径;staging GitHub App ID 为 `3834787`,installation ID 为 `135085075`;真实 GitHub App webhook delivery 和 Check Run smoke 已通过;完整 source checkout worker deployment、monitoring、rollback 和 incident-response evidence 仍需要通过 hosted operational release gate
|
|
370
372
|
- webhook event parser
|
|
371
373
|
- check-run summary renderer
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Hosted Deployed Worker Staging Evidence
|
|
2
|
+
|
|
3
|
+
This document describes the deployed worker staging evidence helper implemented in `src/hosted/deployed-staging.ts`.
|
|
4
|
+
|
|
5
|
+
The package exports `ai-saas-guard/hosted/deployed-staging` with:
|
|
6
|
+
|
|
7
|
+
- `createHostedDeployedWorkerStagingEvidenceBundle`
|
|
8
|
+
- `evaluateHostedDeployedWorkerStagingReleaseGate`
|
|
9
|
+
|
|
10
|
+
The helper is for a deployed Node/container read-only checkout worker release candidate. It turns safe deployment observations into the same hosted operational release-gate evidence used by the rest of the hosted planning layer.
|
|
11
|
+
|
|
12
|
+
It does not deploy cloud resources, create a GitHub App, call GitHub, fetch repositories, upload source code, or publish Check Runs by itself. It is not production hosted exposure. It records whether a deployed staging candidate has enough public-safe evidence to pass the hosted release gate.
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
`createHostedDeployedWorkerStagingEvidenceBundle` expects only bounded evidence:
|
|
17
|
+
|
|
18
|
+
- public HTTPS health probe metadata from the deployed Node/container app
|
|
19
|
+
- signed webhook replay summaries
|
|
20
|
+
- deployed worker success and failure cleanup summaries
|
|
21
|
+
- log-boundary validation output
|
|
22
|
+
- external evidence for CI, workflow static checks, dependency/container scans, monitoring, rollback, and incident response
|
|
23
|
+
- scanner version, collected timestamp, evidence URL base, and evidence owner
|
|
24
|
+
|
|
25
|
+
The public HTTPS health probe must show:
|
|
26
|
+
|
|
27
|
+
- HTTP `200`
|
|
28
|
+
- `ok: true`
|
|
29
|
+
- `platform: "node_container"`
|
|
30
|
+
- both `webhook-ingress` and `scan-worker` roles
|
|
31
|
+
- a scanner version matching the release candidate
|
|
32
|
+
- privacy flags set to false for raw webhook payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens
|
|
33
|
+
|
|
34
|
+
The helper requires a public HTTPS URL for both the deployed ingress and the evidence base. Localhost, private IPs, link-local addresses, non-HTTPS URLs, file URLs, URL credentials, query strings, and fragments are rejected for deployed staging evidence.
|
|
35
|
+
|
|
36
|
+
## Generated Evidence
|
|
37
|
+
|
|
38
|
+
The bundle generates deployed evidence for these hosted gate IDs when the probes are complete:
|
|
39
|
+
|
|
40
|
+
- `webhook_replay`: deployed staging ingress accepted a signed webhook and queued check-run-only work
|
|
41
|
+
- `queue_worker_cleanup`: deployed worker success and failure probes completed and deleted worker checkouts
|
|
42
|
+
- `privacy_retention`: deployed log samples stayed within the safe metadata boundary
|
|
43
|
+
- `release_cleanup`: no deployed worker checkout entries remained active after release probes
|
|
44
|
+
|
|
45
|
+
Other gate IDs still come from external evidence because they belong to CI, workflow analysis, dependency/container scan, monitoring, rollback, and incident-response systems.
|
|
46
|
+
|
|
47
|
+
## Blocking Behavior
|
|
48
|
+
|
|
49
|
+
The helper blocks release-gate readiness when deployed evidence is incomplete. Common blocked reasons include:
|
|
50
|
+
|
|
51
|
+
- `invalid_public_base_url`
|
|
52
|
+
- `unsafe_evidence_base_url`
|
|
53
|
+
- `health_scanner_version_mismatch`
|
|
54
|
+
- `health_missing_scan_worker_role`
|
|
55
|
+
- `health_privacy_flags_unsafe`
|
|
56
|
+
- `worker_failure_cleanup_probe_missing`
|
|
57
|
+
- `worker_cleanup_not_verified`
|
|
58
|
+
- `log_boundary_rejected`
|
|
59
|
+
|
|
60
|
+
Blocked output remains public-safe. It does not return the raw health body, public base URL, raw webhook payload, untrusted PR text, raw source, raw diffs, secrets, customer payloads, private checkout paths, or installation tokens.
|
|
61
|
+
|
|
62
|
+
## Release Gate
|
|
63
|
+
|
|
64
|
+
`evaluateHostedDeployedWorkerStagingReleaseGate` passes the generated bundle to the hosted operational release gate with the release commit, scanner version, deployment target, container digest, and release notes.
|
|
65
|
+
|
|
66
|
+
The evaluator still blocks hosted exposure unless every P0 evidence row is fresh, the deployed artifact has a `sha256:<digest>` container image digest, and release notes avoid positive pentest, certification, and full-audit claims. Wording such as "not a pentest, certification, or full security audit" remains allowed.
|
|
67
|
+
|
|
68
|
+
## Boundary
|
|
69
|
+
|
|
70
|
+
This helper narrows the gap between local source-candidate rehearsals and deployed staging evidence. It is still deterministic and local-first: callers collect evidence outside the package and pass only safe summaries into the helper.
|
|
71
|
+
|
|
72
|
+
It is not a pentest, full audit, or certification. It is not production hosted exposure. It does not prove customer SaaS apps are secure. It only helps decide whether the hosted worker release candidate has enough operational evidence to be exposed for the next staged rollout step.
|
|
@@ -40,7 +40,11 @@ Every hosted release must record:
|
|
|
40
40
|
|
|
41
41
|
The current public package release is still a local CLI and pure hosted-contract release. No hosted production environment is exposed by this release.
|
|
42
42
|
|
|
43
|
-
The pure evaluator `evaluateHostedOperationalReleaseGate` and the exported `HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS` list make the gate machine-checkable for the next hosted service stage. The
|
|
43
|
+
The pure evaluator `evaluateHostedOperationalReleaseGate` and the exported `HOSTED_OPERATIONAL_RELEASE_GATE_REQUIREMENTS` list make the gate machine-checkable for the next hosted service stage. The staging harness also exports `createHostedStagingReleaseEvidenceBundle`, `evaluateHostedStagingReleaseEvidenceBundle`, and `validateHostedLogBoundary` so source-candidate rehearsals can turn webhook replay, success/failure cleanup probes, required safe failure reasons, and log samples into an executable gate decision.
|
|
44
|
+
|
|
45
|
+
Deployed worker staging evidence is documented in [hosted-deployed-worker-staging.md](hosted-deployed-worker-staging.md). The `ai-saas-guard/hosted/deployed-staging` export adds `createHostedDeployedWorkerStagingEvidenceBundle` and `evaluateHostedDeployedWorkerStagingReleaseGate` so a deployed Node/container read-only checkout worker candidate can turn public HTTPS health, signed webhook replay, deployed success/failure cleanup probes, log-boundary samples, and external CI/scan/rollback evidence into this same gate. It does not deploy cloud resources and is not production hosted exposure.
|
|
46
|
+
|
|
47
|
+
The evaluator blocks hosted exposure unless every P0 item has fresh evidence, a `sha256:<digest>` container image digest is recorded, and release notes avoid positive pentest, certification, and full-audit claims. Explicit wording such as "not a pentest, certification, or full security audit" remains allowed.
|
|
44
48
|
|
|
45
49
|
Source-level evidence notes for this release candidate:
|
|
46
50
|
|
|
@@ -48,12 +52,12 @@ Source-level evidence notes for this release candidate:
|
|
|
48
52
|
| --- | --- | --- | --- |
|
|
49
53
|
| `clean_ci` | Clean install, tests, build, CLI help, JSON/SARIF scan, PR-risk, npm audit, pack dry-run | Local release gate plus GitHub Actions CI run from the release commit | Passed for source package |
|
|
50
54
|
| `hosted_contract_tests` | Hosted contract tests for webhook, scope, queue, worker, check summaries, cleanup, retention, and release gate evaluation | `tests/hosted-contracts.test.mjs` | Passed for pure contracts |
|
|
51
|
-
| `webhook_replay` | Valid events queue work; invalid, missing, malformed, replayed, removed, and non-installed events queue nothing | Pure replay coverage in hosted webhook intake tests | Passed for pure contracts;
|
|
55
|
+
| `webhook_replay` | Valid events queue work; invalid, missing, malformed, replayed, removed, and non-installed events queue nothing | Pure replay coverage in hosted webhook intake tests plus deployed worker staging evidence helper | Passed for pure contracts; deployed helper can record public HTTPS staging replay before exposure |
|
|
52
56
|
| `workflow_static_checks` | GitHub Actions static analysis | `actionlint` and `uvx zizmor --offline .github/workflows` | Passed for repository workflows |
|
|
53
57
|
| `dependency_scan` | Dependency scan has no unresolved high or critical production findings | `npm audit --audit-level=high --registry=https://registry.npmjs.org` | Passed for source package |
|
|
54
58
|
| `container_scan` | Container image scan has no unresolved high or critical runtime-layer findings | No hosted container image exists in the public package release | Not applicable to current non-hosted release; required before hosted exposure |
|
|
55
|
-
| `queue_worker_cleanup` | Queue dedupe, running cancellation, terminal cleanup, worker checkout deletion, and no long-running processes | Pure queue, worker, checkout,
|
|
56
|
-
| `privacy_retention` | No raw source, raw diffs, secrets, customer payloads, private URLs, or full file contents; retention and uninstall cleanup are proven | Compact report, Check Run publication, retention/deletion cleanup,
|
|
59
|
+
| `queue_worker_cleanup` | Queue dedupe, running cancellation, terminal cleanup, worker checkout deletion, and no long-running processes | Pure queue, worker, checkout, retention cleanup planner tests, staging harness success/failure cleanup probes, and deployed worker staging cleanup evidence helper | Passed for source candidate; deployed helper can record success/failure cleanup before exposure |
|
|
60
|
+
| `privacy_retention` | No raw source, raw diffs, secrets, customer payloads, private URLs, or full file contents; retention and uninstall cleanup are proven | Compact report, Check Run publication, retention/deletion cleanup, docs tests, `validateHostedLogBoundary` source-candidate log checks, and deployed log-boundary staging evidence | Passed for source candidate; deployed log sampling still required before exposure |
|
|
57
61
|
| `monitoring_alerting` | Ingress, queue depth, worker failures, Check Run failures, cleanup failures, retention failures, and credential rotation alerts | Required alert list remains in this document | Documented; must attach provider evidence before exposure |
|
|
58
62
|
| `manual_rollback` | Worker pause, previous artifact redeploy, queue resume, controlled ingress failure, and affected Check Run identification | Manual rollback procedure remains in this document | Documented; must execute against deployed artifact before exposure |
|
|
59
63
|
| `incident_response` | Owner, backup, credential rotation, queue pause, customer communication, status path, and privacy-safe evidence collection | Incident response checklist remains in this document | Documented; must name live owners before exposure |
|
|
@@ -148,6 +152,7 @@ Required proof:
|
|
|
148
152
|
- runtime credentials reach git through temporary askpass material only.
|
|
149
153
|
- the CLI phase runs after credential material is removed from the environment.
|
|
150
154
|
- the worker command is fixed to the deterministic read-only `pr-risk --json` shape.
|
|
155
|
+
- the worker rejects accepted-looking plans if command, checkout identity, or token scope differs from trusted GitHub event identity.
|
|
151
156
|
- success deletes the worker checkout, askpass material, generated JSON/SARIF scratch files, and local package tarballs.
|
|
152
157
|
- failure cleanup covers clone failure, timeout, CLI failure, malformed JSON output, Check Run write failure, cancellation, and process interruption.
|
|
153
158
|
- cleanup failures create an operator-review event without returning raw source, raw diffs, installation tokens, checkout paths, private URLs, or low-level filesystem errors to users.
|
|
@@ -29,6 +29,10 @@ The hosted release gate still requires fresh deployed evidence for:
|
|
|
29
29
|
- dependency and container artifact scanning for the deployed worker image
|
|
30
30
|
- retention and uninstall cleanup against the deployed provider stores
|
|
31
31
|
|
|
32
|
+
Source-candidate executable evidence now exists in `ai-saas-guard/hosted/staging-harness`: `createHostedStagingReleaseEvidenceBundle` combines signed webhook replay, success and failure cleanup probes, safe worker failure reasons, and `validateHostedLogBoundary` samples into hosted release-gate evidence, then `evaluateHostedStagingReleaseEvidenceBundle` runs the same gate evaluator used by deployment planning. This improves local release readiness, but it is still not production hosted exposure and does not replace deployed worker, logging, metrics, rollback, incident-response, dependency, or container evidence.
|
|
33
|
+
|
|
34
|
+
Deployed worker staging evidence now has its own helper in `ai-saas-guard/hosted/deployed-staging`: `createHostedDeployedWorkerStagingEvidenceBundle` accepts public HTTPS health, deployed webhook replay, worker cleanup, log-boundary, and external CI/scan/rollback evidence summaries, then `evaluateHostedDeployedWorkerStagingReleaseGate` evaluates the same hosted release gate. Use [hosted-deployed-worker-staging.md](hosted-deployed-worker-staging.md) before exposing a Node/container read-only checkout worker beyond staging.
|
|
35
|
+
|
|
32
36
|
## Read-Only Checkout Worker Evidence Checklist
|
|
33
37
|
|
|
34
38
|
Before any hosted source checkout worker is exposed beyond staging, attach fresh evidence for each row below. The current Cloudflare ingress evidence above does not satisfy these rows because it publishes compact PR-risk signals without running a full source checkout scan worker.
|
|
@@ -37,7 +41,7 @@ Before any hosted source checkout worker is exposed beyond staging, attach fresh
|
|
|
37
41
|
| --- | --- | --- |
|
|
38
42
|
| Trusted checkout identity | Worker input is derived from signed GitHub event identity, selected-repository installation scope, and repository `contents: read`; PR title, body, branch names, README, and code cannot choose the repository, token scope, checkout path, or command | Required |
|
|
39
43
|
| Runtime credential boundary | Installation credentials are passed to git only through temporary askpass material, are removed before the CLI scan phase, and are never returned in worker output, compact reports, Check Runs, or logs | Required |
|
|
40
|
-
| Fixed scanner command | Worker runs the fixed read-only command shape `ai-saas-guard pr-risk --root <worker-checkout> --base <trusted-base-sha> --json` without shell parsing or PR-authored arguments | Required |
|
|
44
|
+
| Fixed scanner command | Worker runs the fixed read-only command shape `ai-saas-guard pr-risk --root <worker-checkout> --base <trusted-base-sha> --json` without shell parsing or PR-authored arguments, and rejects command, checkout, or token-scope mutations before running git | Required |
|
|
41
45
|
| Success cleanup | A successful worker run deletes the checkout directory, askpass material, generated JSON/SARIF scratch files, and any local package tarballs | Required |
|
|
42
46
|
| Failure cleanup | A failed clone, timeout, CLI non-zero exit, malformed JSON output, Check Run write failure, cancellation, or process interruption still attempts checkout deletion and records only a safe cleanup status | Required |
|
|
43
47
|
| Log boundary | Logs may include scan key, installation ID, repository ID, PR number, head SHA, scanner version, duration, summary counts, error class, and cleanup status; logs must include no raw source, no raw diffs, no secrets, no installation tokens, no customer payloads, no private URLs, and no checkout paths | Required |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This document collects pure hosted contracts that can be tested before any hosted GitHub App service is deployed. These contracts keep the hosted design inspectable, local-first, and implementation-ready without adding network calls, credentials, queues, workers, or GitHub API writes. They are no network calls contracts by design.
|
|
4
4
|
|
|
5
|
-
The helpers live in `src/hosted/contracts.ts` and are exported from `ai-saas-guard/hosted/contracts`. The production adapter plans live in `src/hosted/production-adapters.ts` and are exported from `ai-saas-guard/hosted/production-adapters`. The Node/container app skeleton lives in `src/hosted/app.ts` and is exported from `ai-saas-guard/hosted/app`. The concrete read-only checkout worker runner lives in `src/hosted/worker.ts` and is exported from `ai-saas-guard/hosted/worker`. The staging deployment planner lives in `src/hosted/staging.ts` and is exported from `ai-saas-guard/hosted/staging`. The local staging harness lives in `src/hosted/staging-harness.ts` and is exported from `ai-saas-guard/hosted/staging-harness`.
|
|
5
|
+
The helpers live in `src/hosted/contracts.ts` and are exported from `ai-saas-guard/hosted/contracts`. The production adapter plans live in `src/hosted/production-adapters.ts` and are exported from `ai-saas-guard/hosted/production-adapters`. The Node/container app skeleton lives in `src/hosted/app.ts` and is exported from `ai-saas-guard/hosted/app`. The concrete read-only checkout worker runner lives in `src/hosted/worker.ts` and is exported from `ai-saas-guard/hosted/worker`. The staging deployment planner lives in `src/hosted/staging.ts` and is exported from `ai-saas-guard/hosted/staging`. The local staging harness lives in `src/hosted/staging-harness.ts` and is exported from `ai-saas-guard/hosted/staging-harness`. The deployed worker staging evidence helper lives in `src/hosted/deployed-staging.ts` and is exported from `ai-saas-guard/hosted/deployed-staging`.
|
|
6
6
|
|
|
7
7
|
## Pull Request Webhook Intake Planner
|
|
8
8
|
|
|
@@ -79,6 +79,8 @@ Default behavior:
|
|
|
79
79
|
- derive the GitHub clone URL only from trusted repository identity
|
|
80
80
|
- require a runtime installation token provider and keep the token out of command arguments, returned results, compact reports, and serialized plans
|
|
81
81
|
- pass the installation token to git only through a temporary askpass helper inside the worker checkout
|
|
82
|
+
- remove askpass material before the CLI phase starts
|
|
83
|
+
- reject accepted-looking plans when command, checkout identity, or token scope differs from the trusted worker plan
|
|
82
84
|
- run `git init`, add the trusted remote, fetch the trusted head and base SHAs with bounded depth, and checkout the trusted head SHA
|
|
83
85
|
- run the fixed `ai-saas-guard pr-risk --root <worker-checkout> --base <baseSha> --json` command without shell parsing
|
|
84
86
|
- cap command timeout and output bytes
|
|
@@ -174,6 +176,9 @@ Default behavior:
|
|
|
174
176
|
- create a temporary worker sandbox during the scan runner phase
|
|
175
177
|
- remove worker sandbox contents before returning the worker result
|
|
176
178
|
- create hosted operational release-gate evidence fixtures with the same shape required by the deployment gate
|
|
179
|
+
- turn success and failure cleanup probes into executable release-gate evidence
|
|
180
|
+
- validate log boundaries without returning sampled log lines or forbidden values
|
|
181
|
+
- evaluate the hosted release gate directly from the generated evidence bundle
|
|
177
182
|
|
|
178
183
|
Privacy boundaries:
|
|
179
184
|
|
|
@@ -181,7 +186,7 @@ Privacy boundaries:
|
|
|
181
186
|
- result objects do not include raw webhook payloads, untrusted PR text, raw source, raw diffs, secrets, customer payloads, checkout paths, or installation tokens
|
|
182
187
|
- local evidence is labeled as harness evidence and must not be used to claim live hosted exposure
|
|
183
188
|
|
|
184
|
-
The exported helpers are `createFileBackedHostedStagingHarness` and `
|
|
189
|
+
The exported helpers are `createFileBackedHostedStagingHarness`, `createHostedStagingHarnessEvidence`, `createHostedStagingReleaseEvidenceBundle`, `evaluateHostedStagingReleaseEvidenceBundle`, and `validateHostedLogBoundary`.
|
|
185
190
|
|
|
186
191
|
## Webhook Event Parser
|
|
187
192
|
|
|
@@ -10,6 +10,9 @@ The package exports `ai-saas-guard/hosted/staging-harness` with:
|
|
|
10
10
|
|
|
11
11
|
- `createFileBackedHostedStagingHarness`
|
|
12
12
|
- `createHostedStagingHarnessEvidence`
|
|
13
|
+
- `createHostedStagingReleaseEvidenceBundle`
|
|
14
|
+
- `evaluateHostedStagingReleaseEvidenceBundle`
|
|
15
|
+
- `validateHostedLogBoundary`
|
|
13
16
|
|
|
14
17
|
The harness composes the hosted service runtime with local adapters:
|
|
15
18
|
|
|
@@ -20,6 +23,8 @@ The harness composes the hosted service runtime with local adapters:
|
|
|
20
23
|
- a temporary worker sandbox under `worker-sandbox/`
|
|
21
24
|
- cleanup verification after a worker tick
|
|
22
25
|
- local hosted release-gate evidence fixtures
|
|
26
|
+
- executable evidence bundles for success and failure cleanup probes
|
|
27
|
+
- log boundary validation for safe hosted metadata samples
|
|
23
28
|
|
|
24
29
|
## Replay Flow
|
|
25
30
|
|
|
@@ -39,6 +44,35 @@ Invalid signatures stop at the signature stage and create no queue, report, or C
|
|
|
39
44
|
|
|
40
45
|
The generated notes are explicit that the evidence is local harness evidence, not hosted exposure.
|
|
41
46
|
|
|
47
|
+
## Executable Evidence Bundle
|
|
48
|
+
|
|
49
|
+
`createHostedStagingReleaseEvidenceBundle` turns concrete harness results into release-gate evidence:
|
|
50
|
+
|
|
51
|
+
- signed webhook replay must queue a check-run-only worker from trusted GitHub event fields
|
|
52
|
+
- worker success must delete the worker sandbox
|
|
53
|
+
- worker failure must still delete the worker sandbox and expose only a safe failure reason
|
|
54
|
+
- callers can require explicit failure reasons such as checkout failure, CLI failure, malformed output, Check Run publication failure, timeout, and cancellation before cleanup evidence passes
|
|
55
|
+
- log boundary validation must pass before `privacy_retention` is marked passed
|
|
56
|
+
- external evidence is still required for CI, workflow static checks, dependency scan, container scan, monitoring, rollback, and incident response
|
|
57
|
+
|
|
58
|
+
`evaluateHostedStagingReleaseEvidenceBundle` passes that bundle into the hosted operational release gate evaluator with the release commit, scanner version, deployment target, container digest, and release notes. This makes the local staging gate executable instead of a hand-maintained checklist.
|
|
59
|
+
|
|
60
|
+
The bundle is still source-candidate evidence. It does not prove a deployed hosted service is ready, and it does not replace deployed provider evidence.
|
|
61
|
+
|
|
62
|
+
## Log Boundary Validation
|
|
63
|
+
|
|
64
|
+
`validateHostedLogBoundary` accepts sampled log metadata and a forbidden-value list for raw source, raw diffs, secret values, customer payloads, installation tokens, checkout paths, private URLs, and untrusted PR prose.
|
|
65
|
+
|
|
66
|
+
The returned result contains only:
|
|
67
|
+
|
|
68
|
+
- pass/fail status
|
|
69
|
+
- blocked reason IDs
|
|
70
|
+
- sample count
|
|
71
|
+
- allowed field names
|
|
72
|
+
- privacy flags
|
|
73
|
+
|
|
74
|
+
It does not return the sampled log lines or the forbidden values. This keeps the evidence useful while avoiding accidental leakage in release notes, compact reports, or Check Runs.
|
|
75
|
+
|
|
42
76
|
## Privacy
|
|
43
77
|
|
|
44
78
|
The harness returns safe status objects and compact artifacts only.
|
|
@@ -58,6 +92,6 @@ The worker sandbox may contain temporary scan input during a worker tick. The ha
|
|
|
58
92
|
|
|
59
93
|
## Current Status
|
|
60
94
|
|
|
61
|
-
The repository can now run a local staging rehearsal across webhook intake, queue persistence, worker execution, compact report storage, Check Run publication, and
|
|
95
|
+
The repository can now run a local staging rehearsal across webhook intake, queue persistence, worker execution, compact report storage, Check Run publication, worker cleanup, success and failure cleanup probes, log boundary validation, and executable release-gate evaluation from the generated evidence bundle.
|
|
62
96
|
|
|
63
97
|
This still is not a live hosted service. A real staging environment still requires deployed platform infrastructure, public HTTPS ingress, platform secret references, durable queue/storage resources, worker isolation, GitHub Checks runtime credentials, monitoring, rollback evidence, and incident-response evidence collected from the deployed artifact.
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current published version: `0.
|
|
8
|
+
- Current published version: `0.32.0`
|
|
9
9
|
- Next source candidate: none
|
|
10
10
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
11
11
|
- First npm-published version: `0.1.1`
|
|
12
|
-
- GitHub Release: `v0.
|
|
12
|
+
- GitHub Release: `v0.32.0`
|
|
13
13
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
14
14
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
15
15
|
- Long-lived npm publish token: not required
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
20
20
|
|
|
21
|
-
1. Create and review a release tag such as `v0.
|
|
21
|
+
1. Create and review a release tag such as `v0.32.0`.
|
|
22
22
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
23
23
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
24
24
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-saas-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Local-first CLI that catches launch blockers in AI-built Next.js/Supabase/Stripe SaaS apps.",
|
|
5
5
|
"readmeFilename": "README.md",
|
|
6
6
|
"type": "module",
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"types": "./dist/hosted/staging-harness.d.ts",
|
|
71
71
|
"default": "./dist/hosted/staging-harness.js"
|
|
72
72
|
},
|
|
73
|
+
"./hosted/deployed-staging": {
|
|
74
|
+
"types": "./dist/hosted/deployed-staging.d.ts",
|
|
75
|
+
"default": "./dist/hosted/deployed-staging.js"
|
|
76
|
+
},
|
|
73
77
|
"./hosted/worker": {
|
|
74
78
|
"types": "./dist/hosted/worker.d.ts",
|
|
75
79
|
"default": "./dist/hosted/worker.js"
|