@stacksjs/ts-cloud 0.7.105 → 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.
- package/dist/alerts/delivery.d.ts +10 -0
- package/dist/alerts/model.d.ts +1 -1
- package/dist/api/client.d.ts +53 -0
- package/dist/api/handler.d.ts +10 -0
- package/dist/api/service.d.ts +1 -0
- package/dist/aws/index.js +10 -10
- package/dist/aws/lambda.d.ts +19 -0
- package/dist/bin/cli.js +1677 -1370
- package/dist/bin/dashboard-server.js +926 -620
- package/dist/{chunk-fmt40tpw.js → chunk-53jatzaw.js} +2 -2
- package/dist/{chunk-ef2dwfrg.js → chunk-8kh577qh.js} +5 -5
- package/dist/{chunk-s9c2x5jv.js → chunk-9v2pwxv9.js} +2 -2
- package/dist/chunk-aa2dkn7b.js +205 -0
- package/dist/{chunk-vkd2y0fs.js → chunk-cktwk9hy.js} +7169 -14800
- package/dist/{chunk-dmzez5f8.js → chunk-d3vx2ekv.js} +123 -28
- package/dist/chunk-grx1rp09.js +12163 -0
- package/dist/{chunk-c6db5m63.js → chunk-t2s3gdq6.js} +253 -8
- package/dist/chunk-va2yd85b.js +200 -0
- package/dist/{chunk-3g4r09dd.js → chunk-wmbep3b5.js} +1 -1
- package/dist/{chunk-ybcz6sxc.js → chunk-x6yzgv7y.js} +33 -0
- package/dist/chunk-xrtwv3v2.js +420 -0
- package/dist/control-plane/types.d.ts +1 -1
- package/dist/deploy/dashboard-bundle-encoding.test.d.ts +1 -0
- package/dist/deploy/dashboard-payload.d.ts +20 -0
- package/dist/deploy/dashboard-payload.test.d.ts +1 -0
- package/dist/deploy/dashboard-protection.test.d.ts +1 -0
- package/dist/deploy/dashboard-spend.test.d.ts +1 -0
- package/dist/deploy/dashboard-topology.d.ts +85 -0
- package/dist/deploy/dashboard-topology.test.d.ts +1 -0
- package/dist/deploy/egress-collection.d.ts +60 -0
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/index.js +23 -8
- package/dist/deploy/local-dashboard-server.d.ts +2 -0
- package/dist/drivers/index.js +4 -2
- package/dist/drivers/shared/protection.d.ts +43 -0
- package/dist/drivers/shared/protection.test.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +364 -102
- package/dist/protection/controls.d.ts +113 -0
- package/dist/protection/controls.test.d.ts +1 -0
- package/dist/protection/ddos.d.ts +130 -0
- package/dist/protection/ddos.test.d.ts +1 -0
- package/dist/protection/index.d.ts +11 -0
- package/dist/protection/index.js +87 -0
- package/dist/protection/ratelimit.d.ts +154 -0
- package/dist/protection/ratelimit.test.d.ts +1 -0
- package/dist/protection/waf.d.ts +143 -0
- package/dist/protection/waf.test.d.ts +1 -0
- package/dist/spend/anomaly-config.d.ts +130 -0
- package/dist/spend/anomaly-config.test.d.ts +1 -0
- package/dist/spend/anomaly.d.ts +114 -0
- package/dist/spend/anomaly.test.d.ts +1 -0
- package/dist/spend/api.d.ts +42 -0
- package/dist/spend/api.test.d.ts +1 -0
- package/dist/spend/appliers.d.ts +126 -0
- package/dist/spend/docs.test.d.ts +1 -0
- package/dist/spend/enforcement.d.ts +111 -0
- package/dist/spend/enforcement.integration.test.d.ts +1 -0
- package/dist/spend/evaluator.d.ts +70 -0
- package/dist/spend/evaluator.test.d.ts +1 -0
- package/dist/spend/focus.d.ts +93 -0
- package/dist/spend/focus.test.d.ts +1 -0
- package/dist/spend/gate.d.ts +96 -0
- package/dist/spend/index.d.ts +20 -0
- package/dist/spend/index.js +173 -0
- package/dist/spend/lease.d.ts +54 -0
- package/dist/spend/lease.test.d.ts +1 -0
- package/dist/spend/meter.d.ts +70 -0
- package/dist/spend/meter.test.d.ts +1 -0
- package/dist/spend/model.d.ts +228 -0
- package/dist/spend/notifications.d.ts +58 -0
- package/dist/spend/pricing.d.ts +109 -0
- package/dist/spend/pricing.test.d.ts +1 -0
- package/dist/spend/projection.d.ts +36 -0
- package/dist/spend/runner.d.ts +130 -0
- package/dist/spend/service.d.ts +129 -0
- package/dist/spend/signals.d.ts +110 -0
- package/dist/spend/signals.test.d.ts +1 -0
- package/dist/spend/sms-sdk.test.d.ts +1 -0
- package/dist/spend/spend.integration.test.d.ts +1 -0
- package/dist/spend/store.d.ts +166 -0
- package/dist/spend/transports.d.ts +169 -0
- package/dist/spend/transports.test.d.ts +1 -0
- package/dist/spend/window.d.ts +52 -0
- package/dist/spend/window.test.d.ts +1 -0
- package/dist/ui/404.html +2 -0
- package/dist/ui/access-denied.html +2 -2
- package/dist/ui/account/automation.html +4 -4
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +4 -4
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +22 -9
- package/dist/ui/data/services.html +22 -9
- package/dist/ui/data/volumes.html +22 -9
- package/dist/ui/index.html +22 -9
- package/dist/ui/infrastructure/topology.html +2223 -0
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +22 -9
- package/dist/ui/operations/configuration.html +4 -4
- package/dist/ui/operations/jobs.html +22 -9
- package/dist/ui/operations/maintenance.html +4 -4
- package/dist/ui/operations/observability.html +22 -9
- package/dist/ui/operations/previews.html +4 -4
- package/dist/ui/operations/queue.html +4 -4
- package/dist/ui/operations/regions.html +3 -3
- package/dist/ui/operations/releases.html +3 -3
- package/dist/ui/operations/spend.html +1634 -0
- package/dist/ui/operations/workloads.html +22 -9
- package/dist/ui/security.html +2 -2
- package/dist/ui/server/actions.html +4 -4
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +4 -4
- package/dist/ui/server/database.html +4 -4
- package/dist/ui/server/deployments.html +4 -4
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +100 -161
- package/dist/ui/server/fleet.html +22 -9
- package/dist/ui/server/logs.html +22 -9
- package/dist/ui/server/metrics.html +130 -14
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +1 -1
- package/dist/ui/server/sites.html +22 -9
- package/dist/ui/server/ssh-keys.html +3 -3
- package/dist/ui/server/team.html +4 -4
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +4 -4
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +2 -2
- package/dist/ui/serverless/data.html +4 -4
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/firewall.html +2 -2
- package/dist/ui/serverless/functions.html +4 -4
- package/dist/ui/serverless/logs.html +22 -9
- package/dist/ui/serverless/metrics.html +2 -2
- package/dist/ui/serverless/queues.html +4 -4
- package/dist/ui/serverless/secrets.html +4 -4
- package/dist/ui/serverless/traces.html +4 -4
- package/dist/ui/serverless.html +4 -4
- package/dist/ui-src/functions/requestJson.ts +41 -0
- package/dist/ui-src/functions/useOpConfirm.ts +132 -0
- package/dist/ui-src/functions/usePolling.ts +98 -0
- package/dist/ui-src/functions/useTerminal.ts +79 -0
- package/dist/ui-src/pages/infrastructure/topology.stx +517 -0
- package/dist/ui-src/pages/operations/spend.stx +225 -0
- package/dist/ui-src/pages/partials/nav.stx +4 -0
- package/dist/ui-src/pages/server/firewall.stx +89 -0
- package/dist/ui-src/pages/server/metrics.stx +140 -6
- package/dist/ui-src/src/topology.ts +485 -0
- package/package.json +11 -3
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ControlPlaneStore } from '../control-plane';
|
|
2
|
+
import type { AnomalyOptions } from './anomaly';
|
|
3
|
+
import type { AlertSeverity } from '../alerts';
|
|
4
|
+
export type AnomalySensitivity = 'low' | 'medium' | 'high';
|
|
5
|
+
/**
|
|
6
|
+
* Robust z-score thresholds per sensitivity.
|
|
7
|
+
*
|
|
8
|
+
* High is 2.5 rather than something dramatic: with a MAD-based score, 2.5 is
|
|
9
|
+
* already well outside ordinary variation, and going lower turns the detector
|
|
10
|
+
* into a random number generator on any noisy signal.
|
|
11
|
+
*/
|
|
12
|
+
export declare const SENSITIVITY_THRESHOLDS: Readonly<Record<AnomalySensitivity, number>>;
|
|
13
|
+
export interface AnomalyConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
organizationId: string;
|
|
16
|
+
projectId?: string;
|
|
17
|
+
environmentId?: string;
|
|
18
|
+
/** `cost`, a meter key, or a telemetry signal like `http.status.5xx`. */
|
|
19
|
+
signal: string;
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
sensitivity: AnomalySensitivity;
|
|
22
|
+
/** Points per season. 24 for a daily shape, 168 for a weekly one. */
|
|
23
|
+
seasonLength: number;
|
|
24
|
+
/** Absolute change below which nothing is reported, in the signal's units. */
|
|
25
|
+
minAbsoluteDelta: number;
|
|
26
|
+
detectDrops: boolean;
|
|
27
|
+
severity: AlertSeverity;
|
|
28
|
+
version: number;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
updatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
export interface AnomalySilence {
|
|
33
|
+
id: string;
|
|
34
|
+
organizationId: string;
|
|
35
|
+
projectId?: string;
|
|
36
|
+
signal?: string;
|
|
37
|
+
/** Glob against the route, e.g. `/webhooks/**`. */
|
|
38
|
+
routePattern?: string;
|
|
39
|
+
statusCode?: number;
|
|
40
|
+
reason: string;
|
|
41
|
+
actorId?: string;
|
|
42
|
+
/** Null means indefinite. A silence with no end is a decision, so it is recorded. */
|
|
43
|
+
expiresAt?: string;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}
|
|
46
|
+
/** Glob matcher, same tiny dialect the rate limiter uses. */
|
|
47
|
+
export declare function routeMatches(pattern: string, route: string): boolean;
|
|
48
|
+
export interface AnomalyCandidate {
|
|
49
|
+
signal: string;
|
|
50
|
+
route?: string;
|
|
51
|
+
statusCode?: number;
|
|
52
|
+
}
|
|
53
|
+
export interface CreateAnomalyConfigInput {
|
|
54
|
+
organizationId: string;
|
|
55
|
+
projectId?: string;
|
|
56
|
+
environmentId?: string;
|
|
57
|
+
signal: string;
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
sensitivity?: AnomalySensitivity;
|
|
60
|
+
seasonLength?: number;
|
|
61
|
+
minAbsoluteDelta?: number;
|
|
62
|
+
detectDrops?: boolean;
|
|
63
|
+
severity?: AlertSeverity;
|
|
64
|
+
}
|
|
65
|
+
export declare class AnomalyConfigStore {
|
|
66
|
+
private readonly controlPlane;
|
|
67
|
+
private readonly options;
|
|
68
|
+
constructor(controlPlane: ControlPlaneStore, options?: {
|
|
69
|
+
now?: () => Date;
|
|
70
|
+
});
|
|
71
|
+
private now;
|
|
72
|
+
private query;
|
|
73
|
+
/** Create or replace the config for a scope and signal. */
|
|
74
|
+
upsert(input: CreateAnomalyConfigInput): AnomalyConfig;
|
|
75
|
+
get(id: string): AnomalyConfig | undefined;
|
|
76
|
+
list(filter: {
|
|
77
|
+
organizationId: string;
|
|
78
|
+
projectId?: string;
|
|
79
|
+
}): AnomalyConfig[];
|
|
80
|
+
delete(id: string): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The detector options for a signal, honouring the most specific config.
|
|
83
|
+
*
|
|
84
|
+
* Falls back to the shipped preset when nothing is configured, so detection
|
|
85
|
+
* works out of the box and configuration is a refinement rather than a
|
|
86
|
+
* prerequisite.
|
|
87
|
+
*/
|
|
88
|
+
optionsFor(scope: {
|
|
89
|
+
organizationId: string;
|
|
90
|
+
projectId?: string;
|
|
91
|
+
environmentId?: string;
|
|
92
|
+
}, signal: string): {
|
|
93
|
+
options: AnomalyOptions;
|
|
94
|
+
config?: AnomalyConfig;
|
|
95
|
+
enabled: boolean;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Silence a pattern.
|
|
99
|
+
*
|
|
100
|
+
* An empty matcher is refused. "Silence everything" is what disabling the
|
|
101
|
+
* config is for, and a silence that matches everything is indistinguishable
|
|
102
|
+
* from a broken detector when someone later asks why nothing fires.
|
|
103
|
+
*/
|
|
104
|
+
silence(input: {
|
|
105
|
+
organizationId: string;
|
|
106
|
+
projectId?: string;
|
|
107
|
+
signal?: string;
|
|
108
|
+
routePattern?: string;
|
|
109
|
+
statusCode?: number;
|
|
110
|
+
reason: string;
|
|
111
|
+
actorId?: string;
|
|
112
|
+
expiresAt?: string;
|
|
113
|
+
}): AnomalySilence;
|
|
114
|
+
listSilences(filter: {
|
|
115
|
+
organizationId: string;
|
|
116
|
+
projectId?: string;
|
|
117
|
+
includeExpired?: boolean;
|
|
118
|
+
}): AnomalySilence[];
|
|
119
|
+
removeSilence(id: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Whether a candidate is silenced.
|
|
122
|
+
*
|
|
123
|
+
* Checked *before* detection runs, not after, so a silenced pattern costs no
|
|
124
|
+
* computation and never reaches the dashboard.
|
|
125
|
+
*/
|
|
126
|
+
isSilenced(scope: {
|
|
127
|
+
organizationId: string;
|
|
128
|
+
projectId?: string;
|
|
129
|
+
}, candidate: AnomalyCandidate): AnomalySilence | undefined;
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anomaly detection for spend and traffic.
|
|
3
|
+
*
|
|
4
|
+
* The naive version of this - mean plus three standard deviations - does not
|
|
5
|
+
* survive contact with real infrastructure data, for two reasons:
|
|
6
|
+
*
|
|
7
|
+
* 1. **The mean is not robust.** One genuine incident poisons the baseline
|
|
8
|
+
* for as long as it stays in the window, so the detector goes quiet
|
|
9
|
+
* exactly when it matters. Median and MAD (median absolute deviation) are
|
|
10
|
+
* unmoved by up to half the sample being garbage.
|
|
11
|
+
* 2. **Infrastructure data is seasonal.** Traffic at 3am Sunday is not
|
|
12
|
+
* traffic at 3pm Tuesday. A flat baseline fires every weekday morning and
|
|
13
|
+
* teaches everyone to ignore it. The baseline here is per-phase: an hour
|
|
14
|
+
* is compared against the same hour on previous days.
|
|
15
|
+
*
|
|
16
|
+
* Two guards keep the noise down further: a warmup (no history, no verdict)
|
|
17
|
+
* and an absolute floor (a jump from $0.01 to $0.09 is 800% and worth nobody's
|
|
18
|
+
* pager).
|
|
19
|
+
*/
|
|
20
|
+
import type { AnomalyDirection } from './model';
|
|
21
|
+
export interface SeriesPoint {
|
|
22
|
+
bucketStart: string;
|
|
23
|
+
value: number;
|
|
24
|
+
}
|
|
25
|
+
export interface AnomalyOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Points per season. 24 for hourly data with a daily cycle (the default),
|
|
28
|
+
* 168 for hourly data with a weekly cycle.
|
|
29
|
+
*/
|
|
30
|
+
seasonLength?: number;
|
|
31
|
+
/** Robust z-score above which a point is anomalous. Lower is more sensitive. */
|
|
32
|
+
threshold?: number;
|
|
33
|
+
/** Minimum same-phase observations before any verdict. Below this: warmup. */
|
|
34
|
+
minHistory?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Absolute change below which nothing is reported, in the series' own units.
|
|
37
|
+
* This is what stops a rounding-error spike from paging anyone.
|
|
38
|
+
*/
|
|
39
|
+
minAbsoluteDelta?: number;
|
|
40
|
+
/** Report drops as well as spikes. Off by default; a spend drop is good news. */
|
|
41
|
+
detectDrops?: boolean;
|
|
42
|
+
/** Score at which an anomaly is `critical` rather than `warning`. */
|
|
43
|
+
criticalThreshold?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AnomalyPoint {
|
|
46
|
+
bucketStart: string;
|
|
47
|
+
observed: number;
|
|
48
|
+
expected: number;
|
|
49
|
+
score: number;
|
|
50
|
+
direction: AnomalyDirection;
|
|
51
|
+
deltaPercent: number;
|
|
52
|
+
severity: 'info' | 'warning' | 'critical';
|
|
53
|
+
/** Same-phase observations the baseline was drawn from. */
|
|
54
|
+
historySize: number;
|
|
55
|
+
/** The robust spread of the baseline, in the series' units. */
|
|
56
|
+
deviation: number;
|
|
57
|
+
}
|
|
58
|
+
export interface AnomalyResult {
|
|
59
|
+
anomalies: AnomalyPoint[];
|
|
60
|
+
/** True when there was not enough history to judge anything. */
|
|
61
|
+
warmingUp: boolean;
|
|
62
|
+
/** Points evaluated (excludes those skipped for lack of history). */
|
|
63
|
+
evaluated: number;
|
|
64
|
+
}
|
|
65
|
+
export declare function median(values: readonly number[]): number;
|
|
66
|
+
/**
|
|
67
|
+
* Median absolute deviation, scaled to be comparable to a standard deviation.
|
|
68
|
+
*
|
|
69
|
+
* The 1.4826 factor makes MAD a consistent estimator of sigma for normal data,
|
|
70
|
+
* so a threshold of 3 means roughly what it means for a classic z-score.
|
|
71
|
+
*/
|
|
72
|
+
export declare function medianAbsoluteDeviation(values: readonly number[], center?: number): number;
|
|
73
|
+
/**
|
|
74
|
+
* Robust z-score.
|
|
75
|
+
*
|
|
76
|
+
* A MAD of zero means the history is perfectly flat - common for a meter that
|
|
77
|
+
* is usually idle. Falling back to a small fraction of the center keeps a real
|
|
78
|
+
* jump detectable without dividing by zero and calling everything infinite.
|
|
79
|
+
*/
|
|
80
|
+
export declare function robustZScore(value: number, history: readonly number[]): {
|
|
81
|
+
score: number;
|
|
82
|
+
center: number;
|
|
83
|
+
deviation: number;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Exponentially weighted moving average.
|
|
87
|
+
*
|
|
88
|
+
* Exposed because it is the right baseline for a series with a trend but no
|
|
89
|
+
* seasonality - a slowly growing storage meter, for instance.
|
|
90
|
+
*/
|
|
91
|
+
export declare function ewma(values: readonly number[], alpha?: number): number[];
|
|
92
|
+
/**
|
|
93
|
+
* Find points that do not fit their own history.
|
|
94
|
+
*
|
|
95
|
+
* Returns anomalies in bucket order. Callers usually only act on the most
|
|
96
|
+
* recent one; the full list is what makes a backfill or a chart useful.
|
|
97
|
+
*/
|
|
98
|
+
export declare function detectAnomalies(series: readonly SeriesPoint[], options?: AnomalyOptions): AnomalyResult;
|
|
99
|
+
/**
|
|
100
|
+
* Judge only the most recent point.
|
|
101
|
+
*
|
|
102
|
+
* This is the shape an evaluation loop wants: it runs every hour and only
|
|
103
|
+
* cares whether the hour that just closed was strange.
|
|
104
|
+
*/
|
|
105
|
+
export declare function detectLatestAnomaly(series: readonly SeriesPoint[], options?: AnomalyOptions): AnomalyPoint | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Sensible option presets per signal class.
|
|
108
|
+
*
|
|
109
|
+
* Cost gets a floor of 25 cents because nobody wants to hear that a $0.02 hour
|
|
110
|
+
* became $0.30. Request counts get a much higher absolute floor for the same
|
|
111
|
+
* reason at a different scale, and a weekly season because request traffic has
|
|
112
|
+
* a strong weekday/weekend shape that a daily season would keep re-discovering.
|
|
113
|
+
*/
|
|
114
|
+
export declare function anomalyOptionsForSignal(signal: string): AnomalyOptions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The billing and usage API.
|
|
3
|
+
*
|
|
4
|
+
* This exists so an *agent* can ask before it acts. The interesting question
|
|
5
|
+
* is not "what did last month cost" - a dashboard answers that - but "if I
|
|
6
|
+
* kick off this deploy, or this batch job, or this backfill, does it fit in
|
|
7
|
+
* what is left?" A CI pipeline, a scheduled job, or a coding agent can read
|
|
8
|
+
* `/api/v1/usage`, see the remaining headroom and the projection, and decide.
|
|
9
|
+
*
|
|
10
|
+
* Which is why every budget in the response carries `remainingCents`,
|
|
11
|
+
* `projectedCents`, `projectionConfidence`, and `timeToCap` rather than just a
|
|
12
|
+
* total: a caller deciding whether to proceed needs the forecast and how much
|
|
13
|
+
* to trust it, not an invoice.
|
|
14
|
+
*
|
|
15
|
+
* Routing lives here rather than in the main handler's if/else chain so the
|
|
16
|
+
* spend surface can be tested on its own.
|
|
17
|
+
*/
|
|
18
|
+
import type { ApiTokenPrincipal } from '../automation';
|
|
19
|
+
import type { AutomationApiService } from '../api/service';
|
|
20
|
+
import type { SpendService } from './service';
|
|
21
|
+
import type { SpendStore } from './store';
|
|
22
|
+
export interface SpendApiContext {
|
|
23
|
+
store: SpendStore;
|
|
24
|
+
service: SpendService;
|
|
25
|
+
/** Reused for scope resolution and capability checks. */
|
|
26
|
+
authorization: AutomationApiService;
|
|
27
|
+
now?: () => Date;
|
|
28
|
+
}
|
|
29
|
+
export interface SpendApiRequest {
|
|
30
|
+
method: string;
|
|
31
|
+
url: URL;
|
|
32
|
+
body?: () => Promise<Record<string, any>>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Handle a spend route, or return undefined if the path is not one of ours.
|
|
36
|
+
*
|
|
37
|
+
* Returning undefined rather than a 404 lets the caller keep its own routing
|
|
38
|
+
* fallthrough intact.
|
|
39
|
+
*/
|
|
40
|
+
export declare function handleSpendRequest(context: SpendApiContext, principal: ApiTokenPrincipal, request: SpendApiRequest): Promise<Record<string, unknown> | undefined>;
|
|
41
|
+
/** OpenAPI path entries for the spend surface, merged into the main document. */
|
|
42
|
+
export declare function spendOpenApiPaths(): Record<string, unknown>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a decision into a real effect.
|
|
3
|
+
*
|
|
4
|
+
* Every applier does the same two things, in this order:
|
|
5
|
+
*
|
|
6
|
+
* 1. Write the gate (see `gate.ts`). This is what makes the cap true.
|
|
7
|
+
* 2. Ask a transport to make the traffic-facing change, if the action has one.
|
|
8
|
+
*
|
|
9
|
+
* Order matters. Gate first means a transport that times out still leaves the
|
|
10
|
+
* cap in force - the builds and deploys stop even if the edge did not get the
|
|
11
|
+
* message. The reverse order would leave a window where the edge is throttled
|
|
12
|
+
* but the control plane thinks nothing happened, and the next evaluation cycle
|
|
13
|
+
* would try to apply it again.
|
|
14
|
+
*
|
|
15
|
+
* Release runs the same two steps in reverse: restore the traffic path first,
|
|
16
|
+
* then open the gate. A caller that gets its service back before the gate
|
|
17
|
+
* opens is fine; the other way round would admit traffic to an edge still
|
|
18
|
+
* serving a 503.
|
|
19
|
+
*/
|
|
20
|
+
import type { JsonValue } from '../control-plane';
|
|
21
|
+
import type { EnforcementApplier, EnforcementReleaser } from './enforcement';
|
|
22
|
+
import type { EnforcementAction } from './model';
|
|
23
|
+
import type { SpendGate } from './gate';
|
|
24
|
+
/**
|
|
25
|
+
* What a driver must implement for the traffic-affecting actions.
|
|
26
|
+
*
|
|
27
|
+
* Deliberately small and provider-neutral: AWS does these through CloudFront
|
|
28
|
+
* and Lambda concurrency, a Hetzner box through rpx and systemd, and a test
|
|
29
|
+
* through a fake. Each method returns the state needed to undo it, because a
|
|
30
|
+
* cap that cannot be lifted is an outage.
|
|
31
|
+
*/
|
|
32
|
+
export interface SpendEnforcementTransport {
|
|
33
|
+
/** Apply an edge rate-limit multiplier. Returns the previous configuration. */
|
|
34
|
+
throttleRequests?(input: {
|
|
35
|
+
projectId?: string;
|
|
36
|
+
environmentId?: string;
|
|
37
|
+
factor: number;
|
|
38
|
+
}): Promise<Record<string, JsonValue>> | Record<string, JsonValue>;
|
|
39
|
+
restoreRequests?(restore: Record<string, JsonValue>): Promise<void> | void;
|
|
40
|
+
/** Stop invoking functions; cached and static responses keep serving. */
|
|
41
|
+
suspendFunctions?(input: {
|
|
42
|
+
projectId?: string;
|
|
43
|
+
environmentId?: string;
|
|
44
|
+
}): Promise<Record<string, JsonValue>> | Record<string, JsonValue>;
|
|
45
|
+
resumeFunctions?(restore: Record<string, JsonValue>): Promise<void> | void;
|
|
46
|
+
/** Serve only the last built static output. */
|
|
47
|
+
serveStatic?(input: {
|
|
48
|
+
projectId?: string;
|
|
49
|
+
environmentId?: string;
|
|
50
|
+
}): Promise<Record<string, JsonValue>> | Record<string, JsonValue>;
|
|
51
|
+
restoreDynamic?(restore: Record<string, JsonValue>): Promise<void> | void;
|
|
52
|
+
/** Park inbound traffic behind a 503. Never removes data. */
|
|
53
|
+
suspendProject?(input: {
|
|
54
|
+
projectId?: string;
|
|
55
|
+
environmentId?: string;
|
|
56
|
+
}): Promise<Record<string, JsonValue>> | Record<string, JsonValue>;
|
|
57
|
+
resumeProject?(restore: Record<string, JsonValue>): Promise<void> | void;
|
|
58
|
+
}
|
|
59
|
+
/** Called when an action fires, so notifications can go out. */
|
|
60
|
+
export type SpendNotifier = (input: {
|
|
61
|
+
action: EnforcementAction;
|
|
62
|
+
budgetId: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
simulated: boolean;
|
|
65
|
+
released: boolean;
|
|
66
|
+
}) => Promise<void> | void;
|
|
67
|
+
export interface EnforcementHandlerOptions {
|
|
68
|
+
gate: SpendGate;
|
|
69
|
+
transport?: SpendEnforcementTransport;
|
|
70
|
+
notify?: SpendNotifier;
|
|
71
|
+
/**
|
|
72
|
+
* Multiplier applied to rate limits when `throttle_requests` fires.
|
|
73
|
+
* Half by default: enough to stop a runaway, gentle enough that a real user
|
|
74
|
+
* mostly does not notice.
|
|
75
|
+
*/
|
|
76
|
+
throttleFactor?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build the `{ apply, release }` pair that `runEnforcement` expects.
|
|
80
|
+
*
|
|
81
|
+
* Without a transport this still produces a fully working cap for builds,
|
|
82
|
+
* deploys, and notifications - the actions that need no box - and records the
|
|
83
|
+
* traffic-affecting ones in the gate so they are visible and reversible even
|
|
84
|
+
* though nothing at the edge changed. That is the honest degradation: the
|
|
85
|
+
* dashboard says what is in force, and `unsupported: true` in the restore
|
|
86
|
+
* payload says why it had no traffic effect.
|
|
87
|
+
*/
|
|
88
|
+
export declare function createEnforcementHandlers(options: EnforcementHandlerOptions): {
|
|
89
|
+
apply: EnforcementApplier;
|
|
90
|
+
release: EnforcementReleaser;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* A transport that records calls instead of making them.
|
|
94
|
+
*
|
|
95
|
+
* Not only for tests: this is what a `--dry-run` cap run uses to show an
|
|
96
|
+
* operator exactly which traffic changes a budget would make, before it is
|
|
97
|
+
* allowed to make them.
|
|
98
|
+
*/
|
|
99
|
+
export declare class RecordingSpendTransport implements SpendEnforcementTransport {
|
|
100
|
+
readonly calls: Array<{
|
|
101
|
+
method: string;
|
|
102
|
+
input: unknown;
|
|
103
|
+
}>;
|
|
104
|
+
private record;
|
|
105
|
+
throttleRequests(input: {
|
|
106
|
+
projectId?: string;
|
|
107
|
+
environmentId?: string;
|
|
108
|
+
factor: number;
|
|
109
|
+
}): Record<string, JsonValue>;
|
|
110
|
+
restoreRequests(restore: Record<string, JsonValue>): void;
|
|
111
|
+
suspendFunctions(input: {
|
|
112
|
+
projectId?: string;
|
|
113
|
+
environmentId?: string;
|
|
114
|
+
}): Record<string, JsonValue>;
|
|
115
|
+
resumeFunctions(restore: Record<string, JsonValue>): void;
|
|
116
|
+
serveStatic(input: {
|
|
117
|
+
projectId?: string;
|
|
118
|
+
environmentId?: string;
|
|
119
|
+
}): Record<string, JsonValue>;
|
|
120
|
+
restoreDynamic(restore: Record<string, JsonValue>): void;
|
|
121
|
+
suspendProject(input: {
|
|
122
|
+
projectId?: string;
|
|
123
|
+
environmentId?: string;
|
|
124
|
+
}): Record<string, JsonValue>;
|
|
125
|
+
resumeProject(restore: Record<string, JsonValue>): void;
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applying and lifting a cap.
|
|
3
|
+
*
|
|
4
|
+
* The design rule is that **enforcement is always reversible and never
|
|
5
|
+
* destructive**. A budget's job is to stop the meter, not to lose anyone's
|
|
6
|
+
* work. So no action here deletes a resource, drops a database, or discards a
|
|
7
|
+
* build; the strongest rung parks traffic behind a static response and even
|
|
8
|
+
* that records exactly what it changed so the restore is mechanical.
|
|
9
|
+
*
|
|
10
|
+
* A second rule: **an enforcement action is a request, not a fact**. Applying
|
|
11
|
+
* one means talking to a driver, and drivers fail. Every record therefore
|
|
12
|
+
* moves pending -> active -> released with an explicit failed state, and a
|
|
13
|
+
* failure to apply never silently reads as "capped".
|
|
14
|
+
*/
|
|
15
|
+
import type { JsonValue } from '../control-plane';
|
|
16
|
+
import type { Budget, EnforcementAction, EnforcementRecord, SpendDecision } from './model';
|
|
17
|
+
import type { SpendStore } from './store';
|
|
18
|
+
/** What an action does, in words an operator can act on. */
|
|
19
|
+
export declare const ENFORCEMENT_DESCRIPTIONS: Readonly<Record<EnforcementAction, string>>;
|
|
20
|
+
/**
|
|
21
|
+
* Actions that change what users see.
|
|
22
|
+
*
|
|
23
|
+
* These need an explicit opt-in on a production environment: silently taking a
|
|
24
|
+
* customer's site off the air to save $20 is worse than the bill.
|
|
25
|
+
*/
|
|
26
|
+
export declare const USER_VISIBLE_ACTIONS: readonly EnforcementAction[];
|
|
27
|
+
export interface EnforcementStep {
|
|
28
|
+
action: EnforcementAction;
|
|
29
|
+
description: string;
|
|
30
|
+
userVisible: boolean;
|
|
31
|
+
/** True when policy blocks this step from running automatically. */
|
|
32
|
+
requiresApproval: boolean;
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
export interface EnforcementPlan {
|
|
36
|
+
budgetId: string;
|
|
37
|
+
simulated: boolean;
|
|
38
|
+
/** Steps to apply, least disruptive first. */
|
|
39
|
+
apply: EnforcementStep[];
|
|
40
|
+
/** Steps to lift, most disruptive first, so a scope recovers safely. */
|
|
41
|
+
release: EnforcementStep[];
|
|
42
|
+
/** Steps withheld pending human approval. */
|
|
43
|
+
withheld: EnforcementStep[];
|
|
44
|
+
}
|
|
45
|
+
export interface EnforcementPolicy {
|
|
46
|
+
/**
|
|
47
|
+
* Environment kinds where user-visible actions need approval rather than
|
|
48
|
+
* running on their own. Production is in this list by default.
|
|
49
|
+
*/
|
|
50
|
+
approvalRequiredFor?: readonly string[];
|
|
51
|
+
/** The kind of the environment being enforced, e.g. `production`. */
|
|
52
|
+
environmentKind?: string;
|
|
53
|
+
/** Ceiling on automatic disruption; nothing above it applies unattended. */
|
|
54
|
+
maxAutomaticAction?: EnforcementAction;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Turn a decision into an ordered, approval-aware plan.
|
|
58
|
+
*
|
|
59
|
+
* Apply runs least-disruptive-first so the cheapest lever gets a chance to
|
|
60
|
+
* work before the expensive one. Release runs most-disruptive-first so a
|
|
61
|
+
* recovering project gets its traffic back before it gets its build queue.
|
|
62
|
+
*/
|
|
63
|
+
export declare function planEnforcement(decision: SpendDecision, policy?: EnforcementPolicy): EnforcementPlan;
|
|
64
|
+
/** Applies one action against the platform. Returns whatever the release needs. */
|
|
65
|
+
export type EnforcementApplier = (action: EnforcementAction, context: {
|
|
66
|
+
budget: Budget;
|
|
67
|
+
decision: SpendDecision;
|
|
68
|
+
record: EnforcementRecord;
|
|
69
|
+
}) => Promise<Record<string, JsonValue>> | Record<string, JsonValue>;
|
|
70
|
+
/** Undoes one action, given the restore payload the applier returned. */
|
|
71
|
+
export type EnforcementReleaser = (action: EnforcementAction, context: {
|
|
72
|
+
budget: Budget;
|
|
73
|
+
record: EnforcementRecord;
|
|
74
|
+
restore: Record<string, JsonValue>;
|
|
75
|
+
}) => Promise<void> | void;
|
|
76
|
+
export interface EnforcementRunResult {
|
|
77
|
+
applied: EnforcementRecord[];
|
|
78
|
+
released: EnforcementRecord[];
|
|
79
|
+
failed: Array<{
|
|
80
|
+
action: EnforcementAction;
|
|
81
|
+
error: string;
|
|
82
|
+
}>;
|
|
83
|
+
withheld: EnforcementStep[];
|
|
84
|
+
simulated: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run a plan against the store, calling out to the driver for each step.
|
|
88
|
+
*
|
|
89
|
+
* A dry-run budget records everything and calls nothing, which is the point:
|
|
90
|
+
* an operator can watch a cap for a month before letting it touch anything.
|
|
91
|
+
* A step whose applier throws lands in `failed` and its record in `failed`
|
|
92
|
+
* state - the caller can retry next cycle, and nothing pretends the cap is on.
|
|
93
|
+
*/
|
|
94
|
+
export declare function runEnforcement(store: SpendStore, budget: Budget, decision: SpendDecision, plan: EnforcementPlan, handlers: {
|
|
95
|
+
apply: EnforcementApplier;
|
|
96
|
+
release: EnforcementReleaser;
|
|
97
|
+
}): Promise<EnforcementRunResult>;
|
|
98
|
+
/** The strongest action currently in force for a scope, if any. */
|
|
99
|
+
export declare function strongestActiveAction(records: readonly EnforcementRecord[]): EnforcementAction | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Whether an operation is allowed right now.
|
|
102
|
+
*
|
|
103
|
+
* The gate every caller should ask before starting work that costs money. It
|
|
104
|
+
* fails *open* on an unknown operation: a cap should never block something it
|
|
105
|
+
* was not designed to reason about.
|
|
106
|
+
*/
|
|
107
|
+
export declare function isOperationAllowed(operation: 'build' | 'deploy' | 'function_invoke' | 'request', active: readonly EnforcementRecord[]): {
|
|
108
|
+
allowed: boolean;
|
|
109
|
+
blockedBy?: EnforcementAction;
|
|
110
|
+
reason?: string;
|
|
111
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cap evaluator: budget + usage in, decision out.
|
|
3
|
+
*
|
|
4
|
+
* Pure and clock-injected, so every rung of the ladder is testable without a
|
|
5
|
+
* database or a real month passing. The store applies the result; this file
|
|
6
|
+
* only decides.
|
|
7
|
+
*
|
|
8
|
+
* Three details are what separate a usable cap from an annoying one:
|
|
9
|
+
*
|
|
10
|
+
* - **Hysteresis.** A scope parked on the line would otherwise enforce and
|
|
11
|
+
* release every evaluation cycle, paging someone each time. An action is
|
|
12
|
+
* only lifted once spend drops a configured margin below its trigger.
|
|
13
|
+
* - **Grace.** A single spiky minute should not suspend production. A breach
|
|
14
|
+
* must persist for `graceSeconds` before enforcement runs; notifications
|
|
15
|
+
* still fire immediately, because a warning has no blast radius.
|
|
16
|
+
* - **Confidence.** Projection-based rungs are ignored while the forecast is
|
|
17
|
+
* still noise. Enforcing on ten minutes of extrapolation is how you take a
|
|
18
|
+
* site down over nothing.
|
|
19
|
+
*/
|
|
20
|
+
import type { Budget, BudgetWindow, EnforcementAction, SpendDecision, SpendLevel } from './model';
|
|
21
|
+
/** Below this confidence a forecast cannot trigger anything but a notification. */
|
|
22
|
+
export declare const MIN_PROJECTION_CONFIDENCE = 0.35;
|
|
23
|
+
export interface EvaluateInput {
|
|
24
|
+
budget: Budget;
|
|
25
|
+
window: BudgetWindow;
|
|
26
|
+
/** Spend so far in the window, cents. */
|
|
27
|
+
actualCents: number;
|
|
28
|
+
series?: ReadonlyArray<{
|
|
29
|
+
bucketStart: string;
|
|
30
|
+
costCents: number;
|
|
31
|
+
}>;
|
|
32
|
+
/** Actions currently in force, so the evaluator can decide what to release. */
|
|
33
|
+
activeActions?: readonly EnforcementAction[];
|
|
34
|
+
/**
|
|
35
|
+
* When the current breach was first observed. Grace is measured from here.
|
|
36
|
+
*
|
|
37
|
+
* Omitting it on a budget with a grace period means "first seen now", so the
|
|
38
|
+
* first cycle of a breach never enforces. That is the safe reading: a caller
|
|
39
|
+
* that is not tracking the clock has no way to know the breach has lasted.
|
|
40
|
+
*/
|
|
41
|
+
breachingSince?: string;
|
|
42
|
+
now?: Date;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The limit a budget's percentages are measured against.
|
|
46
|
+
*
|
|
47
|
+
* The hard limit governs when there is one, so a ladder written as "100% =
|
|
48
|
+
* block deploys" means the same thing whether or not a soft limit exists.
|
|
49
|
+
*/
|
|
50
|
+
export declare function governingLimitCents(budget: Budget): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Evaluate one budget.
|
|
53
|
+
*
|
|
54
|
+
* The returned `actions` are what *should* be in force; `releases` are actions
|
|
55
|
+
* currently in force that should not be. A caller that applies both converges
|
|
56
|
+
* on the right state no matter how many cycles it missed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function evaluateBudget(input: EvaluateInput): SpendDecision;
|
|
59
|
+
/**
|
|
60
|
+
* Combine decisions for overlapping budgets on one scope.
|
|
61
|
+
*
|
|
62
|
+
* The strictest wins: an org-wide cap must not be loosened by a permissive
|
|
63
|
+
* project budget, and an environment cap must be able to tighten. Releases only
|
|
64
|
+
* survive if no other budget still wants the action.
|
|
65
|
+
*/
|
|
66
|
+
export declare function mergeDecisions(decisions: readonly SpendDecision[]): {
|
|
67
|
+
actions: EnforcementAction[];
|
|
68
|
+
releases: EnforcementAction[];
|
|
69
|
+
level: SpendLevel;
|
|
70
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|