@k2b/cloud 0.47.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.
Files changed (55) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/app-access-manager-sql.ts +10 -0
  3. package/src/_internal/bounded-json.ts +7 -1
  4. package/src/_internal/capabilities.ts +8 -0
  5. package/src/_internal/define-app.ts +31 -2
  6. package/src/_internal/heartbeat.ts +2 -0
  7. package/src/_internal/nats-connection.ts +10 -9
  8. package/src/_internal/process-sync.ts +6 -6
  9. package/src/_internal/public-surface.ts +76 -0
  10. package/src/_internal/purpose-name.ts +7 -0
  11. package/src/_internal/registry-validation.ts +1 -0
  12. package/src/_internal/registry.ts +49 -3
  13. package/src/_internal/workload-credential.ts +2 -1
  14. package/src/api/admin-app-domains.ts +62 -0
  15. package/src/api/app-bootstrap.ts +131 -0
  16. package/src/api/capabilities.ts +8 -1
  17. package/src/api/index.ts +5 -0
  18. package/src/capabilities/response.ts +2 -1
  19. package/src/capabilities/server-invoke.ts +28 -2
  20. package/src/cli/admin/app-domains.ts +140 -0
  21. package/src/cli/admin/index.ts +107 -88
  22. package/src/config/env.ts +7 -1
  23. package/src/contracts/app-domains.ts +91 -0
  24. package/src/contracts/app-status.ts +104 -0
  25. package/src/contracts/capabilities.ts +22 -0
  26. package/src/contracts/index.ts +1 -0
  27. package/src/contracts/registry.ts +2 -0
  28. package/src/index.ts +1 -0
  29. package/src/runtime/bootstrap-client.ts +195 -0
  30. package/src/runtime/bootstrap-protocol.ts +60 -0
  31. package/src/runtime/connections.ts +9 -0
  32. package/src/server/app-key.ts +14 -0
  33. package/src/server/index.ts +1 -0
  34. package/src/services/app-access-management.ts +1 -8
  35. package/src/services/app-admin-facts.ts +160 -0
  36. package/src/services/app-bootstrap.ts +134 -0
  37. package/src/services/app-domain-check.ts +152 -0
  38. package/src/services/app-domains.ts +172 -0
  39. package/src/services/app-keys.ts +6 -3
  40. package/src/services/downstream-budget.ts +2 -2
  41. package/src/services/identity/workload-runtime.ts +2 -2
  42. package/src/services/index.ts +4 -0
  43. package/src/services/links/index.ts +16 -0
  44. package/src/services/links/runtime.ts +44 -0
  45. package/src/services/links/store.ts +105 -0
  46. package/src/services/logging/index.ts +7 -5
  47. package/src/services/logging/redaction.ts +6 -1
  48. package/src/services/logging/trace.ts +8 -8
  49. package/src/services/request-cache-redis.ts +2 -2
  50. package/src/services/settings/core-settings.ts +20 -0
  51. package/src/services/settings/crypto.ts +2 -2
  52. package/src/services/settings/store.ts +5 -0
  53. package/src/ssr/admin-navigation.ts +0 -1
  54. package/src/ssr/platform-messages.ts +0 -2
  55. package/src/start.ts +35 -14
@@ -0,0 +1,131 @@
1
+ import type { SQL } from "bun";
2
+ import { Hono, type MiddlewareHandler } from "hono";
3
+ import { getConnInfo } from "hono/bun";
4
+ import {
5
+ BOOTSTRAP_BODY,
6
+ BOOTSTRAP_PATH,
7
+ BOOTSTRAP_RESPONSE_BYTES,
8
+ BootstrapConnectionsSchema,
9
+ BootstrapResponseSchema,
10
+ } from "../runtime/bootstrap-protocol";
11
+ import { connections } from "../runtime/connections";
12
+ import { clientIpBucket } from "../server/client-ip";
13
+ import { type RateLimitResult, ratelimit } from "../server/ratelimit";
14
+ import { authenticateBootstrap, BootstrapProofError, bootstrapRateLimit, consumeBootstrapProof } from "../services/app-bootstrap";
15
+ import { touchAppKey } from "../services/app-keys";
16
+
17
+ type BootstrapContext = { Variables: { bootstrapBody: Uint8Array<ArrayBuffer> } };
18
+
19
+ /** Count actual bytes even when Content-Length is missing or forged. Never await cancellation. */
20
+ const limitBody: MiddlewareHandler<BootstrapContext> = async (c, next) => {
21
+ const max = Buffer.byteLength(BOOTSTRAP_BODY);
22
+ const reject = () => c.json({ code: "body_too_large" }, 413);
23
+ if (Number(c.req.header("Content-Length")) > max) {
24
+ void c.req.raw.body?.cancel().catch(() => undefined);
25
+ return reject();
26
+ }
27
+ const reader = c.req.raw.body?.getReader();
28
+ const bytes = new Uint8Array(max);
29
+ let total = 0;
30
+ if (reader) {
31
+ try {
32
+ while (true) {
33
+ const chunk = await reader.read();
34
+ if (chunk.done) break;
35
+ if (total + chunk.value.byteLength > max) {
36
+ void reader.cancel().catch(() => undefined);
37
+ return reject();
38
+ }
39
+ bytes.set(chunk.value, total);
40
+ total += chunk.value.byteLength;
41
+ }
42
+ } finally {
43
+ reader.releaseLock();
44
+ }
45
+ }
46
+ c.set("bootstrapBody", bytes.slice(0, total));
47
+ await next();
48
+ };
49
+
50
+ type Dependencies = {
51
+ db?: SQL;
52
+ ingress?: MiddlewareHandler;
53
+ admit?: (keyId: string) => Promise<RateLimitResult>;
54
+ };
55
+ const admit = async (kind: "ingress" | "key", identifier: string) => {
56
+ // Use the installation's existing per-second security budget, in separate ingress and authenticated-key buckets.
57
+ const limit = await bootstrapRateLimit();
58
+ return ratelimit({ id: `app-bootstrap-${kind}`, limit }).check(identifier);
59
+ };
60
+
61
+ const ingress: MiddlewareHandler<BootstrapContext> = async (c, next) => {
62
+ // Bootstrap uses the direct Core socket. Unknown peers share one bounded bucket.
63
+ let peer = "unknown";
64
+ try {
65
+ peer = getConnInfo(c).remote.address ?? peer;
66
+ } catch {
67
+ /* In-process requests have no socket. */
68
+ }
69
+ const rate = await admit("ingress", clientIpBucket(peer));
70
+ if (rate.limited) {
71
+ c.header("Retry-After", String(Math.max(1, Math.ceil(rate.resetIn / 1000))));
72
+ return c.json({ code: "bootstrap_ingress_limited" }, 429);
73
+ }
74
+ await next();
75
+ };
76
+
77
+ /** Core-only mount. Capture the startup snapshot once, including NATS file contents. */
78
+ export const createAppBootstrapRoutes = (dependencies: Dependencies = {}) => {
79
+ const { database, valkey, nats, appSecret } = connections();
80
+ const snapshot = BootstrapConnectionsSchema.parse({ database, valkey, nats, appSecret });
81
+ // Refuse an undeliverable Core configuration at startup, using the largest valid identity.
82
+ const largestIdentity = {
83
+ installationId: "11111111-1111-4111-8111-111111111111",
84
+ appId: "a".repeat(80),
85
+ keyId: "22222222-2222-4222-8222-222222222222",
86
+ };
87
+ if (Buffer.byteLength(JSON.stringify({ identity: largestIdentity, connections: snapshot })) > BOOTSTRAP_RESPONSE_BYTES)
88
+ throw new Error("Bootstrap response exceeds protocol budget");
89
+ // The startup snapshot includes appId/sources; only the delivery contract goes on the wire.
90
+ return new Hono<BootstrapContext>()
91
+ .onError(() =>
92
+ Response.json({ code: "bootstrap_unavailable" }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }),
93
+ )
94
+ .post(
95
+ BOOTSTRAP_PATH,
96
+ async (c, next) => {
97
+ c.header("Cache-Control", "no-store");
98
+ await next();
99
+ },
100
+ // The v1 request is exactly two UTF-8 bytes. This runs before proof parsing, even for invalid proofs.
101
+ limitBody,
102
+ dependencies.ingress ?? ingress,
103
+ async (c) => {
104
+ const url = new URL(c.req.url);
105
+ if (url.search) return c.json({ code: "invalid_request" }, 400);
106
+ const body = c.get("bootstrapBody");
107
+ const header = c.req.header("Authorization") ?? "";
108
+ if (!header.startsWith("Bearer ")) return c.json({ code: "invalid_proof" }, 401);
109
+ let verified: Awaited<ReturnType<typeof authenticateBootstrap>>;
110
+ try {
111
+ verified = await authenticateBootstrap(header.slice(7), { method: c.req.method, path: url.pathname, body }, dependencies.db);
112
+ } catch (error) {
113
+ if (error instanceof BootstrapProofError) return c.json({ code: error.code }, error.code === "invalid_proof" ? 401 : 403);
114
+ throw error;
115
+ }
116
+ if (new TextDecoder().decode(body) !== BOOTSTRAP_BODY) return c.json({ code: "invalid_request" }, 400);
117
+ const { identity, jti, exp } = verified;
118
+ const rate = dependencies.admit ? await dependencies.admit(identity.keyId) : await admit("key", identity.keyId);
119
+ if (rate.limited) {
120
+ c.header("Retry-After", String(Math.max(1, Math.ceil(rate.resetIn / 1000))));
121
+ return c.json({ code: "bootstrap_rate_limited" }, 429);
122
+ }
123
+ if (!(await consumeBootstrapProof(identity.keyId, jti, exp, dependencies.db))) return c.json({ code: "replayed_proof" }, 403);
124
+ await touchAppKey(identity, dependencies.db);
125
+ const response = JSON.stringify(BootstrapResponseSchema.parse({ identity, connections: snapshot }));
126
+ if (Buffer.byteLength(response) > BOOTSTRAP_RESPONSE_BYTES) throw new Error("Bootstrap response exceeds protocol budget");
127
+ c.header("Content-Type", "application/json");
128
+ return c.body(response);
129
+ },
130
+ );
131
+ };
@@ -297,6 +297,8 @@ export type CapabilityDispatchParams = {
297
297
  callingAppId?: string;
298
298
  /** Internal observation only: called immediately before attempting the app request. */
299
299
  onUpstreamAttempt?: () => void;
300
+ /** Core-only aggregate budget, charged on upstream bytes before parsing or validation. */
301
+ consumeResponseBytes?: (bytes: number) => void;
300
302
  dependencies?: CapabilityDispatchDependencies;
301
303
  };
302
304
 
@@ -789,7 +791,12 @@ const runCapabilityDispatch = async (
789
791
  let upstreamBody: Awaited<ReturnType<typeof readBoundedJson>>;
790
792
  try {
791
793
  upstreamBody = await deadline.wait(() =>
792
- readBoundedJson(response, params.review ? CAPABILITY_MAX_REVIEW_RESPONSE_BYTES : CAPABILITY_MAX_RESULT_BYTES, signal),
794
+ readBoundedJson(
795
+ response,
796
+ params.review ? CAPABILITY_MAX_REVIEW_RESPONSE_BYTES : CAPABILITY_MAX_RESULT_BYTES,
797
+ signal,
798
+ params.consumeResponseBytes,
799
+ ),
793
800
  );
794
801
  } catch {
795
802
  if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
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 adminAppDomainRoutes from "./admin-app-domains";
29
30
  import adminAppKeyRoutes from "./admin-app-keys";
30
31
  import adminCoreSettingsRoutes from "./admin-core-settings";
31
32
  import adminDirectorySharesRoutes from "./admin-directory-shares";
@@ -107,6 +108,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
107
108
  .route("/admin/core/geo", adminGeoRoutes)
108
109
  .route("/admin/identity/directory-shares", adminDirectorySharesRoutes)
109
110
  .route("/admin/identity", adminIdentityRoutes)
111
+ .route("/admin/apps", adminAppDomainRoutes)
110
112
  .route("/admin/apps", adminAppKeyRoutes)
111
113
  .route("/admin/sync", createSyncOpsProxyRoutes())
112
114
  .route("/admin/lifecycle", adminLifecycleRoutes)
@@ -135,8 +137,11 @@ export const createCoreApiRouter = (options: CoreApiOptions) => {
135
137
  return { api };
136
138
  };
137
139
 
140
+ export { createAdminAppDomainRoutes } from "./admin-app-domains";
138
141
  export { createAdminAppKeyRoutes } from "./admin-app-keys";
139
142
  export { createAppAccessRoutes } from "./app-access";
143
+ /** Core-owned internal startup protocol; applications use @k2b/cloud/start. */
144
+ export { createAppBootstrapRoutes } from "./app-bootstrap";
140
145
  export {
141
146
  CLOUD_MCP_PATH,
142
147
  CLOUD_MCP_PROTECTED_RESOURCE_PATH,
@@ -8,8 +8,9 @@ export const readCapabilityResponse = async <T>(
8
8
  schema: z.ZodType<T>,
9
9
  maxBytes = CAPABILITY_MAX_RESULT_BYTES,
10
10
  signal?: AbortSignal,
11
+ consumeBytes?: (bytes: number) => void,
11
12
  ): Promise<CapabilityResultState<T>> => {
12
- const parsedBody = await readBoundedJson(response, maxBytes, signal);
13
+ const parsedBody = await readBoundedJson(response, maxBytes, signal, consumeBytes);
13
14
  if (!parsedBody.ok && parsedBody.reason === "too_large") {
14
15
  return {
15
16
  ok: false,
@@ -4,6 +4,7 @@ import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabiliti
4
4
  import {
5
5
  CAPABILITY_FRAMEWORK_ERROR_CODES,
6
6
  CAPABILITY_MAX_CATALOG_BYTES,
7
+ CAPABILITY_MAX_RESULT_BYTES,
7
8
  CapabilityCatalogSchema,
8
9
  type CapabilityOrigin,
9
10
  capabilityResultSchema,
@@ -36,6 +37,8 @@ export type CapabilityCaller = {
36
37
  /** BCP 47 locale of the originating request, forwarded as invocation metadata. */
37
38
  locale?: string | null;
38
39
  signal?: AbortSignal;
40
+ /** Request-local aggregate response budget; called before result validation for every received chunk. */
41
+ consumeResponseBytes?: (bytes: number) => void;
39
42
  /** App-only query authority. The workload credential goes only to Core. */
40
43
  workload?: { appId: string };
41
44
  /** Durable background authority. `authorization` must be the owning app's workload credential. */
@@ -113,6 +116,8 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
113
116
  if (caller.authority) {
114
117
  if (caller.authorization || caller.cookie || caller.mandate || caller.workload || caller.transport)
115
118
  throw new Error("Use exactly one capability authority source");
119
+ let charged = false;
120
+ const charge = caller.consumeResponseBytes;
116
121
  const outcome = await dispatchCapability({
117
122
  request,
118
123
  kind: invocation.kind === "query" ? "queries" : "actions",
@@ -122,8 +127,23 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
122
127
  authority: caller.authority,
123
128
  locale: caller.locale ?? undefined,
124
129
  origin: caller.origin ?? "app",
130
+ ...(charge
131
+ ? {
132
+ consumeResponseBytes: (bytes: number) => {
133
+ charged = true;
134
+ charge(bytes);
135
+ },
136
+ }
137
+ : {}),
125
138
  });
126
- return readCapabilityResponse(outcome, capabilityResultSchema(dataSchema, { consumer: true }));
139
+ return readCapabilityResponse(
140
+ outcome,
141
+ capabilityResultSchema(dataSchema, { consumer: true }),
142
+ CAPABILITY_MAX_RESULT_BYTES,
143
+ // Only a budgeted caller cancels reading a response that already arrived; others keep the received result.
144
+ charge ? combineCapabilitySignals(caller.signal, invocation.signal) : undefined,
145
+ charged ? undefined : charge,
146
+ );
127
147
  }
128
148
  const brokerRequest = new Request(request, {
129
149
  body: JSON.stringify(
@@ -170,7 +190,13 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
170
190
  return unavailable(undefined, undefined, caller.locale ?? undefined);
171
191
  }
172
192
  }
173
- return readCapabilityResponse(response, capabilityResultSchema(dataSchema, { consumer: true }));
193
+ return readCapabilityResponse(
194
+ response,
195
+ capabilityResultSchema(dataSchema, { consumer: true }),
196
+ CAPABILITY_MAX_RESULT_BYTES,
197
+ caller.consumeResponseBytes ? combineCapabilitySignals(caller.signal, invocation.signal) : undefined,
198
+ caller.consumeResponseBytes,
199
+ );
174
200
  } catch (cause) {
175
201
  return unavailable(cause, invocation, caller.locale ?? undefined);
176
202
  }
@@ -0,0 +1,140 @@
1
+ import { z } from "zod";
2
+ import { arg, type CloudCliContext, cliText, command, confirmFlag, flag, localizeCloudCliText, printStructured } from "../index";
3
+ import { apiGet, apiJson } from "./shared";
4
+
5
+ const step = z.object({ ok: z.boolean(), detail: z.string() });
6
+ const status = z.object({ checkedAt: z.string(), dns: step, certificate: step.nullable(), reachable: step.nullable() });
7
+ const domain = z.object({
8
+ host: z.string(),
9
+ redirectTo: z.string().nullable(),
10
+ createdAt: z.string(),
11
+ active: z.boolean(),
12
+ status: status.nullable(),
13
+ });
14
+ const path = (appId: string, host?: string) =>
15
+ `/api/admin/apps/${encodeURIComponent(appId)}/domains${host === undefined ? "" : `/${encodeURIComponent(host)}`}`;
16
+ const appArgs = { appId: arg.required({ valueLabel: "appId" }) };
17
+ const domainArgs = { ...appArgs, host: arg.required({ valueLabel: "host" }) };
18
+ const show = (ctx: CloudCliContext, raw: unknown, domains: z.infer<typeof domain>[]) => {
19
+ if (printStructured(ctx, raw)) return;
20
+ const checked = (result: z.infer<typeof step> | null | undefined) =>
21
+ result == null ? "-" : result.ok ? "ok" : cliText(ctx, { en: "failed", de: "fehlgeschlagen" });
22
+ ctx.table(
23
+ domains.map((item) => ({
24
+ host: item.host,
25
+ redirect: item.redirectTo ?? "-",
26
+ active: item.active,
27
+ dns: checked(item.status?.dns),
28
+ certificate: checked(item.status?.certificate),
29
+ reachable: checked(item.status?.reachable),
30
+ })),
31
+ [
32
+ { key: "host", label: "Host" },
33
+ { key: "redirect", label: cliText(ctx, { en: "Redirect", de: "Weiterleitung" }) },
34
+ { key: "active", label: cliText(ctx, { en: "Active", de: "Aktiv" }) },
35
+ { key: "dns", label: "DNS" },
36
+ { key: "certificate", label: cliText(ctx, { en: "Certificate", de: "Zertifikat" }) },
37
+ { key: "reachable", label: cliText(ctx, { en: "Reachable", de: "Erreichbar" }) },
38
+ ],
39
+ );
40
+ };
41
+
42
+ export const appDomainCommands = (locale?: string) => {
43
+ const t = (en: string, de: string) => localizeCloudCliText(locale, { en, de });
44
+ return [
45
+ command("apps domains ls", {
46
+ summary: t(
47
+ "List app domains, redirects and DNS/TLS/gateway status",
48
+ "App-Domains, Weiterleitungen und DNS/TLS/Gateway-Status auflisten",
49
+ ),
50
+ args: appArgs,
51
+ async run({ ctx, args }) {
52
+ const result = z.object({ domains: z.array(domain) }).parse(await apiGet(ctx, path(args.appId)));
53
+ show(ctx, result, result.domains);
54
+ },
55
+ }),
56
+ command("apps domains add", {
57
+ summary: t(
58
+ "Assign a domain to a registered app and start its first check",
59
+ "Eine Domain einer bekannten App zuordnen und erstmals prüfen",
60
+ ),
61
+ args: domainArgs,
62
+ flags: {
63
+ redirectTo: flag.string({
64
+ description: t("Another domain of this app without a redirect", "Eine andere Domain dieser App ohne Weiterleitung"),
65
+ }),
66
+ },
67
+ async run({ ctx, args, flags }) {
68
+ const result = domain.parse(
69
+ await apiJson(ctx, "POST", path(args.appId), { host: args.host, ...(flags.redirectTo ? { redirectTo: flags.redirectTo } : {}) }),
70
+ );
71
+ show(ctx, result, [result]);
72
+ },
73
+ }),
74
+ command("apps domains set", {
75
+ summary: t("Set or clear an app domain redirect", "Eine Domain-Weiterleitung setzen oder entfernen"),
76
+ args: domainArgs,
77
+ flags: {
78
+ redirectTo: flag.string({
79
+ description: t("Another domain of this app without a redirect", "Eine andere Domain dieser App ohne Weiterleitung"),
80
+ }),
81
+ noRedirect: flag.boolean({ description: t("Clear the redirect", "Die Weiterleitung entfernen") }),
82
+ },
83
+ async run({ ctx, args, flags }) {
84
+ if (Boolean(flags.redirectTo) === flags.noRedirect)
85
+ throw new Error(
86
+ cliText(ctx, {
87
+ en: "Use exactly one of --redirect-to or --no-redirect.",
88
+ de: "Nutze genau eine Option: --redirect-to oder --no-redirect.",
89
+ }),
90
+ );
91
+ const result = domain.parse(
92
+ await apiJson(ctx, "PATCH", path(args.appId, args.host), { redirectTo: flags.noRedirect ? null : flags.redirectTo }),
93
+ );
94
+ show(ctx, result, [result]);
95
+ },
96
+ }),
97
+ command("apps domains rm", {
98
+ summary: t("Remove an unreferenced app domain assignment", "Eine App-Domain ohne eingehende Weiterleitungen entfernen"),
99
+ args: domainArgs,
100
+ flags: { yes: confirmFlag(t("Remove this domain assignment", "Diese Domain-Zuordnung entfernen")) },
101
+ async run({ ctx, args, flags }) {
102
+ if (!flags.yes)
103
+ throw new Error(cliText(ctx, { en: "Removing a domain requires --yes.", de: "Das Entfernen einer Domain erfordert --yes." }));
104
+ const result = await apiJson(ctx, "DELETE", path(args.appId, args.host));
105
+ if (!printStructured(ctx, result)) ctx.print(cliText(ctx, { en: "Domain removed.", de: "Domain entfernt." }));
106
+ },
107
+ }),
108
+ command("apps domains check", {
109
+ summary: t(
110
+ "Check DNS, certificate and gateway reachability, at most once per 60 seconds",
111
+ "DNS, Zertifikat und Gateway-Erreichbarkeit höchstens einmal pro 60 Sekunden prüfen",
112
+ ),
113
+ args: domainArgs,
114
+ async run({ ctx, args }) {
115
+ const result = z.object({ status: status.nullable() }).parse(await apiJson(ctx, "POST", `${path(args.appId, args.host)}/check`));
116
+ if (printStructured(ctx, result)) return;
117
+ if (!result.status) {
118
+ ctx.print(
119
+ cliText(ctx, {
120
+ en: "The first check is running. List the domains again shortly.",
121
+ de: "Die erste Prüfung läuft. Liste die Domains gleich erneut auf.",
122
+ }),
123
+ );
124
+ return;
125
+ }
126
+ ctx.table(
127
+ (["dns", "certificate", "reachable"] as const).map((key) => {
128
+ const value = result.status![key];
129
+ return { step: key, ok: value?.ok ?? "-", detail: value?.detail ?? "-" };
130
+ }),
131
+ [
132
+ { key: "step", label: cliText(ctx, { en: "Step", de: "Schritt" }) },
133
+ { key: "ok", label: "OK" },
134
+ { key: "detail", label: "Detail" },
135
+ ],
136
+ );
137
+ },
138
+ }),
139
+ ];
140
+ };
@@ -5,7 +5,9 @@
5
5
  * them. The groups map to the admin UI areas, so a command and the page that
6
6
  * shows the same data stay easy to find together.
7
7
  */
8
+
8
9
  import { defineCliCommands } from "../commands";
10
+ import { type CloudCliContext, localizeCloudCliText } from "../index";
9
11
  import { accountAdministrationCommands } from "./account-administration";
10
12
  import { accountCategoryCommands } from "./account-categories";
11
13
  import { agentCommands } from "./agents";
@@ -14,6 +16,7 @@ import { aiQuotaCommands } from "./ai-quotas";
14
16
  import { aiSkillCommands } from "./ai-skills";
15
17
  import { aiUsageCommands } from "./ai-usage";
16
18
  import { appCredentialCommands } from "./app-credentials";
19
+ import { appDomainCommands } from "./app-domains";
17
20
  import { appKeyCommands } from "./app-keys";
18
21
  import { appSignInCommands } from "./app-sign-in";
19
22
  import { dataCommands } from "./data";
@@ -35,91 +38,107 @@ import { telemetryCommands } from "./telemetry";
35
38
  import { webhookCommands } from "./webhooks";
36
39
  import { workflowCommands } from "./workflows";
37
40
 
38
- export default defineCliCommands({
39
- name: "admin",
40
- summary: "Inspect and operate Cloud administration surfaces.",
41
- groupSummaries: {
42
- accounts: "Manage account policy and account administration.",
43
- ai: "Manage AI models, quotas, Skills, and usage.",
44
- "ai models": "Manage AI model configuration.",
45
- "ai providers": "Manage AI provider connections.",
46
- "ai variants": "Manage named model variants.",
47
- "ai classes": "Manage model classes and tools.",
48
- "ai quotas background": "Inspect and release the background emergency stop.",
49
- "ai skills": "Inspect Assistant Skills and trusted templates.",
50
- "ai usage": "Inspect AI runs and usage facets.",
51
- announcements: "Manage platform announcements and banners.",
52
- "app-credentials": "Manage per-application credentials for background work.",
53
- agents: "Create, list, revoke, and re-key agent accounts with OAuth client credentials, and change their pictures.",
54
- "agents avatar": "Show and change an agent's picture: its robot parts or a photo.",
55
- "app-sign-in": "Manage app sign-in and pairing.",
56
- apps: "Inspect registered apps and manage app keys.",
57
- "apps keys": "Create, list, and revoke Ed25519 app keys.",
58
- documentation: "Manage the administration documentation website.",
59
- geoip: "Replace and inspect sign-in location databases.",
60
- jobs: "Inspect background jobs and their runs.",
61
- logs: "Search, inspect, and clean up retained logs.",
62
- metrics: "Inspect collectors and manage metrics access.",
63
- "metrics tokens": "Manage metrics bearer tokens.",
64
- nats: "Inspect NATS nodes, streams, and consumers.",
65
- "nats consumers": "Inspect NATS consumers.",
66
- "nats streams": "Find NATS streams and buckets.",
67
- "notification-batches": "Manage account notification batches.",
68
- "outgoing-mail": "Manage outgoing mail sender profiles, application access, and the send log.",
69
- "outgoing-mail profiles": "Manage outgoing mail sender profiles.",
70
- "outgoing-mail apps": "Manage application outgoing mail access.",
71
- "outgoing-mail log": "Read and cancel application mail in the send log.",
72
- "outgoing-mail retention": "Show or change how long the send log keeps mail.",
73
- notifications: "Inspect and resend email notifications.",
74
- postgres: "Inspect Postgres diagnostics.",
75
- redis: "Inspect Valkey diagnostics.",
76
- routes: "Inspect gateway routes.",
77
- settings: "Inspect environment migration and change administrator settings.",
78
- sync: "Inspect Sync resources, schedules, and dead letters.",
79
- "sync dead-letters": "Inspect, replay, requeue, or remove dead letters.",
80
- "sync resources": "List Sync resource declarations.",
81
- "sync schedules": "Inspect and run Sync schedules.",
82
- "sync schedules runs": "Read Sync schedule run results.",
83
- telemetry: "Inspect request telemetry.",
84
- webhooks: "Manage gateway health webhooks.",
85
- workflows: "Inspect and operate workflow runs and effects.",
86
- "app-sign-in config": "Manage the trusted app website and pairing policy.",
87
- "accounts config": "Manage account types and sign-in visibility.",
88
- "accounts administration": "Manage account requests and account/group follow-up notices.",
89
- "ai quotas": "Manage direct Assistant chat limits and consumption.",
90
- "ai quotas config": "Export and replace quota policies with revision checks.",
91
- legal: "Manage Terms, Privacy, and Imprint.",
92
- linux: "Prepare Linux identities.",
93
- "linux config": "Manage Linux identity defaults and allocation range.",
94
- },
95
- commands: [
96
- ...appCredentialCommands,
97
- ...appKeyCommands,
98
- ...agentCommands,
99
- ...documentationCommands,
100
- ...appSignInCommands,
101
- ...accountCategoryCommands,
102
- ...accountAdministrationCommands,
103
- ...aiUsageCommands,
104
- ...aiModelCommands,
105
- ...aiQuotaCommands,
106
- ...aiSkillCommands,
107
- ...instanceCommands,
108
- ...gatewayCommands,
109
- ...logCommands,
110
- ...telemetryCommands,
111
- ...jobCommands,
112
- ...legalCommands,
113
- ...geoipCommands,
114
- ...linuxCommands,
115
- ...workflowCommands,
116
- ...dataCommands,
117
- ...notificationCommands,
118
- ...outgoingMailCommands,
119
- ...webhookCommands,
120
- ...metricsCommands,
121
- ...natsCommands,
122
- ...syncCommands,
123
- ...settingsCommands,
124
- ],
125
- });
41
+ const build = (locale?: string) =>
42
+ defineCliCommands({
43
+ name: "admin",
44
+ summary: "Inspect and operate Cloud administration surfaces.",
45
+ groupSummaries: {
46
+ accounts: "Manage account policy and account administration.",
47
+ ai: "Manage AI models, quotas, Skills, and usage.",
48
+ "ai models": "Manage AI model configuration.",
49
+ "ai providers": "Manage AI provider connections.",
50
+ "ai variants": "Manage named model variants.",
51
+ "ai classes": "Manage model classes and tools.",
52
+ "ai quotas background": "Inspect and release the background emergency stop.",
53
+ "ai skills": "Inspect Assistant Skills and trusted templates.",
54
+ "ai usage": "Inspect AI runs and usage facets.",
55
+ announcements: "Manage platform announcements and banners.",
56
+ "app-credentials": "Manage per-application credentials for background work.",
57
+ agents: "Create, list, revoke, and re-key agent accounts with OAuth client credentials, and change their pictures.",
58
+ "agents avatar": "Show and change an agent's picture: its robot parts or a photo.",
59
+ "app-sign-in": "Manage app sign-in and pairing.",
60
+ apps: localizeCloudCliText(locale, {
61
+ en: "Inspect registered apps and manage app keys and domains.",
62
+ de: "Registrierte Apps prüfen und App-Schlüssel und App-Domains verwalten.",
63
+ }),
64
+ "apps domains": localizeCloudCliText(locale, {
65
+ en: "Assign app domains and inspect DNS, TLS and gateway reachability.",
66
+ de: "App-Domains zuordnen und DNS, TLS und Gateway-Erreichbarkeit prüfen.",
67
+ }),
68
+ "apps keys": "Create, list, and revoke Ed25519 app keys.",
69
+ documentation: "Manage the administration documentation website.",
70
+ geoip: "Replace and inspect sign-in location databases.",
71
+ jobs: "Inspect background jobs and their runs.",
72
+ logs: "Search, inspect, and clean up retained logs.",
73
+ metrics: "Inspect collectors and manage metrics access.",
74
+ "metrics tokens": "Manage metrics bearer tokens.",
75
+ nats: "Inspect NATS nodes, streams, and consumers.",
76
+ "nats consumers": "Inspect NATS consumers.",
77
+ "nats streams": "Find NATS streams and buckets.",
78
+ "notification-batches": "Manage account notification batches.",
79
+ "outgoing-mail": "Manage outgoing mail sender profiles, application access, and the send log.",
80
+ "outgoing-mail profiles": "Manage outgoing mail sender profiles.",
81
+ "outgoing-mail apps": "Manage application outgoing mail access.",
82
+ "outgoing-mail log": "Read and cancel application mail in the send log.",
83
+ "outgoing-mail retention": "Show or change how long the send log keeps mail.",
84
+ notifications: "Inspect and resend email notifications.",
85
+ postgres: "Inspect Postgres diagnostics.",
86
+ redis: "Inspect Valkey diagnostics.",
87
+ routes: "Inspect gateway routes.",
88
+ settings: "Inspect environment migration and change administrator settings.",
89
+ sync: "Inspect Sync resources, schedules, and dead letters.",
90
+ "sync dead-letters": "Inspect, replay, requeue, or remove dead letters.",
91
+ "sync resources": "List Sync resource declarations.",
92
+ "sync schedules": "Inspect and run Sync schedules.",
93
+ "sync schedules runs": "Read Sync schedule run results.",
94
+ telemetry: "Inspect request telemetry.",
95
+ webhooks: "Manage gateway health webhooks.",
96
+ workflows: "Inspect and operate workflow runs and effects.",
97
+ "app-sign-in config": "Manage the trusted app website and pairing policy.",
98
+ "accounts config": "Manage account types and sign-in visibility.",
99
+ "accounts administration": "Manage account requests and account/group follow-up notices.",
100
+ "ai quotas": "Manage direct Assistant chat limits and consumption.",
101
+ "ai quotas config": "Export and replace quota policies with revision checks.",
102
+ legal: "Manage Terms, Privacy, and Imprint.",
103
+ linux: "Prepare Linux identities.",
104
+ "linux config": "Manage Linux identity defaults and allocation range.",
105
+ },
106
+ commands: [
107
+ ...appCredentialCommands,
108
+ ...appKeyCommands,
109
+ ...appDomainCommands(locale),
110
+ ...agentCommands,
111
+ ...documentationCommands,
112
+ ...appSignInCommands,
113
+ ...accountCategoryCommands,
114
+ ...accountAdministrationCommands,
115
+ ...aiUsageCommands,
116
+ ...aiModelCommands,
117
+ ...aiQuotaCommands,
118
+ ...aiSkillCommands,
119
+ ...instanceCommands,
120
+ ...gatewayCommands,
121
+ ...logCommands,
122
+ ...telemetryCommands,
123
+ ...jobCommands,
124
+ ...legalCommands,
125
+ ...geoipCommands,
126
+ ...linuxCommands,
127
+ ...workflowCommands,
128
+ ...dataCommands,
129
+ ...notificationCommands,
130
+ ...outgoingMailCommands,
131
+ ...webhookCommands,
132
+ ...metricsCommands,
133
+ ...natsCommands,
134
+ ...syncCommands,
135
+ ...settingsCommands,
136
+ ],
137
+ });
138
+
139
+ const admin = build();
140
+ export default {
141
+ ...admin,
142
+ help: (locale?: string) => build(locale).help?.(locale) ?? "",
143
+ run: (ctx: CloudCliContext) => build(ctx.options.locale).run(ctx),
144
+ };
package/src/config/env.ts CHANGED
@@ -166,9 +166,15 @@ const registry = defineEnv({
166
166
  secretFile: true,
167
167
  doc: "Core and OAuth only: 64-hex secret that lets the OAuth app request token signatures from Core; generate independently with `openssl rand -hex 32`.",
168
168
  },
169
+ CLOUD_APP_KEY: {
170
+ schema: z.string(),
171
+ scope: "secret",
172
+ secretFile: true,
173
+ doc: "App Ed25519 private key for startup connection bootstrap; alternatively mount CLOUD_APP_KEY_FILE. Requires the direct Core origin. Core and Gateway refuse app keys. Keep infrastructure env until the platform-client import migration is complete.",
174
+ },
169
175
  CLOUD_CORE_INTERNAL_ORIGIN: {
170
176
  schema: z.string(),
171
- doc: "Direct Core listener origin for OAuth issuance, mandate-backed background work and internal callbacks; host-run callers use the Core port, not the gateway.",
177
+ doc: "Direct Core listener origin for app connection bootstrap, OAuth issuance, mandate-backed background work and internal callbacks; never the gateway. Cross-host bootstrap requires verified TLS; install private CAs through NODE_EXTRA_CA_CERTS before the first fetch.",
172
178
  },
173
179
  CLOUD_APP_CREDENTIAL: {
174
180
  schema: z.string(),