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,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Marketing Campaign Adapter — real implementation via node:https (zero new dependencies).
|
|
3
|
+
*
|
|
4
|
+
* Implements AdPlatformAdapter for Meta Marketing API v19.0.
|
|
5
|
+
*
|
|
6
|
+
* Meta Marketing API v19.0:
|
|
7
|
+
* Base URL: https://graph.facebook.com/v19.0
|
|
8
|
+
* Auth: access_token query parameter
|
|
9
|
+
* Campaign CRUD via Graph API
|
|
10
|
+
* Reporting via insights endpoint
|
|
11
|
+
* Rate limit: 200 calls/hr/ad account (sliding window)
|
|
12
|
+
*
|
|
13
|
+
* PRD Reference: §9.5, §9.19.10, §9.20.4
|
|
14
|
+
* No Stubs Doctrine: every method makes a real API call or returns documented empty.
|
|
15
|
+
*/
|
|
16
|
+
import { request as httpsRequest } from 'node:https';
|
|
17
|
+
import { toCents, TokenBucketLimiter } from './base.js';
|
|
18
|
+
const META_HOST = 'graph.facebook.com';
|
|
19
|
+
// ── HTTP helpers ─────────────────────────────────────
|
|
20
|
+
async function metaGet(path, accessToken, params) {
|
|
21
|
+
const queryParams = new URLSearchParams({ access_token: accessToken, ...params });
|
|
22
|
+
const query = '?' + queryParams.toString();
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const req = httpsRequest({
|
|
25
|
+
hostname: META_HOST,
|
|
26
|
+
path: `/v19.0${path}${query}`,
|
|
27
|
+
method: 'GET',
|
|
28
|
+
headers: { 'Accept': 'application/json' },
|
|
29
|
+
timeout: 15000,
|
|
30
|
+
}, (res) => {
|
|
31
|
+
let data = '';
|
|
32
|
+
res.on('data', (chunk) => { data += chunk.toString(); });
|
|
33
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 500, body: data }));
|
|
34
|
+
});
|
|
35
|
+
req.on('error', reject);
|
|
36
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Meta API timeout')); });
|
|
37
|
+
req.end();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function metaPost(path, accessToken, params) {
|
|
41
|
+
const payload = new URLSearchParams({ access_token: accessToken, ...params }).toString();
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const req = httpsRequest({
|
|
44
|
+
hostname: META_HOST,
|
|
45
|
+
path: `/v19.0${path}`,
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
49
|
+
'Accept': 'application/json',
|
|
50
|
+
},
|
|
51
|
+
timeout: 15000,
|
|
52
|
+
}, (res) => {
|
|
53
|
+
let data = '';
|
|
54
|
+
res.on('data', (chunk) => { data += chunk.toString(); });
|
|
55
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 500, body: data }));
|
|
56
|
+
});
|
|
57
|
+
req.on('error', reject);
|
|
58
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Meta API timeout')); });
|
|
59
|
+
req.write(payload);
|
|
60
|
+
req.end();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function metaDelete(path, accessToken) {
|
|
64
|
+
const query = `?access_token=${encodeURIComponent(accessToken)}`;
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const req = httpsRequest({
|
|
67
|
+
hostname: META_HOST,
|
|
68
|
+
path: `/v19.0${path}${query}`,
|
|
69
|
+
method: 'DELETE',
|
|
70
|
+
headers: { 'Accept': 'application/json' },
|
|
71
|
+
timeout: 15000,
|
|
72
|
+
}, (res) => {
|
|
73
|
+
let data = '';
|
|
74
|
+
res.on('data', (chunk) => { data += chunk.toString(); });
|
|
75
|
+
res.on('end', () => resolve({ status: res.statusCode ?? 500, body: data }));
|
|
76
|
+
});
|
|
77
|
+
req.on('error', reject);
|
|
78
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Meta API timeout')); });
|
|
79
|
+
req.end();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function safeParseJson(body) {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(body);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return { error: { message: 'Non-JSON response from Meta API' } };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function makePlatformError(code, originalCode, message, retryable = false, retryAfter) {
|
|
91
|
+
return { platform: 'meta', code, originalCode, message, retryable, retryAfter };
|
|
92
|
+
}
|
|
93
|
+
function mapObjective(obj) {
|
|
94
|
+
const map = {
|
|
95
|
+
awareness: 'OUTCOME_AWARENESS',
|
|
96
|
+
traffic: 'OUTCOME_TRAFFIC',
|
|
97
|
+
conversions: 'OUTCOME_SALES',
|
|
98
|
+
};
|
|
99
|
+
return map[obj];
|
|
100
|
+
}
|
|
101
|
+
// ── Adapter Implementation ──────────────────────────
|
|
102
|
+
export class MetaCampaignAdapter {
|
|
103
|
+
config;
|
|
104
|
+
rateLimiter;
|
|
105
|
+
constructor(config) {
|
|
106
|
+
this.config = config;
|
|
107
|
+
// Meta: 200 calls/hr/ad account
|
|
108
|
+
this.rateLimiter = new TokenBucketLimiter({ capacity: 200, refillRate: 200 / 3600 });
|
|
109
|
+
}
|
|
110
|
+
async refreshToken(token) {
|
|
111
|
+
// Meta long-lived tokens: exchange at 80% of 60-day TTL
|
|
112
|
+
await this.rateLimiter.acquire();
|
|
113
|
+
const { status, body } = await metaGet('/oauth/access_token', token.accessToken, {
|
|
114
|
+
grant_type: 'fb_exchange_token',
|
|
115
|
+
fb_exchange_token: token.accessToken,
|
|
116
|
+
});
|
|
117
|
+
if (status !== 200) {
|
|
118
|
+
throw makePlatformError('AUTH_EXPIRED', status, 'Meta token refresh failed');
|
|
119
|
+
}
|
|
120
|
+
const parsed = safeParseJson(body);
|
|
121
|
+
return {
|
|
122
|
+
...token,
|
|
123
|
+
accessToken: parsed.access_token,
|
|
124
|
+
expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString(),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async createCampaign(config) {
|
|
128
|
+
if (config.complianceStatus !== 'passed') {
|
|
129
|
+
throw makePlatformError('UNKNOWN', 400, 'Campaign compliance not passed — cannot create');
|
|
130
|
+
}
|
|
131
|
+
await this.rateLimiter.acquire();
|
|
132
|
+
const { status, body } = await metaPost(`/act_${this.config.adAccountId}/campaigns`, this.config.accessToken, {
|
|
133
|
+
name: config.name,
|
|
134
|
+
objective: mapObjective(config.objective),
|
|
135
|
+
status: 'PAUSED',
|
|
136
|
+
special_ad_categories: '[]',
|
|
137
|
+
// Idempotency per ADR-3: Meta uses request-level dedup via the key
|
|
138
|
+
idempotency_key: config.idempotencyKey,
|
|
139
|
+
});
|
|
140
|
+
if (status !== 200) {
|
|
141
|
+
this.throwApiError(status, body);
|
|
142
|
+
}
|
|
143
|
+
const parsed = safeParseJson(body);
|
|
144
|
+
const externalId = parsed.id;
|
|
145
|
+
return {
|
|
146
|
+
externalId,
|
|
147
|
+
platform: 'meta',
|
|
148
|
+
status: 'created',
|
|
149
|
+
dashboardUrl: `https://www.facebook.com/adsmanager/manage/campaigns?act=${this.config.adAccountId}&campaign_ids=${externalId}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async updateCampaign(id, changes) {
|
|
153
|
+
await this.rateLimiter.acquire();
|
|
154
|
+
const params = {};
|
|
155
|
+
if (changes.name !== undefined)
|
|
156
|
+
params.name = changes.name;
|
|
157
|
+
if (Object.keys(params).length === 0)
|
|
158
|
+
return;
|
|
159
|
+
const { status, body } = await metaPost(`/${id}`, this.config.accessToken, params);
|
|
160
|
+
if (status !== 200) {
|
|
161
|
+
this.throwApiError(status, body);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async pauseCampaign(id) {
|
|
165
|
+
await this.rateLimiter.acquire();
|
|
166
|
+
const { status, body } = await metaPost(`/${id}`, this.config.accessToken, {
|
|
167
|
+
status: 'PAUSED',
|
|
168
|
+
});
|
|
169
|
+
if (status !== 200) {
|
|
170
|
+
this.throwApiError(status, body);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async resumeCampaign(id) {
|
|
174
|
+
await this.rateLimiter.acquire();
|
|
175
|
+
const { status, body } = await metaPost(`/${id}`, this.config.accessToken, {
|
|
176
|
+
status: 'ACTIVE',
|
|
177
|
+
});
|
|
178
|
+
if (status !== 200) {
|
|
179
|
+
this.throwApiError(status, body);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async deleteCampaign(id) {
|
|
183
|
+
await this.rateLimiter.acquire();
|
|
184
|
+
const { status, body } = await metaDelete(`/${id}`, this.config.accessToken);
|
|
185
|
+
if (status !== 200) {
|
|
186
|
+
this.throwApiError(status, body);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async updateBudget(id, dailyBudget) {
|
|
190
|
+
await this.rateLimiter.acquire();
|
|
191
|
+
// Meta budgets are in the account's currency smallest unit (cents for USD)
|
|
192
|
+
const { status, body } = await metaPost(`/${id}`, this.config.accessToken, {
|
|
193
|
+
daily_budget: String(dailyBudget),
|
|
194
|
+
});
|
|
195
|
+
if (status !== 200) {
|
|
196
|
+
this.throwApiError(status, body);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async updateCreative(id, creative) {
|
|
200
|
+
await this.rateLimiter.acquire();
|
|
201
|
+
// Meta creative updates go through the ad object, not campaign
|
|
202
|
+
// Query the first ad under this campaign, then update it
|
|
203
|
+
const { status: queryStatus, body: queryBody } = await metaGet(`/${id}/ads`, this.config.accessToken, { fields: 'id', limit: '1' });
|
|
204
|
+
if (queryStatus !== 200) {
|
|
205
|
+
this.throwApiError(queryStatus, queryBody);
|
|
206
|
+
}
|
|
207
|
+
const queryParsed = safeParseJson(queryBody);
|
|
208
|
+
const ads = queryParsed.data ?? [];
|
|
209
|
+
if (ads.length === 0) {
|
|
210
|
+
throw makePlatformError('CREATIVE_REJECTED', 404, `No ads found for campaign ${id}`);
|
|
211
|
+
}
|
|
212
|
+
const adId = ads[0].id;
|
|
213
|
+
const params = {};
|
|
214
|
+
if (creative.landingUrl) {
|
|
215
|
+
params.creative = JSON.stringify({
|
|
216
|
+
object_story_spec: {
|
|
217
|
+
link_data: {
|
|
218
|
+
link: creative.landingUrl,
|
|
219
|
+
message: creative.descriptions?.[0] ?? '',
|
|
220
|
+
name: creative.headlines?.[0] ?? '',
|
|
221
|
+
call_to_action: creative.callToAction
|
|
222
|
+
? { type: creative.callToAction }
|
|
223
|
+
: undefined,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (Object.keys(params).length === 0)
|
|
229
|
+
return;
|
|
230
|
+
const { status, body } = await metaPost(`/${adId}`, this.config.accessToken, params);
|
|
231
|
+
if (status !== 200) {
|
|
232
|
+
this.throwApiError(status, body);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async getSpend(dateRange) {
|
|
236
|
+
await this.rateLimiter.acquire();
|
|
237
|
+
const { status, body } = await metaGet(`/act_${this.config.adAccountId}/insights`, this.config.accessToken, {
|
|
238
|
+
fields: 'campaign_id,spend,impressions,clicks,conversions',
|
|
239
|
+
time_range: JSON.stringify({ since: dateRange.start, until: dateRange.end }),
|
|
240
|
+
level: 'campaign',
|
|
241
|
+
});
|
|
242
|
+
if (status !== 200) {
|
|
243
|
+
this.throwApiError(status, body);
|
|
244
|
+
}
|
|
245
|
+
const parsed = safeParseJson(body);
|
|
246
|
+
const dataArray = parsed.data ?? [];
|
|
247
|
+
const campaigns = dataArray.map(r => ({
|
|
248
|
+
externalId: r.campaign_id,
|
|
249
|
+
spend: toCents(parseFloat(r.spend)),
|
|
250
|
+
impressions: parseInt(r.impressions),
|
|
251
|
+
clicks: parseInt(r.clicks),
|
|
252
|
+
conversions: parseInt(r.conversions || '0'),
|
|
253
|
+
}));
|
|
254
|
+
const totalSpend = campaigns.reduce((sum, c) => (sum + c.spend), 0);
|
|
255
|
+
return { platform: 'meta', dateRange, totalSpend, campaigns };
|
|
256
|
+
}
|
|
257
|
+
async getPerformance(campaignId) {
|
|
258
|
+
await this.rateLimiter.acquire();
|
|
259
|
+
const { status, body } = await metaGet(`/${campaignId}/insights`, this.config.accessToken, { fields: 'impressions,clicks,conversions,spend,ctr,cpc' });
|
|
260
|
+
if (status !== 200) {
|
|
261
|
+
this.throwApiError(status, body);
|
|
262
|
+
}
|
|
263
|
+
const parsed = safeParseJson(body);
|
|
264
|
+
const dataArray = parsed.data ?? [];
|
|
265
|
+
const d = dataArray[0] ?? {};
|
|
266
|
+
const spend = toCents(parseFloat(d.spend ?? '0'));
|
|
267
|
+
const conversions = parseInt(d.conversions || '0');
|
|
268
|
+
return {
|
|
269
|
+
campaignId,
|
|
270
|
+
impressions: parseInt(d.impressions ?? '0'),
|
|
271
|
+
clicks: parseInt(d.clicks ?? '0'),
|
|
272
|
+
conversions,
|
|
273
|
+
spend,
|
|
274
|
+
ctr: parseFloat(d.ctr ?? '0'),
|
|
275
|
+
cpc: toCents(parseFloat(d.cpc ?? '0')),
|
|
276
|
+
roas: (spend > 0 ? 0 : 0), // Revenue from Stripe, not ad platform
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
async getInsights(campaignId, metrics) {
|
|
280
|
+
await this.rateLimiter.acquire();
|
|
281
|
+
const { status, body } = await metaGet(`/${campaignId}/insights`, this.config.accessToken, { fields: metrics.join(',') });
|
|
282
|
+
if (status !== 200) {
|
|
283
|
+
this.throwApiError(status, body);
|
|
284
|
+
}
|
|
285
|
+
const parsed = safeParseJson(body);
|
|
286
|
+
const dataArray = parsed.data ?? [];
|
|
287
|
+
const row = dataArray[0] ?? {};
|
|
288
|
+
const result = {};
|
|
289
|
+
for (const metric of metrics) {
|
|
290
|
+
result[metric] = parseFloat(row[metric] ?? '0');
|
|
291
|
+
}
|
|
292
|
+
return { campaignId, metrics: result };
|
|
293
|
+
}
|
|
294
|
+
// ── Private helpers ─────────────────────────────────
|
|
295
|
+
throwApiError(status, body) {
|
|
296
|
+
const parsed = safeParseJson(body);
|
|
297
|
+
const errObj = parsed.error;
|
|
298
|
+
const errMsg = errObj?.message ?? `HTTP ${status}`;
|
|
299
|
+
const errCode = errObj?.code;
|
|
300
|
+
if (status === 429 || errCode === 32 || errCode === 4) {
|
|
301
|
+
throw makePlatformError('RATE_LIMITED', status, errMsg, true, 60);
|
|
302
|
+
}
|
|
303
|
+
if (status === 401 || status === 403 || errCode === 190) {
|
|
304
|
+
throw makePlatformError('AUTH_EXPIRED', status, errMsg);
|
|
305
|
+
}
|
|
306
|
+
if (errCode === 2635005 || errMsg.toLowerCase().includes('budget')) {
|
|
307
|
+
throw makePlatformError('BUDGET_EXCEEDED', status, errMsg);
|
|
308
|
+
}
|
|
309
|
+
throw makePlatformError('UNKNOWN', status, errMsg);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox Campaign Adapter — full implementation for development/demo.
|
|
3
|
+
*
|
|
4
|
+
* Returns realistic fake data matching the AdPlatformAdapter interface.
|
|
5
|
+
* Every method returns valid-shaped data. No throws (except invalid operations).
|
|
6
|
+
* This IS a full implementation for a sandbox provider (No Stubs Doctrine).
|
|
7
|
+
*
|
|
8
|
+
* Simulates campaign lifecycle: create → pending_review → (approveCampaign) →
|
|
9
|
+
* active → paused → resumed → completed. Realistic fake metrics: CTR 1.2-3.8%,
|
|
10
|
+
* CPC $0.45-$2.10, ROAS 1.5-4.2x. Idempotency key tracking prevents duplicate creates.
|
|
11
|
+
*
|
|
12
|
+
* PRD Reference: §9.5, §9.19.10, §9.20.4
|
|
13
|
+
*/
|
|
14
|
+
import type { AdPlatformAdapter, CampaignConfig, CampaignResult, CampaignUpdate, CreativeConfig, SpendReport, PerformanceMetrics, InsightData, OAuthTokens, AdPlatform, Cents } from './base.js';
|
|
15
|
+
type SandboxCampaignStatus = 'pending_review' | 'active' | 'paused' | 'completed' | 'deleted';
|
|
16
|
+
export declare class SandboxCampaignAdapter implements AdPlatformAdapter {
|
|
17
|
+
private readonly sandboxPlatform;
|
|
18
|
+
private campaigns;
|
|
19
|
+
private idempotencyKeys;
|
|
20
|
+
constructor(platform?: AdPlatform);
|
|
21
|
+
refreshToken(token: OAuthTokens): Promise<OAuthTokens>;
|
|
22
|
+
createCampaign(config: CampaignConfig): Promise<CampaignResult>;
|
|
23
|
+
updateCampaign(id: string, changes: CampaignUpdate): Promise<void>;
|
|
24
|
+
pauseCampaign(id: string): Promise<void>;
|
|
25
|
+
resumeCampaign(id: string): Promise<void>;
|
|
26
|
+
deleteCampaign(id: string): Promise<void>;
|
|
27
|
+
updateBudget(id: string, dailyBudget: Cents): Promise<void>;
|
|
28
|
+
updateCreative(id: string, creative: CreativeConfig): Promise<void>;
|
|
29
|
+
getSpend(dateRange: {
|
|
30
|
+
start: string;
|
|
31
|
+
end: string;
|
|
32
|
+
}): Promise<SpendReport>;
|
|
33
|
+
getPerformance(campaignId: string): Promise<PerformanceMetrics>;
|
|
34
|
+
getInsights(campaignId: string, metrics: string[]): Promise<InsightData>;
|
|
35
|
+
/** Manually advance campaign from pending_review to active (simulates platform approval). */
|
|
36
|
+
approveCampaign(id: string): void;
|
|
37
|
+
/** Get the current status of a sandbox campaign (for testing/integration). */
|
|
38
|
+
getCampaignStatus(id: string): SandboxCampaignStatus;
|
|
39
|
+
/** Get the count of tracked campaigns (for testing). */
|
|
40
|
+
getCampaignCount(): number;
|
|
41
|
+
private requireCampaign;
|
|
42
|
+
private requireNotDeleted;
|
|
43
|
+
private makePlatformError;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox Campaign Adapter — full implementation for development/demo.
|
|
3
|
+
*
|
|
4
|
+
* Returns realistic fake data matching the AdPlatformAdapter interface.
|
|
5
|
+
* Every method returns valid-shaped data. No throws (except invalid operations).
|
|
6
|
+
* This IS a full implementation for a sandbox provider (No Stubs Doctrine).
|
|
7
|
+
*
|
|
8
|
+
* Simulates campaign lifecycle: create → pending_review → (approveCampaign) →
|
|
9
|
+
* active → paused → resumed → completed. Realistic fake metrics: CTR 1.2-3.8%,
|
|
10
|
+
* CPC $0.45-$2.10, ROAS 1.5-4.2x. Idempotency key tracking prevents duplicate creates.
|
|
11
|
+
*
|
|
12
|
+
* PRD Reference: §9.5, §9.19.10, §9.20.4
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
// ── Deterministic Fake Metrics ──────────────────────
|
|
16
|
+
function fakeCtr() {
|
|
17
|
+
// CTR 1.2% - 3.8%
|
|
18
|
+
return (1.2 + Math.random() * 2.6);
|
|
19
|
+
}
|
|
20
|
+
function fakeRoas() {
|
|
21
|
+
// ROAS 1.5x - 4.2x
|
|
22
|
+
return (1.5 + Math.random() * 2.7);
|
|
23
|
+
}
|
|
24
|
+
function fakeDailySpend(dailyBudget) {
|
|
25
|
+
// Spend 60-95% of daily budget
|
|
26
|
+
const factor = 0.6 + Math.random() * 0.35;
|
|
27
|
+
return Math.round(dailyBudget * factor);
|
|
28
|
+
}
|
|
29
|
+
// ── Sandbox Adapter Implementation ──────────────────
|
|
30
|
+
export class SandboxCampaignAdapter {
|
|
31
|
+
sandboxPlatform;
|
|
32
|
+
campaigns = new Map();
|
|
33
|
+
idempotencyKeys = new Map(); // key → externalId
|
|
34
|
+
constructor(platform = 'meta') {
|
|
35
|
+
this.sandboxPlatform = platform;
|
|
36
|
+
}
|
|
37
|
+
async refreshToken(token) {
|
|
38
|
+
// Sandbox tokens never expire — return same token with extended expiry
|
|
39
|
+
return {
|
|
40
|
+
...token,
|
|
41
|
+
expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async createCampaign(config) {
|
|
45
|
+
// Idempotency: return existing result for duplicate key
|
|
46
|
+
const existing = this.idempotencyKeys.get(config.idempotencyKey);
|
|
47
|
+
if (existing) {
|
|
48
|
+
const campaign = this.campaigns.get(existing);
|
|
49
|
+
if (campaign) {
|
|
50
|
+
return {
|
|
51
|
+
externalId: campaign.externalId,
|
|
52
|
+
platform: config.platform,
|
|
53
|
+
status: campaign.status === 'pending_review' ? 'pending_review' : 'created',
|
|
54
|
+
dashboardUrl: `https://sandbox.ads.example.com/campaigns/${campaign.externalId}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const externalId = `sandbox_campaign_${randomUUID().slice(0, 12)}`;
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
const state = {
|
|
61
|
+
externalId,
|
|
62
|
+
name: config.name,
|
|
63
|
+
platform: config.platform,
|
|
64
|
+
objective: config.objective,
|
|
65
|
+
status: 'pending_review',
|
|
66
|
+
dailyBudget: config.dailyBudget,
|
|
67
|
+
targeting: { ...config.targeting },
|
|
68
|
+
creative: {
|
|
69
|
+
headlines: config.creative.headlines,
|
|
70
|
+
descriptions: config.creative.descriptions,
|
|
71
|
+
callToAction: config.creative.callToAction,
|
|
72
|
+
landingUrl: config.creative.landingUrl,
|
|
73
|
+
imageUrls: config.creative.imageUrls,
|
|
74
|
+
},
|
|
75
|
+
schedule: config.schedule,
|
|
76
|
+
createdAt: now,
|
|
77
|
+
totalSpendCents: 0,
|
|
78
|
+
impressions: 0,
|
|
79
|
+
clicks: 0,
|
|
80
|
+
conversions: 0,
|
|
81
|
+
pollCount: 0,
|
|
82
|
+
};
|
|
83
|
+
this.campaigns.set(externalId, state);
|
|
84
|
+
this.idempotencyKeys.set(config.idempotencyKey, externalId);
|
|
85
|
+
return {
|
|
86
|
+
externalId,
|
|
87
|
+
platform: config.platform,
|
|
88
|
+
status: 'pending_review',
|
|
89
|
+
dashboardUrl: `https://sandbox.ads.example.com/campaigns/${externalId}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async updateCampaign(id, changes) {
|
|
93
|
+
const campaign = this.requireCampaign(id);
|
|
94
|
+
this.requireNotDeleted(campaign);
|
|
95
|
+
if (changes.name !== undefined)
|
|
96
|
+
campaign.name = changes.name;
|
|
97
|
+
if (changes.dailyBudget !== undefined)
|
|
98
|
+
campaign.dailyBudget = changes.dailyBudget;
|
|
99
|
+
if (changes.targeting !== undefined) {
|
|
100
|
+
campaign.targeting = { ...campaign.targeting, ...changes.targeting };
|
|
101
|
+
}
|
|
102
|
+
if (changes.schedule !== undefined)
|
|
103
|
+
campaign.schedule = changes.schedule;
|
|
104
|
+
}
|
|
105
|
+
async pauseCampaign(id) {
|
|
106
|
+
const campaign = this.requireCampaign(id);
|
|
107
|
+
if (campaign.status !== 'active') {
|
|
108
|
+
throw this.makePlatformError(id, `Cannot pause campaign in status: ${campaign.status}`, campaign.status === 'deleted' ? 'UNKNOWN' : 'UNKNOWN', campaign.status === 'deleted' ? 410 : 400);
|
|
109
|
+
}
|
|
110
|
+
campaign.status = 'paused';
|
|
111
|
+
}
|
|
112
|
+
async resumeCampaign(id) {
|
|
113
|
+
const campaign = this.requireCampaign(id);
|
|
114
|
+
if (campaign.status !== 'paused') {
|
|
115
|
+
throw this.makePlatformError(id, `Cannot resume campaign in status: ${campaign.status}`, 'UNKNOWN', campaign.status === 'deleted' ? 410 : 400);
|
|
116
|
+
}
|
|
117
|
+
campaign.status = 'active';
|
|
118
|
+
}
|
|
119
|
+
async deleteCampaign(id) {
|
|
120
|
+
const campaign = this.requireCampaign(id);
|
|
121
|
+
campaign.status = 'deleted';
|
|
122
|
+
}
|
|
123
|
+
async updateBudget(id, dailyBudget) {
|
|
124
|
+
const campaign = this.requireCampaign(id);
|
|
125
|
+
this.requireNotDeleted(campaign);
|
|
126
|
+
campaign.dailyBudget = dailyBudget;
|
|
127
|
+
}
|
|
128
|
+
async updateCreative(id, creative) {
|
|
129
|
+
const campaign = this.requireCampaign(id);
|
|
130
|
+
this.requireNotDeleted(campaign);
|
|
131
|
+
if (creative.headlines !== undefined)
|
|
132
|
+
campaign.creative.headlines = creative.headlines;
|
|
133
|
+
if (creative.descriptions !== undefined)
|
|
134
|
+
campaign.creative.descriptions = creative.descriptions;
|
|
135
|
+
if (creative.callToAction !== undefined)
|
|
136
|
+
campaign.creative.callToAction = creative.callToAction;
|
|
137
|
+
if (creative.landingUrl !== undefined)
|
|
138
|
+
campaign.creative.landingUrl = creative.landingUrl;
|
|
139
|
+
if (creative.imageUrls !== undefined)
|
|
140
|
+
campaign.creative.imageUrls = creative.imageUrls;
|
|
141
|
+
}
|
|
142
|
+
async getSpend(dateRange) {
|
|
143
|
+
const campaignSpends = Array.from(this.campaigns.values())
|
|
144
|
+
.filter(c => c.status === 'active' || c.status === 'paused' || c.status === 'completed')
|
|
145
|
+
.map(c => ({
|
|
146
|
+
externalId: c.externalId,
|
|
147
|
+
spend: c.totalSpendCents,
|
|
148
|
+
impressions: c.impressions,
|
|
149
|
+
clicks: c.clicks,
|
|
150
|
+
conversions: c.conversions,
|
|
151
|
+
}));
|
|
152
|
+
const totalSpend = campaignSpends.reduce((sum, c) => (sum + c.spend), 0);
|
|
153
|
+
return {
|
|
154
|
+
platform: this.sandboxPlatform,
|
|
155
|
+
dateRange,
|
|
156
|
+
totalSpend,
|
|
157
|
+
campaigns: campaignSpends,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async getPerformance(campaignId) {
|
|
161
|
+
const campaign = this.requireCampaign(campaignId);
|
|
162
|
+
this.requireNotDeleted(campaign);
|
|
163
|
+
// Advance metrics on each poll for active campaigns
|
|
164
|
+
if (campaign.status === 'active') {
|
|
165
|
+
campaign.pollCount += 1;
|
|
166
|
+
const daySpend = fakeDailySpend(campaign.dailyBudget);
|
|
167
|
+
campaign.totalSpendCents = (campaign.totalSpendCents + daySpend);
|
|
168
|
+
const newImpressions = Math.floor(1000 + Math.random() * 9000);
|
|
169
|
+
campaign.impressions += newImpressions;
|
|
170
|
+
campaign.clicks += Math.floor(newImpressions * (fakeCtr() / 100));
|
|
171
|
+
campaign.conversions += Math.floor(campaign.clicks * (0.02 + Math.random() * 0.08));
|
|
172
|
+
}
|
|
173
|
+
const ctr = campaign.impressions > 0
|
|
174
|
+
? (campaign.clicks / campaign.impressions * 100)
|
|
175
|
+
: 0;
|
|
176
|
+
const cpc = campaign.clicks > 0
|
|
177
|
+
? Math.round(campaign.totalSpendCents / campaign.clicks)
|
|
178
|
+
: 0;
|
|
179
|
+
return {
|
|
180
|
+
campaignId,
|
|
181
|
+
impressions: campaign.impressions,
|
|
182
|
+
clicks: campaign.clicks,
|
|
183
|
+
conversions: campaign.conversions,
|
|
184
|
+
spend: campaign.totalSpendCents,
|
|
185
|
+
ctr,
|
|
186
|
+
cpc,
|
|
187
|
+
roas: fakeRoas(),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async getInsights(campaignId, metrics) {
|
|
191
|
+
const campaign = this.requireCampaign(campaignId);
|
|
192
|
+
this.requireNotDeleted(campaign);
|
|
193
|
+
const result = {};
|
|
194
|
+
for (const metric of metrics) {
|
|
195
|
+
switch (metric) {
|
|
196
|
+
case 'impressions':
|
|
197
|
+
result[metric] = campaign.impressions;
|
|
198
|
+
break;
|
|
199
|
+
case 'clicks':
|
|
200
|
+
result[metric] = campaign.clicks;
|
|
201
|
+
break;
|
|
202
|
+
case 'conversions':
|
|
203
|
+
result[metric] = campaign.conversions;
|
|
204
|
+
break;
|
|
205
|
+
case 'spend':
|
|
206
|
+
result[metric] = campaign.totalSpendCents;
|
|
207
|
+
break;
|
|
208
|
+
case 'ctr':
|
|
209
|
+
result[metric] = campaign.impressions > 0
|
|
210
|
+
? campaign.clicks / campaign.impressions * 100 : 0;
|
|
211
|
+
break;
|
|
212
|
+
default: result[metric] = 0;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
campaignId,
|
|
217
|
+
metrics: result,
|
|
218
|
+
recommendations: campaign.status === 'active'
|
|
219
|
+
? ['Consider increasing budget — ROAS is healthy']
|
|
220
|
+
: undefined,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// ── Sandbox-Specific Methods ────────────────────────
|
|
224
|
+
/** Manually advance campaign from pending_review to active (simulates platform approval). */
|
|
225
|
+
approveCampaign(id) {
|
|
226
|
+
const campaign = this.requireCampaign(id);
|
|
227
|
+
if (campaign.status === 'pending_review') {
|
|
228
|
+
campaign.status = 'active';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** Get the current status of a sandbox campaign (for testing/integration). */
|
|
232
|
+
getCampaignStatus(id) {
|
|
233
|
+
return this.requireCampaign(id).status;
|
|
234
|
+
}
|
|
235
|
+
/** Get the count of tracked campaigns (for testing). */
|
|
236
|
+
getCampaignCount() {
|
|
237
|
+
return this.campaigns.size;
|
|
238
|
+
}
|
|
239
|
+
// ── Private Helpers ─────────────────────────────────
|
|
240
|
+
requireCampaign(id) {
|
|
241
|
+
const campaign = this.campaigns.get(id);
|
|
242
|
+
if (!campaign) {
|
|
243
|
+
throw this.makePlatformError(id, 'Campaign not found', 'UNKNOWN', 404);
|
|
244
|
+
}
|
|
245
|
+
return campaign;
|
|
246
|
+
}
|
|
247
|
+
requireNotDeleted(campaign) {
|
|
248
|
+
if (campaign.status === 'deleted') {
|
|
249
|
+
throw this.makePlatformError(campaign.externalId, 'Campaign is deleted', 'UNKNOWN', 410);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
makePlatformError(campaignId, message, code = 'UNKNOWN', originalCode = 400) {
|
|
253
|
+
return {
|
|
254
|
+
platform: this.sandboxPlatform,
|
|
255
|
+
code,
|
|
256
|
+
originalCode,
|
|
257
|
+
message: `Sandbox: ${message} (campaign: ${campaignId})`,
|
|
258
|
+
retryable: false,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TikTok Marketing Campaign Adapter — real implementation via node:https (zero new dependencies).
|
|
3
|
+
*
|
|
4
|
+
* Implements AdPlatformAdapter for TikTok Marketing API v1.3.
|
|
5
|
+
*
|
|
6
|
+
* TikTok Marketing API v1.3:
|
|
7
|
+
* Base URL: https://business-api.tiktok.com/open_api/v1.3
|
|
8
|
+
* Auth: Access-Token header
|
|
9
|
+
* Campaign CRUD via campaign/create, campaign/update
|
|
10
|
+
* Reporting via report/integrated/get
|
|
11
|
+
* Rate limit: 10 calls/sec
|
|
12
|
+
*
|
|
13
|
+
* PRD Reference: §9.5, §9.19.10, §9.20.4
|
|
14
|
+
* No Stubs Doctrine: every method makes a real API call or returns documented empty.
|
|
15
|
+
*/
|
|
16
|
+
import type { AdPlatformAdapter, CampaignConfig, CampaignResult, CampaignUpdate, CreativeConfig, SpendReport, PerformanceMetrics, InsightData, OAuthTokens, Cents } from './base.js';
|
|
17
|
+
interface TikTokCampaignConfig {
|
|
18
|
+
appId: string;
|
|
19
|
+
accessToken: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class TikTokCampaignAdapter implements AdPlatformAdapter {
|
|
22
|
+
private readonly config;
|
|
23
|
+
private readonly rateLimiter;
|
|
24
|
+
constructor(config: TikTokCampaignConfig);
|
|
25
|
+
refreshToken(token: OAuthTokens): Promise<OAuthTokens>;
|
|
26
|
+
createCampaign(config: CampaignConfig): Promise<CampaignResult>;
|
|
27
|
+
updateCampaign(id: string, changes: CampaignUpdate): Promise<void>;
|
|
28
|
+
pauseCampaign(id: string): Promise<void>;
|
|
29
|
+
resumeCampaign(id: string): Promise<void>;
|
|
30
|
+
deleteCampaign(id: string): Promise<void>;
|
|
31
|
+
updateBudget(id: string, dailyBudget: Cents): Promise<void>;
|
|
32
|
+
updateCreative(id: string, creative: CreativeConfig): Promise<void>;
|
|
33
|
+
getSpend(dateRange: {
|
|
34
|
+
start: string;
|
|
35
|
+
end: string;
|
|
36
|
+
}): Promise<SpendReport>;
|
|
37
|
+
getPerformance(campaignId: string): Promise<PerformanceMetrics>;
|
|
38
|
+
getInsights(campaignId: string, metrics: string[]): Promise<InsightData>;
|
|
39
|
+
}
|
|
40
|
+
export {};
|