thevoidforge 21.0.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/dist/scripts/vault-read.d.ts +11 -0
- package/dist/scripts/vault-read.js +89 -0
- package/dist/scripts/voidforge.d.ts +20 -0
- package/dist/scripts/voidforge.js +404 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/wizard/api/auth.d.ts +5 -0
- package/dist/wizard/api/auth.js +133 -0
- package/dist/wizard/api/blueprint.d.ts +45 -0
- package/dist/wizard/api/blueprint.js +184 -0
- package/dist/wizard/api/cloud-providers.d.ts +16 -0
- package/dist/wizard/api/cloud-providers.js +363 -0
- package/dist/wizard/api/credentials.d.ts +1 -0
- package/dist/wizard/api/credentials.js +258 -0
- package/dist/wizard/api/danger-room.d.ts +18 -0
- package/dist/wizard/api/danger-room.js +401 -0
- package/dist/wizard/api/deploy.d.ts +4 -0
- package/dist/wizard/api/deploy.js +164 -0
- package/dist/wizard/api/prd.d.ts +1 -0
- package/dist/wizard/api/prd.js +363 -0
- package/dist/wizard/api/project.d.ts +1 -0
- package/dist/wizard/api/project.js +239 -0
- package/dist/wizard/api/projects.d.ts +6 -0
- package/dist/wizard/api/projects.js +648 -0
- package/dist/wizard/api/provision.d.ts +4 -0
- package/dist/wizard/api/provision.js +535 -0
- package/dist/wizard/api/terminal.d.ts +25 -0
- package/dist/wizard/api/terminal.js +241 -0
- package/dist/wizard/api/users.d.ts +6 -0
- package/dist/wizard/api/users.js +244 -0
- package/dist/wizard/api/war-room.d.ts +14 -0
- package/dist/wizard/api/war-room.js +45 -0
- package/dist/wizard/lib/ad-platform-core.d.ts +6 -0
- package/dist/wizard/lib/ad-platform-core.js +1 -0
- package/dist/wizard/lib/adapters/index.d.ts +52 -0
- package/dist/wizard/lib/adapters/index.js +38 -0
- package/dist/wizard/lib/adapters/sandbox-bank.d.ts +17 -0
- package/dist/wizard/lib/adapters/sandbox-bank.js +77 -0
- package/dist/wizard/lib/adapters/sandbox.d.ts +39 -0
- package/dist/wizard/lib/adapters/sandbox.js +174 -0
- package/dist/wizard/lib/adapters/stripe.d.ts +19 -0
- package/dist/wizard/lib/adapters/stripe.js +143 -0
- package/dist/wizard/lib/adapters/types.d.ts +9 -0
- package/dist/wizard/lib/adapters/types.js +10 -0
- package/dist/wizard/lib/agent-memory.d.ts +36 -0
- package/dist/wizard/lib/agent-memory.js +114 -0
- package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
- package/dist/wizard/lib/anomaly-detection.js +122 -0
- package/dist/wizard/lib/anthropic.d.ts +21 -0
- package/dist/wizard/lib/anthropic.js +105 -0
- package/dist/wizard/lib/asset-scanner.d.ts +23 -0
- package/dist/wizard/lib/asset-scanner.js +107 -0
- package/dist/wizard/lib/audit-log.d.ts +23 -0
- package/dist/wizard/lib/audit-log.js +70 -0
- package/dist/wizard/lib/autonomy-controller.d.ts +76 -0
- package/dist/wizard/lib/autonomy-controller.js +183 -0
- package/dist/wizard/lib/body-parser.d.ts +2 -0
- package/dist/wizard/lib/body-parser.js +36 -0
- package/dist/wizard/lib/build-analytics.d.ts +39 -0
- package/dist/wizard/lib/build-analytics.js +91 -0
- package/dist/wizard/lib/build-step.d.ts +21 -0
- package/dist/wizard/lib/build-step.js +104 -0
- package/dist/wizard/lib/campaign-proposer.d.ts +39 -0
- package/dist/wizard/lib/campaign-proposer.js +180 -0
- package/dist/wizard/lib/campaign-state-machine.d.ts +63 -0
- package/dist/wizard/lib/campaign-state-machine.js +114 -0
- package/dist/wizard/lib/ci-generator.d.ts +14 -0
- package/dist/wizard/lib/ci-generator.js +187 -0
- package/dist/wizard/lib/claude-merge.d.ts +38 -0
- package/dist/wizard/lib/claude-merge.js +115 -0
- package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/erd-gen.js +98 -0
- package/dist/wizard/lib/codegen/integrations.d.ts +18 -0
- package/dist/wizard/lib/codegen/integrations.js +189 -0
- package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
- package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
- package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
- package/dist/wizard/lib/codegen/prisma-types.js +44 -0
- package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
- package/dist/wizard/lib/codegen/seed-gen.js +128 -0
- package/dist/wizard/lib/compliance.d.ts +51 -0
- package/dist/wizard/lib/compliance.js +112 -0
- package/dist/wizard/lib/correlation-engine.d.ts +59 -0
- package/dist/wizard/lib/correlation-engine.js +151 -0
- package/dist/wizard/lib/cost-estimator.d.ts +22 -0
- package/dist/wizard/lib/cost-estimator.js +72 -0
- package/dist/wizard/lib/cost-tracker.d.ts +27 -0
- package/dist/wizard/lib/cost-tracker.js +37 -0
- package/dist/wizard/lib/daemon-aggregator.d.ts +71 -0
- package/dist/wizard/lib/daemon-aggregator.js +204 -0
- package/dist/wizard/lib/daemon-core.d.ts +6 -0
- package/dist/wizard/lib/daemon-core.js +5 -0
- package/dist/wizard/lib/dashboard-data.d.ts +132 -0
- package/dist/wizard/lib/dashboard-data.js +336 -0
- package/dist/wizard/lib/dashboard-ws.d.ts +25 -0
- package/dist/wizard/lib/dashboard-ws.js +91 -0
- package/dist/wizard/lib/deep-current.d.ts +77 -0
- package/dist/wizard/lib/deep-current.js +234 -0
- package/dist/wizard/lib/deploy-coordinator.d.ts +40 -0
- package/dist/wizard/lib/deploy-coordinator.js +86 -0
- package/dist/wizard/lib/deploy-log.d.ts +28 -0
- package/dist/wizard/lib/deploy-log.js +52 -0
- package/dist/wizard/lib/desktop-notify.d.ts +27 -0
- package/dist/wizard/lib/desktop-notify.js +98 -0
- package/dist/wizard/lib/dns/cloudflare-dns.d.ts +35 -0
- package/dist/wizard/lib/dns/cloudflare-dns.js +216 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.d.ts +31 -0
- package/dist/wizard/lib/dns/cloudflare-registrar.js +148 -0
- package/dist/wizard/lib/dns/types.d.ts +22 -0
- package/dist/wizard/lib/dns/types.js +4 -0
- package/dist/wizard/lib/document-discovery.d.ts +33 -0
- package/dist/wizard/lib/document-discovery.js +145 -0
- package/dist/wizard/lib/env-validator.d.ts +14 -0
- package/dist/wizard/lib/env-validator.js +205 -0
- package/dist/wizard/lib/env-writer.d.ts +13 -0
- package/dist/wizard/lib/env-writer.js +26 -0
- package/dist/wizard/lib/exec.d.ts +30 -0
- package/dist/wizard/lib/exec.js +52 -0
- package/dist/wizard/lib/experiment.d.ts +70 -0
- package/dist/wizard/lib/experiment.js +169 -0
- package/dist/wizard/lib/extensions.d.ts +20 -0
- package/dist/wizard/lib/extensions.js +183 -0
- package/dist/wizard/lib/financial/adapter-factory.d.ts +47 -0
- package/dist/wizard/lib/financial/adapter-factory.js +225 -0
- package/dist/wizard/lib/financial/billing/base.d.ts +6 -0
- package/dist/wizard/lib/financial/billing/base.js +1 -0
- package/dist/wizard/lib/financial/billing/google-billing.d.ts +56 -0
- package/dist/wizard/lib/financial/billing/google-billing.js +298 -0
- package/dist/wizard/lib/financial/billing/meta-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/meta-billing.js +243 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.d.ts +54 -0
- package/dist/wizard/lib/financial/billing/tiktok-billing.js +260 -0
- package/dist/wizard/lib/financial/campaign/base.d.ts +13 -0
- package/dist/wizard/lib/financial/campaign/base.js +1 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.d.ts +42 -0
- package/dist/wizard/lib/financial/campaign/google-campaign.js +388 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.d.ts +41 -0
- package/dist/wizard/lib/financial/campaign/meta-campaign.js +311 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.d.ts +45 -0
- package/dist/wizard/lib/financial/campaign/sandbox-campaign.js +261 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.d.ts +40 -0
- package/dist/wizard/lib/financial/campaign/tiktok-campaign.js +350 -0
- package/dist/wizard/lib/financial/funding-auto.d.ts +44 -0
- package/dist/wizard/lib/financial/funding-auto.js +52 -0
- package/dist/wizard/lib/financial/funding-policy.d.ts +60 -0
- package/dist/wizard/lib/financial/funding-policy.js +179 -0
- package/dist/wizard/lib/financial/platform-planner.d.ts +47 -0
- package/dist/wizard/lib/financial/platform-planner.js +134 -0
- package/dist/wizard/lib/financial/reconciliation-engine.d.ts +78 -0
- package/dist/wizard/lib/financial/reconciliation-engine.js +193 -0
- package/dist/wizard/lib/financial/registry.d.ts +22 -0
- package/dist/wizard/lib/financial/registry.js +26 -0
- package/dist/wizard/lib/financial/reporting.d.ts +96 -0
- package/dist/wizard/lib/financial/reporting.js +198 -0
- package/dist/wizard/lib/financial/stablecoin/base.d.ts +6 -0
- package/dist/wizard/lib/financial/stablecoin/base.js +1 -0
- package/dist/wizard/lib/financial/stablecoin/circle.d.ts +54 -0
- package/dist/wizard/lib/financial/stablecoin/circle.js +367 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.d.ts +24 -0
- package/dist/wizard/lib/financial/stablecoin/mercury.js +171 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.d.ts +47 -0
- package/dist/wizard/lib/financial/stablecoin/sandbox-stablecoin.js +202 -0
- package/dist/wizard/lib/financial/treasury-planner.d.ts +52 -0
- package/dist/wizard/lib/financial/treasury-planner.js +128 -0
- package/dist/wizard/lib/financial-core.d.ts +6 -0
- package/dist/wizard/lib/financial-core.js +5 -0
- package/dist/wizard/lib/financial-vault.d.ts +34 -0
- package/dist/wizard/lib/financial-vault.js +199 -0
- package/dist/wizard/lib/frontmatter.d.ts +30 -0
- package/dist/wizard/lib/frontmatter.js +96 -0
- package/dist/wizard/lib/gap-analysis.d.ts +37 -0
- package/dist/wizard/lib/gap-analysis.js +218 -0
- package/dist/wizard/lib/github.d.ts +22 -0
- package/dist/wizard/lib/github.js +261 -0
- package/dist/wizard/lib/headless-deploy.d.ts +14 -0
- package/dist/wizard/lib/headless-deploy.js +452 -0
- package/dist/wizard/lib/health-monitor.d.ts +15 -0
- package/dist/wizard/lib/health-monitor.js +91 -0
- package/dist/wizard/lib/health-poller.d.ts +9 -0
- package/dist/wizard/lib/health-poller.js +123 -0
- package/dist/wizard/lib/heartbeat.d.ts +15 -0
- package/dist/wizard/lib/heartbeat.js +827 -0
- package/dist/wizard/lib/http-helpers.d.ts +9 -0
- package/dist/wizard/lib/http-helpers.js +24 -0
- package/dist/wizard/lib/image-gen.d.ts +56 -0
- package/dist/wizard/lib/image-gen.js +159 -0
- package/dist/wizard/lib/instance-sizing.d.ts +26 -0
- package/dist/wizard/lib/instance-sizing.js +51 -0
- package/dist/wizard/lib/kongo/analytics.d.ts +29 -0
- package/dist/wizard/lib/kongo/analytics.js +179 -0
- package/dist/wizard/lib/kongo/campaigns.d.ts +52 -0
- package/dist/wizard/lib/kongo/campaigns.js +91 -0
- package/dist/wizard/lib/kongo/client.d.ts +58 -0
- package/dist/wizard/lib/kongo/client.js +221 -0
- package/dist/wizard/lib/kongo/jobs.d.ts +57 -0
- package/dist/wizard/lib/kongo/jobs.js +122 -0
- package/dist/wizard/lib/kongo/pages.d.ts +60 -0
- package/dist/wizard/lib/kongo/pages.js +150 -0
- package/dist/wizard/lib/kongo/provisioner.d.ts +64 -0
- package/dist/wizard/lib/kongo/provisioner.js +116 -0
- package/dist/wizard/lib/kongo/seed.d.ts +49 -0
- package/dist/wizard/lib/kongo/seed.js +237 -0
- package/dist/wizard/lib/kongo/types.d.ts +323 -0
- package/dist/wizard/lib/kongo/types.js +11 -0
- package/dist/wizard/lib/kongo/variants.d.ts +57 -0
- package/dist/wizard/lib/kongo/variants.js +88 -0
- package/dist/wizard/lib/kongo/webhooks.d.ts +41 -0
- package/dist/wizard/lib/kongo/webhooks.js +112 -0
- package/dist/wizard/lib/marker.d.ts +28 -0
- package/dist/wizard/lib/marker.js +79 -0
- package/dist/wizard/lib/migrator.d.ts +35 -0
- package/dist/wizard/lib/migrator.js +190 -0
- package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
- package/dist/wizard/lib/natural-language-deploy.js +186 -0
- package/dist/wizard/lib/network.d.ts +22 -0
- package/dist/wizard/lib/network.js +72 -0
- package/dist/wizard/lib/oauth-core.d.ts +6 -0
- package/dist/wizard/lib/oauth-core.js +5 -0
- package/dist/wizard/lib/open-browser.d.ts +1 -0
- package/dist/wizard/lib/open-browser.js +26 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.d.ts +209 -0
- package/dist/wizard/lib/patterns/ad-billing-adapter.js +269 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.d.ts +200 -0
- package/dist/wizard/lib/patterns/ad-platform-adapter.js +212 -0
- package/dist/wizard/lib/patterns/daemon-process.d.ts +88 -0
- package/dist/wizard/lib/patterns/daemon-process.js +271 -0
- package/dist/wizard/lib/patterns/financial-transaction.d.ts +161 -0
- package/dist/wizard/lib/patterns/financial-transaction.js +132 -0
- package/dist/wizard/lib/patterns/funding-plan.d.ts +136 -0
- package/dist/wizard/lib/patterns/funding-plan.js +200 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.d.ts +94 -0
- package/dist/wizard/lib/patterns/oauth-token-lifecycle.js +139 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.d.ts +67 -0
- package/dist/wizard/lib/patterns/outbound-rate-limiter.js +216 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.d.ts +96 -0
- package/dist/wizard/lib/patterns/revenue-source-adapter.js +182 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.d.ts +218 -0
- package/dist/wizard/lib/patterns/stablecoin-adapter.js +264 -0
- package/dist/wizard/lib/prd-validator.d.ts +39 -0
- package/dist/wizard/lib/prd-validator.js +137 -0
- package/dist/wizard/lib/project-init.d.ts +24 -0
- package/dist/wizard/lib/project-init.js +193 -0
- package/dist/wizard/lib/project-registry.d.ts +86 -0
- package/dist/wizard/lib/project-registry.js +359 -0
- package/dist/wizard/lib/provision-manifest.d.ts +44 -0
- package/dist/wizard/lib/provision-manifest.js +164 -0
- package/dist/wizard/lib/provisioner-registry.d.ts +15 -0
- package/dist/wizard/lib/provisioner-registry.js +34 -0
- package/dist/wizard/lib/provisioners/aws-vps.d.ts +6 -0
- package/dist/wizard/lib/provisioners/aws-vps.js +643 -0
- package/dist/wizard/lib/provisioners/cloudflare.d.ts +6 -0
- package/dist/wizard/lib/provisioners/cloudflare.js +300 -0
- package/dist/wizard/lib/provisioners/docker.d.ts +6 -0
- package/dist/wizard/lib/provisioners/docker.js +75 -0
- package/dist/wizard/lib/provisioners/http-client.d.ts +20 -0
- package/dist/wizard/lib/provisioners/http-client.js +79 -0
- package/dist/wizard/lib/provisioners/railway.d.ts +6 -0
- package/dist/wizard/lib/provisioners/railway.js +413 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/caddyfile.js +54 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/deploy-vps.js +112 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.d.ts +11 -0
- package/dist/wizard/lib/provisioners/scripts/docker-compose.js +91 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.d.ts +5 -0
- package/dist/wizard/lib/provisioners/scripts/dockerfile.js +185 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/ecosystem-config.js +36 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.d.ts +14 -0
- package/dist/wizard/lib/provisioners/scripts/provision-vps.js +202 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.d.ts +10 -0
- package/dist/wizard/lib/provisioners/scripts/rollback-vps.js +67 -0
- package/dist/wizard/lib/provisioners/self-deploy.d.ts +41 -0
- package/dist/wizard/lib/provisioners/self-deploy.js +185 -0
- package/dist/wizard/lib/provisioners/static-s3.d.ts +6 -0
- package/dist/wizard/lib/provisioners/static-s3.js +235 -0
- package/dist/wizard/lib/provisioners/types.d.ts +40 -0
- package/dist/wizard/lib/provisioners/types.js +4 -0
- package/dist/wizard/lib/provisioners/vercel.d.ts +6 -0
- package/dist/wizard/lib/provisioners/vercel.js +287 -0
- package/dist/wizard/lib/pty-manager.d.ts +42 -0
- package/dist/wizard/lib/pty-manager.js +231 -0
- package/dist/wizard/lib/rate-limiter-core.d.ts +5 -0
- package/dist/wizard/lib/rate-limiter-core.js +5 -0
- package/dist/wizard/lib/reconciliation.d.ts +43 -0
- package/dist/wizard/lib/reconciliation.js +173 -0
- package/dist/wizard/lib/revenue-types.d.ts +5 -0
- package/dist/wizard/lib/revenue-types.js +1 -0
- package/dist/wizard/lib/route-optimizer.d.ts +28 -0
- package/dist/wizard/lib/route-optimizer.js +93 -0
- package/dist/wizard/lib/s3-deploy.d.ts +19 -0
- package/dist/wizard/lib/s3-deploy.js +156 -0
- package/dist/wizard/lib/safety-tiers.d.ts +76 -0
- package/dist/wizard/lib/safety-tiers.js +134 -0
- package/dist/wizard/lib/sentry-generator.d.ts +15 -0
- package/dist/wizard/lib/sentry-generator.js +116 -0
- package/dist/wizard/lib/server-config.d.ts +13 -0
- package/dist/wizard/lib/server-config.js +23 -0
- package/dist/wizard/lib/service-install.d.ts +18 -0
- package/dist/wizard/lib/service-install.js +182 -0
- package/dist/wizard/lib/site-scanner.d.ts +80 -0
- package/dist/wizard/lib/site-scanner.js +262 -0
- package/dist/wizard/lib/ssh-deploy.d.ts +25 -0
- package/dist/wizard/lib/ssh-deploy.js +225 -0
- package/dist/wizard/lib/templates.d.ts +24 -0
- package/dist/wizard/lib/templates.js +219 -0
- package/dist/wizard/lib/totp.d.ts +35 -0
- package/dist/wizard/lib/totp.js +276 -0
- package/dist/wizard/lib/tower-auth.d.ts +43 -0
- package/dist/wizard/lib/tower-auth.js +352 -0
- package/dist/wizard/lib/tower-rate-limit.d.ts +14 -0
- package/dist/wizard/lib/tower-rate-limit.js +61 -0
- package/dist/wizard/lib/tower-session.d.ts +28 -0
- package/dist/wizard/lib/tower-session.js +119 -0
- package/dist/wizard/lib/treasury-backup.d.ts +23 -0
- package/dist/wizard/lib/treasury-backup.js +126 -0
- package/dist/wizard/lib/treasury-heartbeat.d.ts +82 -0
- package/dist/wizard/lib/treasury-heartbeat.js +1104 -0
- package/dist/wizard/lib/updater.d.ts +29 -0
- package/dist/wizard/lib/updater.js +190 -0
- package/dist/wizard/lib/user-manager.d.ts +39 -0
- package/dist/wizard/lib/user-manager.js +182 -0
- package/dist/wizard/lib/vault.d.ts +26 -0
- package/dist/wizard/lib/vault.js +161 -0
- package/dist/wizard/router.d.ts +5 -0
- package/dist/wizard/router.js +15 -0
- package/dist/wizard/server.d.ts +18 -0
- package/dist/wizard/server.js +436 -0
- package/package.json +59 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: OAuth Token Lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Refresh at 80% of TTL (not at expiry — prevents race conditions)
|
|
6
|
+
* - Multi-grant-type support (authorization_code, refresh_token)
|
|
7
|
+
* - Vault integration — tokens encrypted at rest in financial vault
|
|
8
|
+
* - Failure escalation: retry 3x → pause platform → alert → requires_reauth
|
|
9
|
+
* - Token stored as encrypted blob in vault, keyed by platform name
|
|
10
|
+
* - Session token (daemon) rotates every 24 hours (§9.19.15)
|
|
11
|
+
*
|
|
12
|
+
* Agents: Breeze (platform relations), Dockson (vault)
|
|
13
|
+
*
|
|
14
|
+
* PRD Reference: §9.5 (token refresh strategy), §9.18 (vault session), §9.19.15
|
|
15
|
+
*/
|
|
16
|
+
type AdPlatform = 'meta' | 'google' | 'tiktok' | 'linkedin' | 'twitter' | 'reddit';
|
|
17
|
+
interface OAuthTokenSet {
|
|
18
|
+
platform: AdPlatform;
|
|
19
|
+
accessToken: string;
|
|
20
|
+
refreshToken: string;
|
|
21
|
+
expiresAt: string;
|
|
22
|
+
tokenType: string;
|
|
23
|
+
scopes: string[];
|
|
24
|
+
grantedAt: string;
|
|
25
|
+
lastRefreshedAt: string;
|
|
26
|
+
}
|
|
27
|
+
type TokenHealth = 'healthy' | 'expiring_soon' | 'expired' | 'refresh_failed' | 'requires_reauth' | 'revoked';
|
|
28
|
+
interface TokenStatus {
|
|
29
|
+
platform: AdPlatform;
|
|
30
|
+
health: TokenHealth;
|
|
31
|
+
expiresAt: string;
|
|
32
|
+
lastRefresh: string;
|
|
33
|
+
consecutiveFailures: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
interface PlatformTokenConfig {
|
|
37
|
+
platform: AdPlatform;
|
|
38
|
+
accessTokenTtlHours: number;
|
|
39
|
+
refreshTokenTtlDays: number;
|
|
40
|
+
refreshEndpoint: string;
|
|
41
|
+
revokeEndpoint?: string;
|
|
42
|
+
}
|
|
43
|
+
declare const PLATFORM_CONFIGS: PlatformTokenConfig[];
|
|
44
|
+
declare const REFRESH_AT_TTL_PERCENT = 0.8;
|
|
45
|
+
declare const MAX_CONSECUTIVE_FAILURES = 3;
|
|
46
|
+
/**
|
|
47
|
+
* Check if a token needs refresh.
|
|
48
|
+
* Returns true if the token is past 80% of its TTL.
|
|
49
|
+
*/
|
|
50
|
+
declare function needsRefresh(token: OAuthTokenSet): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Determine the health status of a token.
|
|
53
|
+
*/
|
|
54
|
+
declare function getTokenHealth(token: OAuthTokenSet, failures: number): TokenHealth;
|
|
55
|
+
interface RefreshResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
newTokens?: OAuthTokenSet;
|
|
58
|
+
error?: string;
|
|
59
|
+
requiresReauth?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Handle a refresh failure. Implements the escalation:
|
|
63
|
+
* 1st failure: retry after 30s
|
|
64
|
+
* 2nd failure: retry after 60s
|
|
65
|
+
* 3rd failure: pause campaigns on this platform, set requires_reauth, alert
|
|
66
|
+
*
|
|
67
|
+
* The `invalid_grant` error means the refresh token itself is expired/revoked.
|
|
68
|
+
* This does NOT count toward the 3-failure pause trigger (§9.18) —
|
|
69
|
+
* it goes straight to requires_reauth.
|
|
70
|
+
*/
|
|
71
|
+
declare function handleRefreshFailure(platform: AdPlatform, error: string, consecutiveFailures: number): {
|
|
72
|
+
action: 'retry' | 'pause_and_alert' | 'reauth';
|
|
73
|
+
retryAfterMs?: number;
|
|
74
|
+
};
|
|
75
|
+
declare function tokenVaultKey(platform: AdPlatform): string;
|
|
76
|
+
/**
|
|
77
|
+
* Serialize tokens for vault storage.
|
|
78
|
+
* The vault stores strings — tokens are JSON-serialized.
|
|
79
|
+
*/
|
|
80
|
+
declare function serializeTokens(tokens: OAuthTokenSet): string;
|
|
81
|
+
declare function deserializeTokens(data: string): OAuthTokenSet;
|
|
82
|
+
declare const SESSION_TOKEN_TTL_MS: number;
|
|
83
|
+
declare const SESSION_TOKEN_GRACE_MS: number;
|
|
84
|
+
interface SessionTokenState {
|
|
85
|
+
current: string;
|
|
86
|
+
previous?: string;
|
|
87
|
+
rotatedAt: number;
|
|
88
|
+
previousExpiresAt?: number;
|
|
89
|
+
}
|
|
90
|
+
declare function shouldRotateSessionToken(state: SessionTokenState): boolean;
|
|
91
|
+
declare function rotateSessionToken(state: SessionTokenState, newToken: string): SessionTokenState;
|
|
92
|
+
declare function validateSessionToken(provided: string, state: SessionTokenState): boolean;
|
|
93
|
+
export type { OAuthTokenSet, TokenHealth, TokenStatus, PlatformTokenConfig, RefreshResult, SessionTokenState };
|
|
94
|
+
export { PLATFORM_CONFIGS, REFRESH_AT_TTL_PERCENT, MAX_CONSECUTIVE_FAILURES, needsRefresh, getTokenHealth, handleRefreshFailure, tokenVaultKey, serializeTokens, deserializeTokens, SESSION_TOKEN_TTL_MS, SESSION_TOKEN_GRACE_MS, shouldRotateSessionToken, rotateSessionToken, validateSessionToken, };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: OAuth Token Lifecycle
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Refresh at 80% of TTL (not at expiry — prevents race conditions)
|
|
6
|
+
* - Multi-grant-type support (authorization_code, refresh_token)
|
|
7
|
+
* - Vault integration — tokens encrypted at rest in financial vault
|
|
8
|
+
* - Failure escalation: retry 3x → pause platform → alert → requires_reauth
|
|
9
|
+
* - Token stored as encrypted blob in vault, keyed by platform name
|
|
10
|
+
* - Session token (daemon) rotates every 24 hours (§9.19.15)
|
|
11
|
+
*
|
|
12
|
+
* Agents: Breeze (platform relations), Dockson (vault)
|
|
13
|
+
*
|
|
14
|
+
* PRD Reference: §9.5 (token refresh strategy), §9.18 (vault session), §9.19.15
|
|
15
|
+
*/
|
|
16
|
+
const PLATFORM_CONFIGS = [
|
|
17
|
+
{ platform: 'meta', accessTokenTtlHours: 1440, refreshTokenTtlDays: 0, refreshEndpoint: 'https://graph.facebook.com/v19.0/oauth/access_token' },
|
|
18
|
+
{ platform: 'google', accessTokenTtlHours: 1, refreshTokenTtlDays: 0, refreshEndpoint: 'https://oauth2.googleapis.com/token' },
|
|
19
|
+
{ platform: 'tiktok', accessTokenTtlHours: 24, refreshTokenTtlDays: 365, refreshEndpoint: 'https://business-api.tiktok.com/open_api/v1.3/oauth2/access_token/' },
|
|
20
|
+
{ platform: 'linkedin', accessTokenTtlHours: 1440, refreshTokenTtlDays: 365, refreshEndpoint: 'https://www.linkedin.com/oauth/v2/accessToken' },
|
|
21
|
+
{ platform: 'twitter', accessTokenTtlHours: 0, refreshTokenTtlDays: 0, refreshEndpoint: '' }, // OAuth 1.0a — tokens don't expire
|
|
22
|
+
{ platform: 'reddit', accessTokenTtlHours: 1, refreshTokenTtlDays: 0, refreshEndpoint: 'https://www.reddit.com/api/v1/access_token' },
|
|
23
|
+
];
|
|
24
|
+
// ── Refresh Logic ─────────────────────────────────────
|
|
25
|
+
const REFRESH_AT_TTL_PERCENT = 0.80; // Refresh at 80% of TTL
|
|
26
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
27
|
+
/**
|
|
28
|
+
* Check if a token needs refresh.
|
|
29
|
+
* Returns true if the token is past 80% of its TTL.
|
|
30
|
+
*/
|
|
31
|
+
function needsRefresh(token) {
|
|
32
|
+
const config = PLATFORM_CONFIGS.find(c => c.platform === token.platform);
|
|
33
|
+
if (!config || config.accessTokenTtlHours === 0)
|
|
34
|
+
return false; // Never expires (Twitter OAuth 1.0a)
|
|
35
|
+
const expiresAt = new Date(token.expiresAt).getTime();
|
|
36
|
+
const lastRefresh = new Date(token.lastRefreshedAt).getTime();
|
|
37
|
+
const ttlMs = config.accessTokenTtlHours * 60 * 60 * 1000;
|
|
38
|
+
const refreshAt = lastRefresh + (ttlMs * REFRESH_AT_TTL_PERCENT);
|
|
39
|
+
return Date.now() >= refreshAt;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Determine the health status of a token.
|
|
43
|
+
*/
|
|
44
|
+
function getTokenHealth(token, failures) {
|
|
45
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES)
|
|
46
|
+
return 'requires_reauth';
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const expiresAt = new Date(token.expiresAt).getTime();
|
|
49
|
+
if (expiresAt < now)
|
|
50
|
+
return 'expired';
|
|
51
|
+
const config = PLATFORM_CONFIGS.find(c => c.platform === token.platform);
|
|
52
|
+
if (!config || config.accessTokenTtlHours === 0)
|
|
53
|
+
return 'healthy'; // Never expires
|
|
54
|
+
const ttlMs = config.accessTokenTtlHours * 60 * 60 * 1000;
|
|
55
|
+
const warningAt = expiresAt - (ttlMs * 0.2); // Warn at 80% consumed
|
|
56
|
+
if (now >= warningAt)
|
|
57
|
+
return 'expiring_soon';
|
|
58
|
+
if (failures > 0)
|
|
59
|
+
return 'refresh_failed';
|
|
60
|
+
return 'healthy';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Handle a refresh failure. Implements the escalation:
|
|
64
|
+
* 1st failure: retry after 30s
|
|
65
|
+
* 2nd failure: retry after 60s
|
|
66
|
+
* 3rd failure: pause campaigns on this platform, set requires_reauth, alert
|
|
67
|
+
*
|
|
68
|
+
* The `invalid_grant` error means the refresh token itself is expired/revoked.
|
|
69
|
+
* This does NOT count toward the 3-failure pause trigger (§9.18) —
|
|
70
|
+
* it goes straight to requires_reauth.
|
|
71
|
+
*/
|
|
72
|
+
function handleRefreshFailure(platform, error, consecutiveFailures) {
|
|
73
|
+
// invalid_grant = refresh token revoked/expired — immediate reauth
|
|
74
|
+
if (error.includes('invalid_grant') || error.includes('revoked')) {
|
|
75
|
+
return { action: 'reauth' };
|
|
76
|
+
}
|
|
77
|
+
const newCount = consecutiveFailures + 1;
|
|
78
|
+
if (newCount >= MAX_CONSECUTIVE_FAILURES) {
|
|
79
|
+
return { action: 'pause_and_alert' };
|
|
80
|
+
}
|
|
81
|
+
// Exponential backoff: 30s, 60s
|
|
82
|
+
const retryAfterMs = 30000 * Math.pow(2, consecutiveFailures);
|
|
83
|
+
return { action: 'retry', retryAfterMs };
|
|
84
|
+
}
|
|
85
|
+
// ── Vault Integration ─────────────────────────────────
|
|
86
|
+
// Tokens are stored in the financial vault, keyed by platform name.
|
|
87
|
+
// The daemon reads them at startup and holds them in memory.
|
|
88
|
+
// On refresh, the daemon writes the updated token back to the vault.
|
|
89
|
+
const TOKEN_VAULT_KEY_PREFIX = 'growth/tokens/';
|
|
90
|
+
function tokenVaultKey(platform) {
|
|
91
|
+
return TOKEN_VAULT_KEY_PREFIX + platform;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Serialize tokens for vault storage.
|
|
95
|
+
* The vault stores strings — tokens are JSON-serialized.
|
|
96
|
+
*/
|
|
97
|
+
function serializeTokens(tokens) {
|
|
98
|
+
return JSON.stringify(tokens);
|
|
99
|
+
}
|
|
100
|
+
function deserializeTokens(data) {
|
|
101
|
+
return JSON.parse(data);
|
|
102
|
+
}
|
|
103
|
+
// ── Session Token Rotation (§9.19.15) ─────────────────
|
|
104
|
+
// The daemon session token (heartbeat.token) rotates every 24 hours.
|
|
105
|
+
// During rotation, accept both old and new tokens for 30-second grace period.
|
|
106
|
+
const SESSION_TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
107
|
+
const SESSION_TOKEN_GRACE_MS = 30 * 1000; // 30 seconds
|
|
108
|
+
function shouldRotateSessionToken(state) {
|
|
109
|
+
return Date.now() - state.rotatedAt >= SESSION_TOKEN_TTL_MS;
|
|
110
|
+
}
|
|
111
|
+
function rotateSessionToken(state, newToken) {
|
|
112
|
+
return {
|
|
113
|
+
current: newToken,
|
|
114
|
+
previous: state.current,
|
|
115
|
+
rotatedAt: Date.now(),
|
|
116
|
+
previousExpiresAt: Date.now() + SESSION_TOKEN_GRACE_MS,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function validateSessionToken(provided, state) {
|
|
120
|
+
const { timingSafeEqual } = require('node:crypto');
|
|
121
|
+
// Check current token
|
|
122
|
+
if (provided.length === state.current.length) {
|
|
123
|
+
const a = Buffer.from(provided);
|
|
124
|
+
const b = Buffer.from(state.current);
|
|
125
|
+
if (timingSafeEqual(a, b))
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
// Check previous token during grace period
|
|
129
|
+
if (state.previous && state.previousExpiresAt && Date.now() < state.previousExpiresAt) {
|
|
130
|
+
if (provided.length === state.previous.length) {
|
|
131
|
+
const a = Buffer.from(provided);
|
|
132
|
+
const b = Buffer.from(state.previous);
|
|
133
|
+
if (timingSafeEqual(a, b))
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
export { PLATFORM_CONFIGS, REFRESH_AT_TTL_PERCENT, MAX_CONSECUTIVE_FAILURES, needsRefresh, getTokenHealth, handleRefreshFailure, tokenVaultKey, serializeTokens, deserializeTokens, SESSION_TOKEN_TTL_MS, SESSION_TOKEN_GRACE_MS, shouldRotateSessionToken, rotateSessionToken, validateSessionToken, };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Outbound Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Token bucket per-platform with configurable capacity and refill rate
|
|
6
|
+
* - Safety margin reservation: hold back 10% of capacity for health checks
|
|
7
|
+
* - Queue overflow handling: reject with RATE_LIMITED error, don't block forever
|
|
8
|
+
* - Per-platform configuration (Meta: 200/hr, Google: 15000/day, etc.)
|
|
9
|
+
* - Exponential backoff on 429 responses from platforms
|
|
10
|
+
* - Daily quota tracking for platforms with daily limits (Google, LinkedIn)
|
|
11
|
+
*
|
|
12
|
+
* Agents: Breeze (platform relations), Wax (paid ads)
|
|
13
|
+
*
|
|
14
|
+
* PRD Reference: §9.5 (rate limit strategy), §9.17 (outbound-rate-limiter.ts)
|
|
15
|
+
*/
|
|
16
|
+
type AdPlatform = 'meta' | 'google' | 'tiktok' | 'linkedin' | 'twitter' | 'reddit';
|
|
17
|
+
interface PlatformRateConfig {
|
|
18
|
+
platform: AdPlatform;
|
|
19
|
+
capacity: number;
|
|
20
|
+
refillRate: number;
|
|
21
|
+
dailyQuota?: number;
|
|
22
|
+
burstAllowed: boolean;
|
|
23
|
+
safetyMargin: number;
|
|
24
|
+
backoffBase: number;
|
|
25
|
+
maxRetries: number;
|
|
26
|
+
}
|
|
27
|
+
declare const PLATFORM_RATES: Record<AdPlatform, PlatformRateConfig>;
|
|
28
|
+
declare class OutboundRateLimiter {
|
|
29
|
+
private tokens;
|
|
30
|
+
private lastRefill;
|
|
31
|
+
private dailyUsed;
|
|
32
|
+
private dailyResetAt;
|
|
33
|
+
private readonly config;
|
|
34
|
+
private readonly effectiveCapacity;
|
|
35
|
+
constructor(platform: AdPlatform);
|
|
36
|
+
/**
|
|
37
|
+
* Acquire a token. Resolves when a token is available.
|
|
38
|
+
* Throws if the daily quota is exhausted.
|
|
39
|
+
*/
|
|
40
|
+
acquire(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Execute a request with automatic retry on 429 responses.
|
|
43
|
+
* Uses exponential backoff with platform-specific base delay.
|
|
44
|
+
*/
|
|
45
|
+
executeWithRetry<T>(fn: () => Promise<T>): Promise<T>;
|
|
46
|
+
/** Get current rate limiter status for monitoring */
|
|
47
|
+
getStatus(): {
|
|
48
|
+
tokens: number;
|
|
49
|
+
capacity: number;
|
|
50
|
+
dailyUsed: number;
|
|
51
|
+
dailyQuota: number | undefined;
|
|
52
|
+
};
|
|
53
|
+
/** Reserve N tokens for a batch operation. Returns false if insufficient. */
|
|
54
|
+
canReserve(count: number): boolean;
|
|
55
|
+
private refill;
|
|
56
|
+
private checkDailyReset;
|
|
57
|
+
private nextMidnightUTC;
|
|
58
|
+
}
|
|
59
|
+
declare class RateLimitError extends Error {
|
|
60
|
+
readonly platform: string;
|
|
61
|
+
readonly retryAfterMs: number;
|
|
62
|
+
constructor(platform: string, message: string, retryAfterMs: number);
|
|
63
|
+
}
|
|
64
|
+
declare function isRateLimitResponse(err: unknown): boolean;
|
|
65
|
+
declare function getLimiter(platform: AdPlatform): OutboundRateLimiter;
|
|
66
|
+
export { OutboundRateLimiter, RateLimitError, PLATFORM_RATES, getLimiter, isRateLimitResponse };
|
|
67
|
+
export type { PlatformRateConfig };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Outbound Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Token bucket per-platform with configurable capacity and refill rate
|
|
6
|
+
* - Safety margin reservation: hold back 10% of capacity for health checks
|
|
7
|
+
* - Queue overflow handling: reject with RATE_LIMITED error, don't block forever
|
|
8
|
+
* - Per-platform configuration (Meta: 200/hr, Google: 15000/day, etc.)
|
|
9
|
+
* - Exponential backoff on 429 responses from platforms
|
|
10
|
+
* - Daily quota tracking for platforms with daily limits (Google, LinkedIn)
|
|
11
|
+
*
|
|
12
|
+
* Agents: Breeze (platform relations), Wax (paid ads)
|
|
13
|
+
*
|
|
14
|
+
* PRD Reference: §9.5 (rate limit strategy), §9.17 (outbound-rate-limiter.ts)
|
|
15
|
+
*/
|
|
16
|
+
const PLATFORM_RATES = {
|
|
17
|
+
meta: {
|
|
18
|
+
platform: 'meta',
|
|
19
|
+
capacity: 200, // 200 calls/hr/ad account
|
|
20
|
+
refillRate: 200 / 3600, // ~0.056 tokens/sec
|
|
21
|
+
burstAllowed: true,
|
|
22
|
+
safetyMargin: 0.10,
|
|
23
|
+
backoffBase: 1000,
|
|
24
|
+
maxRetries: 5,
|
|
25
|
+
},
|
|
26
|
+
google: {
|
|
27
|
+
platform: 'google',
|
|
28
|
+
capacity: 100, // Batch operations — 15000/day but bursty
|
|
29
|
+
refillRate: 15000 / 86400, // ~0.174 tokens/sec
|
|
30
|
+
dailyQuota: 15000, // Mutate operations/day
|
|
31
|
+
burstAllowed: true,
|
|
32
|
+
safetyMargin: 0.10,
|
|
33
|
+
backoffBase: 1000,
|
|
34
|
+
maxRetries: 5,
|
|
35
|
+
},
|
|
36
|
+
tiktok: {
|
|
37
|
+
platform: 'tiktok',
|
|
38
|
+
capacity: 10, // 10 req/sec
|
|
39
|
+
refillRate: 10, // 10 tokens/sec
|
|
40
|
+
burstAllowed: false, // Strict per-second
|
|
41
|
+
safetyMargin: 0.10,
|
|
42
|
+
backoffBase: 500,
|
|
43
|
+
maxRetries: 5,
|
|
44
|
+
},
|
|
45
|
+
linkedin: {
|
|
46
|
+
platform: 'linkedin',
|
|
47
|
+
capacity: 100, // 100 calls/day — very restrictive
|
|
48
|
+
refillRate: 100 / 86400, // ~0.0012 tokens/sec
|
|
49
|
+
dailyQuota: 100,
|
|
50
|
+
burstAllowed: false,
|
|
51
|
+
safetyMargin: 0.20, // Higher margin — can't afford waste
|
|
52
|
+
backoffBase: 5000,
|
|
53
|
+
maxRetries: 3, // Fewer retries — each costs a daily call
|
|
54
|
+
},
|
|
55
|
+
twitter: {
|
|
56
|
+
platform: 'twitter',
|
|
57
|
+
capacity: 450, // 450 req/15min
|
|
58
|
+
refillRate: 450 / 900, // 0.5 tokens/sec
|
|
59
|
+
burstAllowed: true,
|
|
60
|
+
safetyMargin: 0.10,
|
|
61
|
+
backoffBase: 1000,
|
|
62
|
+
maxRetries: 5,
|
|
63
|
+
},
|
|
64
|
+
reddit: {
|
|
65
|
+
platform: 'reddit',
|
|
66
|
+
capacity: 60, // Conservative: 60 req/min (not documented)
|
|
67
|
+
refillRate: 1, // 1 token/sec
|
|
68
|
+
burstAllowed: false,
|
|
69
|
+
safetyMargin: 0.15,
|
|
70
|
+
backoffBase: 2000,
|
|
71
|
+
maxRetries: 3,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
// ── Token Bucket Implementation ───────────────────────
|
|
75
|
+
class OutboundRateLimiter {
|
|
76
|
+
tokens;
|
|
77
|
+
lastRefill;
|
|
78
|
+
dailyUsed = 0;
|
|
79
|
+
dailyResetAt;
|
|
80
|
+
config;
|
|
81
|
+
effectiveCapacity;
|
|
82
|
+
constructor(platform) {
|
|
83
|
+
this.config = PLATFORM_RATES[platform];
|
|
84
|
+
this.effectiveCapacity = Math.floor(this.config.capacity * (1 - this.config.safetyMargin));
|
|
85
|
+
this.tokens = this.effectiveCapacity;
|
|
86
|
+
this.lastRefill = Date.now();
|
|
87
|
+
this.dailyResetAt = this.nextMidnightUTC();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Acquire a token. Resolves when a token is available.
|
|
91
|
+
* Throws if the daily quota is exhausted.
|
|
92
|
+
*/
|
|
93
|
+
async acquire() {
|
|
94
|
+
this.checkDailyReset();
|
|
95
|
+
// Daily quota check
|
|
96
|
+
if (this.config.dailyQuota && this.dailyUsed >= this.config.dailyQuota) {
|
|
97
|
+
const hoursUntilReset = (this.dailyResetAt - Date.now()) / 3600000;
|
|
98
|
+
throw new RateLimitError(this.config.platform, `Daily quota exhausted (${this.dailyUsed}/${this.config.dailyQuota}). Resets in ${hoursUntilReset.toFixed(1)} hours.`, this.dailyResetAt - Date.now());
|
|
99
|
+
}
|
|
100
|
+
this.refill();
|
|
101
|
+
if (this.tokens >= 1) {
|
|
102
|
+
this.tokens -= 1;
|
|
103
|
+
this.dailyUsed += 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Wait for a token
|
|
107
|
+
const waitMs = Math.ceil((1 / this.config.refillRate) * 1000);
|
|
108
|
+
if (waitMs > 30000) {
|
|
109
|
+
// Don't wait more than 30 seconds — reject instead
|
|
110
|
+
throw new RateLimitError(this.config.platform, 'Rate limited — next token available in ' + (waitMs / 1000).toFixed(1) + 's', waitMs);
|
|
111
|
+
}
|
|
112
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
113
|
+
this.refill();
|
|
114
|
+
this.tokens -= 1;
|
|
115
|
+
this.dailyUsed += 1;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Execute a request with automatic retry on 429 responses.
|
|
119
|
+
* Uses exponential backoff with platform-specific base delay.
|
|
120
|
+
*/
|
|
121
|
+
async executeWithRetry(fn) {
|
|
122
|
+
let attempt = 0;
|
|
123
|
+
while (attempt < this.config.maxRetries) {
|
|
124
|
+
await this.acquire();
|
|
125
|
+
try {
|
|
126
|
+
return await fn();
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
if (isRateLimitResponse(err) && attempt < this.config.maxRetries - 1) {
|
|
130
|
+
const delay = this.config.backoffBase * Math.pow(2, attempt);
|
|
131
|
+
const retryAfter = extractRetryAfter(err);
|
|
132
|
+
const waitMs = retryAfter ? retryAfter * 1000 : delay;
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
134
|
+
attempt++;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new RateLimitError(this.config.platform, `Max retries (${this.config.maxRetries}) exceeded`, 0);
|
|
141
|
+
}
|
|
142
|
+
/** Get current rate limiter status for monitoring */
|
|
143
|
+
getStatus() {
|
|
144
|
+
this.refill();
|
|
145
|
+
return {
|
|
146
|
+
tokens: Math.floor(this.tokens),
|
|
147
|
+
capacity: this.effectiveCapacity,
|
|
148
|
+
dailyUsed: this.dailyUsed,
|
|
149
|
+
dailyQuota: this.config.dailyQuota,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** Reserve N tokens for a batch operation. Returns false if insufficient. */
|
|
153
|
+
canReserve(count) {
|
|
154
|
+
this.refill();
|
|
155
|
+
return this.tokens >= count;
|
|
156
|
+
}
|
|
157
|
+
refill() {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const elapsed = (now - this.lastRefill) / 1000;
|
|
160
|
+
this.tokens = Math.min(this.effectiveCapacity, this.tokens + elapsed * this.config.refillRate);
|
|
161
|
+
this.lastRefill = now;
|
|
162
|
+
}
|
|
163
|
+
checkDailyReset() {
|
|
164
|
+
if (Date.now() >= this.dailyResetAt) {
|
|
165
|
+
this.dailyUsed = 0;
|
|
166
|
+
this.dailyResetAt = this.nextMidnightUTC();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
nextMidnightUTC() {
|
|
170
|
+
const d = new Date();
|
|
171
|
+
d.setUTCDate(d.getUTCDate() + 1);
|
|
172
|
+
d.setUTCHours(0, 0, 0, 0);
|
|
173
|
+
return d.getTime();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ── Error Types ───────────────────────────────────────
|
|
177
|
+
class RateLimitError extends Error {
|
|
178
|
+
platform;
|
|
179
|
+
retryAfterMs;
|
|
180
|
+
constructor(platform, message, retryAfterMs) {
|
|
181
|
+
super(`[${platform}] Rate limited: ${message}`);
|
|
182
|
+
this.platform = platform;
|
|
183
|
+
this.retryAfterMs = retryAfterMs;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function isRateLimitResponse(err) {
|
|
187
|
+
if (err instanceof RateLimitError)
|
|
188
|
+
return true;
|
|
189
|
+
// Check for HTTP 429 in various error shapes
|
|
190
|
+
const e = err;
|
|
191
|
+
if (e.status === 429 || e.statusCode === 429)
|
|
192
|
+
return true;
|
|
193
|
+
if (typeof e.code === 'string' && e.code === 'RATE_LIMITED')
|
|
194
|
+
return true;
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
function extractRetryAfter(err) {
|
|
198
|
+
const e = err;
|
|
199
|
+
if (typeof e.retryAfter === 'number')
|
|
200
|
+
return e.retryAfter;
|
|
201
|
+
if (e.headers && typeof e.headers['retry-after'] === 'string') {
|
|
202
|
+
return parseInt(e.headers['retry-after']);
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
// ── Limiter Registry ──────────────────────────────────
|
|
207
|
+
const limiters = new Map();
|
|
208
|
+
function getLimiter(platform) {
|
|
209
|
+
let limiter = limiters.get(platform);
|
|
210
|
+
if (!limiter) {
|
|
211
|
+
limiter = new OutboundRateLimiter(platform);
|
|
212
|
+
limiters.set(platform, limiter);
|
|
213
|
+
}
|
|
214
|
+
return limiter;
|
|
215
|
+
}
|
|
216
|
+
export { OutboundRateLimiter, RateLimitError, PLATFORM_RATES, getLimiter, isRateLimitResponse };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern: Revenue Source Adapter (Read-Only)
|
|
3
|
+
*
|
|
4
|
+
* Key principles:
|
|
5
|
+
* - Read-only interface — VoidForge never processes payments directly
|
|
6
|
+
* - Separate from AdPlatformAdapter — revenue in, ad spend out
|
|
7
|
+
* - Polling with overlapping windows for gap-free coverage (ADR-5)
|
|
8
|
+
* - Dedup by externalId to prevent double-counting
|
|
9
|
+
* - Webhook signature verification mandatory when webhooks are implemented
|
|
10
|
+
* - USD-only enforcement (ADR-6) at connection time
|
|
11
|
+
*
|
|
12
|
+
* Agents: Dockson (treasury), Vin (analytics)
|
|
13
|
+
*
|
|
14
|
+
* PRD Reference: §9.4, §9.9, §9.17 (polling improvements), ADR-5/ADR-6
|
|
15
|
+
*/
|
|
16
|
+
type Cents = number & {
|
|
17
|
+
readonly __brand: 'Cents';
|
|
18
|
+
};
|
|
19
|
+
type RevenueSource = 'stripe' | 'paddle';
|
|
20
|
+
interface RevenueSourceAdapter {
|
|
21
|
+
/** Establish connection and verify credentials work */
|
|
22
|
+
connect(credentials: RevenueCredentials): Promise<ConnectionResult>;
|
|
23
|
+
/** Detect the account's currency for ADR-6 enforcement */
|
|
24
|
+
detectCurrency(credentials: RevenueCredentials): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* Fetch transactions in a date range.
|
|
27
|
+
* Uses overlapping windows: fetch from (lastPollTime - 5 minutes) to now.
|
|
28
|
+
* Dedup by externalId at the caller.
|
|
29
|
+
*/
|
|
30
|
+
getTransactions(range: DateRange, cursor?: string): Promise<TransactionPage>;
|
|
31
|
+
/** Get current account balance (optional — not all sources support this) */
|
|
32
|
+
getBalance?(): Promise<BalanceResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Verify webhook signature (mandatory when webhooks are implemented).
|
|
35
|
+
* Deferred to remote mode per ADR-5 — not called in v11.x polling mode.
|
|
36
|
+
*/
|
|
37
|
+
verifyWebhookSignature?(payload: Buffer, signature: string, secret: string): boolean;
|
|
38
|
+
}
|
|
39
|
+
interface RevenueCredentials {
|
|
40
|
+
source: RevenueSource;
|
|
41
|
+
apiKey?: string;
|
|
42
|
+
accessToken?: string;
|
|
43
|
+
}
|
|
44
|
+
interface ConnectionResult {
|
|
45
|
+
connected: boolean;
|
|
46
|
+
accountId?: string;
|
|
47
|
+
accountName?: string;
|
|
48
|
+
currency?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
}
|
|
51
|
+
interface DateRange {
|
|
52
|
+
start: string;
|
|
53
|
+
end: string;
|
|
54
|
+
}
|
|
55
|
+
interface TransactionPage {
|
|
56
|
+
transactions: RevenueTransaction[];
|
|
57
|
+
hasMore: boolean;
|
|
58
|
+
cursor?: string;
|
|
59
|
+
}
|
|
60
|
+
interface RevenueTransaction {
|
|
61
|
+
externalId: string;
|
|
62
|
+
type: 'charge' | 'subscription' | 'refund' | 'dispute';
|
|
63
|
+
amount: Cents;
|
|
64
|
+
currency: 'USD';
|
|
65
|
+
description: string;
|
|
66
|
+
customerId?: string;
|
|
67
|
+
subscriptionId?: string;
|
|
68
|
+
metadata: Record<string, string>;
|
|
69
|
+
createdAt: string;
|
|
70
|
+
}
|
|
71
|
+
interface BalanceResult {
|
|
72
|
+
available: Cents;
|
|
73
|
+
pending: Cents;
|
|
74
|
+
currency: 'USD';
|
|
75
|
+
}
|
|
76
|
+
declare class StripeAdapter implements RevenueSourceAdapter {
|
|
77
|
+
private apiKey;
|
|
78
|
+
private readonly baseUrl;
|
|
79
|
+
connect(credentials: RevenueCredentials): Promise<ConnectionResult>;
|
|
80
|
+
detectCurrency(credentials: RevenueCredentials): Promise<string>;
|
|
81
|
+
getTransactions(range: DateRange, cursor?: string): Promise<TransactionPage>;
|
|
82
|
+
getBalance(): Promise<BalanceResult>;
|
|
83
|
+
verifyWebhookSignature(payload: Buffer, signature: string, secret: string): boolean;
|
|
84
|
+
private apiCall;
|
|
85
|
+
}
|
|
86
|
+
declare class PaddleAdapter implements RevenueSourceAdapter {
|
|
87
|
+
private apiKey;
|
|
88
|
+
private readonly baseUrl;
|
|
89
|
+
connect(credentials: RevenueCredentials): Promise<ConnectionResult>;
|
|
90
|
+
detectCurrency(credentials: RevenueCredentials): Promise<string>;
|
|
91
|
+
getTransactions(range: DateRange, cursor?: string): Promise<TransactionPage>;
|
|
92
|
+
private apiCall;
|
|
93
|
+
}
|
|
94
|
+
declare function hashCustomerId(customerId: string): string;
|
|
95
|
+
export type { RevenueSourceAdapter, RevenueCredentials, ConnectionResult, TransactionPage, RevenueTransaction, BalanceResult, DateRange };
|
|
96
|
+
export { StripeAdapter, PaddleAdapter, hashCustomerId };
|