@yiaany/ghostapi 0.1.6 → 0.1.8
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/CHANGELOG.md +48 -2
- package/CONTRIBUTING.md +119 -117
- package/LICENSE +21 -21
- package/README.md +208 -185
- package/SECURITY.md +53 -37
- package/dist/actions/gateway.d.ts +179 -0
- package/dist/actions/gateway.js +514 -0
- package/dist/actions/gateway.js.map +1 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/ai/aiClient.js +4 -4
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.d.ts +2 -2
- package/dist/ai/aiGenerator.js +21 -72
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/prompts.js +51 -51
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.d.ts +150 -0
- package/dist/approvals/approvalInbox.js +412 -0
- package/dist/approvals/approvalInbox.js.map +1 -0
- package/dist/approvals/index.d.ts +2 -0
- package/dist/approvals/index.js +2 -0
- package/dist/approvals/index.js.map +1 -0
- package/dist/behavior/behaviorStore.d.ts +2 -0
- package/dist/behavior/behaviorStore.js +50 -46
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.js +58 -14
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +438 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.d.ts +10 -0
- package/dist/cli/openUrl.js +31 -0
- package/dist/cli/openUrl.js.map +1 -0
- package/dist/cli/parser.d.ts +155 -0
- package/dist/cli/parser.js +504 -2
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.d.ts +31 -0
- package/dist/config/dataPaths.js +45 -0
- package/dist/config/dataPaths.js.map +1 -0
- package/dist/config/localConfig.d.ts +1 -2
- package/dist/config/localConfig.js +27 -21
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.d.ts +2 -0
- package/dist/config/serverConfig.js +8 -1
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.d.ts +76 -0
- package/dist/contracts/contract.js +603 -0
- package/dist/contracts/contract.js.map +1 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/credentials/credentialBroker.d.ts +207 -0
- package/dist/credentials/credentialBroker.js +588 -0
- package/dist/credentials/credentialBroker.js.map +1 -0
- package/dist/credentials/index.d.ts +2 -0
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/dashboard/app.js +685 -615
- package/dist/dashboard/index.html +13 -7
- package/dist/dashboard/styles.css +349 -349
- package/dist/egress/capabilities.d.ts +32 -0
- package/dist/egress/capabilities.js +165 -0
- package/dist/egress/capabilities.js.map +1 -0
- package/dist/egress/linuxBootstrap.d.ts +1 -0
- package/dist/egress/linuxBootstrap.js +121 -0
- package/dist/egress/linuxBootstrap.js.map +1 -0
- package/dist/egress/run.d.ts +18 -0
- package/dist/egress/run.js +305 -0
- package/dist/egress/run.js.map +1 -0
- package/dist/errors/errorEngine.d.ts +2 -2
- package/dist/errors/errorEngine.js +3 -40
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/evals/evals.d.ts +126 -0
- package/dist/evals/evals.js +466 -0
- package/dist/evals/evals.js.map +1 -0
- package/dist/evals/index.d.ts +2 -0
- package/dist/evals/index.js +2 -0
- package/dist/evals/index.js.map +1 -0
- package/dist/evidence/index.d.ts +2 -0
- package/dist/evidence/index.js +2 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/evidence/report.d.ts +143 -0
- package/dist/evidence/report.js +556 -0
- package/dist/evidence/report.js.map +1 -0
- package/dist/fault/faultLab.d.ts +5 -4
- package/dist/fault/faultLab.js +66 -24
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +46 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +12 -0
- package/dist/inventory/analysis.js +457 -0
- package/dist/inventory/analysis.js.map +1 -0
- package/dist/inventory/index.d.ts +5 -0
- package/dist/inventory/index.js +4 -0
- package/dist/inventory/index.js.map +1 -0
- package/dist/inventory/inventoryController.d.ts +53 -0
- package/dist/inventory/inventoryController.js +1044 -0
- package/dist/inventory/inventoryController.js.map +1 -0
- package/dist/inventory/types.d.ts +579 -0
- package/dist/inventory/types.js +787 -0
- package/dist/inventory/types.js.map +1 -0
- package/dist/landing/assets/index-CxWsopzP.css +1 -0
- package/dist/landing/assets/index-DZlpyhqd.js +55 -0
- package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
- package/dist/landing/index.html +15 -15
- package/dist/ledger/actionLedger.d.ts +125 -0
- package/dist/ledger/actionLedger.js +594 -0
- package/dist/ledger/actionLedger.js.map +1 -0
- package/dist/ledger/index.d.ts +2 -0
- package/dist/ledger/index.js +2 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/mcp/server.js +4 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/policy.d.ts +14 -0
- package/dist/policy/policy.js +282 -0
- package/dist/policy/policy.js.map +1 -0
- package/dist/policy/types.d.ts +52 -0
- package/dist/policy/types.js +2 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/productTelemetry/index.d.ts +16 -0
- package/dist/productTelemetry/index.js +142 -0
- package/dist/productTelemetry/index.js.map +1 -0
- package/dist/providers/conformance.d.ts +7 -0
- package/dist/providers/conformance.js +48 -0
- package/dist/providers/conformance.js.map +1 -0
- package/dist/providers/index.d.ts +4 -2
- package/dist/providers/index.js +3 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/packs/resendPack.d.ts +2 -0
- package/dist/providers/packs/resendPack.js +147 -0
- package/dist/providers/packs/resendPack.js.map +1 -0
- package/dist/providers/packs/stripePack.d.ts +2 -0
- package/dist/providers/packs/stripePack.js +852 -0
- package/dist/providers/packs/stripePack.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -3
- package/dist/providers/registry.js +55 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/resend.js +2 -11
- package/dist/providers/resend.js.map +1 -1
- package/dist/providers/runtime.d.ts +8 -0
- package/dist/providers/runtime.js +48 -0
- package/dist/providers/runtime.js.map +1 -0
- package/dist/providers/stripeWebhook.d.ts +13 -0
- package/dist/providers/stripeWebhook.js +50 -0
- package/dist/providers/stripeWebhook.js.map +1 -0
- package/dist/providers/types.d.ts +153 -0
- package/dist/proxy/cacheKey.js +3 -3
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.d.ts +2 -7
- package/dist/proxy/providerDetector.js +4 -4
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +116 -12
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.d.ts +0 -1
- package/dist/proxy/requestNormalizer.js +2 -5
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.d.ts +175 -0
- package/dist/reliability/costGovernance.js +515 -0
- package/dist/reliability/costGovernance.js.map +1 -0
- package/dist/reliability/index.d.ts +8 -0
- package/dist/reliability/index.js +5 -0
- package/dist/reliability/index.js.map +1 -0
- package/dist/reliability/reconciliation.d.ts +157 -0
- package/dist/reliability/reconciliation.js +522 -0
- package/dist/reliability/reconciliation.js.map +1 -0
- package/dist/reliability/runtimeHealth.d.ts +60 -0
- package/dist/reliability/runtimeHealth.js +356 -0
- package/dist/reliability/runtimeHealth.js.map +1 -0
- package/dist/reliability/slo.d.ts +131 -0
- package/dist/reliability/slo.js +375 -0
- package/dist/reliability/slo.js.map +1 -0
- package/dist/report/safetyReport.js +7 -3
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/safety/index.js.map +1 -0
- package/dist/safety/safetyController.d.ts +187 -0
- package/dist/safety/safetyController.js +477 -0
- package/dist/safety/safetyController.js.map +1 -0
- package/dist/scenarios/scenarioBundle.d.ts +75 -0
- package/dist/scenarios/scenarioBundle.js +750 -0
- package/dist/scenarios/scenarioBundle.js.map +1 -0
- package/dist/scenarios/scenarioStore.d.ts +1 -0
- package/dist/scenarios/scenarioStore.js +47 -32
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.d.ts +2 -0
- package/dist/security/headerSanitizer.js +36 -1
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +24 -4
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.d.ts +5 -0
- package/dist/server/accessControl.js +100 -0
- package/dist/server/accessControl.js.map +1 -0
- package/dist/server/createServer.d.ts +0 -5
- package/dist/server/createServer.js +9 -29
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.d.ts +6 -1
- package/dist/server/eventsStore.js +83 -15
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +36 -10
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.d.ts +2 -1
- package/dist/server/sse.js +15 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/setupGenerator.js +53 -6
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateStore.d.ts +5 -0
- package/dist/state/stateStore.js +48 -62
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.d.ts +10 -0
- package/dist/storage/fileStore.js +131 -0
- package/dist/storage/fileStore.js.map +1 -0
- package/dist/teamControl/controlPlane.d.ts +255 -0
- package/dist/teamControl/controlPlane.js +918 -0
- package/dist/teamControl/controlPlane.js.map +1 -0
- package/dist/teamControl/deployment.d.ts +33 -0
- package/dist/teamControl/deployment.js +67 -0
- package/dist/teamControl/deployment.js.map +1 -0
- package/dist/teamControl/index.d.ts +4 -0
- package/dist/teamControl/index.js +3 -0
- package/dist/teamControl/index.js.map +1 -0
- package/dist/tests/testGenerator.js +4 -1
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -0
- package/dist/trust/index.js +2 -0
- package/dist/trust/index.js.map +1 -0
- package/dist/trust/trustLadder.d.ts +182 -0
- package/dist/trust/trustLadder.js +560 -0
- package/dist/trust/trustLadder.js.map +1 -0
- package/dist/worlds/index.d.ts +2 -0
- package/dist/worlds/index.js +2 -0
- package/dist/worlds/index.js.map +1 -0
- package/dist/worlds/worlds.d.ts +160 -0
- package/dist/worlds/worlds.js +491 -0
- package/dist/worlds/worlds.js.map +1 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -0
- package/docs/commercial/README.md +29 -0
- package/docs/commercial/data-inventory.md +36 -0
- package/docs/commercial/manual-invoicing.md +44 -0
- package/docs/commercial/metrics.md +51 -0
- package/docs/commercial/pricing.md +103 -0
- package/docs/commercial/privacy-policy-draft.md +41 -0
- package/docs/commercial/terms-topics-for-counsel.md +51 -0
- package/docs/design-partners/README.md +52 -0
- package/docs/design-partners/demo-narratives.md +33 -0
- package/docs/design-partners/design-partner-offer.md +39 -0
- package/docs/design-partners/discovery-questions.md +28 -0
- package/docs/design-partners/feedback-capture-template.md +48 -0
- package/docs/design-partners/icp-one-pager.md +43 -0
- package/docs/design-partners/onboarding-checklist.md +33 -0
- package/docs/design-partners/pilot-success-criteria.md +27 -0
- package/docs/design-partners/pricing-interview-script.md +18 -0
- package/docs/design-partners/telemetry-plan.md +48 -0
- package/docs/development/baseline.md +114 -0
- package/docs/development/onboarding-smoke.md +37 -0
- package/docs/development/verification-0.1.8.md +25 -0
- package/docs/enterprise-product-roadmap-ru.md +1879 -0
- package/docs/fundraising/README.md +87 -0
- package/docs/fundraising/data-room-checklist.md +72 -0
- package/docs/fundraising/demo-script.md +52 -0
- package/docs/fundraising/design-partners-50.md +74 -0
- package/docs/fundraising/launch-posts.md +37 -0
- package/docs/fundraising/metrics-and-evidence.md +30 -0
- package/docs/fundraising/roadmap-12-month.md +25 -0
- package/docs/fundraising/technical-due-diligence-index.md +37 -0
- package/docs/fundraising/yc-application.md +25 -0
- package/docs/github-actions.md +84 -0
- package/docs/hosted-pilot.md +111 -0
- package/docs/mcp.md +52 -50
- package/docs/operations/disaster-recovery-runbook.md +81 -0
- package/docs/operations/kill-switch-runbook.md +32 -0
- package/docs/policy.md +61 -0
- package/docs/providers/authoring-packs.md +175 -0
- package/docs/providers/stripe-core-pack.md +82 -0
- package/docs/release-checklist.md +83 -71
- package/docs/release-migration-and-rollback.md +63 -0
- package/docs/release-readiness.md +39 -0
- package/docs/security/action-gateway-threat-model.md +34 -0
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
- package/docs/security/approval-inbox-threat-model.md +35 -0
- package/docs/security/credential-broker-threat-model.md +52 -0
- package/docs/security/egress-threat-model.md +126 -0
- package/docs/security/inventory-threat-model.md +104 -0
- package/docs/security/kill-switch-budgets-threat-model.md +37 -0
- package/docs/security/reliability-threat-model.md +111 -0
- package/docs/security/trust-ladder-threat-model.md +33 -0
- package/docs/team-control-plane.md +72 -0
- package/docs/usage.md +571 -57
- package/examples/README.md +10 -0
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -0
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
- package/examples/ci-smoke/package.json +9 -0
- package/examples/ci-smoke/production-egress.mjs +2 -0
- package/examples/ci-smoke/safe.mjs +17 -0
- package/examples/evals/README.md +24 -0
- package/examples/evals/retry-after.eval.json +52 -0
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +35 -35
- package/examples/openai-streaming/README.md +16 -0
- package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
- package/examples/policy/ghostapi.policy.yaml +27 -0
- package/examples/record-replay/README.md +19 -0
- package/examples/record-replay/replay-requests.json +22 -0
- package/examples/record-replay/stripe-checkout.har.json +30 -0
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -29
- package/examples/stripe-node/checkout-flow.mjs +61 -0
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -0
- package/examples/worlds/subscription-recovery.mjs +12 -0
- package/package.json +26 -11
- package/dist/providers/stripe.d.ts +0 -2
- package/dist/providers/stripe.js +0 -15
- package/dist/providers/stripe.js.map +0 -1
- package/docs/assets/ghostapi-avatar.png +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Hosted Pilot Architecture
|
|
2
|
+
|
|
3
|
+
## Scope And Truthful Guarantees
|
|
4
|
+
|
|
5
|
+
The hosted pilot is a separate Bun/Elysia application under `hosted/`. The existing Node.js local runtime remains account-free and does not communicate with the hosted service by default.
|
|
6
|
+
|
|
7
|
+
The design targets 1,000 sustained and 5,000 peak API requests per second, with CI report ingestion and scenario reads as the first workflows. These are capacity targets to prove with production-shaped load tests, not current guarantees.
|
|
8
|
+
|
|
9
|
+
Supabase read replicas are asynchronous and read-only. All writes use one primary region, and immediate reads after a write also use that primary. Fly may run stateless API instances near clients later, but it must not claim multi-region writes while Supabase has one writable primary. Supabase PITR supports fine-grained recovery, but restoration takes the project offline and duration depends on database size; this stack does not prove RPO under one minute or RTO under five minutes until a vendor-supported configuration and repeated DR drills demonstrate both.
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart LR
|
|
15
|
+
CI[CI runner or agent] -->|ingest key + idempotency key| API[Bun + Elysia write API]
|
|
16
|
+
Agent[Agent] -->|session| API
|
|
17
|
+
API --> Primary[(Supabase PostgreSQL primary)]
|
|
18
|
+
API --> Redis[Upstash Redis: rate limits + cache only]
|
|
19
|
+
Primary --> Outbox[Transactional outbox]
|
|
20
|
+
Dispatcher[Outbox dispatcher] --> QStash[Upstash QStash]
|
|
21
|
+
QStash --> Worker[Elysia internal worker]
|
|
22
|
+
Worker --> Primary
|
|
23
|
+
Reader[Scenario reader] --> API
|
|
24
|
+
API --> Primary
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Redis is deliberately not the source of truth for reports, authorizations, idempotency, or job completion. QStash is at-least-once transport only. PostgreSQL owns durable acceptance, idempotency, outbox state, job receipts, scenario registry, report state, and audit data.
|
|
28
|
+
|
|
29
|
+
The migration enables RLS on the application tables. The hosted API uses a dedicated least-privilege `service_role` database role with explicit policies; browsers never receive that connection string. Tenant filtering remains explicit in every API query, so authorization does not rely on a future client-side `set_config` convention.
|
|
30
|
+
|
|
31
|
+
## Request Paths
|
|
32
|
+
|
|
33
|
+
### Report Intake
|
|
34
|
+
|
|
35
|
+
`POST /v1/projects/:projectId/reports` requires:
|
|
36
|
+
|
|
37
|
+
- `Authorization: Bearer <CI ingest secret>`
|
|
38
|
+
- `X-GhostAPI-Ingest-Key-Id: <public UUID>`
|
|
39
|
+
- `Idempotency-Key: <16-256 character key>`
|
|
40
|
+
|
|
41
|
+
The API hashes the canonical request. In one primary-Postgres transaction it validates the key, creates or resolves the idempotency ledger entry, writes the bounded sanitized report, and writes an outbox event. Only then does it respond with `202 Accepted`.
|
|
42
|
+
|
|
43
|
+
If the API crashes after commit but before replying, the CI runner retries with the same idempotency key and receives the original report id. If the same key has a different body, it receives `409`.
|
|
44
|
+
|
|
45
|
+
### CI Ingest Keys
|
|
46
|
+
|
|
47
|
+
Project developers provision a key with `POST /v1/projects/:projectId/ingest-keys` and an `expiresInDays` value from 1 to 90. The response returns an id, non-sensitive prefix, expiry, and a plaintext secret exactly once. Only the SHA-256 digest is written to PostgreSQL. Developers revoke a key with `POST /v1/projects/:projectId/ingest-keys/:keyId/revoke`; create and revoke actions write tenant audit events. The dashboard/client must not persist, log, or re-display the plaintext secret.
|
|
48
|
+
|
|
49
|
+
### Scenario Reads
|
|
50
|
+
|
|
51
|
+
Scenario versions are immutable. Publishing creates a new `{ scenario_key, version }` row. CI runs pin a scenario version; agents read by key/version or a selected current version. Cursor pagination and a read cache belong at the API edge only after primary-query correctness is load-tested.
|
|
52
|
+
|
|
53
|
+
Read endpoints that must observe a preceding write go to the primary and return `X-Consistency: primary`. A future replica endpoint may return `X-Consistency: eventual`; it cannot back read-your-writes behavior.
|
|
54
|
+
|
|
55
|
+
### Workers
|
|
56
|
+
|
|
57
|
+
Outbox dispatch uses `FOR UPDATE SKIP LOCKED`, leases a bounded batch, then publishes an event id to QStash. The QStash receiver verifies the request signature before parsing its body. The worker transaction creates a permanent `job_receipts` record and performs report processing atomically. Redelivery returns a duplicate success without duplicating scenario results.
|
|
58
|
+
|
|
59
|
+
The dispatcher can publish the same event more than once after an ambiguous network outcome. That is intentional: QStash deduplication has a limited window, so the permanent PostgreSQL receipt is the correctness boundary. A redelivery while an unexpired worker lease is active returns a retryable `503`; a stale lease can be safely acquired again. A completed receipt returns success without duplicating results.
|
|
60
|
+
|
|
61
|
+
## PostgreSQL Data Model
|
|
62
|
+
|
|
63
|
+
`hosted/migrations/001_core.sql` creates:
|
|
64
|
+
|
|
65
|
+
| Table | Purpose |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `app.organizations`, `app.organization_memberships`, `app.projects` | Tenant hierarchy and membership. |
|
|
68
|
+
| `app.scenario_versions` | Immutable shared scenario definitions with checksum. |
|
|
69
|
+
| `app.ci_ingest_keys` | Public id plus SHA-256-only CI secret digest and scope. |
|
|
70
|
+
| `app.reports` | Bounded sanitized CI report payload and processing state. |
|
|
71
|
+
| `app.idempotency_ledger` | Durable `(project_id, key)` replay contract. |
|
|
72
|
+
| `app.outbox_events` | Transactional event publication source. |
|
|
73
|
+
| `app.job_receipts`, `app.scenario_run_results` | At-least-once worker deduplication and results. |
|
|
74
|
+
| `app.audit_events` | Hosted control-plane audit metadata. |
|
|
75
|
+
|
|
76
|
+
Better Auth manages its own `auth` schema/tables through its migration CLI. Configure `AUTH_DATABASE_URL` with `search_path=auth`; application `DATABASE_URL` should use `search_path=app`. Keep application memberships separate from Better Auth accounts so a later SAML/OIDC identity mapping does not rewrite tenant authorization.
|
|
77
|
+
|
|
78
|
+
## Queue And Rate Limits
|
|
79
|
+
|
|
80
|
+
- Queue only identifiers and schema version, never raw report JSON, authorization headers, cookies, or provider credentials.
|
|
81
|
+
- Do not use a single FIFO queue. Shard flow-control keys by report/project as needed; workers make every handler idempotent.
|
|
82
|
+
- Verify QStash signatures on every internal job request.
|
|
83
|
+
- Redis keys are opaque non-secret project/principal identifiers, not bearer tokens, email addresses, or raw session ids. The default CI key budget is 300,000 requests per minute per project only as a pilot starting point; derive final limits from tenant budgets and load tests.
|
|
84
|
+
- Rate limits must cover unauthenticated auth routes, CI ingest key id, user/organization, project, and QStash worker endpoint separately.
|
|
85
|
+
- Do not enable automatic retries for non-idempotent external effects. Scenario processing remains local metadata until a typed action gateway exists.
|
|
86
|
+
|
|
87
|
+
## Capacity Plan
|
|
88
|
+
|
|
89
|
+
At 1,000 sustained writes per second, report size and retention determine cost and storage more than framework choice. Before accepting pilot load, measure actual p50/p95/p99 report bytes, write/read ratio, tenant count, retention requirements, index growth, WAL throughput, database pool waits, QStash backlog, and worker drain rate.
|
|
90
|
+
|
|
91
|
+
Initial deployment is one API write region co-located with the Supabase primary, with at least two Fly machines. Use bounded database pools per machine and Supavisor transaction pooling. Add read replicas only after primary query/index behavior is correct and clients tolerate eventual consistency.
|
|
92
|
+
|
|
93
|
+
## Required Pre-Launch Tests
|
|
94
|
+
|
|
95
|
+
1. Kill the API after Postgres commit but before response; retry must return one report id and one outbox event.
|
|
96
|
+
2. Force QStash redelivery and worker failure; expect one job receipt and one scenario result per report/version.
|
|
97
|
+
3. Soak 1,000 sustained RPS and burst 5,000 peak RPS using production p99 payloads; verify intake p95/p99, pool waits, WAL, queue depth, worker drain, and zero lost accepted report ids.
|
|
98
|
+
4. Verify primary read-your-writes and clearly labeled replica staleness.
|
|
99
|
+
5. Run a production-equivalent DR drill. Reject the stated RPO/RTO target until observed accepted-data loss is below 60 seconds and restored write availability is below 5 minutes.
|
|
100
|
+
6. Run authorization tests for every organization/project boundary, disabled/revoked ingest key, expired session, body/key mismatch, and direct worker invocation without a valid QStash signature.
|
|
101
|
+
|
|
102
|
+
## Identity
|
|
103
|
+
|
|
104
|
+
The hosted app mounts Better Auth for Google OAuth and email/password. Better Auth owns authentication accounts/sessions; GhostAPI owns organization membership and authorization. A later enterprise identity layer maps a verified OIDC/SAML subject to an internal user id. It must not accept arbitrary issuer, email-domain, or client-provided role claims.
|
|
105
|
+
|
|
106
|
+
## Sources Checked 2026-08-08
|
|
107
|
+
|
|
108
|
+
- Elysia validation and handler status behavior: official Elysia documentation.
|
|
109
|
+
- Better Auth PostgreSQL and Elysia integration: official Better Auth documentation.
|
|
110
|
+
- Supabase read replicas and PITR recovery limits: official Supabase documentation.
|
|
111
|
+
- QStash deduplication, publish, receiver verification, and at-least-once guidance: official Upstash documentation.
|
package/docs/mcp.md
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
# GhostAPI MCP Setup
|
|
2
|
-
|
|
3
|
-
GhostAPI ships with a local stdio MCP server so coding agents can inspect and control the local API world.
|
|
4
|
-
|
|
5
|
-
## Start MCP
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npx @yiaany/ghostapi mcp
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Universal Config
|
|
12
|
-
|
|
13
|
-
```json
|
|
14
|
-
{
|
|
15
|
-
"mcpServers": {
|
|
16
|
-
"ghostapi": {
|
|
17
|
-
"command": "npx",
|
|
18
|
-
"args": ["-y", "@yiaany/ghostapi", "mcp"]
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Generate Client Snippets
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npx @yiaany/ghostapi setup --write
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
This writes copy-ready local snippets under `.ghostapi/agent-configs/` and project instructions for supported agent clients.
|
|
31
|
-
|
|
32
|
-
## Tools
|
|
33
|
-
|
|
34
|
-
| Tool | Purpose |
|
|
35
|
-
| --- | --- |
|
|
36
|
-
| `inspect_state` | Read local API objects from
|
|
37
|
-
| `get_traffic_logs` | Inspect recent captured API traffic. |
|
|
38
|
-
| `set_api_behavior` | Force a deterministic local response for `method + path`. |
|
|
39
|
-
| `toggle_chaos_mode` | Enable or disable local latency and error injection. |
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
# GhostAPI MCP Setup
|
|
2
|
+
|
|
3
|
+
GhostAPI ships with a local stdio MCP server so coding agents can inspect and control the local API world.
|
|
4
|
+
|
|
5
|
+
## Start MCP
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @yiaany/ghostapi mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Universal Config
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"ghostapi": {
|
|
17
|
+
"command": "npx",
|
|
18
|
+
"args": ["-y", "@yiaany/ghostapi", "mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Generate Client Snippets
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @yiaany/ghostapi setup --write
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This writes copy-ready local snippets under `.ghostapi/agent-configs/` and project instructions for supported agent clients.
|
|
31
|
+
|
|
32
|
+
## Tools
|
|
33
|
+
|
|
34
|
+
| Tool | Purpose |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `inspect_state` | Read sanitized local API objects from the configured GhostAPI data directory. |
|
|
37
|
+
| `get_traffic_logs` | Inspect recent captured API traffic. |
|
|
38
|
+
| `set_api_behavior` | Force a deterministic local response for `method + path`. |
|
|
39
|
+
| `toggle_chaos_mode` | Enable or disable local latency and error injection. |
|
|
40
|
+
|
|
41
|
+
The HTTP server and MCP process must use the same `GHOSTAPI_DATA_DIR` to share state, behaviors, and persisted Fault Lab configuration. Updates use local inter-process locking; this is not distributed coordination for network filesystems.
|
|
42
|
+
|
|
43
|
+
## Agent Prompt
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
Use the GhostAPI MCP server.
|
|
47
|
+
|
|
48
|
+
Keep all third-party API calls local on http://127.0.0.1:8080.
|
|
49
|
+
Do not call real providers.
|
|
50
|
+
|
|
51
|
+
Use GhostAPI MCP tools to inspect state, read traffic logs, configure deterministic responses, and test failure scenarios.
|
|
52
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Disaster-Recovery Runbook
|
|
2
|
+
|
|
3
|
+
Local backup and restore for the GhostAPI data directory (`GHOSTAPI_DATA_DIR`, default `.ghostapi/`).
|
|
4
|
+
|
|
5
|
+
## When To Use This
|
|
6
|
+
|
|
7
|
+
- You are replacing or moving the local data directory and need a verified copy.
|
|
8
|
+
- A store is corrupt (`ghostapi` reports a degraded runtime health) and you want to restore from a known-good backup.
|
|
9
|
+
- You are running a quarterly DR drill: back up, destroy, restore, verify.
|
|
10
|
+
|
|
11
|
+
## Before You Start
|
|
12
|
+
|
|
13
|
+
- Run `ghostapi start` is NOT required. Backup and restore work on a stopped or running instance, but restoring over a running instance is not supported — stop the instance first.
|
|
14
|
+
- Backups are local directory copies with sha256 manifests. They are not encrypted, do not contain upstream secrets (the credential broker persists metadata only), and are not a substitute for object-storage or offsite backup.
|
|
15
|
+
- The default backup destination is `.ghostapi/reliability/backups/backup-<timestamp>-<id>/`. Because the `backups` directory is excluded from future backups, backups never recursively include one another.
|
|
16
|
+
|
|
17
|
+
## Backup
|
|
18
|
+
|
|
19
|
+
Backup is available programmatically:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { backupRuntime } from "@yiaany/ghostapi";
|
|
23
|
+
const result = await backupRuntime({ destinationDir: ".ghostapi/reliability/backups/manual-drill" });
|
|
24
|
+
// result: { backupId, path, fileCount, totalBytes, verified: true, createdAt }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The backup verifies every copied file (size + sha256 + JSON structure for `.json` entries) and refuses to overwrite an existing destination. `result.verified` is only true after full verification.
|
|
28
|
+
|
|
29
|
+
## Restore
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { restoreRuntimeBackup } from "@yiaany/ghostapi";
|
|
33
|
+
await restoreRuntimeBackup({ sourceDir: ".ghostapi/reliability/backups/manual-drill", targetDir: ".ghostapi-restored" });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then point the runtime at the restored directory:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export GHOSTAPI_DATA_DIR=/absolute/path/to/.ghostapi-restored
|
|
40
|
+
ghostapi doctor
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Restore refuses:
|
|
44
|
+
|
|
45
|
+
- A backup whose manifest was not verified at creation time.
|
|
46
|
+
- Any manifest entry path that escapes the backup root (path traversal).
|
|
47
|
+
- A target that is the backup source itself or inside it.
|
|
48
|
+
- A target directory that is not empty (restore copies into a fresh directory).
|
|
49
|
+
- Any entry whose bytes no longer match the manifest (tamper detection).
|
|
50
|
+
|
|
51
|
+
## Verification Steps After Restore
|
|
52
|
+
|
|
53
|
+
1. `ghostapi doctor` reports healthy.
|
|
54
|
+
2. `curl http://127.0.0.1:8080/health` returns `{ "ok": true, "ready": true }`.
|
|
55
|
+
3. `curl http://127.0.0.1:8080/health/readiness` returns HTTP 200 with a `ready: true` report. A 503 means the restored directory has a degraded store — do not continue.
|
|
56
|
+
4. Spot-check state: list actions (`ghostapi actions` or the equivalent CLI), run a reconciliation run, and confirm SLO sample counts are non-zero.
|
|
57
|
+
|
|
58
|
+
## Destroy-The-World Drill (Non-Destructive)
|
|
59
|
+
|
|
60
|
+
1. `backupRuntime` into `.ghostapi/reliability/backups/drill-<date>`.
|
|
61
|
+
2. `Remove-Item -Recurse -Force .ghostapi` (or `rm -rf`) to simulate data loss.
|
|
62
|
+
3. `restoreRuntimeBackup` into a fresh directory.
|
|
63
|
+
4. Point `GHOSTAPI_DATA_DIR` at the restored directory and run the verification steps above.
|
|
64
|
+
5. Confirm the backup directory still exists untouched (restore copies out of it; it never deletes).
|
|
65
|
+
|
|
66
|
+
## Failure Modes
|
|
67
|
+
|
|
68
|
+
| Symptom | Meaning | Action |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| "Backup destination already exists" | Refusing to overwrite | Use a new destination or remove the old backup deliberately |
|
|
71
|
+
| "Backup source entry is not valid JSON" | A `.json` file in the data dir is corrupt | Find and fix/delete the corrupt store, then re-back up |
|
|
72
|
+
| "Backup file content changed during copy" | Concurrent write during backup | Stop writers, re-run backup |
|
|
73
|
+
| "failed integrity verification" on restore | Backup was tampered or the disk changed | Do not restore; find the original source or a different backup |
|
|
74
|
+
| "path escapes its root directory" | Crafted/tampered manifest | Reject the backup as untrusted |
|
|
75
|
+
| `/health/readiness` returns 503 | A restored store is corrupt | Inspect the `degraded` store in the report and repair/delete it, then re-check |
|
|
76
|
+
|
|
77
|
+
## Retention Guidance
|
|
78
|
+
|
|
79
|
+
- Keep at least two rotating backups: the previous known-good and the current.
|
|
80
|
+
- Back up before any release or before switching `GHOSTAPI_DATA_DIR`.
|
|
81
|
+
- The 64 MiB backup cap covers the bounded local stores; if your worlds/actions exceed it, prune old synthetic data first.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Kill Switch Runbook
|
|
2
|
+
|
|
3
|
+
## Detection
|
|
4
|
+
|
|
5
|
+
Use this procedure when policy violations, abnormal action velocity, budget exhaustion, reconciliation mismatch, latency/failure circuit opening, or suspected agent compromise is observed. An alert is evidence only; it is not a stop. Verify the affected organization, project, environment, agent/workload, provider, operation, or risk class before selecting scope.
|
|
6
|
+
|
|
7
|
+
## Stop
|
|
8
|
+
|
|
9
|
+
1. Use an independently authenticated emergency operator with `safety.stop`.
|
|
10
|
+
2. Call the local `LocalSafetyController.stop({ identity, scope, reason })` API with the narrowest safe scope. Use `global` when the scope cannot be established quickly.
|
|
11
|
+
3. Record a bounded factual reason without credentials, raw requests, personal data, or provider payloads.
|
|
12
|
+
4. Confirm the persisted switch is enabled and examine the safety audit chain. Matching queued records must be in the dead-letter queue, not still pending.
|
|
13
|
+
5. Treat actions that passed their final commit check as potentially completed. Do not retry unknown outcomes; investigate or reconcile them first.
|
|
14
|
+
|
|
15
|
+
## Investigation
|
|
16
|
+
|
|
17
|
+
1. Preserve the local safety, action, approval, and credential-broker state files as evidence under the existing local trust model.
|
|
18
|
+
2. Inspect action receipt status, controller audit records, budget ledger, circuit reason, and dead letters.
|
|
19
|
+
3. Identify the exact idempotency key and action hash. A changed action must be submitted as a new reviewed action, never forced through a prior reservation.
|
|
20
|
+
4. For any future provider integration, reconcile against the provider before retrying or compensating. This local synthetic controller cannot prove external outcome.
|
|
21
|
+
|
|
22
|
+
## Recovery
|
|
23
|
+
|
|
24
|
+
1. Correct the policy/configuration or isolate the compromised workload.
|
|
25
|
+
2. Review budgets, circuit thresholds, and the root cause. Do not clear audit, ledger, or dead-letter state to make a control appear healthy.
|
|
26
|
+
3. Require a separately authenticated operator with `safety.reenable`; the stop permission alone is insufficient.
|
|
27
|
+
4. Call `reenable({ identity, scope, reason })` and confirm the audit record and persisted switch state.
|
|
28
|
+
5. Re-admit only a reviewed action. Approved artifacts still require current policy, identity, idempotency, budget, circuit, kill-switch, and final-commit checks.
|
|
29
|
+
|
|
30
|
+
## Game Day
|
|
31
|
+
|
|
32
|
+
The `GhostAPI Kill Switch Game Day` workflow runs at `03:17 UTC` each Monday and can be started manually. It executes only the local synthetic controller test. Investigate a failed drill before trusting the control: a successful alert without a verified blocked action is not sufficient.
|
package/docs/policy.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Policy As Code
|
|
2
|
+
|
|
3
|
+
GhostAPI reads one local, versioned YAML policy. It has no remote includes, environment-variable interpolation, templates, or executable expressions.
|
|
4
|
+
|
|
5
|
+
Copy [`examples/policy/ghostapi.policy.yaml`](../examples/policy/ghostapi.policy.yaml) into a project as `ghostapi.policy.yaml`, then validate it:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
ghostapi policy validate
|
|
9
|
+
ghostapi policy explain network api.stripe.com --provider stripe
|
|
10
|
+
ghostapi policy explain stripe.card_declined
|
|
11
|
+
ghostapi policy explain report 0 0 0
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`ghostapi run --policy ghostapi.policy.yaml -- <command>` loads and validates the file once before any namespace preflight or target spawn. The run evidence records only the policy SHA-256 and required scenario IDs, not policy file content or command arguments. Policy changes during a run do not reload or alter that run.
|
|
15
|
+
|
|
16
|
+
`ghostapi evidence generate --policy ghostapi.policy.yaml --ci` evaluates required scenarios and report thresholds against the generated evidence artifact. Pass `--contract-baseline` and `--contract-candidate` to include contract drift. CI mode exits non-zero when required scenarios are missing, production-egress, forbidden-credential, or breaking-contract thresholds are exceeded, or other fail findings are present.
|
|
17
|
+
|
|
18
|
+
## Schema V1
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
version: 1
|
|
22
|
+
network:
|
|
23
|
+
default: deny
|
|
24
|
+
allow:
|
|
25
|
+
- host: localhost
|
|
26
|
+
- provider: stripe
|
|
27
|
+
deny:
|
|
28
|
+
- host: api.stripe.com
|
|
29
|
+
productionHosts:
|
|
30
|
+
- '*.stripe.com'
|
|
31
|
+
credentials:
|
|
32
|
+
forbid:
|
|
33
|
+
- sk_live_*
|
|
34
|
+
requiredScenarios:
|
|
35
|
+
- stripe-payment-intent-card-declined
|
|
36
|
+
enforcement:
|
|
37
|
+
allowedModes:
|
|
38
|
+
- linux-network-namespace
|
|
39
|
+
reports:
|
|
40
|
+
maxProductionEgressAttempts: 0
|
|
41
|
+
maxForbiddenCredentialMatches: 0
|
|
42
|
+
maxBreakingContractChanges: 0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `network.default`: `allow` or `deny` when no rule matches.
|
|
46
|
+
- `network.allow` and `network.deny`: rules contain exactly one `host` or `provider` field.
|
|
47
|
+
- `network.productionHosts`: classifies hostnames for decision traces and CI/report evaluation.
|
|
48
|
+
- `credentials.forbid`: bounded `*` globs matched against an explicit input; GhostAPI never expands environment variables in policy.
|
|
49
|
+
- `requiredScenarios`: scenario IDs the evidence report must mark complete.
|
|
50
|
+
- `enforcement.allowedModes`: currently `linux-network-namespace` or `proxy-guidance`.
|
|
51
|
+
- `reports`: maximum allowed production-egress attempts, forbidden-credential matches, and breaking contract changes for a report decision. `maxBreakingContractChanges` defaults to `0` when omitted by an existing policy.
|
|
52
|
+
|
|
53
|
+
## Precedence And Limits
|
|
54
|
+
|
|
55
|
+
For a network decision, a matching `network.deny` rule always wins over a matching `network.allow` rule. Otherwise `network.default` decides. `ghostapi policy explain` prints the selected rule and decision trace.
|
|
56
|
+
|
|
57
|
+
Policies are limited to 128 KiB and list fields to 200 entries. Unknown fields, duplicate YAML keys, unsupported versions, anchors/aliases, interpolation syntax, path traversal, symlink policy files and multiple YAML documents are rejected with a path-aware error.
|
|
58
|
+
|
|
59
|
+
## Migration
|
|
60
|
+
|
|
61
|
+
Schema versions are exact. GhostAPI does not coerce or silently upgrade an unknown version. A future version must add an explicit migration command that reads one known old version, emits a new file for review, and requires a separate validation step. It must never rewrite a policy while `ghostapi run` is active.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Authoring Provider Packs
|
|
2
|
+
|
|
3
|
+
Provider packs are the versioned contract layer for provider-specific behavior in GhostAPI. Resend and Stripe are migrated built-in packs. Twilio, GitHub, Discord, and OpenAI remain on the legacy adapter path until they can be migrated in small reviewed changes. Generic REST remains the fallback.
|
|
4
|
+
|
|
5
|
+
## Scope And Trust Boundary
|
|
6
|
+
|
|
7
|
+
Provider packs are built-in TypeScript modules compiled with GhostAPI. GhostAPI does not load uploaded, downloaded, or third-party executable provider code.
|
|
8
|
+
|
|
9
|
+
A pack receives only explicit inputs:
|
|
10
|
+
|
|
11
|
+
- detection receives a normalized path only;
|
|
12
|
+
- parsing, validation, and handling receive the sanitized `NormalizedRequest`;
|
|
13
|
+
- clock, ID generation, and a read-only state snapshot require explicit runtime capabilities;
|
|
14
|
+
- no network, filesystem, environment, or secret capability is available to a pack.
|
|
15
|
+
|
|
16
|
+
The source regression test rejects pack modules that import Node network/filesystem modules or access `process.env` or `fetch`. This is a maintenance boundary, not a secure JavaScript sandbox. Do not advertise arbitrary plugin isolation.
|
|
17
|
+
|
|
18
|
+
## Public Interfaces
|
|
19
|
+
|
|
20
|
+
The stable authoring surface is exported from `src/index.ts`:
|
|
21
|
+
|
|
22
|
+
- `ProviderPack`;
|
|
23
|
+
- `ProviderPackManifest`;
|
|
24
|
+
- `ProviderConformanceFixture`;
|
|
25
|
+
- `ProviderRuntime` and `ProviderRuntimeCapabilities`;
|
|
26
|
+
- `ProviderScenario` and `ProviderScenarioStep`;
|
|
27
|
+
- `ProviderWebhookHook`;
|
|
28
|
+
- `createProviderRuntime()`;
|
|
29
|
+
- `runProviderPackConformance()`.
|
|
30
|
+
|
|
31
|
+
The request pipeline, registry lookup, prepared execution object, response headers, state persistence, cache, Fault Lab, dashboard events, and scenario persistence are internal interfaces. Packs must not import stores or server modules directly.
|
|
32
|
+
|
|
33
|
+
## Required Contract
|
|
34
|
+
|
|
35
|
+
Every pack declares:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
export const examplePack: ProviderPack = {
|
|
39
|
+
name: "example",
|
|
40
|
+
displayName: "Example",
|
|
41
|
+
manifest: {
|
|
42
|
+
schemaVersion: 1,
|
|
43
|
+
name: "example",
|
|
44
|
+
displayName: "Example",
|
|
45
|
+
implementation: "pack",
|
|
46
|
+
packVersion: "1.0.0",
|
|
47
|
+
apiVersions: { default: "v1", supported: ["v1"] },
|
|
48
|
+
capabilities: {
|
|
49
|
+
detection: true,
|
|
50
|
+
requestParsing: true,
|
|
51
|
+
validation: true,
|
|
52
|
+
deterministicResponses: true,
|
|
53
|
+
stateTransitions: true,
|
|
54
|
+
providerErrors: true,
|
|
55
|
+
scenarios: false,
|
|
56
|
+
webhooks: false,
|
|
57
|
+
conformanceFixtures: true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
detection: { priority: 100, matches: ({ path }) => path.startsWith("/example") },
|
|
61
|
+
parseRequest: (request) => request.body,
|
|
62
|
+
selectApiVersion: () => ({ version: "v1" }),
|
|
63
|
+
validate: () => null,
|
|
64
|
+
handleDeterministic: ({ runtime }) => ({
|
|
65
|
+
status: 200,
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
body: { id: runtime.requireCapability("idGenerator").create("example_mock") }
|
|
68
|
+
}),
|
|
69
|
+
createResponseHeaders: () => ({}),
|
|
70
|
+
transitionState: () => null,
|
|
71
|
+
stateful: false,
|
|
72
|
+
formatError: (details) => ({ error: { message: details.message } }),
|
|
73
|
+
promptHints: [],
|
|
74
|
+
scenarios: [],
|
|
75
|
+
conformanceFixtures: []
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The manifest is returned by `GET /api/providers`, shown in dashboard provider filters, and printed by `ghostapi providers list` and `ghostapi providers inspect <name>`.
|
|
80
|
+
|
|
81
|
+
## API Version Selection
|
|
82
|
+
|
|
83
|
+
Pack API versions are GhostAPI compatibility versions, not an automatic claim of full parity with a provider's live API. A pack must:
|
|
84
|
+
|
|
85
|
+
1. declare one default version and all supported versions;
|
|
86
|
+
2. select a version before request validation;
|
|
87
|
+
3. return a provider-shaped `ProviderErrorDetails` for unsupported explicit versions;
|
|
88
|
+
4. include version-sensitive request headers in cache identity;
|
|
89
|
+
5. change `packVersion` when pack behavior changes and add a new API version when the simulated contract changes incompatibly.
|
|
90
|
+
|
|
91
|
+
The Resend pack accepts `x-ghostapi-api-version: v1`. Successful responses include:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
x-ghostapi-provider-pack: resend@1.0.0
|
|
95
|
+
x-ghostapi-api-version: v1
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Determinism
|
|
99
|
+
|
|
100
|
+
Do not call `Date.now()`, `new Date()`, random generators, environment variables, or external services inside a pack. Request runtime capabilities explicitly:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const now = runtime.requireCapability("clock").now();
|
|
104
|
+
const id = runtime.requireCapability("idGenerator").create("item_mock");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Tests must inject fixed implementations with `createProviderRuntime()`. `createResponseHeaders()` can add deterministic provider headers such as Stripe request IDs to successful or early-error responses. Asking for an undeclared capability fails with `Unknown provider capability: <name>`.
|
|
108
|
+
|
|
109
|
+
## Validation And Errors
|
|
110
|
+
|
|
111
|
+
`parseRequest()` converts the sanitized normalized request into the provider's internal request shape. `validate()` returns `null` or `ProviderErrorDetails`. Core formats the details through the registered provider error formatter.
|
|
112
|
+
|
|
113
|
+
Keep validation order stable when migrating a public route. Error status, field names, messages, and provider shape are observable behavior.
|
|
114
|
+
|
|
115
|
+
## State Transitions
|
|
116
|
+
|
|
117
|
+
`transitionState()` describes persistence but does not write it. Core owns storage and requires keys to stay in the exact `<provider>:<id>` namespace. A pack that declares `stateful: true` executes under one atomic local-state transaction and can inspect the injected read-only snapshot while deciding its response. It still cannot access a store directly.
|
|
118
|
+
|
|
119
|
+
A transition must be derived from the handled response and must not read ambient state. The conformance fixture should assert both the key and value. Core rejects cross-provider or empty keys before persistence.
|
|
120
|
+
|
|
121
|
+
## Scenarios And Webhooks
|
|
122
|
+
|
|
123
|
+
Pack scenarios use the shared `ProviderScenario` shape and are merged into the existing scenario registry. Scenario replay still installs deterministic method/path behaviors and runs before faults, validation, state, cache, and generation.
|
|
124
|
+
|
|
125
|
+
`ProviderWebhookHook` is part of the authoring vocabulary, but no built-in pack currently advertises webhook capability. Do not set `webhooks: true` until event synthesis, signing, delivery timing, retries, and failure semantics have tests.
|
|
126
|
+
|
|
127
|
+
## Conformance Fixtures
|
|
128
|
+
|
|
129
|
+
Every migrated pack must provide at least one fixture that exercises its main mutation or read flow. Run it through the common harness:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const runtime = createProviderRuntime({
|
|
133
|
+
clock: { now: () => new Date("2026-08-06T12:00:00.000Z") },
|
|
134
|
+
idGenerator: { create: (prefix) => `${prefix}_fixture` }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
runProviderPackConformance(examplePack, runtime);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The harness:
|
|
141
|
+
|
|
142
|
+
1. selects an API version;
|
|
143
|
+
2. parses and validates the fixture request;
|
|
144
|
+
3. generates a deterministic response;
|
|
145
|
+
4. runs the fixture response assertion;
|
|
146
|
+
5. computes and validates the state transition;
|
|
147
|
+
6. runs the fixture transition assertion.
|
|
148
|
+
|
|
149
|
+
Malformed responses and wrong state keys must fail the harness.
|
|
150
|
+
|
|
151
|
+
## Adding Or Migrating A Pack
|
|
152
|
+
|
|
153
|
+
1. Add one pack under `src/providers/packs/`.
|
|
154
|
+
2. Register it in the ordered built-in pack list in `src/providers/registry.ts`.
|
|
155
|
+
3. Remove only that provider's legacy detection, validation, generation, prompt, and scenario branches.
|
|
156
|
+
4. Preserve route detection precedence and generic REST fallback.
|
|
157
|
+
5. Add conformance, provider regression, manifest API, state, cache, and provider-shaped error tests.
|
|
158
|
+
6. Run `npm run typecheck`, `npm test`, and `npm run build`.
|
|
159
|
+
7. Inspect built CLI output and package contents before release.
|
|
160
|
+
|
|
161
|
+
## Migration Plan
|
|
162
|
+
|
|
163
|
+
The remaining providers should move one at a time:
|
|
164
|
+
|
|
165
|
+
| Order | Provider | Migration focus | Deferred risk |
|
|
166
|
+
| --- | --- | --- | --- |
|
|
167
|
+
| 1 | Twilio | Form parsing, validation, SID generation, error shape | Message status transitions and callbacks |
|
|
168
|
+
| 2 | GitHub | Version header, deterministic issue/repository shapes, scenarios | Pagination and conditional requests |
|
|
169
|
+
| 3 | OpenAI | API version policy, typed response families, list state | Streaming and token accounting |
|
|
170
|
+
| 4 | Discord | Route/body detection and message shapes | Webhook and interaction semantics |
|
|
171
|
+
| Done | Stripe | Core pack for customers, payment intents, payment methods, checkout sessions, refunds, pagination, and idempotency | Webhooks, subscriptions, invoices, disputes, and broader state-machine parity |
|
|
172
|
+
|
|
173
|
+
Stripe now demonstrates the deeper stateful pack path. See [`stripe-core-pack.md`](stripe-core-pack.md) for the supported contract and limits.
|
|
174
|
+
|
|
175
|
+
Generic REST should not become a provider pack. It remains the explicit fallback so unknown APIs continue to work without pretending to have a provider contract.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Stripe Core Pack
|
|
2
|
+
|
|
3
|
+
The built-in Stripe core pack is a deterministic local simulation for the most common payment workflows. It is not full Stripe compatibility and it never makes outbound network requests.
|
|
4
|
+
|
|
5
|
+
## Version And Verification
|
|
6
|
+
|
|
7
|
+
- Verified against Stripe's official API reference and Stripe Node SDK behavior on 2026-08-06.
|
|
8
|
+
- GhostAPI compatibility API version: `2026-02-25.clover`.
|
|
9
|
+
- Pack version: `1.1.0`.
|
|
10
|
+
- Successful pack responses include `x-ghostapi-provider-pack: stripe@1.1.0`, `stripe-version: 2026-02-25.clover`, and a deterministic `request-id`.
|
|
11
|
+
|
|
12
|
+
The version is an explicit GhostAPI compatibility contract, not a claim that all live Stripe endpoints or fields are supported.
|
|
13
|
+
|
|
14
|
+
## Supported Operations
|
|
15
|
+
|
|
16
|
+
| Resource | Operations |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| Customers | Create, retrieve, update, delete, list |
|
|
19
|
+
| Payment Intents | Create, retrieve, update, confirm, list |
|
|
20
|
+
| Payment Methods | Create minimal `card` method, retrieve, list |
|
|
21
|
+
| Checkout Sessions | Create, retrieve, list |
|
|
22
|
+
| Refunds | Create, retrieve, list |
|
|
23
|
+
| Products | Create, retrieve, update, list |
|
|
24
|
+
| Prices | Create recurring or one-time prices, retrieve, update, list |
|
|
25
|
+
| Subscriptions | Create, retrieve, update, cancel, local `renew` control, list |
|
|
26
|
+
| Invoices | Retrieve, pay, void, list |
|
|
27
|
+
| Events | List, retrieve, and local signed delivery |
|
|
28
|
+
|
|
29
|
+
List endpoints return Stripe-shaped `{ object: "list", data, has_more, url }` responses and accept `limit`, `starting_after`, and `ending_before` cursors.
|
|
30
|
+
|
|
31
|
+
The pack accepts form-encoded nested parameters such as `metadata[source]=sdk` and `line_items[0][price_data][unit_amount]=2500`, as emitted by the Stripe Node SDK. JSON bodies are accepted for direct local clients.
|
|
32
|
+
|
|
33
|
+
## Determinism And State
|
|
34
|
+
|
|
35
|
+
Objects receive deterministic IDs, timestamps, and request IDs when a fixed runtime clock and ID generator are injected in tests. Normal runtime IDs remain unique across requests. Stateful requests run under GhostAPI's local atomic state transaction; the pack does not access storage itself.
|
|
36
|
+
|
|
37
|
+
`Idempotency-Key` is represented only by a SHA-256-derived local digest in events and state. A repeated mutation with the same key and parameters returns the original response with `x-ghostapi-idempotency: REPLAY`. Reusing the key with different parameters returns a Stripe-shaped `idempotency_error`. Receipts expire after 24 hours, allowing a new mutation for the same key after the local retention window.
|
|
38
|
+
|
|
39
|
+
## Billing Lifecycle
|
|
40
|
+
|
|
41
|
+
Subscriptions use one recurring Price. Creating with `trial_period_days` creates a `trialing` subscription; `POST /v1/subscriptions/:id/renew` is a GhostAPI-only local test control that advances it. A normal renewal produces an Invoice, succeeded Payment Intent, and `active` subscription. `pm_card_chargeDeclined` produces a Stripe `card_error`, preserves an open Invoice, and sets the subscription to `past_due`; `POST /v1/invoices/:id/pay` can recover it. Cancellation marks the subscription `canceled`. Refunds create a `charge.refunded` event and update the local Payment Intent refund total.
|
|
42
|
+
|
|
43
|
+
## Webhook Testing
|
|
44
|
+
|
|
45
|
+
State transitions create persisted Stripe-shaped Events for subscription, invoice, payment-intent, and refund lifecycle changes. GhostAPI deliberately does not accept arbitrary webhook target URLs and never sends outbound webhook HTTP requests. Instead, tests pull a raw signed payload from:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
GET /v1/events/:event_id/deliver?delivery_mode=normal|duplicate|delayed|out_of_order|invalid_signature
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The response includes `stripe-signature`, generated with HMAC-SHA256 over `timestamp.rawPayload`. The default local-only secret is `whsec_ghostapi_local_test_secret`; set `GHOSTAPI_STRIPE_WEBHOOK_SECRET` to use another test secret. The secret is not stored in state, events, scenarios, or responses. `duplicate` reliably returns the same event ID and payload on repeat calls, `delayed` accepts bounded `delay_ms` (0-10000), `out_of_order` reverses the event-list ordering, and `invalid_signature` corrupts the signature. Use the official `stripe.webhooks.constructEvent` verifier against the raw response body.
|
|
52
|
+
|
|
53
|
+
Rate-limit (`429` + `Retry-After`), bounded delay/timeout simulation, and `5xx` behavior remain controlled through GhostAPI Fault Lab. These are injected before pack execution, so a client retry with an idempotency key exercises the normal atomic replay path after the fault is disabled.
|
|
54
|
+
|
|
55
|
+
## Test Card Outcomes
|
|
56
|
+
|
|
57
|
+
Use the Stripe test payment-method identifier `pm_card_chargeDeclined` (or legacy `tok_chargeDeclined`) when creating or confirming a Payment Intent to receive:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"error": {
|
|
62
|
+
"type": "card_error",
|
|
63
|
+
"code": "card_declined",
|
|
64
|
+
"decline_code": "generic_decline"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
No PAN or CVC is required. If a direct client submits card-like fields, GhostAPI masks them before event or snapshot persistence. The returned synthetic Payment Method exposes only fixed non-sensitive card metadata such as `last4`.
|
|
70
|
+
|
|
71
|
+
## Deliberate Limits
|
|
72
|
+
|
|
73
|
+
- Disputes, Connect, payment links, Setup Intents, hosted Checkout, tax, proration, dunning, smart retries, and most Stripe APIs remain unsupported.
|
|
74
|
+
- Unsupported operations return a diagnostic `invalid_request_error`; GhostAPI does not generate a fake success.
|
|
75
|
+
- Checkout Sessions are local objects. Their returned URL is a local placeholder and not a hosted checkout page.
|
|
76
|
+
- Payment Intent confirmation has a simplified state model: normal confirmation succeeds; the documented decline token fails.
|
|
77
|
+
- Refunds support `payment_intent` or `charge` input, but only validate a referenced Payment Intent when one is supplied.
|
|
78
|
+
- No live keys, PANs, CVCs, provider credentials, or real network traffic are retained or used.
|
|
79
|
+
|
|
80
|
+
## Runnable SDK Example
|
|
81
|
+
|
|
82
|
+
Start GhostAPI and run `examples/stripe-node/checkout-flow.mjs`. The example configures the official Node SDK with its documented host, port, protocol, and API-version client options, then verifies a locally delivered webhook using the SDK; no SDK source patch is required.
|