ai-saas-guard 0.16.0 → 0.17.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 +7 -5
- package/README.zh-CN.md +5 -3
- package/dist/hosted/service.d.ts +116 -0
- package/dist/hosted/service.js +250 -0
- package/docs/github-action.md +1 -1
- package/docs/github-app-design.md +3 -1
- package/docs/hosted-service-runtime.md +86 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +3 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -73,8 +73,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
73
73
|
| JSON and SARIF output | Available |
|
|
74
74
|
| Composite GitHub Action | Available |
|
|
75
75
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
76
|
-
| Versioned Action tags | `v0.
|
|
77
|
-
| npm package | `ai-saas-guard@0.
|
|
76
|
+
| Versioned Action tags | `v0.17.0`, `v0` |
|
|
77
|
+
| npm package | `ai-saas-guard@0.17.0` |
|
|
78
78
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
79
79
|
|
|
80
80
|
## Quick Start
|
|
@@ -208,19 +208,21 @@ The first hosted service slice is defined in [docs/hosted-first-service-slice.md
|
|
|
208
208
|
|
|
209
209
|
The hosted deployment model is documented in [docs/hosted-deployment-model.md](docs/hosted-deployment-model.md). It chooses a containerized Node.js ingress and worker model with a managed durable queue, platform secret manager, structured redacted logs, installation/repository rate limits, and rollback/incident response paths.
|
|
210
210
|
|
|
211
|
+
The hosted service runtime is documented in [docs/hosted-service-runtime.md](docs/hosted-service-runtime.md). It exports `createHostedServiceRuntime` from `ai-saas-guard/hosted/service` and implements the provider-independent service core for signed webhook intake, idempotent queue upsert, read-only worker orchestration, compact report storage, Check Run publication adapters, and worker cleanup planning. It does not deploy a public hosted environment by itself.
|
|
212
|
+
|
|
211
213
|
The hosted operational release gate is documented in [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md). It defines the hosted-specific CI, replay, queue, worker cleanup, privacy, monitoring, rollback, and incident-response evidence required before any hosted environment is exposed to users. The pure gate evaluator exported from `ai-saas-guard/hosted/contracts` blocks hosted exposure unless every P0 evidence item is fresh, a container digest is recorded, and release notes avoid pentest, certification, and full-audit claims.
|
|
212
214
|
|
|
213
215
|
Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninstall-data-deletion.md](docs/hosted-uninstall-data-deletion.md). It defines repository removal, full app uninstall, compact report deletion, queue cancellation, audit record retention, repeated cleanup, and user-facing deletion wording.
|
|
214
216
|
|
|
215
217
|
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.
|
|
216
218
|
|
|
217
|
-
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`, 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, and an operational release gate evaluator that requires fresh CI, replay, static-check, dependency, container, cleanup, privacy, monitoring, rollback, incident-response, and release-cleanup evidence before hosted exposure. PR comments remain a later workflow or paid hosted feature, not part of the hosted MVP contract.
|
|
219
|
+
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`, 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, and an operational release gate evaluator that requires fresh CI, replay, static-check, dependency, container, cleanup, privacy, monitoring, rollback, incident-response, and release-cleanup evidence before hosted exposure. 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.
|
|
218
220
|
|
|
219
221
|
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.
|
|
220
222
|
|
|
221
223
|
The proposed hosted app permission boundary is intentionally narrow: repository contents read, pull requests read, checks write, and metadata read for the first version. Optional PR comments require repository policy opt-in, and broad permissions such as administration, deployments, Actions write, and repository secrets are out of scope.
|
|
222
224
|
|
|
223
|
-
The repository also includes
|
|
225
|
+
The repository also includes hosted contract helpers and runtime tests for webhook intake order, webhook verification, installation token scoping, durable queue idempotency, compact reports, retention limits, uninstall cleanup, repeated cleanup idempotency, scoped deletion planning, operational release gate blocking, and provider-independent hosted service orchestration. These helpers do not deploy a public hosted service.
|
|
224
226
|
|
|
225
227
|
Users should prefer the local CLI for private repositories, offline review, or no-account workflows where hosted code processing is not acceptable.
|
|
226
228
|
|
|
@@ -254,7 +256,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
254
256
|
|
|
255
257
|
## GitHub Action
|
|
256
258
|
|
|
257
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
259
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.17.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
258
260
|
|
|
259
261
|
```yaml
|
|
260
262
|
name: ai-saas-guard
|
package/README.zh-CN.md
CHANGED
|
@@ -55,7 +55,7 @@ AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是
|
|
|
55
55
|
|
|
56
56
|
这个仓库是公开 GitHub 仓库。
|
|
57
57
|
|
|
58
|
-
CLI 已发布到 npm:`ai-saas-guard@0.
|
|
58
|
+
CLI 已发布到 npm:`ai-saas-guard@0.17.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.17.0`。
|
|
59
59
|
|
|
60
60
|
| 模块 | 状态 |
|
|
61
61
|
| --- | --- |
|
|
@@ -66,8 +66,8 @@ CLI 已发布到 npm:`ai-saas-guard@0.16.0`。GitHub Action 支持 `v0` 浮动
|
|
|
66
66
|
| Markdown PR summary | 已可用 |
|
|
67
67
|
| GitHub Action | 已可用 |
|
|
68
68
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖和 fail threshold |
|
|
69
|
-
| 当前版本 | `0.
|
|
70
|
-
| Action 标签 | `v0.
|
|
69
|
+
| 当前版本 | `0.17.0` |
|
|
70
|
+
| Action 标签 | `v0.17.0`、`v0` |
|
|
71
71
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
72
72
|
|
|
73
73
|
## 快速开始
|
|
@@ -245,6 +245,7 @@ jobs:
|
|
|
245
245
|
- [docs/github-app-design.md](docs/github-app-design.md)
|
|
246
246
|
- [docs/hosted-first-service-slice.md](docs/hosted-first-service-slice.md)
|
|
247
247
|
- [docs/hosted-deployment-model.md](docs/hosted-deployment-model.md)
|
|
248
|
+
- [docs/hosted-service-runtime.md](docs/hosted-service-runtime.md)
|
|
248
249
|
- [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md)
|
|
249
250
|
- [docs/hosted-uninstall-data-deletion.md](docs/hosted-uninstall-data-deletion.md)
|
|
250
251
|
- [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md)
|
|
@@ -255,6 +256,7 @@ jobs:
|
|
|
255
256
|
- pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
|
|
256
257
|
- durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
|
|
257
258
|
- worker read-only scan planner:只用 trusted identity 规划临时 worker checkout,要求 repository `contents: read`,固定运行 `ai-saas-guard pr-risk --json`,并忽略 PR 正文里的 repo 名、token scope 或命令
|
|
259
|
+
- hosted service runtime:`ai-saas-guard/hosted/service` 导出 `createHostedServiceRuntime`,把签名 webhook intake、幂等 queue upsert、read-only worker 编排、compact report 存储、Check Run 发布 adapter 和 worker cleanup 串成可测试的服务核心;它本身不部署公开 hosted 环境
|
|
258
260
|
- webhook event parser
|
|
259
261
|
- check-run summary renderer
|
|
260
262
|
- Check Run publication planner:要求 repository `checks: write`,只从 compact report 生成有长度上限的 Check Run payload,包含 review categories、优先 review 文件、verification steps 和本地 CLI 复现命令;MVP 不发 PR comment
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type CompactHostedFinding, type CompactHostedReport, type HostedCheckRunPublicationPlan, type HostedScanQueueRecord, type HostedScanQueueUpsertDecision, type HostedWorkerCheckoutCleanupPlan, type HostedWorkerReadOnlyScanPlan } from "./contracts.js";
|
|
2
|
+
type RepositoryIdSource = Record<number, number[]> | Map<number, number[]>;
|
|
3
|
+
export interface HostedServiceWebhookRequest {
|
|
4
|
+
payload: string | Buffer;
|
|
5
|
+
signatureHeader?: string;
|
|
6
|
+
deliveryId?: string;
|
|
7
|
+
manualRerun?: boolean;
|
|
8
|
+
allowDraft?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type HostedServiceWebhookStage = "signature" | "payload" | "event" | "installation_scope" | "queue";
|
|
11
|
+
export interface HostedServiceWebhookResult {
|
|
12
|
+
accepted: boolean;
|
|
13
|
+
stage: HostedServiceWebhookStage;
|
|
14
|
+
reason?: string;
|
|
15
|
+
deliveryId?: string;
|
|
16
|
+
queueDecision?: HostedScanQueueUpsertDecision;
|
|
17
|
+
shouldFetchRepository: boolean;
|
|
18
|
+
shouldCreateCheckRun: boolean;
|
|
19
|
+
shouldCreatePrComment: false;
|
|
20
|
+
privacy: {
|
|
21
|
+
includesRawWebhookPayload: false;
|
|
22
|
+
includesUntrustedPrText: false;
|
|
23
|
+
includesRawSource: false;
|
|
24
|
+
includesRawDiffs: false;
|
|
25
|
+
includesSecrets: false;
|
|
26
|
+
includesCustomerPayloads: false;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface HostedServiceQueueAdapter {
|
|
30
|
+
records: Map<string, HostedScanQueueRecord>;
|
|
31
|
+
}
|
|
32
|
+
export interface HostedCompactReportStoreRecord {
|
|
33
|
+
id: string;
|
|
34
|
+
jobKey: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
report: CompactHostedReport;
|
|
37
|
+
}
|
|
38
|
+
export interface HostedCompactReportStore {
|
|
39
|
+
records?: HostedCompactReportStoreRecord[];
|
|
40
|
+
save(record: HostedCompactReportStoreRecord): Promise<void> | void;
|
|
41
|
+
}
|
|
42
|
+
export interface HostedCheckRunRequest {
|
|
43
|
+
method: "POST";
|
|
44
|
+
endpoint: string;
|
|
45
|
+
payload: NonNullable<HostedCheckRunPublicationPlan["request"]>["payload"];
|
|
46
|
+
}
|
|
47
|
+
export interface HostedCheckRunPublisher {
|
|
48
|
+
requests?: HostedCheckRunRequest[];
|
|
49
|
+
publish(request: HostedCheckRunRequest): Promise<void> | void;
|
|
50
|
+
}
|
|
51
|
+
export interface HostedServiceScanRunnerResult {
|
|
52
|
+
summaryCounts: Record<string, number>;
|
|
53
|
+
findings: CompactHostedFinding[];
|
|
54
|
+
retentionDays?: number;
|
|
55
|
+
rawSource?: string;
|
|
56
|
+
rawDiff?: string;
|
|
57
|
+
secretValues?: string[];
|
|
58
|
+
customerPayload?: unknown;
|
|
59
|
+
}
|
|
60
|
+
export interface HostedServiceScanRunnerInput {
|
|
61
|
+
plan: HostedWorkerReadOnlyScanPlan & {
|
|
62
|
+
accepted: true;
|
|
63
|
+
};
|
|
64
|
+
queueRecord: HostedScanQueueRecord;
|
|
65
|
+
}
|
|
66
|
+
export type HostedServiceScanRunner = (input: HostedServiceScanRunnerInput) => Promise<HostedServiceScanRunnerResult> | HostedServiceScanRunnerResult;
|
|
67
|
+
export interface HostedServiceRuntimeOptions {
|
|
68
|
+
signingKey: string | Buffer;
|
|
69
|
+
scannerVersion: string;
|
|
70
|
+
selectedRepositoryIdsByInstallation: RepositoryIdSource;
|
|
71
|
+
removedRepositoryIdsByInstallation?: RepositoryIdSource;
|
|
72
|
+
queue: HostedServiceQueueAdapter;
|
|
73
|
+
compactReportStore: HostedCompactReportStore;
|
|
74
|
+
checkRunPublisher: HostedCheckRunPublisher;
|
|
75
|
+
scanRunner: HostedServiceScanRunner;
|
|
76
|
+
now?: () => string;
|
|
77
|
+
}
|
|
78
|
+
export interface HostedServiceRuntime {
|
|
79
|
+
handlePullRequestWebhook(request: HostedServiceWebhookRequest): HostedServiceWebhookResult;
|
|
80
|
+
runNextQueuedScan(): Promise<HostedServiceWorkerResult>;
|
|
81
|
+
}
|
|
82
|
+
export type HostedServiceWorkerResult = {
|
|
83
|
+
processed: false;
|
|
84
|
+
reason: "empty_queue";
|
|
85
|
+
} | {
|
|
86
|
+
processed: true;
|
|
87
|
+
status: "completed";
|
|
88
|
+
queueRecord: HostedScanQueueRecord;
|
|
89
|
+
workerPlan: HostedWorkerReadOnlyScanPlan & {
|
|
90
|
+
accepted: true;
|
|
91
|
+
};
|
|
92
|
+
report: CompactHostedReport;
|
|
93
|
+
checkRunPublication: HostedCheckRunPublicationPlan;
|
|
94
|
+
cleanup: HostedWorkerCheckoutCleanupPlan;
|
|
95
|
+
} | {
|
|
96
|
+
processed: true;
|
|
97
|
+
status: "failed";
|
|
98
|
+
queueRecord: HostedScanQueueRecord;
|
|
99
|
+
reason?: string;
|
|
100
|
+
errorClass: "worker_plan_rejected" | "check_run_publication_rejected" | "scan_runner_failed";
|
|
101
|
+
workerPlan?: HostedWorkerReadOnlyScanPlan;
|
|
102
|
+
checkRunPublication?: HostedCheckRunPublicationPlan;
|
|
103
|
+
cleanup?: HostedWorkerCheckoutCleanupPlan;
|
|
104
|
+
};
|
|
105
|
+
export interface InMemoryHostedServiceAdapters {
|
|
106
|
+
queue: HostedServiceQueueAdapter;
|
|
107
|
+
compactReportStore: HostedCompactReportStore & {
|
|
108
|
+
records: HostedCompactReportStoreRecord[];
|
|
109
|
+
};
|
|
110
|
+
checkRunPublisher: HostedCheckRunPublisher & {
|
|
111
|
+
requests: HostedCheckRunRequest[];
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export declare function createInMemoryHostedServiceAdapters(): InMemoryHostedServiceAdapters;
|
|
115
|
+
export declare function createHostedServiceRuntime(options: HostedServiceRuntimeOptions): HostedServiceRuntime;
|
|
116
|
+
export {};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { authorizeInstallationTokenScope, createCompactHostedReport, createHostedWorkerCheckoutCleanupPlan, parseHostedPullRequestEvent, planHostedCheckRunPublication, planHostedScanQueueUpsert, planHostedWorkerReadOnlyScan, verifyGitHubWebhook } from "./contracts.js";
|
|
2
|
+
export function createInMemoryHostedServiceAdapters() {
|
|
3
|
+
const compactReportRecords = [];
|
|
4
|
+
const checkRunRequests = [];
|
|
5
|
+
return {
|
|
6
|
+
queue: { records: new Map() },
|
|
7
|
+
compactReportStore: {
|
|
8
|
+
records: compactReportRecords,
|
|
9
|
+
save(record) {
|
|
10
|
+
compactReportRecords.push(record);
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
checkRunPublisher: {
|
|
14
|
+
requests: checkRunRequests,
|
|
15
|
+
publish(request) {
|
|
16
|
+
checkRunRequests.push(request);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function createHostedServiceRuntime(options) {
|
|
22
|
+
const seenDeliveryIds = new Set();
|
|
23
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
24
|
+
return {
|
|
25
|
+
handlePullRequestWebhook(request) {
|
|
26
|
+
const signatureDecision = verifyGitHubWebhook({
|
|
27
|
+
payload: request.payload,
|
|
28
|
+
signatureHeader: request.signatureHeader,
|
|
29
|
+
signingKey: options.signingKey,
|
|
30
|
+
deliveryId: request.deliveryId,
|
|
31
|
+
seenDeliveryIds
|
|
32
|
+
});
|
|
33
|
+
if (!signatureDecision.accepted) {
|
|
34
|
+
return rejectWebhookRequest("signature", signatureDecision.reason ?? "invalid_signature", request.deliveryId);
|
|
35
|
+
}
|
|
36
|
+
if (!request.deliveryId) {
|
|
37
|
+
return rejectWebhookRequest("event", "missing_delivery_id");
|
|
38
|
+
}
|
|
39
|
+
const parsedPayload = parseWebhookPayload(request.payload);
|
|
40
|
+
if (!parsedPayload) {
|
|
41
|
+
return rejectWebhookRequest("payload", "invalid_json", request.deliveryId);
|
|
42
|
+
}
|
|
43
|
+
const eventDecision = parseHostedPullRequestEvent({
|
|
44
|
+
payload: parsedPayload,
|
|
45
|
+
scannerVersion: options.scannerVersion,
|
|
46
|
+
allowDraft: request.allowDraft
|
|
47
|
+
});
|
|
48
|
+
if (!eventDecision.accepted || !eventDecision.identity) {
|
|
49
|
+
return rejectWebhookRequest("event", eventDecision.reason ?? "missing_required_field", request.deliveryId);
|
|
50
|
+
}
|
|
51
|
+
const selectedRepositoryIds = repositoryIdsFor(options.selectedRepositoryIdsByInstallation, eventDecision.identity.installationId);
|
|
52
|
+
const removedRepositoryIds = repositoryIdsFor(options.removedRepositoryIdsByInstallation, eventDecision.identity.installationId);
|
|
53
|
+
const scopeDecision = authorizeInstallationTokenScope({
|
|
54
|
+
identity: eventDecision.identity,
|
|
55
|
+
installationId: eventDecision.identity.installationId,
|
|
56
|
+
selectedRepositoryIds,
|
|
57
|
+
removedRepositoryIds
|
|
58
|
+
});
|
|
59
|
+
if (!scopeDecision.authorized) {
|
|
60
|
+
return rejectWebhookRequest("installation_scope", scopeDecision.reason ?? "repository_not_installed", request.deliveryId);
|
|
61
|
+
}
|
|
62
|
+
const queueDecision = planHostedScanQueueUpsert({
|
|
63
|
+
identity: eventDecision.identity,
|
|
64
|
+
deliveryId: request.deliveryId,
|
|
65
|
+
requestedAt: now(),
|
|
66
|
+
queue: options.queue.records,
|
|
67
|
+
manualRerun: request.manualRerun
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
accepted: true,
|
|
71
|
+
stage: "queue",
|
|
72
|
+
deliveryId: request.deliveryId,
|
|
73
|
+
queueDecision,
|
|
74
|
+
shouldFetchRepository: queueDecision.shouldEnqueueWorker,
|
|
75
|
+
shouldCreateCheckRun: queueDecision.shouldCreateCheckRun,
|
|
76
|
+
shouldCreatePrComment: false,
|
|
77
|
+
privacy: hostedServiceWebhookPrivacy()
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
async runNextQueuedScan() {
|
|
81
|
+
const queuedRecord = nextQueuedRecord(options.queue.records);
|
|
82
|
+
if (!queuedRecord) {
|
|
83
|
+
return { processed: false, reason: "empty_queue" };
|
|
84
|
+
}
|
|
85
|
+
const requestedAt = now();
|
|
86
|
+
queuedRecord.status = "running";
|
|
87
|
+
queuedRecord.updatedAt = requestedAt;
|
|
88
|
+
const selectedRepositoryIds = repositoryIdsFor(options.selectedRepositoryIdsByInstallation, queuedRecord.identity.installationId);
|
|
89
|
+
const removedRepositoryIds = repositoryIdsFor(options.removedRepositoryIdsByInstallation, queuedRecord.identity.installationId);
|
|
90
|
+
const workerPlan = planHostedWorkerReadOnlyScan({
|
|
91
|
+
identity: queuedRecord.identity,
|
|
92
|
+
jobKey: queuedRecord.key,
|
|
93
|
+
requestedAt,
|
|
94
|
+
installationId: queuedRecord.identity.installationId,
|
|
95
|
+
selectedRepositoryIds,
|
|
96
|
+
removedRepositoryIds,
|
|
97
|
+
installationTokenPermissions: { contents: "read" }
|
|
98
|
+
});
|
|
99
|
+
if (!workerPlan.accepted) {
|
|
100
|
+
queuedRecord.status = "failed";
|
|
101
|
+
queuedRecord.updatedAt = now();
|
|
102
|
+
return {
|
|
103
|
+
processed: true,
|
|
104
|
+
status: "failed",
|
|
105
|
+
queueRecord: cloneQueueRecord(queuedRecord),
|
|
106
|
+
reason: workerPlan.reason,
|
|
107
|
+
errorClass: "worker_plan_rejected",
|
|
108
|
+
workerPlan
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const acceptedWorkerPlan = workerPlan;
|
|
112
|
+
try {
|
|
113
|
+
const scanResult = await options.scanRunner({
|
|
114
|
+
plan: acceptedWorkerPlan,
|
|
115
|
+
queueRecord: cloneQueueRecord(queuedRecord)
|
|
116
|
+
});
|
|
117
|
+
const report = createCompactHostedReport({
|
|
118
|
+
identity: queuedRecord.identity,
|
|
119
|
+
summaryCounts: scanResult.summaryCounts,
|
|
120
|
+
findings: scanResult.findings,
|
|
121
|
+
retentionDays: scanResult.retentionDays,
|
|
122
|
+
rawDiff: scanResult.rawDiff,
|
|
123
|
+
secretValues: scanResult.secretValues,
|
|
124
|
+
customerPayload: scanResult.customerPayload
|
|
125
|
+
});
|
|
126
|
+
const reportId = `${queuedRecord.key}:${queuedRecord.attempt}`;
|
|
127
|
+
await options.compactReportStore.save({
|
|
128
|
+
id: reportId,
|
|
129
|
+
jobKey: queuedRecord.key,
|
|
130
|
+
createdAt: now(),
|
|
131
|
+
report
|
|
132
|
+
});
|
|
133
|
+
const checkRunPublication = planHostedCheckRunPublication({
|
|
134
|
+
identity: queuedRecord.identity,
|
|
135
|
+
report,
|
|
136
|
+
jobKey: queuedRecord.key,
|
|
137
|
+
requestedAt: now(),
|
|
138
|
+
installationId: queuedRecord.identity.installationId,
|
|
139
|
+
selectedRepositoryIds,
|
|
140
|
+
removedRepositoryIds,
|
|
141
|
+
installationTokenPermissions: { checks: "write" }
|
|
142
|
+
});
|
|
143
|
+
if (!checkRunPublication.accepted || !checkRunPublication.request) {
|
|
144
|
+
queuedRecord.status = "failed";
|
|
145
|
+
queuedRecord.updatedAt = now();
|
|
146
|
+
return {
|
|
147
|
+
processed: true,
|
|
148
|
+
status: "failed",
|
|
149
|
+
queueRecord: cloneQueueRecord(queuedRecord),
|
|
150
|
+
reason: checkRunPublication.reason,
|
|
151
|
+
errorClass: "check_run_publication_rejected",
|
|
152
|
+
workerPlan: acceptedWorkerPlan,
|
|
153
|
+
checkRunPublication
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
await options.checkRunPublisher.publish(checkRunPublication.request);
|
|
157
|
+
const cleanup = createHostedWorkerCheckoutCleanupPlan({
|
|
158
|
+
identity: queuedRecord.identity,
|
|
159
|
+
jobKey: queuedRecord.key,
|
|
160
|
+
terminalState: "success",
|
|
161
|
+
finishedAt: now()
|
|
162
|
+
});
|
|
163
|
+
queuedRecord.status = "completed";
|
|
164
|
+
queuedRecord.reportId = reportId;
|
|
165
|
+
queuedRecord.updatedAt = now();
|
|
166
|
+
return {
|
|
167
|
+
processed: true,
|
|
168
|
+
status: "completed",
|
|
169
|
+
queueRecord: cloneQueueRecord(queuedRecord),
|
|
170
|
+
workerPlan: acceptedWorkerPlan,
|
|
171
|
+
report,
|
|
172
|
+
checkRunPublication,
|
|
173
|
+
cleanup
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
const cleanup = createHostedWorkerCheckoutCleanupPlan({
|
|
178
|
+
identity: queuedRecord.identity,
|
|
179
|
+
jobKey: queuedRecord.key,
|
|
180
|
+
terminalState: "failure",
|
|
181
|
+
finishedAt: now()
|
|
182
|
+
});
|
|
183
|
+
queuedRecord.status = "failed";
|
|
184
|
+
queuedRecord.updatedAt = now();
|
|
185
|
+
return {
|
|
186
|
+
processed: true,
|
|
187
|
+
status: "failed",
|
|
188
|
+
queueRecord: cloneQueueRecord(queuedRecord),
|
|
189
|
+
errorClass: "scan_runner_failed",
|
|
190
|
+
workerPlan: acceptedWorkerPlan,
|
|
191
|
+
cleanup
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function rejectWebhookRequest(stage, reason, deliveryId) {
|
|
198
|
+
return {
|
|
199
|
+
accepted: false,
|
|
200
|
+
stage,
|
|
201
|
+
reason,
|
|
202
|
+
...(deliveryId === undefined ? {} : { deliveryId }),
|
|
203
|
+
shouldFetchRepository: false,
|
|
204
|
+
shouldCreateCheckRun: false,
|
|
205
|
+
shouldCreatePrComment: false,
|
|
206
|
+
privacy: hostedServiceWebhookPrivacy()
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function hostedServiceWebhookPrivacy() {
|
|
210
|
+
return {
|
|
211
|
+
includesRawWebhookPayload: false,
|
|
212
|
+
includesUntrustedPrText: false,
|
|
213
|
+
includesRawSource: false,
|
|
214
|
+
includesRawDiffs: false,
|
|
215
|
+
includesSecrets: false,
|
|
216
|
+
includesCustomerPayloads: false
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function parseWebhookPayload(payload) {
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(Buffer.isBuffer(payload) ? payload.toString("utf8") : payload);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function repositoryIdsFor(source, installationId) {
|
|
228
|
+
if (!source) {
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
if (source instanceof Map) {
|
|
232
|
+
return source.get(installationId) ?? [];
|
|
233
|
+
}
|
|
234
|
+
return source[installationId] ?? [];
|
|
235
|
+
}
|
|
236
|
+
function nextQueuedRecord(records) {
|
|
237
|
+
for (const record of records.values()) {
|
|
238
|
+
if (record.status === "queued") {
|
|
239
|
+
return record;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
function cloneQueueRecord(record) {
|
|
245
|
+
return {
|
|
246
|
+
...record,
|
|
247
|
+
identity: { ...record.identity },
|
|
248
|
+
deliveryIds: [...record.deliveryIds]
|
|
249
|
+
};
|
|
250
|
+
}
|
package/docs/github-action.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
|
|
4
4
|
|
|
5
|
-
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.
|
|
5
|
+
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.17.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
6
|
|
|
7
7
|
## PR Summary
|
|
8
8
|
|
|
@@ -50,13 +50,15 @@ The first hosted service slice is scoped in [docs/hosted-first-service-slice.md]
|
|
|
50
50
|
|
|
51
51
|
The hosted deployment model is scoped in [docs/hosted-deployment-model.md](hosted-deployment-model.md). It chooses containerized Node.js ingress and worker roles connected by a managed durable queue, with platform-managed secrets, structured redacted logs, installation/repository rate limits, rollback, and incident response paths.
|
|
52
52
|
|
|
53
|
+
The hosted service runtime is scoped in [docs/hosted-service-runtime.md](hosted-service-runtime.md). It implements the provider-independent core for signed webhook intake, idempotent queue upsert, read-only worker orchestration, compact report storage, Check Run publication adapters, and worker cleanup planning. It does not deploy a public hosted environment by itself.
|
|
54
|
+
|
|
53
55
|
The hosted operational release gate is scoped in [docs/hosted-operational-release-gate.md](hosted-operational-release-gate.md). It blocks hosted exposure unless CI, webhook replay, signature verification, token scoping, idempotency, privacy and retention, worker cleanup, monitoring, alerting, rollback, and incident response evidence are fresh for the release candidate.
|
|
54
56
|
|
|
55
57
|
Hosted uninstall and data deletion behavior is scoped in [docs/hosted-uninstall-data-deletion.md](hosted-uninstall-data-deletion.md). It defines repository removal, full app uninstall, compact report deletion, queue cancellation, limited audit record retention, repeated cleanup idempotency, and user-facing deletion wording.
|
|
56
58
|
|
|
57
59
|
Hosted pricing and packaging boundaries are scoped in [docs/hosted-pricing-packaging.md](hosted-pricing-packaging.md). Core local scanning stays useful without an account; future hosted plans may charge for workflow convenience, saved reports, team policy, private repo hosted behavior, and optional human review, but not for access to useful local scanning.
|
|
58
60
|
|
|
59
|
-
Hosted pre-implementation pure contracts are scoped in [docs/hosted-preimplementation-contracts.md](hosted-preimplementation-contracts.md). They define service-free helpers such as queue-safe pull request event parsing, bounded check-run summary rendering, idempotent queue cleanup planning,
|
|
61
|
+
Hosted pre-implementation pure contracts are scoped in [docs/hosted-preimplementation-contracts.md](hosted-preimplementation-contracts.md). They define service-free helpers such as queue-safe pull request event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and operational release gate evaluation. The hosted service runtime composes those helpers behind replaceable adapters.
|
|
60
62
|
|
|
61
63
|
The hosted compact report schema fixture is published at [examples/hosted-compact-report.json](../examples/hosted-compact-report.json). It is synthetic and public-safe, and it documents the compact evidence shape without raw source, raw diffs, secrets, customer payloads, or worker checkout paths.
|
|
62
64
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Hosted Service Runtime
|
|
2
|
+
|
|
3
|
+
This document describes the first real hosted service runtime now implemented in `src/hosted/service.ts`.
|
|
4
|
+
|
|
5
|
+
It is not a public hosted deployment announcement. The runtime is a provider-independent service core that can be wired to a real queue, compact report store, GitHub Checks API client, and scanner worker in the next deployment stage. It does not deploy a public hosted environment.
|
|
6
|
+
|
|
7
|
+
## What Exists
|
|
8
|
+
|
|
9
|
+
The runtime exports `createHostedServiceRuntime` from `ai-saas-guard/hosted/service`.
|
|
10
|
+
|
|
11
|
+
It implements the first hosted service slice:
|
|
12
|
+
|
|
13
|
+
- signed GitHub App pull request webhook intake
|
|
14
|
+
- signature verification before JSON parsing, queue writes, repository lookup, token scope checks, or worker dispatch
|
|
15
|
+
- trusted scan identity from GitHub event fields only
|
|
16
|
+
- selected-repository installation authorization
|
|
17
|
+
- idempotent durable queue upsert through an adapter
|
|
18
|
+
- read-only worker planning
|
|
19
|
+
- scan runner adapter boundary
|
|
20
|
+
- compact report storage adapter boundary
|
|
21
|
+
- Check Run publication adapter boundary
|
|
22
|
+
- worker checkout cleanup planning after success or failure
|
|
23
|
+
|
|
24
|
+
## Runtime Roles
|
|
25
|
+
|
|
26
|
+
The service core keeps the two production roles from the deployment model:
|
|
27
|
+
|
|
28
|
+
- `webhook-ingress`: call `handlePullRequestWebhook`.
|
|
29
|
+
- `scan-worker`: call `runNextQueuedScan`.
|
|
30
|
+
|
|
31
|
+
The runtime does not start an HTTP server by itself. That keeps framework and cloud choices out of the package while preserving the exact trust-boundary order the hosted service needs.
|
|
32
|
+
|
|
33
|
+
## Adapter Boundaries
|
|
34
|
+
|
|
35
|
+
Production deployments must provide durable adapters:
|
|
36
|
+
|
|
37
|
+
- queue adapter backed by a managed durable queue or relational job table
|
|
38
|
+
- compact report store backed by managed storage
|
|
39
|
+
- Check Run publisher backed by GitHub App installation authentication
|
|
40
|
+
- scan runner backed by isolated worker checkout and the deterministic CLI
|
|
41
|
+
|
|
42
|
+
The exported `createInMemoryHostedServiceAdapters` is only for tests, local smoke runs, and examples. It is not a production queue or production data store.
|
|
43
|
+
|
|
44
|
+
## Privacy
|
|
45
|
+
|
|
46
|
+
The runtime intentionally returns safe planning and status objects only.
|
|
47
|
+
|
|
48
|
+
It does not return:
|
|
49
|
+
|
|
50
|
+
- raw webhook payloads
|
|
51
|
+
- untrusted PR text
|
|
52
|
+
- raw source
|
|
53
|
+
- raw diffs
|
|
54
|
+
- secrets
|
|
55
|
+
- customer payloads
|
|
56
|
+
- private checkout paths
|
|
57
|
+
- low-level worker exception messages
|
|
58
|
+
|
|
59
|
+
Compact reports continue to include only trusted identity, summary counts, rule IDs, compact evidence paths and line numbers, retention metadata, and model-training disabled status.
|
|
60
|
+
|
|
61
|
+
## Failure Behavior
|
|
62
|
+
|
|
63
|
+
Invalid webhooks stop at the signature stage and create no queue, worker, report, or Check Run side effects.
|
|
64
|
+
|
|
65
|
+
Worker failures are recorded with a cleanup-safe `scan_runner_failed` error class. The runtime still plans worker checkout deletion, but it does not expose raw exception text or private checkout paths.
|
|
66
|
+
|
|
67
|
+
## Tests
|
|
68
|
+
|
|
69
|
+
`tests/hosted-service.test.mjs` covers:
|
|
70
|
+
|
|
71
|
+
- a signed pull request webhook queues one idempotent job and the worker publishes one Check Run request
|
|
72
|
+
- duplicate deliveries reuse the logical queue record
|
|
73
|
+
- invalid signatures create no side effects
|
|
74
|
+
- worker failures preserve cleanup behavior without leaking private paths or low-level errors
|
|
75
|
+
|
|
76
|
+
## Deployment Status
|
|
77
|
+
|
|
78
|
+
This runtime makes the hosted service implementation-ready inside the repository. A public hosted environment still requires the next deployment stage:
|
|
79
|
+
|
|
80
|
+
- real GitHub App credentials
|
|
81
|
+
- platform secret manager
|
|
82
|
+
- managed queue
|
|
83
|
+
- compact report storage
|
|
84
|
+
- container image and digest
|
|
85
|
+
- live monitoring and rollback evidence
|
|
86
|
+
- hosted operational release gate evidence from the deployed artifact
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.
|
|
8
|
+
- Current version: `0.17.0`
|
|
9
9
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
10
10
|
- First npm-published version: `0.1.1`
|
|
11
|
-
- GitHub Release: `v0.
|
|
11
|
+
- GitHub Release: `v0.17.0`
|
|
12
12
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
13
13
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
14
14
|
- Long-lived npm publish token: not required
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
19
19
|
|
|
20
|
-
1. Create and review a release tag such as `v0.
|
|
20
|
+
1. Create and review a release tag such as `v0.17.0`.
|
|
21
21
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
22
22
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
23
23
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
package/docs/project-handoff.md
CHANGED
|
@@ -57,9 +57,10 @@ Implemented surfaces:
|
|
|
57
57
|
- hosted operational release gate document requiring hosted CI, webhook replay, dependency and container scanning, privacy and retention verification, worker cleanup, monitoring and alerting, manual rollback, and incident response evidence before exposure
|
|
58
58
|
- hosted uninstall and data deletion document defining repository removal, full app uninstall, compact report deletion, queue cancellation, audit record retention, repeated cleanup idempotency, and user-facing deletion wording
|
|
59
59
|
- hosted pricing and packaging document defining open-source CLI boundaries, free/public repo hosted behavior, private repo hosted behavior, PR comments, saved reports, team policy, optional Launch Review, and no pentest/certification/full-audit claims
|
|
60
|
+
- hosted service runtime document and provider-independent runtime core for signed webhook intake, idempotent queue upsert, read-only worker orchestration, compact report storage, Check Run publication adapters, and worker cleanup planning
|
|
60
61
|
- hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, durable scan queue upsert planning, worker read-only scan planning, Check Run publication planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, retention/deletion cleanup planning, and operational release gate evaluation
|
|
61
62
|
- implementation-ready hosted GitHub App permission contract for required permissions, optional PR comment permissions, selected repository installation, and out-of-scope broad permissions
|
|
62
|
-
-
|
|
63
|
+
- hosted GitHub App contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable scan queue idempotency, compact reports, retention limits, uninstall cleanup, repeated cleanup idempotency, scoped deletion planning, operational release gate blocking, and provider-independent service runtime orchestration
|
|
63
64
|
- GitHub issue templates for bug reports, false positives, false negatives, rule requests, and public-safe security reports
|
|
64
65
|
- CODEOWNERS for source, tests, docs, workflows, Action, and package metadata
|
|
65
66
|
- JSON output
|
|
@@ -130,7 +131,7 @@ CI:
|
|
|
130
131
|
Publishing:
|
|
131
132
|
|
|
132
133
|
- npm package: `ai-saas-guard`
|
|
133
|
-
- Current release line: `v0.
|
|
134
|
+
- Current release line: `v0.17.0`
|
|
134
135
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
135
136
|
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
136
137
|
- Long-lived npm publish tokens should not be required.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-saas-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/zr9959/ai-saas-guard#readme",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./hosted/contracts": {
|
|
34
34
|
"types": "./dist/hosted/contracts.d.ts",
|
|
35
35
|
"default": "./dist/hosted/contracts.js"
|
|
36
|
+
},
|
|
37
|
+
"./hosted/service": {
|
|
38
|
+
"types": "./dist/hosted/service.d.ts",
|
|
39
|
+
"default": "./dist/hosted/service.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"bin": {
|