@k2b/cloud 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -4
- package/scripts/build.ts +5 -1
- package/scripts/preload.ts +5 -1
- package/src/_internal/app-access-manager-sql.ts +10 -0
- package/src/_internal/bounded-json.ts +7 -1
- package/src/_internal/capabilities.ts +8 -0
- package/src/_internal/define-app.ts +69 -2
- package/src/_internal/heartbeat.ts +21 -0
- package/src/_internal/nats-connection.ts +10 -9
- package/src/_internal/postgres-application-name.ts +11 -0
- package/src/_internal/process-sync.ts +15 -6
- package/src/_internal/public-surface.ts +76 -0
- package/src/_internal/purpose-name.ts +7 -0
- package/src/_internal/readiness.ts +6 -1
- package/src/_internal/registry-validation.ts +1 -0
- package/src/_internal/registry.ts +49 -3
- package/src/_internal/resource-principals.ts +4 -34
- package/src/_internal/workload-credential.ts +154 -0
- package/src/_internal/workload-protocol.ts +36 -0
- package/src/ai/application-tasks.ts +5 -2
- package/src/ai/chart-block.ts +36 -711
- package/src/ai/default-tool-contracts.ts +21 -259
- package/src/api/admin-app-domains.ts +62 -0
- package/src/api/admin-app-keys.ts +66 -0
- package/src/api/admin-core-settings.ts +2 -0
- package/src/api/admin-identity.ts +27 -1
- package/src/api/admin-settings-env.ts +120 -0
- package/src/api/app-bootstrap.ts +131 -0
- package/src/api/capabilities.ts +17 -4
- package/src/api/index.ts +8 -0
- package/src/api/settings-ops.ts +35 -0
- package/src/app-key.ts +125 -0
- package/src/browser/CloudResourceSearch.tsx +48 -41
- package/src/capabilities/discussion-providers.ts +3 -2
- package/src/capabilities/resource-readers.ts +13 -2
- package/src/capabilities/response.ts +6 -2
- package/src/capabilities/server-invoke.ts +73 -29
- package/src/capabilities/server.ts +1 -1
- package/src/capabilities/types.ts +1 -1
- package/src/cli/admin/app-credentials.ts +32 -7
- package/src/cli/admin/app-domains.ts +140 -0
- package/src/cli/admin/app-keys.ts +226 -0
- package/src/cli/admin/index.ts +109 -84
- package/src/cli/admin/settings.ts +62 -0
- package/src/config/define-env.ts +63 -8
- package/src/config/env.ts +34 -22
- package/src/config/index.ts +12 -1
- package/src/config/workflow-ai.ts +28 -0
- package/src/contracts/app-domains.ts +91 -0
- package/src/contracts/app-status.ts +104 -0
- package/src/contracts/capabilities.ts +22 -0
- package/src/contracts/index.ts +1 -0
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/settings-env.ts +21 -0
- package/src/contracts/settings-types.ts +6 -0
- package/src/index.ts +10 -0
- package/src/runtime/bootstrap-client.ts +195 -0
- package/src/runtime/bootstrap-protocol.ts +60 -0
- package/src/runtime/clients.ts +72 -0
- package/src/runtime/connections.ts +46 -0
- package/src/runtime/resolve-connections.ts +91 -0
- package/src/server/app-key.ts +14 -0
- package/src/server/client-ip.ts +1 -1
- package/src/server/index.ts +1 -0
- package/src/services/app-access-management.ts +1 -8
- package/src/services/app-admin-facts.ts +160 -0
- package/src/services/app-bootstrap.ts +134 -0
- package/src/services/app-domain-check.ts +152 -0
- package/src/services/app-domains.ts +172 -0
- package/src/services/app-keys.ts +301 -0
- package/src/services/downstream-budget.ts +2 -2
- package/src/services/gateway.ts +6 -0
- package/src/services/identity/index.ts +3 -0
- package/src/services/identity/resource-readers.ts +80 -0
- package/src/services/identity/runtime-config.ts +14 -6
- package/src/services/identity/workload-runtime.ts +253 -0
- package/src/services/index.ts +8 -1
- package/src/services/links/index.ts +16 -0
- package/src/services/links/runtime.ts +44 -0
- package/src/services/links/store.ts +105 -0
- package/src/services/logging/index.ts +7 -5
- package/src/services/logging/redaction.ts +6 -1
- package/src/services/logging/trace.ts +8 -8
- package/src/services/outgoing-mail/resolve-host.ts +5 -4
- package/src/services/request-cache-redis.ts +2 -2
- package/src/services/service-account-credentials.ts +63 -21
- package/src/services/settings/api.ts +3 -3
- package/src/services/settings/binding.ts +45 -0
- package/src/services/settings/core-settings.ts +119 -1
- package/src/services/settings/crypto.ts +2 -2
- package/src/services/settings/defaults.ts +16 -2
- package/src/services/settings/env-status.ts +69 -0
- package/src/services/settings/index.ts +69 -26
- package/src/services/settings/store.ts +78 -8
- package/src/services/settings/validation.ts +40 -0
- package/src/shared/index.ts +11 -10
- package/src/shared/markdown/extensions/tables.ts +10 -1
- package/src/ssr/admin-navigation.ts +0 -1
- package/src/ssr/platform-messages.ts +0 -2
- package/src/start.ts +57 -0
- package/src/workflows/ai/index.ts +1 -0
- package/src/workflows/ai/runtime.ts +16 -8
- package/src/workflows/ai/settings.ts +5 -0
- package/src/shared/markdown/formula.ts +0 -1283
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { chartDataIssue,
|
|
2
|
+
import { chartDataIssue, chartDrawingIssue, cloudAiChartMarks } from "./chart-block";
|
|
3
3
|
|
|
4
4
|
const ToneSchema = z.enum(["neutral", "blue", "teal", "green", "amber", "red"]);
|
|
5
5
|
|
|
@@ -28,261 +28,30 @@ export const CLOUD_AI_VIEW_MAX_MARKS = CLOUD_AI_CHART_MAX_SERIES * CLOUD_AI_CHAR
|
|
|
28
28
|
*/
|
|
29
29
|
export const CLOUD_AI_GROUP_MAX_CHARTS = 6;
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Intl.NumberFormat supports this precision; table formats already use 0..10.
|
|
44
|
-
decimals: z.number().int().min(0).max(10).optional(),
|
|
45
|
-
compact: z.boolean().optional(),
|
|
46
|
-
})
|
|
47
|
-
.superRefine((value, ctx) => {
|
|
48
|
-
if ((value.style === "currency") !== (value.currency !== undefined))
|
|
49
|
-
ctx.addIssue({ code: "custom", path: ["currency"], message: "Currency is required exactly when style is currency." });
|
|
50
|
-
});
|
|
51
|
-
const dateFormatSchema = () => z.strictObject({ date: z.enum(["day", "minute", "second"]) });
|
|
52
|
-
export const cloudAiFormatSchema = () => z.union([cloudAiNumberFormatSchema(), dateFormatSchema()]);
|
|
53
|
-
export type CloudAiNumberFormat = z.output<ReturnType<typeof cloudAiNumberFormatSchema>>;
|
|
54
|
-
export type CloudAiDateFormat = z.output<ReturnType<typeof dateFormatSchema>>;
|
|
55
|
-
export type CloudAiFormat = CloudAiNumberFormat | CloudAiDateFormat;
|
|
31
|
+
import {
|
|
32
|
+
type ChartInput as CloudAiChartInput,
|
|
33
|
+
type ChartDateFormat as CloudAiDateFormat,
|
|
34
|
+
type ChartFormat as CloudAiFormat,
|
|
35
|
+
type ChartNumberFormat as CloudAiNumberFormat,
|
|
36
|
+
chartKinds,
|
|
37
|
+
chartParts,
|
|
38
|
+
chartSchema,
|
|
39
|
+
chartKindSchema as cloudAiChartKindSchema,
|
|
40
|
+
chartFormatSchema as cloudAiFormatSchema,
|
|
41
|
+
chartNumberFormatSchema as cloudAiNumberFormatSchema,
|
|
42
|
+
} from "@k2b/editor/charts";
|
|
56
43
|
|
|
57
|
-
export
|
|
58
|
-
|
|
44
|
+
export type { CloudAiChartInput, CloudAiDateFormat, CloudAiFormat, CloudAiNumberFormat };
|
|
45
|
+
// Portable formats and chart types retain their existing Cloud export names.
|
|
46
|
+
export { cloudAiChartKindSchema, cloudAiFormatSchema, cloudAiNumberFormatSchema };
|
|
59
47
|
|
|
60
|
-
const
|
|
61
|
-
const ChartLabel = limits ? z.string().trim().min(1).max(80) : z.union([z.string(), z.number().transform(String)]);
|
|
62
|
-
const ChartTooltip = z
|
|
63
|
-
.strictObject({ label: ChartLabel.optional(), format: cloudAiNumberFormatSchema().optional() })
|
|
64
|
-
.describe("All kinds: what tooltips and the data table call the value and how they format it; axes keep their own format.");
|
|
65
|
-
// Missing observations stay numeric so the renderer can omit them with its finite-value checks.
|
|
66
|
-
const ChartNumber = limits
|
|
67
|
-
? z.number()
|
|
68
|
-
: z
|
|
69
|
-
.custom<number>((value) => typeof value === "number", "Expected a number, null or undefined.")
|
|
70
|
-
.nullable()
|
|
71
|
-
.optional()
|
|
72
|
-
.transform((value) => value ?? Number.NaN);
|
|
73
|
-
const list = <T extends z.ZodType>(item: T, min: number, max?: number, message?: string) => {
|
|
74
|
-
const array = z.array(item);
|
|
75
|
-
const bounded = limits && min > 0 ? array.min(min) : array;
|
|
76
|
-
return limits && max !== undefined ? bounded.max(max, message) : bounded;
|
|
77
|
-
};
|
|
78
|
-
const ChartDate = z
|
|
79
|
-
.string()
|
|
80
|
-
.regex(/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2})?)?$/)
|
|
81
|
-
.refine((value) => chartDateTime(value) !== undefined, "Not a valid date.");
|
|
82
|
-
const ChartPoint = z.object({
|
|
83
|
-
x: z.union([ChartNumber, ChartDate]).describe("line, scatter: number or YYYY-MM-DD[THH:mm[:ss]], wall-clock time without an offset."),
|
|
84
|
-
y: ChartNumber.describe("line, scatter: value."),
|
|
85
|
-
});
|
|
86
|
-
// As many lines as series a reader can tell apart.
|
|
87
|
-
const ChartReferences = list(
|
|
88
|
-
z.object({ value: z.union([z.number(), ChartDate]), axis: z.enum(["x", "y"]).optional(), label: ChartLabel.optional() }),
|
|
89
|
-
0,
|
|
90
|
-
CLOUD_AI_CHART_MAX_SERIES,
|
|
91
|
-
`Use at most ${CLOUD_AI_CHART_MAX_SERIES} references.`,
|
|
92
|
-
);
|
|
93
|
-
// One palette color per band.
|
|
94
|
-
const ChartThresholds = list(
|
|
95
|
-
z.object({ value: z.number(), label: ChartLabel.optional() }),
|
|
96
|
-
1,
|
|
97
|
-
CLOUD_AI_CHART_MAX_SERIES,
|
|
98
|
-
`Use at most ${CLOUD_AI_CHART_MAX_SERIES} thresholds.`,
|
|
99
|
-
);
|
|
100
|
-
return {
|
|
101
|
-
ChartLabel,
|
|
102
|
-
ChartTooltip,
|
|
103
|
-
ChartNumber,
|
|
104
|
-
list,
|
|
105
|
-
ChartReferences,
|
|
106
|
-
ChartThresholds,
|
|
107
|
-
ChartAxis: z.object({
|
|
108
|
-
label: ChartLabel.optional(),
|
|
109
|
-
domain: z.array(z.number()).length(2).optional().describe("Exact [min, max]; must contain every value and reference."),
|
|
110
|
-
ticks: (limits ? z.number().int().min(2).max(12) : z.number().int().min(2)).optional(),
|
|
111
|
-
scale: z.enum(["linear", "log"]).optional().describe("log needs values above zero."),
|
|
112
|
-
format: cloudAiFormatSchema().optional(),
|
|
113
|
-
}),
|
|
114
|
-
ChartCategory: z.object({ label: ChartLabel, value: ChartNumber }),
|
|
115
|
-
ChartSlice: z.object({ label: ChartLabel, value: limits ? z.number().min(0) : ChartNumber }),
|
|
116
|
-
ChartSeries: (points: z.ZodArray<typeof ChartPoint>) =>
|
|
117
|
-
z.object({
|
|
118
|
-
label: ChartLabel.optional(),
|
|
119
|
-
data: points,
|
|
120
|
-
role: z.literal("reference").optional(),
|
|
121
|
-
}),
|
|
122
|
-
ChartPoints: list(ChartPoint, 0, CLOUD_AI_CHART_MAX_VALUES),
|
|
123
|
-
ChartBins: z
|
|
124
|
-
.union([
|
|
125
|
-
limits ? z.number().int().min(1).max(CLOUD_AI_CHART_MAX_VALUES) : z.number().int().min(1),
|
|
126
|
-
limits
|
|
127
|
-
? z
|
|
128
|
-
.array(z.number())
|
|
129
|
-
.min(2)
|
|
130
|
-
.max(CLOUD_AI_CHART_MAX_VALUES + 1)
|
|
131
|
-
: z.array(z.number()).min(2),
|
|
132
|
-
])
|
|
133
|
-
.describe("histogram: a bin count or ascending bin edges."),
|
|
134
|
-
ChartCommon: {
|
|
135
|
-
title: (limits ? z.string().trim().min(1).max(160) : z.string()).describe("All kinds and groups: chart title."),
|
|
136
|
-
subtitle: (limits ? z.string().trim().min(1).max(300) : z.string())
|
|
137
|
-
.optional()
|
|
138
|
-
.describe("All kinds and groups: unit, period or source."),
|
|
139
|
-
},
|
|
140
|
-
ChartUnit: limits ? z.string().trim().min(1).max(12) : z.string(),
|
|
141
|
-
ChartBox: z.object({ label: ChartLabel, values: list(ChartNumber, 1) }),
|
|
142
|
-
ChartCell: z.object({ x: ChartLabel, y: ChartLabel, value: ChartNumber }),
|
|
143
|
-
ChartBarGauge: z.object({ label: ChartLabel, value: ChartNumber, min: z.number().optional(), max: z.number().optional() }),
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const chartKinds = (limits = true) => {
|
|
148
|
-
const {
|
|
149
|
-
ChartNumber,
|
|
150
|
-
list,
|
|
151
|
-
ChartAxis,
|
|
152
|
-
ChartBins,
|
|
153
|
-
ChartCategory,
|
|
154
|
-
ChartCommon,
|
|
155
|
-
ChartTooltip,
|
|
156
|
-
ChartLabel,
|
|
157
|
-
ChartPoints,
|
|
158
|
-
ChartSeries,
|
|
159
|
-
ChartSlice,
|
|
160
|
-
ChartReferences,
|
|
161
|
-
ChartThresholds,
|
|
162
|
-
ChartUnit,
|
|
163
|
-
ChartBox,
|
|
164
|
-
ChartCell,
|
|
165
|
-
ChartBarGauge,
|
|
166
|
-
} = chartParts(limits);
|
|
167
|
-
const ChartKindCommon = { ...ChartCommon, tooltip: ChartTooltip.optional() };
|
|
168
|
-
const numberFormat = cloudAiNumberFormatSchema();
|
|
169
|
-
return z.discriminatedUnion("kind", [
|
|
170
|
-
z.object({
|
|
171
|
-
kind: z.literal("bar"),
|
|
172
|
-
...ChartKindCommon,
|
|
173
|
-
data: list(ChartCategory, 1, CLOUD_AI_CHART_MAX_VALUES),
|
|
174
|
-
xAxis: ChartAxis.pick({ label: true }).strict().optional(),
|
|
175
|
-
yAxis: ChartAxis.optional(),
|
|
176
|
-
colorByBar: z.boolean().optional(),
|
|
177
|
-
showValues: z.boolean().optional(),
|
|
178
|
-
legend: z.boolean().optional(),
|
|
179
|
-
references: ChartReferences.optional(),
|
|
180
|
-
}),
|
|
181
|
-
z.object({
|
|
182
|
-
kind: z.literal("line"),
|
|
183
|
-
...ChartKindCommon,
|
|
184
|
-
series: list(
|
|
185
|
-
ChartSeries(
|
|
186
|
-
limits
|
|
187
|
-
? ChartPoints.min(2, "A line needs at least two points per series; show single values as a bar or scatter chart.")
|
|
188
|
-
: ChartPoints,
|
|
189
|
-
).extend({ style: z.enum(["solid", "dashed", "dotted"]).optional() }),
|
|
190
|
-
1,
|
|
191
|
-
CLOUD_AI_CHART_MAX_SERIES,
|
|
192
|
-
),
|
|
193
|
-
xAxis: ChartAxis.optional(),
|
|
194
|
-
yAxis: ChartAxis.optional(),
|
|
195
|
-
legend: z.boolean().optional(),
|
|
196
|
-
area: z.boolean().optional(),
|
|
197
|
-
smooth: z.boolean().optional(),
|
|
198
|
-
references: ChartReferences.optional(),
|
|
199
|
-
}),
|
|
200
|
-
z.object({
|
|
201
|
-
kind: z.literal("scatter"),
|
|
202
|
-
...ChartKindCommon,
|
|
203
|
-
series: list(ChartSeries(limits ? ChartPoints.min(1) : ChartPoints), 1, CLOUD_AI_CHART_MAX_SERIES),
|
|
204
|
-
xAxis: ChartAxis.optional(),
|
|
205
|
-
yAxis: ChartAxis.optional(),
|
|
206
|
-
legend: z.boolean().optional(),
|
|
207
|
-
references: ChartReferences.optional(),
|
|
208
|
-
}),
|
|
209
|
-
z.object({
|
|
210
|
-
kind: z.enum(["pie", "donut"]),
|
|
211
|
-
...ChartKindCommon,
|
|
212
|
-
data: list(ChartSlice, 1, CLOUD_AI_CHART_MAX_VALUES),
|
|
213
|
-
legend: z.boolean().optional(),
|
|
214
|
-
showLabels: z.boolean().optional(),
|
|
215
|
-
format: numberFormat.optional(),
|
|
216
|
-
}),
|
|
217
|
-
z.object({
|
|
218
|
-
kind: z.literal("histogram"),
|
|
219
|
-
...ChartKindCommon,
|
|
220
|
-
data: list(ChartNumber, 1, CLOUD_AI_CHART_MAX_VALUES),
|
|
221
|
-
bins: ChartBins.optional(),
|
|
222
|
-
references: ChartReferences.optional(),
|
|
223
|
-
}),
|
|
224
|
-
z.object({
|
|
225
|
-
kind: z.literal("gauge"),
|
|
226
|
-
...ChartKindCommon,
|
|
227
|
-
value: ChartNumber,
|
|
228
|
-
min: z.number().optional(),
|
|
229
|
-
max: z.number().optional(),
|
|
230
|
-
label: ChartLabel.optional(),
|
|
231
|
-
unit: ChartUnit.optional(),
|
|
232
|
-
format: numberFormat.optional(),
|
|
233
|
-
thresholds: ChartThresholds.optional(),
|
|
234
|
-
}),
|
|
235
|
-
z.object({
|
|
236
|
-
kind: z.literal("sparkline"),
|
|
237
|
-
...ChartKindCommon,
|
|
238
|
-
data: list(ChartNumber, 2, CLOUD_AI_CHART_MAX_VALUES),
|
|
239
|
-
area: z.boolean().optional(),
|
|
240
|
-
}),
|
|
241
|
-
z.object({
|
|
242
|
-
kind: z.literal("boxplot"),
|
|
243
|
-
...ChartKindCommon,
|
|
244
|
-
data: list(ChartBox, 1, CLOUD_AI_CHART_MAX_VALUES),
|
|
245
|
-
yAxis: ChartAxis.optional(),
|
|
246
|
-
references: ChartReferences.optional(),
|
|
247
|
-
}),
|
|
248
|
-
z.object({
|
|
249
|
-
kind: z.literal("heatmap"),
|
|
250
|
-
...ChartKindCommon,
|
|
251
|
-
data: list(ChartCell, 1, CLOUD_AI_VIEW_MAX_MARKS),
|
|
252
|
-
format: numberFormat.optional(),
|
|
253
|
-
showValues: z.boolean().optional(),
|
|
254
|
-
min: z.number().optional(),
|
|
255
|
-
max: z.number().optional(),
|
|
256
|
-
}),
|
|
257
|
-
z.object({
|
|
258
|
-
kind: z.literal("barGauge"),
|
|
259
|
-
...ChartKindCommon,
|
|
260
|
-
// The shared renderer draws 280 units high and a bar gauge row is 28 units, so 8 rows fit without a taller drawing.
|
|
261
|
-
data: list(ChartBarGauge, 1, CLOUD_AI_CHART_MAX_SERIES, `Use at most ${CLOUD_AI_CHART_MAX_SERIES} bar gauge items.`),
|
|
262
|
-
min: z.number().optional(),
|
|
263
|
-
max: z.number().optional(),
|
|
264
|
-
unit: ChartUnit.optional(),
|
|
265
|
-
format: numberFormat.optional(),
|
|
266
|
-
thresholds: ChartThresholds.optional(),
|
|
267
|
-
}),
|
|
268
|
-
z.object({
|
|
269
|
-
kind: z.literal("stat"),
|
|
270
|
-
...ChartKindCommon,
|
|
271
|
-
value: ChartNumber,
|
|
272
|
-
format: numberFormat.optional(),
|
|
273
|
-
unit: ChartUnit.optional(),
|
|
274
|
-
delta: limits ? z.number().optional() : ChartNumber,
|
|
275
|
-
better: z.enum(["up", "down"]).optional(),
|
|
276
|
-
sparkline: list(ChartNumber, 2, CLOUD_AI_CHART_MAX_VALUES).optional(),
|
|
277
|
-
}),
|
|
278
|
-
]);
|
|
279
|
-
};
|
|
48
|
+
const chartLimits = { maxSeries: CLOUD_AI_CHART_MAX_SERIES, maxValues: CLOUD_AI_CHART_MAX_VALUES, maxMarks: CLOUD_AI_VIEW_MAX_MARKS };
|
|
280
49
|
|
|
281
50
|
/** Checks run on complete input only, so out-of-range input fails on its bounds before anything is computed or drawn. */
|
|
282
51
|
const whenValid = { when: (payload: { issues: readonly unknown[] }) => payload.issues.length === 0 };
|
|
283
52
|
|
|
284
53
|
export const cloudAiChartSchema = () =>
|
|
285
|
-
chartKinds().superRefine((chart, context) => {
|
|
54
|
+
chartKinds(chartLimits).superRefine((chart, context) => {
|
|
286
55
|
const total = cloudAiChartMarks(chart);
|
|
287
56
|
const issue =
|
|
288
57
|
total > CLOUD_AI_VIEW_MAX_MARKS
|
|
@@ -291,16 +60,9 @@ export const cloudAiChartSchema = () =>
|
|
|
291
60
|
if (issue) context.addIssue({ code: "custom", message: issue });
|
|
292
61
|
}, whenValid);
|
|
293
62
|
/** Apps retain the renderer's permissive data contract, except values that would stall tick generation. */
|
|
294
|
-
export const cloudAiAppChartSchema =
|
|
295
|
-
chartKinds(false).superRefine((chart, context) => {
|
|
296
|
-
const issue = chartSteppingIssue(chart);
|
|
297
|
-
if (issue) context.addIssue({ code: "custom", message: issue });
|
|
298
|
-
}, whenValid);
|
|
63
|
+
export const cloudAiAppChartSchema = chartSchema;
|
|
299
64
|
const ChartSchema = /* @__PURE__ */ cloudAiChartSchema();
|
|
300
65
|
|
|
301
|
-
/** One chart as the renderers read it: a `cloud.chart()` kind with its options. */
|
|
302
|
-
export type CloudAiChartInput = z.output<ReturnType<typeof chartKinds>>;
|
|
303
|
-
|
|
304
66
|
/**
|
|
305
67
|
* Reads one chart without drawing it; undefined when it is not a chart. Writers have drawn it once already, so
|
|
306
68
|
* renderers draw only for display.
|
|
@@ -311,7 +73,7 @@ export const parseCloudAiChartInput = (value: unknown): CloudAiChartInput | unde
|
|
|
311
73
|
};
|
|
312
74
|
|
|
313
75
|
export const cloudAiGroupSchema = () => {
|
|
314
|
-
const { ChartCommon } = chartParts();
|
|
76
|
+
const { ChartCommon } = chartParts(chartLimits);
|
|
315
77
|
return z
|
|
316
78
|
.strictObject({
|
|
317
79
|
...ChartCommon,
|
|
@@ -354,7 +116,7 @@ const flatChartSchema = () => {
|
|
|
354
116
|
ChartThresholds,
|
|
355
117
|
ChartUnit,
|
|
356
118
|
ChartTooltip,
|
|
357
|
-
} = chartParts();
|
|
119
|
+
} = chartParts(chartLimits);
|
|
358
120
|
return z.object({
|
|
359
121
|
kind: cloudAiChartKindSchema().optional().describe("Single charts: kind. Omit for groups."),
|
|
360
122
|
...ChartCommon,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { type AuthContext, auth, respond, v } from "../server";
|
|
5
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
6
|
+
import { appDomains } from "../services/app-domains";
|
|
7
|
+
import { logger } from "../services/logging";
|
|
8
|
+
|
|
9
|
+
const appParams = z.object({ appId: z.string().regex(/^[a-z][a-z0-9-]{0,79}$/) });
|
|
10
|
+
const domainParams = appParams.extend({ host: z.string().max(1024) });
|
|
11
|
+
const host = z.string().max(1024);
|
|
12
|
+
const addInput = z.object({ host, redirectTo: host.optional() }).strict();
|
|
13
|
+
const setInput = z.object({ redirectTo: host.nullable() }).strict();
|
|
14
|
+
const describe = (summary: string) => describeRoute({ tags: ["App domains"], summary });
|
|
15
|
+
|
|
16
|
+
export const createAdminAppDomainRoutes = (
|
|
17
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
18
|
+
service: typeof appDomains = appDomains,
|
|
19
|
+
) =>
|
|
20
|
+
new Hono<AuthContext>()
|
|
21
|
+
.use("/:appId/domains/*", noTransform())
|
|
22
|
+
.use("/:appId/domains/*", authenticate)
|
|
23
|
+
.use("/:appId/domains/*", async (c, next) => {
|
|
24
|
+
c.header("Cache-Control", "no-store");
|
|
25
|
+
if (c.req.method !== "GET") return auth.requireOAuthScope("admin")(c, next);
|
|
26
|
+
return next();
|
|
27
|
+
})
|
|
28
|
+
.get("/:appId/domains", describe("List assigned domains and their last check"), v("param", appParams), (c) =>
|
|
29
|
+
respond(c, () => service.list({ actor: c.get("user"), ...c.req.valid("param") })),
|
|
30
|
+
)
|
|
31
|
+
.post(
|
|
32
|
+
"/:appId/domains",
|
|
33
|
+
describe("Assign a domain and start its first check in the background"),
|
|
34
|
+
v("param", appParams),
|
|
35
|
+
v("json", addInput),
|
|
36
|
+
async (c) => {
|
|
37
|
+
const input = { actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("json") };
|
|
38
|
+
const result = await service.add(input);
|
|
39
|
+
if (result.ok)
|
|
40
|
+
void service.check({ ...input, host: result.data.host }).catch((error: unknown) => {
|
|
41
|
+
logger("core:app-domains").error("Initial domain check failed; retry through the domain check endpoint", {
|
|
42
|
+
host: result.data.host,
|
|
43
|
+
error: error instanceof Error ? error.message : String(error),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
return respond(c, result, 201);
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
.patch("/:appId/domains/:host", describe("Change or clear the domain redirect"), v("param", domainParams), v("json", setInput), (c) =>
|
|
50
|
+
respond(c, () => service.set({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("json") })),
|
|
51
|
+
)
|
|
52
|
+
.delete("/:appId/domains/:host", describe("Remove an unreferenced domain assignment"), v("param", domainParams), (c) =>
|
|
53
|
+
respond(c, () => service.remove({ actor: c.get("user"), ...c.req.valid("param") })),
|
|
54
|
+
)
|
|
55
|
+
.post(
|
|
56
|
+
"/:appId/domains/:host/check",
|
|
57
|
+
describe("Check DNS, certificate and gateway reachability, at most once per 60 seconds"),
|
|
58
|
+
v("param", domainParams),
|
|
59
|
+
(c) => respond(c, () => service.check({ actor: c.get("user"), ...c.req.valid("param") })),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export default createAdminAppDomainRoutes();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { validateAppKeyAppId } from "../app-key";
|
|
6
|
+
import { type AuthContext, auth, respond, v } from "../server";
|
|
7
|
+
import { noTransform } from "../server/middleware/no-transform";
|
|
8
|
+
import { appKeys } from "../services/app-keys";
|
|
9
|
+
|
|
10
|
+
const appId = z.string().refine((value) => {
|
|
11
|
+
try {
|
|
12
|
+
validateAppKeyAppId(value);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}, "Invalid or reserved app ID");
|
|
18
|
+
const params = z.object({ appId });
|
|
19
|
+
const pagination = z.object({
|
|
20
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
21
|
+
perPage: z.coerce.number().int().min(1).max(500).default(100),
|
|
22
|
+
});
|
|
23
|
+
const input = z
|
|
24
|
+
.object({ publicKey: z.string().length(43), name: z.string().trim().min(1).max(120).optional(), expiresAt: z.iso.datetime().optional() })
|
|
25
|
+
.strict();
|
|
26
|
+
|
|
27
|
+
export const createAdminAppKeyRoutes = (
|
|
28
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
29
|
+
service: typeof appKeys = appKeys,
|
|
30
|
+
) =>
|
|
31
|
+
new Hono<AuthContext>()
|
|
32
|
+
.use("*", noTransform())
|
|
33
|
+
.use(authenticate)
|
|
34
|
+
.use("*", async (c, next) => {
|
|
35
|
+
c.header("Cache-Control", "no-store");
|
|
36
|
+
if (["POST", "DELETE"].includes(c.req.method)) return auth.requireOAuthScope("write", "admin")(c, next);
|
|
37
|
+
return next();
|
|
38
|
+
})
|
|
39
|
+
.get(
|
|
40
|
+
"/keys",
|
|
41
|
+
describeRoute({ tags: ["App keys"], summary: "List apps with enrolled keys and the installation ID" }),
|
|
42
|
+
v("query", pagination),
|
|
43
|
+
(c) => respond(c, () => service.listApps({ actor: c.get("user"), ...c.req.valid("query") })),
|
|
44
|
+
)
|
|
45
|
+
.get(
|
|
46
|
+
"/:appId/keys",
|
|
47
|
+
describeRoute({ tags: ["App keys"], summary: "List app public keys and lifecycle metadata" }),
|
|
48
|
+
v("param", params),
|
|
49
|
+
v("query", pagination),
|
|
50
|
+
(c) => respond(c, () => service.list({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("query") })),
|
|
51
|
+
)
|
|
52
|
+
.post(
|
|
53
|
+
"/:appId/keys",
|
|
54
|
+
describeRoute({ tags: ["App keys"], summary: "Idempotently enroll an Ed25519 public key; at most two active keys per app" }),
|
|
55
|
+
v("param", params),
|
|
56
|
+
v("json", input),
|
|
57
|
+
(c) => respond(c, () => service.register({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("json") })),
|
|
58
|
+
)
|
|
59
|
+
.delete(
|
|
60
|
+
"/:appId/keys/:keyId",
|
|
61
|
+
describeRoute({ tags: ["App keys"], summary: "Idempotently revoke an app key" }),
|
|
62
|
+
v("param", params.extend({ keyId: z.uuid() })),
|
|
63
|
+
(c) => respond(c, () => service.revoke({ actor: c.get("user"), ...c.req.valid("param") })),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export default createAdminAppKeyRoutes();
|
|
@@ -27,6 +27,7 @@ import { SETTINGS_MAP, validateSettingValue } from "../services/settings/default
|
|
|
27
27
|
import { assertGeoBudgetPair, GeoBudgetSettingError } from "../services/settings/geo-budget";
|
|
28
28
|
import { invalidateSettingsCacheForAdmin } from "../services/settings/store";
|
|
29
29
|
import { assertSettingVersions } from "../services/settings/versions";
|
|
30
|
+
import { adminSettingsEnvRoutes } from "./admin-settings-env";
|
|
30
31
|
|
|
31
32
|
const BulkUpdatesSchema = z
|
|
32
33
|
.object({
|
|
@@ -72,6 +73,7 @@ const invalidateCommittedSettings = async (keys: readonly string[]): Promise<voi
|
|
|
72
73
|
const liveSettingKeys = async () => (await listApps()).flatMap((app) => [...(app.settingKeys ?? [])]);
|
|
73
74
|
|
|
74
75
|
const app = new Hono<AuthContext>()
|
|
76
|
+
.route("/env", adminSettingsEnvRoutes)
|
|
75
77
|
.get("/web-vitals", auth.requireRole("admin"), async (c) =>
|
|
76
78
|
c.json({ enabled: (await settings.get("observability.web_vitals.enabled")) === true }),
|
|
77
79
|
)
|
|
@@ -86,7 +86,10 @@ type IdentityAdminDependencies = {
|
|
|
86
86
|
status: typeof getIdentitySigningKeyStatus;
|
|
87
87
|
rewrap: typeof rewrapIdentitySigningKeys;
|
|
88
88
|
revoke: typeof revokeIdentitySigningKey;
|
|
89
|
-
serviceAccounts: Pick<
|
|
89
|
+
serviceAccounts: Pick<
|
|
90
|
+
typeof serviceAccounts,
|
|
91
|
+
"getByResource" | "getOrCreateResourceBound" | "createStandalone" | "listStandalone" | "get" | "setStatus"
|
|
92
|
+
>;
|
|
90
93
|
credentials: Pick<
|
|
91
94
|
typeof serviceAccountCredentials,
|
|
92
95
|
"createResourceApiToken" | "createStandaloneApiToken" | "getOverview" | "listOverview" | "revoke"
|
|
@@ -151,6 +154,29 @@ export const createAdminIdentityRoutes = (
|
|
|
151
154
|
}),
|
|
152
155
|
),
|
|
153
156
|
)
|
|
157
|
+
.patch(
|
|
158
|
+
"/workloads/:appId",
|
|
159
|
+
describeRoute({ tags: ["App credentials"], summary: "Enable or disable app workload issuance and calls across all instances" }),
|
|
160
|
+
v("param", WorkloadAppParamsSchema),
|
|
161
|
+
v("json", ServiceAccountStatusInputSchema),
|
|
162
|
+
async (c) =>
|
|
163
|
+
respond(c, async () => {
|
|
164
|
+
const { appId } = c.req.valid("param");
|
|
165
|
+
const { status } = c.req.valid("json");
|
|
166
|
+
const account = await service.serviceAccounts.getByResource({ appId, resourceType: "cloud.app", resourceId: appId });
|
|
167
|
+
if (!account) return fail(err.notFound("Workload service account"));
|
|
168
|
+
const updated = await service.serviceAccounts.setStatus({ id: account.id, status });
|
|
169
|
+
if (!updated.ok) return updated;
|
|
170
|
+
await service.audit.record({
|
|
171
|
+
action: status === "disabled" ? "service_account.disable" : "service_account.enable",
|
|
172
|
+
outcome: "allowed",
|
|
173
|
+
actor: auditActor(c.get("user")),
|
|
174
|
+
target: { type: "service_account", id: account.id, label: account.name },
|
|
175
|
+
metadata: { appId, serviceAccountId: account.id, status },
|
|
176
|
+
});
|
|
177
|
+
return ok({ appId, status });
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
154
180
|
.post(
|
|
155
181
|
"/workloads/:appId/credentials",
|
|
156
182
|
describeRoute({ tags: ["App credentials"], summary: "Create an app-bound credential; return its token once" }),
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { readBoundedJson } from "../_internal/bounded-json";
|
|
5
|
+
import { getApp, listApps } from "../_internal/registry";
|
|
6
|
+
import { EnvSettingStatusSchema, type EnvSettingsProcessSchema, EnvSettingsStatusSchema } from "../contracts/settings-env";
|
|
7
|
+
import { type AuthContext, auth, jsonResponse, requiresAdmin, setRequestTimeout, v } from "../server";
|
|
8
|
+
import { listGatewayRouteSnapshots } from "../services/gateway";
|
|
9
|
+
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
10
|
+
import { issueInvocationToken } from "../services/identity/issue-invocation";
|
|
11
|
+
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
12
|
+
import { readEnvSettingsStatus } from "../services/settings/env-status";
|
|
13
|
+
import { settingsInvocationOperation } from "./settings-ops";
|
|
14
|
+
|
|
15
|
+
const reply = z.object({ settings: z.array(EnvSettingStatusSchema) });
|
|
16
|
+
export const adminSettingsEnvRoutes = new Hono<AuthContext>()
|
|
17
|
+
.use("*", auth.requireRole("admin"))
|
|
18
|
+
.use("*", auth.requireOAuthScope("admin"))
|
|
19
|
+
.get(
|
|
20
|
+
"/status",
|
|
21
|
+
describeRoute({
|
|
22
|
+
tags: ["Administration"],
|
|
23
|
+
summary: "Check environment-backed settings without returning values",
|
|
24
|
+
...requiresAdmin,
|
|
25
|
+
responses: { 200: jsonResponse(EnvSettingsStatusSchema, "Migration status") },
|
|
26
|
+
}),
|
|
27
|
+
async (c) => {
|
|
28
|
+
setRequestTimeout(c, 240);
|
|
29
|
+
const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(230_000)]);
|
|
30
|
+
try {
|
|
31
|
+
const processes: z.infer<typeof EnvSettingsProcessSchema>[] = [
|
|
32
|
+
{ appId: "core", status: "ok", settings: await readEnvSettingsStatus() },
|
|
33
|
+
];
|
|
34
|
+
for (const app of await listApps()) {
|
|
35
|
+
if (app.id === "core") continue;
|
|
36
|
+
try {
|
|
37
|
+
const response = await requestSettings(app, "GET", "env", auth.getAuthority(c), undefined, signal);
|
|
38
|
+
const data = await readBoundedJson(response, 4 * 1024 * 1024);
|
|
39
|
+
if (!response.ok || !data.ok) throw new Error("Unavailable");
|
|
40
|
+
processes.push({ appId: app.id, status: "ok", settings: reply.parse(data.data).settings });
|
|
41
|
+
} catch {
|
|
42
|
+
processes.push({ appId: app.id, status: "unavailable", settings: [] });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const gateways = await listGatewayRouteSnapshots();
|
|
46
|
+
if (!gateways.length) processes.push({ appId: "gateway", status: "unavailable", settings: [] });
|
|
47
|
+
for (const gateway of gateways) {
|
|
48
|
+
const parsed = z.array(EnvSettingStatusSchema).safeParse(gateway.envSettings);
|
|
49
|
+
processes.push({
|
|
50
|
+
appId: "gateway",
|
|
51
|
+
instanceId: gateway.instanceId,
|
|
52
|
+
sampledAt: gateway.updatedAt,
|
|
53
|
+
status: parsed.success ? "ok" : "unavailable",
|
|
54
|
+
settings: parsed.success ? parsed.data : [],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return c.json(
|
|
58
|
+
{ needsAttention: processes.some((p) => p.status !== "ok" || p.settings.some((s) => s.needsAttention)), processes },
|
|
59
|
+
200,
|
|
60
|
+
{ "cache-control": "no-store" },
|
|
61
|
+
);
|
|
62
|
+
} finally {
|
|
63
|
+
setRequestTimeout(c, 10);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
.put("/:appId/:key", v("json", z.object({ value: z.unknown(), replaceUnreadable: z.boolean().optional() })), async (c) => {
|
|
68
|
+
const app = await getApp(c.req.param("appId") ?? "");
|
|
69
|
+
if (!app) return c.json({ message: "App is not registered; start it before changing its settings" }, 404);
|
|
70
|
+
try {
|
|
71
|
+
const response = await requestSettings(
|
|
72
|
+
app,
|
|
73
|
+
"PUT",
|
|
74
|
+
c.req.param("key") ?? "",
|
|
75
|
+
auth.getAuthority(c),
|
|
76
|
+
c.req.valid("json"),
|
|
77
|
+
c.req.raw.signal,
|
|
78
|
+
);
|
|
79
|
+
const body = await readBoundedJson(response, 4 * 1024 * 1024);
|
|
80
|
+
if (!body.ok) throw new Error("Unavailable");
|
|
81
|
+
return Response.json(body.data, { status: response.status, headers: { "cache-control": "no-store" } });
|
|
82
|
+
} catch {
|
|
83
|
+
return c.json({ message: "Application settings unavailable" }, 502);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const requestSettings = async (
|
|
88
|
+
app: { id: string; baseUrl: string },
|
|
89
|
+
method: "GET" | "PUT",
|
|
90
|
+
key: string,
|
|
91
|
+
authority: Parameters<typeof invocationAuthorityFromRequest>[0],
|
|
92
|
+
body: unknown,
|
|
93
|
+
parentSignal: AbortSignal,
|
|
94
|
+
) => {
|
|
95
|
+
const signal = AbortSignal.any([parentSignal, AbortSignal.timeout(5_000)]);
|
|
96
|
+
const signed = await withActiveIdentitySigner(
|
|
97
|
+
"invocation",
|
|
98
|
+
(signer, db) =>
|
|
99
|
+
issueInvocationToken(
|
|
100
|
+
{
|
|
101
|
+
targetAppId: app.id,
|
|
102
|
+
callingAppId: "core",
|
|
103
|
+
operation: settingsInvocationOperation(method, key),
|
|
104
|
+
schemaHash: null,
|
|
105
|
+
authority: invocationAuthorityFromRequest(authority),
|
|
106
|
+
signer,
|
|
107
|
+
issuer: signer.issuer,
|
|
108
|
+
},
|
|
109
|
+
{ db, groupsAdmin: signer.groupsAdmin, signal },
|
|
110
|
+
),
|
|
111
|
+
{ signal, timeoutMs: 5_000 },
|
|
112
|
+
);
|
|
113
|
+
return fetch(new URL(`/_internal/settings/${encodeURIComponent(key)}`, app.baseUrl), {
|
|
114
|
+
method,
|
|
115
|
+
headers: { authorization: `Bearer ${signed.token}`, "content-type": "application/json" },
|
|
116
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
117
|
+
signal,
|
|
118
|
+
redirect: "manual",
|
|
119
|
+
});
|
|
120
|
+
};
|