@k2b/cloud 0.46.0 → 0.47.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/define-app.ts +38 -0
- package/src/_internal/heartbeat.ts +19 -0
- package/src/_internal/postgres-application-name.ts +11 -0
- package/src/_internal/process-sync.ts +9 -0
- package/src/_internal/readiness.ts +6 -1
- package/src/_internal/resource-principals.ts +4 -34
- package/src/_internal/workload-credential.ts +153 -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-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/capabilities.ts +9 -3
- package/src/api/index.ts +3 -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 +4 -1
- package/src/capabilities/server-invoke.ts +46 -28
- 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-keys.ts +226 -0
- package/src/cli/admin/index.ts +7 -1
- package/src/cli/admin/settings.ts +62 -0
- package/src/config/define-env.ts +63 -8
- package/src/config/env.ts +27 -21
- package/src/config/index.ts +12 -1
- package/src/config/workflow-ai.ts +28 -0
- package/src/contracts/settings-env.ts +21 -0
- package/src/contracts/settings-types.ts +6 -0
- package/src/index.ts +9 -0
- package/src/runtime/clients.ts +72 -0
- package/src/runtime/connections.ts +37 -0
- package/src/runtime/resolve-connections.ts +91 -0
- package/src/server/client-ip.ts +1 -1
- package/src/services/app-keys.ts +298 -0
- 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 +4 -1
- package/src/services/outgoing-mail/resolve-host.ts +5 -4
- 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 +99 -1
- 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 +73 -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/start.ts +36 -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,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
|
+
};
|
package/src/api/capabilities.ts
CHANGED
|
@@ -260,11 +260,17 @@ const errorResponse = (code: string, message: string, status: number, details?:
|
|
|
260
260
|
status,
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
-
const capabilityJsonResponse = (body: unknown, status: number): Response =>
|
|
264
|
-
|
|
263
|
+
const capabilityJsonResponse = (body: unknown, status: number): Response => {
|
|
264
|
+
const limited =
|
|
265
|
+
status === 429 ? z.object({ details: z.object({ retryAfter: z.number().int().positive().safe() }) }).safeParse(body) : null;
|
|
266
|
+
return Response.json(body, {
|
|
265
267
|
status,
|
|
266
|
-
headers: {
|
|
268
|
+
headers: {
|
|
269
|
+
"content-type": "application/json",
|
|
270
|
+
...(limited?.success ? { "retry-after": String(limited.data.details.retryAfter) } : {}),
|
|
271
|
+
},
|
|
267
272
|
});
|
|
273
|
+
};
|
|
268
274
|
|
|
269
275
|
export type CapabilityDispatchParams = {
|
|
270
276
|
/** Trusted Core continuation binding; never read from client headers. */
|
package/src/api/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import adminAiProjectsRoutes from "./admin-ai-projects";
|
|
|
26
26
|
import adminAiQuotaRoutes from "./admin-ai-quotas";
|
|
27
27
|
import adminAiSkillsRoutes from "./admin-ai-skills";
|
|
28
28
|
import adminAiUsageRoutes from "./admin-ai-usage";
|
|
29
|
+
import adminAppKeyRoutes from "./admin-app-keys";
|
|
29
30
|
import adminCoreSettingsRoutes from "./admin-core-settings";
|
|
30
31
|
import adminDirectorySharesRoutes from "./admin-directory-shares";
|
|
31
32
|
import adminGeoRoutes from "./admin-geo";
|
|
@@ -106,6 +107,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
|
|
|
106
107
|
.route("/admin/core/geo", adminGeoRoutes)
|
|
107
108
|
.route("/admin/identity/directory-shares", adminDirectorySharesRoutes)
|
|
108
109
|
.route("/admin/identity", adminIdentityRoutes)
|
|
110
|
+
.route("/admin/apps", adminAppKeyRoutes)
|
|
109
111
|
.route("/admin/sync", createSyncOpsProxyRoutes())
|
|
110
112
|
.route("/admin/lifecycle", adminLifecycleRoutes)
|
|
111
113
|
.route("/ai/approval-preferences", approvalPreferenceRoutes)
|
|
@@ -133,6 +135,7 @@ export const createCoreApiRouter = (options: CoreApiOptions) => {
|
|
|
133
135
|
return { api };
|
|
134
136
|
};
|
|
135
137
|
|
|
138
|
+
export { createAdminAppKeyRoutes } from "./admin-app-keys";
|
|
136
139
|
export { createAppAccessRoutes } from "./app-access";
|
|
137
140
|
export {
|
|
138
141
|
CLOUD_MCP_PATH,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// Concrete modules, not the server barrel: defineApp imports this file, and the barrel imports defineApp.
|
|
4
|
+
import { type AuthContext, auth } from "../server/middleware/auth";
|
|
5
|
+
import { v } from "../server/middleware/validator";
|
|
6
|
+
import { settingsService } from "../services/settings/app";
|
|
7
|
+
import { SETTINGS_MAP } from "../services/settings/defaults";
|
|
8
|
+
import { readEnvSettingsStatus } from "../services/settings/env-status";
|
|
9
|
+
|
|
10
|
+
export const settingsInvocationOperation = (method: string, key: string) => {
|
|
11
|
+
try {
|
|
12
|
+
return `settings.${method === "GET" ? "env" : "set"}:${decodeURIComponent(key)}`;
|
|
13
|
+
} catch {
|
|
14
|
+
return "settings.invalid";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const createSettingsOpsRoutes = (keys: readonly string[]) =>
|
|
19
|
+
new Hono<AuthContext>()
|
|
20
|
+
.use("*", async (c, next) => {
|
|
21
|
+
if (!c.get("user")?.roles.includes("admin")) return c.json({ message: "Insufficient permissions" }, 403);
|
|
22
|
+
return next();
|
|
23
|
+
})
|
|
24
|
+
.use("*", auth.requireOAuthScope("admin"))
|
|
25
|
+
.get("/env", async (c) => c.json({ settings: await readEnvSettingsStatus() }, 200, { "cache-control": "no-store" }))
|
|
26
|
+
.put("/:key", v("json", z.object({ value: z.unknown(), replaceUnreadable: z.boolean().optional() })), async (c) => {
|
|
27
|
+
const key = c.req.param("key") ?? "";
|
|
28
|
+
const def = SETTINGS_MAP.get(key);
|
|
29
|
+
if (!keys.includes(key) || !def || !(def.envBootstrap || def.envFallback))
|
|
30
|
+
return c.json({ message: "Unknown application setting" }, 400);
|
|
31
|
+
const body = c.req.valid("json");
|
|
32
|
+
const result = await settingsService.entry.update({ key, value: body.value, replaceUnreadable: body.replaceUnreadable });
|
|
33
|
+
if (!result.ok) return c.json({ message: result.error.message }, 400);
|
|
34
|
+
return c.json({ saved: true });
|
|
35
|
+
});
|