@littlebearapps/platform-admin-sdk 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import type { ScheduledEvent, ExecutionContext } from '@cloudflare/workers-types';
|
|
3
|
+
import stripeConnector from '../../workers/stripe-connector';
|
|
4
|
+
import adsConnector from '../../workers/ads-connector';
|
|
5
|
+
import plausibleConnector from '../../workers/plausible-connector';
|
|
6
|
+
import ga4Connector from '../../workers/ga4-connector';
|
|
7
|
+
import { MockD1Database, MockKVNamespace, MockQueue } from '../helpers/mock-storage';
|
|
8
|
+
|
|
9
|
+
// The Worker runtime provides these types; in tests we use simple placeholders
|
|
10
|
+
const scheduledEvent = {} as ScheduledEvent;
|
|
11
|
+
|
|
12
|
+
// Mock ExecutionContext for SDK integration
|
|
13
|
+
const mockCtx = {
|
|
14
|
+
waitUntil: () => {},
|
|
15
|
+
passThroughOnException: () => {},
|
|
16
|
+
} as unknown as ExecutionContext;
|
|
17
|
+
|
|
18
|
+
describe('Business intelligence connectors', () => {
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Stripe connector', () => {
|
|
24
|
+
let env: {
|
|
25
|
+
PLATFORM_DB: MockD1Database;
|
|
26
|
+
PLATFORM_CACHE: MockKVNamespace;
|
|
27
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
28
|
+
STRIPE_API_KEY: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
env = {
|
|
33
|
+
PLATFORM_DB: new MockD1Database(),
|
|
34
|
+
PLATFORM_CACHE: new MockKVNamespace(),
|
|
35
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
36
|
+
STRIPE_API_KEY: 'sk_test',
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('stores revenue metrics and raises alerts on anomalies', async () => {
|
|
41
|
+
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
42
|
+
async () =>
|
|
43
|
+
new Response(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
data: [
|
|
46
|
+
{
|
|
47
|
+
status: 'active',
|
|
48
|
+
items: {
|
|
49
|
+
data: [{ price: { unit_amount: 2500, recurring: { interval: 'month' } } }],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
status: 'trialing',
|
|
54
|
+
items: {
|
|
55
|
+
data: [{ price: { unit_amount: 12000, recurring: { interval: 'year' } } }],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
status: 'canceled',
|
|
60
|
+
items: {
|
|
61
|
+
data: [{ price: { unit_amount: 1000, recurring: { interval: 'month' } } }],
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
fetchMock.mockImplementationOnce(
|
|
69
|
+
async () =>
|
|
70
|
+
new Response(
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
data: [{ status: 'paid' }, { status: 'open' }, { status: 'uncollectible' }],
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
env.PLATFORM_DB.queueFirstResult({ value: 200 }); // previous MRR
|
|
78
|
+
env.PLATFORM_DB.queueFirstResult({ value: 0.01 }); // previous churn rate
|
|
79
|
+
env.PLATFORM_DB.queueFirstResult({ value: 0.01 }); // previous failed payment rate
|
|
80
|
+
|
|
81
|
+
await stripeConnector.scheduled(scheduledEvent, env as any, mockCtx);
|
|
82
|
+
|
|
83
|
+
const metricStatements = env.PLATFORM_DB.statements.filter((stmt) =>
|
|
84
|
+
stmt.sql.includes('INSERT INTO revenue_metrics')
|
|
85
|
+
);
|
|
86
|
+
expect(metricStatements.length).toBeGreaterThanOrEqual(4);
|
|
87
|
+
|
|
88
|
+
const alertStatement = env.PLATFORM_DB.statements.find((stmt) =>
|
|
89
|
+
stmt.sql.includes('INSERT INTO alerts')
|
|
90
|
+
);
|
|
91
|
+
expect(alertStatement).toBeTruthy();
|
|
92
|
+
expect(env.PLATFORM_CACHE.store.has('stripe:mrr')).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Google Ads connector', () => {
|
|
97
|
+
let env: {
|
|
98
|
+
PLATFORM_DB: MockD1Database;
|
|
99
|
+
PLATFORM_CACHE: MockKVNamespace;
|
|
100
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
101
|
+
GOOGLE_ADS_DEVELOPER_TOKEN: string;
|
|
102
|
+
GOOGLE_ADS_CLIENT_ID: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
env = {
|
|
107
|
+
PLATFORM_DB: new MockD1Database(),
|
|
108
|
+
PLATFORM_CACHE: new MockKVNamespace(),
|
|
109
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
110
|
+
GOOGLE_ADS_DEVELOPER_TOKEN: 'dev-token',
|
|
111
|
+
GOOGLE_ADS_CLIENT_ID: 'client-id',
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('calculates CAC and ROAS metrics with anomaly detection', async () => {
|
|
116
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
117
|
+
new Response(
|
|
118
|
+
JSON.stringify([
|
|
119
|
+
{
|
|
120
|
+
results: [
|
|
121
|
+
{
|
|
122
|
+
metrics: {
|
|
123
|
+
costMicros: 5_000_000_000,
|
|
124
|
+
conversions: 100,
|
|
125
|
+
installs: 200,
|
|
126
|
+
conversionsValue: 12_000,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
])
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
env.PLATFORM_DB.queueFirstResult({ value: 20 }); // previous CAC
|
|
136
|
+
env.PLATFORM_DB.queueFirstResult({ value: 5 }); // previous ROAS
|
|
137
|
+
|
|
138
|
+
await adsConnector.scheduled(scheduledEvent, env as any, mockCtx);
|
|
139
|
+
|
|
140
|
+
const cacheKeys = Array.from(env.PLATFORM_CACHE.store.keys());
|
|
141
|
+
expect(cacheKeys).toContain('ads:cac');
|
|
142
|
+
|
|
143
|
+
const alert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
144
|
+
stmt.sql.includes('INSERT INTO alerts')
|
|
145
|
+
);
|
|
146
|
+
expect(alert).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('Plausible connector', () => {
|
|
151
|
+
let env: {
|
|
152
|
+
PLATFORM_DB: MockD1Database;
|
|
153
|
+
PLATFORM_CACHE: MockKVNamespace;
|
|
154
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
155
|
+
PLAUSIBLE_API_KEY: string;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
env = {
|
|
160
|
+
PLATFORM_DB: new MockD1Database(),
|
|
161
|
+
PLATFORM_CACHE: new MockKVNamespace(),
|
|
162
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
163
|
+
PLAUSIBLE_API_KEY: 'plausible-key',
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('stores usage metrics and raises alerts when usage drops', async () => {
|
|
168
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
169
|
+
new Response(
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
results: {
|
|
172
|
+
visitors: { value: 300, returning: 90 },
|
|
173
|
+
events: {
|
|
174
|
+
'feature:pdf_support': 30,
|
|
175
|
+
'feature:other': 70,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
env.PLATFORM_DB.queueFirstResult({ value: 600 });
|
|
183
|
+
|
|
184
|
+
await plausibleConnector.scheduled(scheduledEvent, env as any, mockCtx);
|
|
185
|
+
|
|
186
|
+
expect(env.PLATFORM_CACHE.store.has('plausible:active_users')).toBe(true);
|
|
187
|
+
const alert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
188
|
+
stmt.sql.includes('INSERT INTO alerts')
|
|
189
|
+
);
|
|
190
|
+
expect(alert).toBeTruthy();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('GA4 connector', () => {
|
|
195
|
+
let env: {
|
|
196
|
+
PLATFORM_DB: MockD1Database;
|
|
197
|
+
PLATFORM_CACHE: MockKVNamespace;
|
|
198
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
199
|
+
GA4_PROPERTY_ID: string;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
beforeEach(() => {
|
|
203
|
+
env = {
|
|
204
|
+
PLATFORM_DB: new MockD1Database(),
|
|
205
|
+
PLATFORM_CACHE: new MockKVNamespace(),
|
|
206
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
207
|
+
GA4_PROPERTY_ID: 'properties/1234',
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('tracks install and review sentiment metrics with alerts on regression', async () => {
|
|
212
|
+
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
213
|
+
new Response(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
rows: [
|
|
216
|
+
{
|
|
217
|
+
metricValues: [
|
|
218
|
+
{ value: '1000' },
|
|
219
|
+
{ value: '150' },
|
|
220
|
+
{ value: '80' },
|
|
221
|
+
{ value: '100' },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
env.PLATFORM_DB.queueFirstResult({ value: 0.25 }); // previous install rate
|
|
230
|
+
env.PLATFORM_DB.queueFirstResult({ value: 0.9 }); // previous positive review rate
|
|
231
|
+
|
|
232
|
+
await ga4Connector.scheduled(scheduledEvent, env as any, mockCtx);
|
|
233
|
+
|
|
234
|
+
expect(env.PLATFORM_CACHE.store.has('ga4:install_rate')).toBe(true);
|
|
235
|
+
const alert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
236
|
+
stmt.sql.includes('INSERT INTO alerts')
|
|
237
|
+
);
|
|
238
|
+
expect(alert).toBeTruthy();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import githubMonitor from '../../workers/github-monitor';
|
|
3
|
+
import { createGitHubMonitorEnv } from '../helpers/mock-storage';
|
|
4
|
+
import type {
|
|
5
|
+
MockD1Database,
|
|
6
|
+
MockKVNamespace,
|
|
7
|
+
MockR2Bucket,
|
|
8
|
+
MockQueue,
|
|
9
|
+
} from '../helpers/mock-storage';
|
|
10
|
+
|
|
11
|
+
// Mock ExecutionContext for SDK integration
|
|
12
|
+
const mockCtx = {
|
|
13
|
+
waitUntil: () => {},
|
|
14
|
+
passThroughOnException: () => {},
|
|
15
|
+
} as unknown as ExecutionContext;
|
|
16
|
+
|
|
17
|
+
describe('GitHub Monitor Worker', () => {
|
|
18
|
+
let env: {
|
|
19
|
+
PLATFORM_DB: MockD1Database;
|
|
20
|
+
PLATFORM_CACHE: MockKVNamespace;
|
|
21
|
+
PLATFORM_ARCHIVES: MockR2Bucket;
|
|
22
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
23
|
+
GITHUB_WEBHOOK_SECRET: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
env = createGitHubMonitorEnv();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects invalid signatures', async () => {
|
|
31
|
+
const body = JSON.stringify({ action: 'opened' });
|
|
32
|
+
const request = new Request('https://example.com/webhook', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
body,
|
|
35
|
+
headers: {
|
|
36
|
+
'x-hub-signature-256': 'sha256=invalid',
|
|
37
|
+
'x-github-event': 'issues',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const response = await githubMonitor.fetch(request, env as any, mockCtx);
|
|
42
|
+
expect(response.status).toBe(401);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('stores CloakPipe robot issues', async () => {
|
|
46
|
+
const payload = {
|
|
47
|
+
action: 'opened',
|
|
48
|
+
repository: { full_name: 'littlebearapps/cloakpipe' },
|
|
49
|
+
issue: {
|
|
50
|
+
number: 42,
|
|
51
|
+
title: 'Robot detected anomaly',
|
|
52
|
+
labels: [{ name: 'robot' }],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const response = await invokeWorker(payload, env, 'issues');
|
|
57
|
+
|
|
58
|
+
expect(response.status).toBe(200);
|
|
59
|
+
const insertStatement = env.PLATFORM_DB.statements.find((stmt) =>
|
|
60
|
+
stmt.sql.includes('INSERT INTO github_events')
|
|
61
|
+
);
|
|
62
|
+
expect(insertStatement).toBeTruthy();
|
|
63
|
+
expect(env.PLATFORM_ARCHIVES.objects.size).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('records HomeoStat bot pull requests', async () => {
|
|
67
|
+
const payload = {
|
|
68
|
+
action: 'opened',
|
|
69
|
+
repository: { full_name: 'littlebearapps/homeostat' },
|
|
70
|
+
pull_request: {
|
|
71
|
+
number: 99,
|
|
72
|
+
merged: false,
|
|
73
|
+
user: { login: 'homeostat-bot' },
|
|
74
|
+
labels: [],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await invokeWorker(payload, env, 'pull_request');
|
|
79
|
+
|
|
80
|
+
const insertStatement = env.PLATFORM_DB.statements.find(
|
|
81
|
+
(stmt) => stmt.sql.includes('INSERT INTO github_events') && stmt.params.includes('pr')
|
|
82
|
+
);
|
|
83
|
+
expect(insertStatement).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('records GateKeeper workflow failures and raises alerts', async () => {
|
|
87
|
+
env.PLATFORM_DB.queueFirstResult({ count: 11 });
|
|
88
|
+
|
|
89
|
+
const payload = {
|
|
90
|
+
repository: { full_name: 'littlebearapps/gatekeeper' },
|
|
91
|
+
workflow_run: {
|
|
92
|
+
name: 'extension publish',
|
|
93
|
+
status: 'completed',
|
|
94
|
+
conclusion: 'failure',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
await invokeWorker(payload, env, 'workflow_run');
|
|
99
|
+
|
|
100
|
+
const alertInsert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
101
|
+
stmt.sql.includes('INSERT INTO alerts')
|
|
102
|
+
);
|
|
103
|
+
expect(alertInsert).toBeTruthy();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
async function invokeWorker(
|
|
108
|
+
payload: unknown,
|
|
109
|
+
env: ReturnType<typeof createGitHubMonitorEnv>,
|
|
110
|
+
event: string
|
|
111
|
+
) {
|
|
112
|
+
const body = JSON.stringify(payload);
|
|
113
|
+
const signature = await createSignature(body, env.GITHUB_WEBHOOK_SECRET);
|
|
114
|
+
|
|
115
|
+
const request = new Request('https://example.com/webhook', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body,
|
|
118
|
+
headers: {
|
|
119
|
+
'x-hub-signature-256': signature,
|
|
120
|
+
'x-github-event': event,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return githubMonitor.fetch(request, env as any, mockCtx);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function createSignature(body: string, secret: string): Promise<string> {
|
|
128
|
+
const encoder = new TextEncoder();
|
|
129
|
+
const key = await crypto.subtle.importKey(
|
|
130
|
+
'raw',
|
|
131
|
+
encoder.encode(secret),
|
|
132
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
133
|
+
false,
|
|
134
|
+
['sign']
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const buffer = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
|
|
138
|
+
const hex = Array.from(new Uint8Array(buffer))
|
|
139
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
140
|
+
.join('');
|
|
141
|
+
|
|
142
|
+
return `sha256=${hex}`;
|
|
143
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import ingestGitHub from '../../workers/ingest-github';
|
|
3
|
+
import ingestStripe from '../../workers/ingest-stripe';
|
|
4
|
+
import ingestLogger from '../../workers/ingest-logger';
|
|
5
|
+
import { MockD1Database, MockR2Bucket, MockQueue } from '../helpers/mock-storage';
|
|
6
|
+
|
|
7
|
+
declare const crypto: Crypto;
|
|
8
|
+
|
|
9
|
+
// Mock ExecutionContext for SDK integration
|
|
10
|
+
const mockCtx = {
|
|
11
|
+
waitUntil: () => {},
|
|
12
|
+
passThroughOnException: () => {},
|
|
13
|
+
} as unknown as ExecutionContext;
|
|
14
|
+
|
|
15
|
+
describe('Webhook ingestion workers', () => {
|
|
16
|
+
describe('GitHub ingestion', () => {
|
|
17
|
+
let env: {
|
|
18
|
+
PLATFORM_DB: MockD1Database;
|
|
19
|
+
PLATFORM_ARCHIVES: MockR2Bucket;
|
|
20
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
21
|
+
GITHUB_WEBHOOK_SECRET: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
env = {
|
|
26
|
+
PLATFORM_DB: new MockD1Database(),
|
|
27
|
+
PLATFORM_ARCHIVES: new MockR2Bucket(),
|
|
28
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
29
|
+
GITHUB_WEBHOOK_SECRET: 'github-secret',
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('rejects invalid signatures', async () => {
|
|
34
|
+
const request = new Request('https://example.com', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
body: JSON.stringify({}),
|
|
37
|
+
headers: {
|
|
38
|
+
'x-github-event': 'issues',
|
|
39
|
+
'x-hub-signature-256': 'sha256=invalid',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const response = await ingestGitHub.fetch(request, env as any, mockCtx);
|
|
44
|
+
expect(response.status).toBe(401);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('stores robot issues', async () => {
|
|
48
|
+
const payload = {
|
|
49
|
+
action: 'opened',
|
|
50
|
+
repository: { full_name: 'littlebearapps/cloakpipe' },
|
|
51
|
+
issue: {
|
|
52
|
+
number: 101,
|
|
53
|
+
title: 'Robot triggered error',
|
|
54
|
+
labels: [{ name: 'robot' }],
|
|
55
|
+
node_id: 'MDU6SXNzdWUx',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const body = JSON.stringify(payload);
|
|
60
|
+
const signature = await signGitHubPayload(body, env.GITHUB_WEBHOOK_SECRET);
|
|
61
|
+
const request = new Request('https://example.com', {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body,
|
|
64
|
+
headers: {
|
|
65
|
+
'x-github-event': 'issues',
|
|
66
|
+
'x-hub-signature-256': signature,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const response = await ingestGitHub.fetch(request, env as any, mockCtx);
|
|
71
|
+
expect(response.status).toBe(200);
|
|
72
|
+
const insert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
73
|
+
stmt.sql.includes('INSERT INTO github_events')
|
|
74
|
+
);
|
|
75
|
+
expect(insert).toBeTruthy();
|
|
76
|
+
expect(env.PLATFORM_ARCHIVES.objects.size).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('Stripe ingestion', () => {
|
|
81
|
+
let env: {
|
|
82
|
+
PLATFORM_DB: MockD1Database;
|
|
83
|
+
PLATFORM_ARCHIVES: MockR2Bucket;
|
|
84
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
85
|
+
STRIPE_WEBHOOK_SECRET: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
env = {
|
|
90
|
+
PLATFORM_DB: new MockD1Database(),
|
|
91
|
+
PLATFORM_ARCHIVES: new MockR2Bucket(),
|
|
92
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
93
|
+
STRIPE_WEBHOOK_SECRET: 'whsec_test',
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('validates signatures and stores events', async () => {
|
|
98
|
+
const event = {
|
|
99
|
+
id: 'evt_123',
|
|
100
|
+
type: 'invoice.paid',
|
|
101
|
+
data: { object: { amount_paid: 5500, status: 'paid' } },
|
|
102
|
+
};
|
|
103
|
+
const body = JSON.stringify(event);
|
|
104
|
+
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
105
|
+
const signature = await signStripePayload(timestamp, body, env.STRIPE_WEBHOOK_SECRET);
|
|
106
|
+
const request = new Request('https://example.com', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body,
|
|
109
|
+
headers: {
|
|
110
|
+
'stripe-signature': `t=${timestamp},v1=${signature}`,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const response = await ingestStripe.fetch(request, env as any, mockCtx);
|
|
115
|
+
expect(response.status).toBe(200);
|
|
116
|
+
const metricInsert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
117
|
+
stmt.sql.includes('INSERT INTO revenue_metrics')
|
|
118
|
+
);
|
|
119
|
+
expect(metricInsert).toBeTruthy();
|
|
120
|
+
expect(env.PLATFORM_ARCHIVES.objects.size).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('CloakPipe logger ingestion', () => {
|
|
125
|
+
let env: {
|
|
126
|
+
PLATFORM_DB: MockD1Database;
|
|
127
|
+
PLATFORM_ARCHIVES: MockR2Bucket;
|
|
128
|
+
PLATFORM_TELEMETRY: MockQueue;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
beforeEach(() => {
|
|
132
|
+
env = {
|
|
133
|
+
PLATFORM_DB: new MockD1Database(),
|
|
134
|
+
PLATFORM_ARCHIVES: new MockR2Bucket(),
|
|
135
|
+
PLATFORM_TELEMETRY: new MockQueue(),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('validates schemas and stores error reports', async () => {
|
|
140
|
+
const envelope = {
|
|
141
|
+
id: '01JBQH7YFQM9K3W8XTZP5VN2QR',
|
|
142
|
+
source: 'cloakpipe',
|
|
143
|
+
type: 'error_report',
|
|
144
|
+
timestamp: '2025-10-25T14:30:00.000Z',
|
|
145
|
+
data: {
|
|
146
|
+
extension_id: 'convert-my-file',
|
|
147
|
+
fingerprint: 'TypeError-getNativeInputProps-undefined',
|
|
148
|
+
message: 'Cannot read properties of undefined',
|
|
149
|
+
stack_trace: 'TypeError: Cannot read properties of undefined',
|
|
150
|
+
breadcrumbs: [
|
|
151
|
+
{
|
|
152
|
+
timestamp: '2025-10-25T14:29:58.000Z',
|
|
153
|
+
category: 'user',
|
|
154
|
+
message: 'Clicked upload button',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
user_agent: 'Mozilla/5.0...',
|
|
158
|
+
extension_version: '1.2.3',
|
|
159
|
+
},
|
|
160
|
+
metadata: {
|
|
161
|
+
schema_version: '1.0.0',
|
|
162
|
+
correlation_id: '01JBQH7YFQM9K3W8XTZP5VN2QS',
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const request = new Request('https://example.com', {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
body: JSON.stringify(envelope),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const response = await ingestLogger.fetch(request, env as any, mockCtx);
|
|
172
|
+
expect(response.status).toBe(200);
|
|
173
|
+
const insert = env.PLATFORM_DB.statements.find((stmt) =>
|
|
174
|
+
stmt.sql.includes('INSERT INTO error_reports')
|
|
175
|
+
);
|
|
176
|
+
expect(insert).toBeTruthy();
|
|
177
|
+
expect(env.PLATFORM_ARCHIVES.objects.size).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
async function signGitHubPayload(body: string, secret: string): Promise<string> {
|
|
183
|
+
const encoder = new TextEncoder();
|
|
184
|
+
const key = await crypto.subtle.importKey(
|
|
185
|
+
'raw',
|
|
186
|
+
encoder.encode(secret),
|
|
187
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
188
|
+
false,
|
|
189
|
+
['sign']
|
|
190
|
+
);
|
|
191
|
+
const buffer = await crypto.subtle.sign('HMAC', key, encoder.encode(body));
|
|
192
|
+
const hex = Array.from(new Uint8Array(buffer))
|
|
193
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
194
|
+
.join('');
|
|
195
|
+
return `sha256=${hex}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function signStripePayload(timestamp: string, body: string, secret: string): Promise<string> {
|
|
199
|
+
const encoder = new TextEncoder();
|
|
200
|
+
const key = await crypto.subtle.importKey(
|
|
201
|
+
'raw',
|
|
202
|
+
encoder.encode(secret),
|
|
203
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
204
|
+
false,
|
|
205
|
+
['sign']
|
|
206
|
+
);
|
|
207
|
+
const buffer = await crypto.subtle.sign('HMAC', key, encoder.encode(`${timestamp}.${body}`));
|
|
208
|
+
return Array.from(new Uint8Array(buffer))
|
|
209
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
210
|
+
.join('');
|
|
211
|
+
}
|