@stacksjs/ts-cloud 0.7.108 → 0.7.109

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.
Files changed (135) hide show
  1. package/dist/alerts/delivery.d.ts +10 -0
  2. package/dist/alerts/model.d.ts +1 -1
  3. package/dist/api/client.d.ts +53 -0
  4. package/dist/api/handler.d.ts +10 -0
  5. package/dist/api/service.d.ts +1 -0
  6. package/dist/aws/index.js +10 -10
  7. package/dist/aws/lambda.d.ts +19 -0
  8. package/dist/bin/cli.js +1677 -1369
  9. package/dist/bin/dashboard-server.js +927 -621
  10. package/dist/{chunk-fmt40tpw.js → chunk-53jatzaw.js} +2 -2
  11. package/dist/{chunk-ef2dwfrg.js → chunk-8kh577qh.js} +5 -5
  12. package/dist/{chunk-s9c2x5jv.js → chunk-9v2pwxv9.js} +2 -2
  13. package/dist/chunk-aa2dkn7b.js +205 -0
  14. package/dist/{chunk-eg8ck25v.js → chunk-cktwk9hy.js} +6935 -15274
  15. package/dist/{chunk-33sy72r8.js → chunk-d3vx2ekv.js} +71 -25
  16. package/dist/chunk-grx1rp09.js +12163 -0
  17. package/dist/{chunk-c6db5m63.js → chunk-t2s3gdq6.js} +253 -8
  18. package/dist/chunk-va2yd85b.js +200 -0
  19. package/dist/{chunk-3g4r09dd.js → chunk-wmbep3b5.js} +1 -1
  20. package/dist/{chunk-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
  21. package/dist/chunk-xrtwv3v2.js +420 -0
  22. package/dist/control-plane/types.d.ts +1 -1
  23. package/dist/deploy/dashboard-protection.test.d.ts +1 -0
  24. package/dist/deploy/dashboard-spend.test.d.ts +1 -0
  25. package/dist/deploy/index.js +11 -8
  26. package/dist/deploy/local-dashboard-server.d.ts +2 -0
  27. package/dist/drivers/index.js +4 -2
  28. package/dist/drivers/shared/protection.d.ts +43 -0
  29. package/dist/drivers/shared/protection.test.d.ts +1 -0
  30. package/dist/index.d.ts +3 -0
  31. package/dist/index.js +364 -102
  32. package/dist/protection/controls.d.ts +113 -0
  33. package/dist/protection/controls.test.d.ts +1 -0
  34. package/dist/protection/ddos.d.ts +130 -0
  35. package/dist/protection/ddos.test.d.ts +1 -0
  36. package/dist/protection/index.d.ts +11 -0
  37. package/dist/protection/index.js +87 -0
  38. package/dist/protection/ratelimit.d.ts +154 -0
  39. package/dist/protection/ratelimit.test.d.ts +1 -0
  40. package/dist/protection/waf.d.ts +143 -0
  41. package/dist/protection/waf.test.d.ts +1 -0
  42. package/dist/spend/anomaly-config.d.ts +130 -0
  43. package/dist/spend/anomaly-config.test.d.ts +1 -0
  44. package/dist/spend/anomaly.d.ts +114 -0
  45. package/dist/spend/anomaly.test.d.ts +1 -0
  46. package/dist/spend/api.d.ts +42 -0
  47. package/dist/spend/api.test.d.ts +1 -0
  48. package/dist/spend/appliers.d.ts +126 -0
  49. package/dist/spend/docs.test.d.ts +1 -0
  50. package/dist/spend/enforcement.d.ts +111 -0
  51. package/dist/spend/enforcement.integration.test.d.ts +1 -0
  52. package/dist/spend/evaluator.d.ts +70 -0
  53. package/dist/spend/evaluator.test.d.ts +1 -0
  54. package/dist/spend/focus.d.ts +93 -0
  55. package/dist/spend/focus.test.d.ts +1 -0
  56. package/dist/spend/gate.d.ts +96 -0
  57. package/dist/spend/index.d.ts +20 -0
  58. package/dist/spend/index.js +173 -0
  59. package/dist/spend/lease.d.ts +54 -0
  60. package/dist/spend/lease.test.d.ts +1 -0
  61. package/dist/spend/meter.d.ts +70 -0
  62. package/dist/spend/meter.test.d.ts +1 -0
  63. package/dist/spend/model.d.ts +228 -0
  64. package/dist/spend/notifications.d.ts +58 -0
  65. package/dist/spend/pricing.d.ts +109 -0
  66. package/dist/spend/pricing.test.d.ts +1 -0
  67. package/dist/spend/projection.d.ts +36 -0
  68. package/dist/spend/runner.d.ts +130 -0
  69. package/dist/spend/service.d.ts +129 -0
  70. package/dist/spend/signals.d.ts +110 -0
  71. package/dist/spend/signals.test.d.ts +1 -0
  72. package/dist/spend/sms-sdk.test.d.ts +1 -0
  73. package/dist/spend/spend.integration.test.d.ts +1 -0
  74. package/dist/spend/store.d.ts +166 -0
  75. package/dist/spend/transports.d.ts +169 -0
  76. package/dist/spend/transports.test.d.ts +1 -0
  77. package/dist/spend/window.d.ts +52 -0
  78. package/dist/spend/window.test.d.ts +1 -0
  79. package/dist/ui/404.html +1 -0
  80. package/dist/ui/access-denied.html +2 -2
  81. package/dist/ui/account/automation.html +3 -3
  82. package/dist/ui/account/security.html +2 -2
  83. package/dist/ui/applications/compose.html +3 -3
  84. package/dist/ui/applications/new.html +2 -2
  85. package/dist/ui/data/backups.html +4 -4
  86. package/dist/ui/data/services.html +4 -4
  87. package/dist/ui/data/volumes.html +4 -4
  88. package/dist/ui/index.html +4 -4
  89. package/dist/ui/infrastructure/topology.html +4 -4
  90. package/dist/ui/integrations.html +1 -1
  91. package/dist/ui/operations/alerts.html +4 -4
  92. package/dist/ui/operations/configuration.html +4 -4
  93. package/dist/ui/operations/jobs.html +4 -4
  94. package/dist/ui/operations/maintenance.html +4 -4
  95. package/dist/ui/operations/observability.html +4 -4
  96. package/dist/ui/operations/previews.html +4 -4
  97. package/dist/ui/operations/queue.html +4 -4
  98. package/dist/ui/operations/regions.html +4 -4
  99. package/dist/ui/operations/releases.html +4 -4
  100. package/dist/ui/operations/spend.html +1634 -0
  101. package/dist/ui/operations/workloads.html +4 -4
  102. package/dist/ui/security.html +2 -2
  103. package/dist/ui/server/actions.html +4 -4
  104. package/dist/ui/server/activity.html +2 -2
  105. package/dist/ui/server/capacity.html +4 -4
  106. package/dist/ui/server/database.html +4 -4
  107. package/dist/ui/server/deployments.html +4 -4
  108. package/dist/ui/server/diagnostics.html +2 -2
  109. package/dist/ui/server/firewall.html +100 -161
  110. package/dist/ui/server/fleet.html +4 -4
  111. package/dist/ui/server/logs.html +4 -4
  112. package/dist/ui/server/metrics.html +4 -4
  113. package/dist/ui/server/security.html +2 -2
  114. package/dist/ui/server/services.html +2 -2
  115. package/dist/ui/server/sites.html +3 -3
  116. package/dist/ui/server/ssh-keys.html +4 -4
  117. package/dist/ui/server/team.html +4 -4
  118. package/dist/ui/server/terminal.html +2 -2
  119. package/dist/ui/serverless/alarms.html +4 -4
  120. package/dist/ui/serverless/assets.html +2 -2
  121. package/dist/ui/serverless/cost.html +2 -2
  122. package/dist/ui/serverless/data.html +4 -4
  123. package/dist/ui/serverless/deployments.html +1 -1
  124. package/dist/ui/serverless/firewall.html +2 -2
  125. package/dist/ui/serverless/functions.html +4 -4
  126. package/dist/ui/serverless/logs.html +4 -4
  127. package/dist/ui/serverless/metrics.html +2 -2
  128. package/dist/ui/serverless/queues.html +4 -4
  129. package/dist/ui/serverless/secrets.html +4 -4
  130. package/dist/ui/serverless/traces.html +4 -4
  131. package/dist/ui/serverless.html +4 -4
  132. package/dist/ui-src/pages/operations/spend.stx +225 -0
  133. package/dist/ui-src/pages/partials/nav.stx +2 -0
  134. package/dist/ui-src/pages/server/firewall.stx +89 -0
  135. package/package.json +11 -3
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The spend service: one object that runs a full cycle.
3
+ *
4
+ * Everything below it is pure or a plain store, which is what makes the
5
+ * subsystem testable. This is the seam where they meet: ingest usage, evaluate
6
+ * every budget that governs a scope, apply or lift enforcement, and look for
7
+ * anomalies - in that order, because enforcement decisions should be made
8
+ * against the usage we just recorded, not the previous cycle's.
9
+ */
10
+ import type { TelemetryRecord } from '../telemetry';
11
+ import type { AnomalyOptions } from './anomaly';
12
+ import type { EnforcementApplier, EnforcementPolicy, EnforcementReleaser, EnforcementRunResult } from './enforcement';
13
+ import type { Budget, EnforcementAction, SpendAnomaly, SpendDecision, UsageDelta } from './model';
14
+ import type { IngestResult, SpendStore, UsageSummary } from './store';
15
+ import { AnomalyConfigStore } from './anomaly-config';
16
+ import { SignalSource } from './signals';
17
+ export interface SpendCycleOptions {
18
+ organizationId: string;
19
+ projectId?: string;
20
+ environmentId?: string;
21
+ /** Environment kind, so production can require approval for visible actions. */
22
+ environmentKind?: string;
23
+ now?: Date;
24
+ policy?: EnforcementPolicy;
25
+ handlers?: {
26
+ apply: EnforcementApplier;
27
+ release: EnforcementReleaser;
28
+ };
29
+ }
30
+ export interface BudgetStatus {
31
+ budget: Budget;
32
+ decision: SpendDecision;
33
+ summary: UsageSummary;
34
+ /** Enforcement currently in force for this budget. */
35
+ activeActions: EnforcementAction[];
36
+ /** Plain-language time until the cap, e.g. `2d 4h`. Empty when not projected. */
37
+ timeToCap: string;
38
+ }
39
+ export interface SpendCycleResult {
40
+ organizationId: string;
41
+ statuses: BudgetStatus[];
42
+ /** Strictest outcome across every governing budget. */
43
+ level: SpendDecision['level'];
44
+ actions: EnforcementAction[];
45
+ releases: EnforcementAction[];
46
+ enforcement: EnforcementRunResult[];
47
+ anomalies: SpendAnomaly[];
48
+ evaluatedAt: string;
49
+ }
50
+ export declare class SpendService {
51
+ private readonly store;
52
+ private readonly counters;
53
+ private readonly breaching;
54
+ /** Per-scope detector tuning and silences. Optional: detection works without it. */
55
+ readonly anomalyConfigs: AnomalyConfigStore;
56
+ /** Sources every detectable series: usage rollups and request telemetry. */
57
+ readonly signals: SignalSource;
58
+ constructor(store: SpendStore);
59
+ /** Meter a telemetry batch and fold it into the rollups. */
60
+ ingestTelemetry(organizationId: string, records: readonly TelemetryRecord[], defaultProvider?: string): IngestResult;
61
+ /** Fold pre-computed usage in directly. For collectors that already meter. */
62
+ ingestUsage(deltas: readonly UsageDelta[], aggregate?: boolean): IngestResult;
63
+ /** Evaluate one budget without touching enforcement. Safe to call anywhere. */
64
+ status(budget: Budget, now?: Date): BudgetStatus;
65
+ /**
66
+ * Run a full cycle for a scope.
67
+ *
68
+ * Without `handlers` this evaluates and records but applies nothing - the
69
+ * read-only mode a dashboard or a `--dry-run` CLI wants.
70
+ */
71
+ runCycle(options: SpendCycleOptions): Promise<SpendCycleResult>;
72
+ /**
73
+ * Grace-period bookkeeping.
74
+ *
75
+ * The clock starts when a budget first wants a non-notify action and is
76
+ * cleared the moment it stops. Keeping it here rather than in the database
77
+ * is deliberate: a grace period that survives a control-plane restart would
78
+ * let a long-resolved blip enforce hours later.
79
+ */
80
+ private trackBreach;
81
+ /**
82
+ * Look for an anomalous hour in the trailing cost series.
83
+ *
84
+ * Runs over a fixed 14-day lookback rather than the budget window: a monthly
85
+ * budget on day 2 has no history to speak of, and the seasonal baseline needs
86
+ * several same-phase observations before it means anything.
87
+ */
88
+ /**
89
+ * Look for anomalies across every detectable signal.
90
+ *
91
+ * Runs over a fixed 14-day lookback rather than the budget window: a monthly
92
+ * budget on day 2 has no history to speak of, and a seasonal baseline needs
93
+ * several same-phase observations before it means anything. Only complete
94
+ * hours are judged, since a partial hour always reads low.
95
+ *
96
+ * Per-route detection is opt-in through `routes`, and bounded to the busiest
97
+ * few. A long tail of one-hit routes produces noise rather than insight, and
98
+ * the routes that matter are the ones with enough traffic for a baseline.
99
+ */
100
+ detectAnomalies(options: SpendCycleOptions & {
101
+ signals?: string[];
102
+ routes?: boolean;
103
+ }, now?: Date, overrides?: AnomalyOptions): SpendAnomaly[];
104
+ /** Detect on one signal in one scope. Returns at most one anomaly per call. */
105
+ private detectSignal;
106
+ /**
107
+ * The payload behind the usage API.
108
+ *
109
+ * Shaped for a caller that has to decide something - an agent asking "can I
110
+ * afford this deploy?" needs the remaining headroom and the projection, not
111
+ * a pile of rollup rows.
112
+ */
113
+ usageReport(options: {
114
+ organizationId: string;
115
+ projectId?: string;
116
+ environmentId?: string;
117
+ period?: Budget['period'];
118
+ timezone?: string;
119
+ now?: Date;
120
+ }): Record<string, unknown>;
121
+ }
122
+ /** Insert zero-cost buckets for hours with no usage, so seasonal phases line up. */
123
+ export declare function zeroFill(points: ReadonlyArray<{
124
+ bucketStart: string;
125
+ value: number;
126
+ }>, from: string, to: string, maxBuckets?: number): Array<{
127
+ bucketStart: string;
128
+ value: number;
129
+ }>;
@@ -0,0 +1,110 @@
1
+ import type { ControlPlaneStore } from '../control-plane';
2
+ import type { SeriesPoint } from './anomaly';
3
+ import type { SpendStore } from './store';
4
+ /** How an empty bucket should be treated. See rule 1 above. */
5
+ export type GapPolicy = 'zero' | 'gap';
6
+ export interface SignalDefinition {
7
+ key: string;
8
+ label: string;
9
+ /** `cost` and meters come from usage rollups; the rest from telemetry. */
10
+ source: 'usage' | 'telemetry';
11
+ gapPolicy: GapPolicy;
12
+ /**
13
+ * Minimum observations in a bucket before its value is trusted.
14
+ * Zero for counts - a count of three is still exactly three.
15
+ */
16
+ minSamples: number;
17
+ /** Units for the operator-facing floor, so `minAbsoluteDelta` reads sensibly. */
18
+ unit: string;
19
+ /** True when the signal is a ratio in 0-1. */
20
+ ratio?: boolean;
21
+ }
22
+ /**
23
+ * The signals detection can run on.
24
+ *
25
+ * Deliberately short. Every signal here is one an operator can act on: a spike
26
+ * in 5xx, a collapse in throughput, a latency regression, a cost jump. Adding
27
+ * signals nobody would act on makes the dashboard noisier without making
28
+ * anything safer.
29
+ */
30
+ export declare const DETECTABLE_SIGNALS: readonly SignalDefinition[];
31
+ export declare function signalDefinition(key: string): SignalDefinition | undefined;
32
+ export interface SignalScope {
33
+ organizationId: string;
34
+ projectId?: string;
35
+ environmentId?: string;
36
+ /** Restrict to one route template, for per-route detection. */
37
+ route?: string;
38
+ }
39
+ export interface SignalWindow {
40
+ from: string;
41
+ to: string;
42
+ }
43
+ /** A series plus what it took to build it, so a caller can judge the verdict. */
44
+ export interface SignalSeries {
45
+ signal: string;
46
+ points: SeriesPoint[];
47
+ /** Buckets that carried enough observations to be trusted. */
48
+ populated: number;
49
+ /** Buckets dropped for falling under `minSamples`. */
50
+ suppressed: number;
51
+ unit: string;
52
+ }
53
+ export declare class SignalSource {
54
+ private readonly controlPlane;
55
+ private readonly spend;
56
+ constructor(controlPlane: ControlPlaneStore, spend: SpendStore);
57
+ private query;
58
+ /** Usage-backed series: cost, or one meter's quantity. */
59
+ private usageSeries;
60
+ /**
61
+ * Telemetry-backed series, aggregated in SQL.
62
+ *
63
+ * Request records are the source rather than pre-aggregated metrics, because
64
+ * a ratio has to be computed from the same rows on both sides. Dividing a
65
+ * request-count metric by an error-count metric collected separately gives a
66
+ * rate that is wrong whenever the two were sampled differently.
67
+ */
68
+ private telemetrySeries;
69
+ /**
70
+ * p95 latency per hour.
71
+ *
72
+ * Computed with a window function rather than by pulling durations into
73
+ * memory: a busy hour is hundreds of thousands of rows, and the point of
74
+ * doing this in SQL is not having to hold them.
75
+ */
76
+ private latencySeries;
77
+ /** The series for one signal. Returns an all-gap series for an unknown one. */
78
+ series(signal: string, scope: SignalScope, window: SignalWindow): SignalSeries;
79
+ /**
80
+ * Route templates worth evaluating individually.
81
+ *
82
+ * Bounded and ordered by traffic: per-route detection on a long tail of
83
+ * one-hit routes produces noise, not insight, and the routes that matter are
84
+ * the ones carrying enough traffic for a baseline to exist.
85
+ */
86
+ busiestRoutes(scope: SignalScope, window: SignalWindow, limit?: number, minRequests?: number): string[];
87
+ }
88
+ /**
89
+ * Detector options per signal.
90
+ *
91
+ * The floors are in each signal's own units, which is the only way they mean
92
+ * anything: 25 cents, 1,000 requests, and 2 percentage points are all "small
93
+ * enough to ignore" for their signal and nonsense for the others.
94
+ */
95
+ export declare function optionsForSignal(signal: string): {
96
+ seasonLength: number;
97
+ threshold: number;
98
+ minHistory: number;
99
+ minAbsoluteDelta: number;
100
+ detectDrops: boolean;
101
+ };
102
+ /**
103
+ * How far back a signal needs to look.
104
+ *
105
+ * A weekly season with three same-phase observations needs three weeks of
106
+ * history, not two. A fixed 14-day lookback silently starves every weekly
107
+ * signal: the detector finds two prior points, falls under `minHistory`, and
108
+ * reports nothing forever while looking perfectly healthy.
109
+ */
110
+ export declare function lookbackHoursForSignal(signal: string): number;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,166 @@
1
+ import type { ControlPlaneStore, JsonValue } from '../control-plane';
2
+ import type { PriceBook } from './pricing';
3
+ import type { Budget, BudgetPeriod, BudgetThreshold, EnforcementAction, EnforcementRecord, EnforcementState, MeterKey, SpendAnomaly, SpendDecision, UsageDelta, UsageRollup } from './model';
4
+ import { budgetWindow } from './window';
5
+ /** Collapse a nullable scope into a non-empty string for uniqueness purposes. */
6
+ export declare function scopeKey(organizationId: string, projectId?: string, environmentId?: string): string;
7
+ export interface CreateBudgetInput {
8
+ organizationId: string;
9
+ projectId?: string;
10
+ environmentId?: string;
11
+ name: string;
12
+ period: BudgetPeriod;
13
+ timezone?: string;
14
+ currency?: string;
15
+ softLimitCents?: number;
16
+ hardLimitCents?: number;
17
+ thresholds?: BudgetThreshold[];
18
+ meters?: MeterKey[];
19
+ graceSeconds?: number;
20
+ hysteresisPercent?: number;
21
+ dryRun?: boolean;
22
+ enabled?: boolean;
23
+ }
24
+ export interface UsageQuery {
25
+ organizationId: string;
26
+ projectId?: string;
27
+ environmentId?: string;
28
+ resourceId?: string;
29
+ meters?: MeterKey[];
30
+ providers?: string[];
31
+ from: string;
32
+ to: string;
33
+ }
34
+ export interface UsageTotal {
35
+ meter: MeterKey;
36
+ provider: string;
37
+ quantity: number;
38
+ costCents: number;
39
+ sampleCount: number;
40
+ }
41
+ export interface UsageSummary {
42
+ from: string;
43
+ to: string;
44
+ totalCents: number;
45
+ byMeter: UsageTotal[];
46
+ /** Hourly cost series across the window, ascending. Drives projection and anomalies. */
47
+ series: Array<{
48
+ bucketStart: string;
49
+ costCents: number;
50
+ quantity: number;
51
+ }>;
52
+ }
53
+ export interface IngestResult {
54
+ applied: number;
55
+ duplicates: number;
56
+ costCents: number;
57
+ unpricedMeters: string[];
58
+ }
59
+ /**
60
+ * The default ladder, used when a budget declares no thresholds of its own.
61
+ *
62
+ * The shape is deliberate: warn early and twice, act only at the limit, and
63
+ * make the projection rung fire before the actual one. A project that is on
64
+ * track to blow its cap in three days is far cheaper to fix than one that
65
+ * already has.
66
+ */
67
+ export declare const DEFAULT_THRESHOLDS: readonly BudgetThreshold[];
68
+ export declare class SpendStore {
69
+ readonly controlPlane: ControlPlaneStore;
70
+ private readonly options;
71
+ constructor(controlPlane: ControlPlaneStore, options?: {
72
+ now?: () => Date;
73
+ priceBook?: PriceBook;
74
+ });
75
+ now(): string;
76
+ get priceBook(): PriceBook;
77
+ private query;
78
+ createBudget(input: CreateBudgetInput): Budget;
79
+ getBudget(id: string): Budget | undefined;
80
+ listBudgets(filter: {
81
+ organizationId: string;
82
+ projectId?: string;
83
+ enabledOnly?: boolean;
84
+ }): Budget[];
85
+ /**
86
+ * Every budget that governs a scope, most specific last.
87
+ *
88
+ * Order matters to callers that apply the strictest cap: an environment
89
+ * budget should be able to tighten, never loosen, the project's.
90
+ */
91
+ budgetsForScope(organizationId: string, projectId?: string, environmentId?: string): Budget[];
92
+ updateBudget(id: string, patch: Partial<CreateBudgetInput>, expectedVersion?: number): Budget;
93
+ deleteBudget(id: string): boolean;
94
+ /**
95
+ * Fold usage deltas into hourly rollups, pricing each as it lands.
96
+ *
97
+ * Costs are computed against the period total already recorded for the same
98
+ * meter, so a tier boundary or a free allowance is crossed exactly once per
99
+ * window regardless of how the deltas were batched.
100
+ */
101
+ ingestUsage(deltas: ReadonlyArray<UsageDelta & {
102
+ key?: string;
103
+ }>): IngestResult;
104
+ private deltaFallbackKey;
105
+ /**
106
+ * Quantity already recorded this billing month for a delta's meter.
107
+ *
108
+ * Allowances and tiers are monthly on every provider we price, so the
109
+ * calendar month is the right accumulator regardless of a budget's own
110
+ * period - a weekly budget still sits inside a monthly free tier.
111
+ */
112
+ private periodQuantity;
113
+ private usageClauses;
114
+ /** Totals per meter plus the hourly cost series, for one window. */
115
+ summarizeUsage(query: UsageQuery): UsageSummary;
116
+ listRollups(query: UsageQuery, limit?: number): UsageRollup[];
117
+ /** Spend in a budget's current window, honouring its meter filter. */
118
+ budgetSpendCents(budget: Budget, now?: Date): {
119
+ window: ReturnType<typeof budgetWindow>;
120
+ summary: UsageSummary;
121
+ };
122
+ /** Drop rollups and receipts older than `days`. Rollups are cheap; receipts are not. */
123
+ pruneUsage(days: number, now?: Date): {
124
+ rollups: number;
125
+ receipts: number;
126
+ };
127
+ recordDecision(decision: SpendDecision): void;
128
+ listDecisions(budgetId: string, limit?: number): Array<Record<string, unknown>>;
129
+ /**
130
+ * Open an enforcement, or return the one already live for this budget+action.
131
+ *
132
+ * Idempotent by construction: the partial unique index makes a second insert
133
+ * for a live action fail, and re-evaluating a budget every minute must not
134
+ * pile up duplicate enforcements.
135
+ */
136
+ openEnforcement(input: {
137
+ budget: Budget;
138
+ action: EnforcementAction;
139
+ reason: string;
140
+ triggeredAtPercent: number;
141
+ restore?: Record<string, JsonValue>;
142
+ simulated?: boolean;
143
+ }): EnforcementRecord;
144
+ getEnforcement(id: string): EnforcementRecord | undefined;
145
+ activeEnforcement(budgetId: string, action: EnforcementAction): EnforcementRecord | undefined;
146
+ listEnforcements(filter: {
147
+ organizationId: string;
148
+ budgetId?: string;
149
+ activeOnly?: boolean;
150
+ }): EnforcementRecord[];
151
+ transitionEnforcement(id: string, state: EnforcementState, patch?: {
152
+ error?: string;
153
+ restore?: Record<string, JsonValue>;
154
+ }): EnforcementRecord;
155
+ /** Record an anomaly, ignoring a repeat for the same signal and bucket. */
156
+ recordAnomaly(input: Omit<SpendAnomaly, 'id' | 'createdAt'>): SpendAnomaly | undefined;
157
+ getAnomaly(id: string): SpendAnomaly | undefined;
158
+ listAnomalies(filter: {
159
+ organizationId: string;
160
+ projectId?: string;
161
+ since?: string;
162
+ unacknowledgedOnly?: boolean;
163
+ limit?: number;
164
+ }): SpendAnomaly[];
165
+ acknowledgeAnomaly(id: string): boolean;
166
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Making an enforcement action affect real traffic.
3
+ *
4
+ * `appliers.ts` writes the gate; this is the part that talks to a provider. The
5
+ * split matters: the gate is what makes a cap true, and a transport is a
6
+ * best-effort effect on top of it, so a provider outage leaves the cap in force
7
+ * rather than silently open.
8
+ *
9
+ * Two rules every method here obeys:
10
+ *
11
+ * 1. **Return what it takes to undo.** A cap that cannot be lifted is an
12
+ * outage, so each method returns the prior state rather than assuming a
13
+ * default to restore to. "There was no reserved concurrency" and "reserved
14
+ * concurrency was 50" need different restores.
15
+ * 2. **Never widen the blast radius.** On a shared box, stopping the gateway
16
+ * would take down every other tenant on it to save one project money. The
17
+ * compute transport stops the project's own units and says so.
18
+ */
19
+ import type { CloudFrontClient } from '../aws/cloudfront';
20
+ import type { LambdaClient } from '../aws/lambda';
21
+ import type { JsonValue } from '../control-plane';
22
+ import type { SpendEnforcementTransport } from './appliers';
23
+ import type { DdosConfig } from '../protection/ddos';
24
+ /** Shell access to one box. Matches `sshExec` so a driver can pass it straight in. */
25
+ export type RemoteExec = (host: string, command: string) => Promise<{
26
+ code: number;
27
+ stdout: string;
28
+ stderr: string;
29
+ }>;
30
+ export interface AwsSpendTransportOptions {
31
+ lambda: LambdaClient;
32
+ cloudfront?: CloudFrontClient;
33
+ /**
34
+ * Resolve the Lambda functions in scope. A project usually has more than one
35
+ * (http, queue, scheduler) and a cap should stop all of them, not just the
36
+ * one that happens to be named first.
37
+ */
38
+ functions: (scope: {
39
+ projectId?: string;
40
+ environmentId?: string;
41
+ }) => Promise<string[]> | string[];
42
+ /** Publish + alias flip after an env change, for provisioned-concurrency stacks. */
43
+ republish?: (functionName: string) => Promise<void>;
44
+ }
45
+ /**
46
+ * AWS serverless enforcement.
47
+ *
48
+ * `suspend_functions` sets reserved concurrency to zero: triggers still fire,
49
+ * Lambda rejects the invocation, nothing runs, and one call reverses it. The
50
+ * alternatives are worse — deleting the function loses its triggers, and
51
+ * detaching the event source mapping is a multi-step change that is easy to
52
+ * half-restore.
53
+ *
54
+ * `throttle_requests` is deliberately absent. Rate limiting in front of
55
+ * CloudFront needs AWS WAF, which ts-cloud does not provision, and pretending
56
+ * otherwise would report a cap as applied when nothing changed. The applier
57
+ * marks it `unsupported: true` instead.
58
+ */
59
+ export declare class AwsSpendTransport implements SpendEnforcementTransport {
60
+ private readonly options;
61
+ constructor(options: AwsSpendTransportOptions);
62
+ private targets;
63
+ suspendFunctions(scope: {
64
+ projectId?: string;
65
+ environmentId?: string;
66
+ }): Promise<Record<string, JsonValue>>;
67
+ resumeFunctions(restore: Record<string, JsonValue>): Promise<void>;
68
+ /**
69
+ * Serve only what is already static.
70
+ *
71
+ * On AWS that is the same lever as suspending functions: the origin stops
72
+ * rendering and CloudFront keeps serving S3 objects and cached responses.
73
+ * Two ladder rungs mapping to one mechanism is honest here — the rungs differ
74
+ * on other providers, and the restore payload records which one applied it.
75
+ */
76
+ serveStatic(scope: {
77
+ projectId?: string;
78
+ environmentId?: string;
79
+ }): Promise<Record<string, JsonValue>>;
80
+ restoreDynamic(restore: Record<string, JsonValue>): Promise<void>;
81
+ /**
82
+ * Park all traffic behind a 503.
83
+ *
84
+ * `MAINTENANCE_MODE` is read by the serverless runtime adapter, so the
85
+ * function still runs and answers 503 rather than erroring — which keeps the
86
+ * bypass header working for whoever is fixing the problem.
87
+ */
88
+ suspendProject(scope: {
89
+ projectId?: string;
90
+ environmentId?: string;
91
+ }): Promise<Record<string, JsonValue>>;
92
+ resumeProject(restore: Record<string, JsonValue>): Promise<void>;
93
+ }
94
+ export interface ComputeSpendTransportOptions {
95
+ host: string;
96
+ exec: RemoteExec;
97
+ /**
98
+ * Systemd unit bases for this scope, e.g. `acme-web`. The deploy runs each
99
+ * release as `<base>@<releaseId>.service`, so the wildcard is what stops
100
+ * whichever release is currently live.
101
+ */
102
+ units: (scope: {
103
+ projectId?: string;
104
+ environmentId?: string;
105
+ }) => Promise<string[]> | string[];
106
+ /** Current L3/L4 config, so a throttle can re-render it with tighter limits. */
107
+ ddos?: DdosConfig;
108
+ /** Renders the install script. Injected so the transport stays testable. */
109
+ renderDdos?: (config: DdosConfig) => string;
110
+ }
111
+ /**
112
+ * Enforcement on a box we control over SSH.
113
+ *
114
+ * **What this deliberately does not do:** stop `rpx-gateway.service`. On a
115
+ * shared box that gateway fronts every tenant, and taking it down to cap one
116
+ * project's spend would be an outage for everyone else. Suspension here stops
117
+ * the project's own units; requests then get whatever the gateway returns for
118
+ * an origin that is down. That is a real, bounded effect, and the restore
119
+ * payload records exactly which units were stopped.
120
+ */
121
+ export declare class ComputeSpendTransport implements SpendEnforcementTransport {
122
+ private readonly options;
123
+ constructor(options: ComputeSpendTransportOptions);
124
+ private unitsFor;
125
+ /** Stop the app units, recording the instances that were actually running. */
126
+ private stopUnits;
127
+ private startUnits;
128
+ suspendFunctions(scope: {
129
+ projectId?: string;
130
+ environmentId?: string;
131
+ }): Promise<Record<string, JsonValue>>;
132
+ resumeFunctions(restore: Record<string, JsonValue>): Promise<void>;
133
+ serveStatic(scope: {
134
+ projectId?: string;
135
+ environmentId?: string;
136
+ }): Promise<Record<string, JsonValue>>;
137
+ restoreDynamic(restore: Record<string, JsonValue>): Promise<void>;
138
+ suspendProject(scope: {
139
+ projectId?: string;
140
+ environmentId?: string;
141
+ }): Promise<Record<string, JsonValue>>;
142
+ resumeProject(restore: Record<string, JsonValue>): Promise<void>;
143
+ /**
144
+ * Tighten the kernel's per-source connection limits.
145
+ *
146
+ * Reuses the same generator the deploy uses, so a throttle is the ordinary
147
+ * ruleset with scaled thresholds rather than a second, divergent one. The
148
+ * restore payload carries the original thresholds so lifting it re-renders
149
+ * exactly what was there before.
150
+ */
151
+ throttleRequests(input: {
152
+ projectId?: string;
153
+ environmentId?: string;
154
+ factor: number;
155
+ }): Promise<Record<string, JsonValue>>;
156
+ restoreRequests(restore: Record<string, JsonValue>): Promise<void>;
157
+ }
158
+ /**
159
+ * Fan one action out to several transports.
160
+ *
161
+ * A project that serves static assets from CloudFront and dynamic routes from a
162
+ * box needs both capped, and capping one is worse than capping neither: traffic
163
+ * shifts to whichever half is still up and the bill moves rather than stops.
164
+ *
165
+ * Every leg runs even if an earlier one throws, and the first error is rethrown
166
+ * once all have been attempted. Stopping at the first failure would leave the
167
+ * others uncapped with no record of it.
168
+ */
169
+ export declare function compositeSpendTransport(transports: readonly SpendEnforcementTransport[]): SpendEnforcementTransport;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Budget period windows, in the operator's timezone.
3
+ *
4
+ * A monthly cap that rolls over at UTC midnight is wrong for most of the world
5
+ * - it resets in the middle of a Californian afternoon, and the "month" it
6
+ * measures does not match the month on the invoice. Periods are therefore
7
+ * computed against an IANA timezone.
8
+ *
9
+ * Dependency-free: `Intl.DateTimeFormat` already knows every zone and every DST
10
+ * rule, so the offset is read from it rather than from a bundled tz database.
11
+ * DST is why the offset is resolved twice - the naive instant for local
12
+ * midnight can land on the wrong side of a transition, and the second pass
13
+ * corrects it.
14
+ */
15
+ import type { BudgetPeriod, BudgetWindow } from './model';
16
+ interface ZonedParts {
17
+ year: number;
18
+ month: number;
19
+ day: number;
20
+ hour: number;
21
+ minute: number;
22
+ second: number;
23
+ weekday: number;
24
+ }
25
+ /** Validate a timezone once; an unknown zone should fail loudly, not silently bill in UTC. */
26
+ export declare function isValidTimeZone(timeZone: string): boolean;
27
+ export declare function zonedParts(instant: Date, timeZone: string): ZonedParts;
28
+ /** Milliseconds the zone is ahead of UTC at `instant`. */
29
+ export declare function zoneOffsetMs(instant: Date, timeZone: string): number;
30
+ /**
31
+ * The UTC instant of a given local wall-clock time.
32
+ *
33
+ * Two passes: the first offset is read at the naive guess, the second at the
34
+ * corrected instant. On a DST boundary those differ, and only the second is
35
+ * right. Times that do not exist locally (the spring-forward gap) resolve
36
+ * forward, which is the conventional and the safe direction for a period start.
37
+ */
38
+ export declare function utcInstantOfLocal(timeZone: string, year: number, month: number, day: number, hour?: number): Date;
39
+ /**
40
+ * The window `now` falls inside for a period, plus how far through it we are.
41
+ *
42
+ * Weeks start Monday: a Sunday-start week splits the working week across two
43
+ * budgets, which makes every week-over-week comparison lie.
44
+ */
45
+ export declare function budgetWindow(period: BudgetPeriod, timeZone: string, now?: Date): BudgetWindow;
46
+ /** The window immediately before the one containing `now`. Used for comparisons. */
47
+ export declare function previousBudgetWindow(period: BudgetPeriod, timeZone: string, now?: Date): BudgetWindow;
48
+ /** Truncate an instant to the start of its UTC hour - the rollup bucket. */
49
+ export declare function hourBucket(instant: Date | string): string;
50
+ /** Every hour bucket in `[from, to)`, ascending. Bounded so a bad range cannot hang. */
51
+ export declare function hourBucketsBetween(from: string, to: string, maxBuckets?: number): string[];
52
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/ui/404.html CHANGED
@@ -782,6 +782,7 @@ video {
782
782
  <a class="" href="/operations/workloads?env=production">Workloads</a>
783
783
  <a class="" href="/operations/observability?env=production">Observability</a>
784
784
  <a class="" href="/operations/alerts?env=production">Alerts & health</a>
785
+ <a class="" href="/operations/spend?env=production">Spend & budgets</a>
785
786
  <a href="/server/metrics?env=production">Metrics</a><a href="/server/services?env=production">Runtime services</a>
786
787
  <a class="" href="/operations/jobs?env=production">Workers & schedules</a>
787
788
  <a class="" href="/operations/configuration?env=production">Environment &amp; secrets</a>