@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,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 & secrets</a>
|
|
@@ -793,6 +794,7 @@ video {
|
|
|
793
794
|
|
|
794
795
|
<section class="nav-group">
|
|
795
796
|
<h2>Infrastructure</h2>
|
|
797
|
+
<a class="" href="/infrastructure/topology?env=production">Infrastructure map</a>
|
|
796
798
|
<a class="" href="/data/services?env=production">Data services</a>
|
|
797
799
|
<a class="" href="/data/volumes?env=production">Persistent volumes</a>
|
|
798
800
|
<a href="/server/firewall?env=production">Network & firewall</a><a href="/server/ssh-keys?env=production">SSH keys</a>
|