@k2b/cloud 0.47.0 → 0.49.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 (240) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/app-access-manager-sql.ts +10 -0
  3. package/src/_internal/app-access-resources.ts +2 -1
  4. package/src/_internal/app-capabilities.ts +73 -0
  5. package/src/_internal/app-event-catalog.ts +1 -1
  6. package/src/_internal/app-queries.ts +1 -1
  7. package/src/_internal/application-package.ts +2 -1
  8. package/src/_internal/bounded-json.ts +7 -1
  9. package/src/_internal/capabilities.ts +25 -2
  10. package/src/_internal/define-app.ts +33 -2
  11. package/src/_internal/heartbeat.ts +2 -0
  12. package/src/_internal/nats-connection.ts +10 -9
  13. package/src/_internal/process-sync.ts +6 -6
  14. package/src/_internal/public-surface.ts +76 -0
  15. package/src/_internal/purpose-name.ts +7 -0
  16. package/src/_internal/registry-validation.ts +1 -0
  17. package/src/_internal/registry.ts +49 -3
  18. package/src/_internal/workload-credential.ts +2 -1
  19. package/src/ai/app-skill-store.ts +2 -1
  20. package/src/ai/app-skills.ts +2 -1
  21. package/src/ai/approvals.ts +2 -1
  22. package/src/ai/chat-tasks.ts +2 -1
  23. package/src/ai/credentials.ts +2 -1
  24. package/src/ai/dictation-runtime.ts +1 -1
  25. package/src/ai/dictations.ts +1 -1
  26. package/src/ai/files-store.ts +2 -1
  27. package/src/ai/inference-calls.ts +2 -1
  28. package/src/ai/map-store.ts +1 -1
  29. package/src/ai/map-tool.ts +1 -1
  30. package/src/ai/memories.ts +1 -1
  31. package/src/ai/memory-learning-runs.ts +1 -1
  32. package/src/ai/memory-learning.ts +1 -1
  33. package/src/ai/memory-workflow-evidence.ts +1 -1
  34. package/src/ai/message-queue.ts +2 -1
  35. package/src/ai/migrate.ts +1 -1
  36. package/src/ai/model-access-migrate.ts +1 -1
  37. package/src/ai/model-access.ts +1 -1
  38. package/src/ai/model-catalog.ts +1 -1
  39. package/src/ai/model-delete.ts +1 -1
  40. package/src/ai/model-usage.ts +2 -1
  41. package/src/ai/open-questions.ts +1 -1
  42. package/src/ai/openai-api-slots.ts +2 -1
  43. package/src/ai/prefs.ts +2 -1
  44. package/src/ai/projects.ts +2 -1
  45. package/src/ai/provider-connections.ts +1 -1
  46. package/src/ai/quota-limits.ts +2 -1
  47. package/src/ai/quota-provider.ts +2 -1
  48. package/src/ai/quota-report.ts +2 -1
  49. package/src/ai/quotas-migrate.ts +2 -1
  50. package/src/ai/quotas.ts +2 -1
  51. package/src/ai/request-headers.ts +2 -1
  52. package/src/ai/review-content.ts +2 -1
  53. package/src/ai/skill-search.ts +2 -1
  54. package/src/ai/skills.ts +2 -1
  55. package/src/ai/store.ts +2 -1
  56. package/src/ai/structured-runs.ts +2 -1
  57. package/src/ai/tool-audit.ts +2 -1
  58. package/src/ai/turn-timing.ts +1 -1
  59. package/src/ai/usage.ts +2 -1
  60. package/src/ai/web-provenance.ts +2 -1
  61. package/src/api/admin-ai-quotas.ts +1 -1
  62. package/src/api/admin-ai.ts +1 -1
  63. package/src/api/admin-app-domains.ts +62 -0
  64. package/src/api/admin-app-keys.ts +47 -3
  65. package/src/api/admin-core-settings.ts +1 -1
  66. package/src/api/admin-outgoing-mail.ts +1 -1
  67. package/src/api/app-bootstrap.ts +153 -0
  68. package/src/api/app-events.ts +1 -1
  69. package/src/api/auth.ts +1 -1
  70. package/src/api/authenticate-app-request.ts +129 -0
  71. package/src/api/capabilities.ts +87 -6
  72. package/src/api/index.ts +7 -0
  73. package/src/api/me.ts +1 -1
  74. package/src/api/search.bench.ts +1 -1
  75. package/src/app-pwa/service.ts +2 -1
  76. package/src/app-pwa/transaction.ts +4 -2
  77. package/src/capabilities/claims.ts +2 -1
  78. package/src/capabilities/executions.ts +8 -4
  79. package/src/capabilities/geo-client.ts +1 -1
  80. package/src/capabilities/migrate.ts +3 -1
  81. package/src/capabilities/resource-readers.ts +2 -2
  82. package/src/capabilities/response.ts +2 -1
  83. package/src/capabilities/server-invoke.ts +73 -20
  84. package/src/capabilities/server.ts +2 -1
  85. package/src/cli/admin/app-domains.ts +140 -0
  86. package/src/cli/admin/app-keys.ts +25 -3
  87. package/src/cli/admin/index.ts +107 -88
  88. package/src/config/env.ts +10 -4
  89. package/src/contracts/app-domains.ts +91 -0
  90. package/src/contracts/app-status.ts +104 -0
  91. package/src/contracts/capabilities.ts +37 -2
  92. package/src/contracts/index.ts +1 -0
  93. package/src/contracts/outgoing-mail.ts +1 -1
  94. package/src/contracts/registry.ts +2 -0
  95. package/src/events/app-event-delivery.ts +1 -1
  96. package/src/events/live-delivery.ts +2 -1
  97. package/src/index.ts +1 -0
  98. package/src/runtime/bootstrap-client.ts +195 -0
  99. package/src/runtime/bootstrap-protocol.ts +67 -0
  100. package/src/runtime/clients.ts +6 -2
  101. package/src/runtime/connections.ts +9 -0
  102. package/src/server/app-key.ts +14 -0
  103. package/src/server/index.ts +2 -0
  104. package/src/server/ratelimit.ts +1 -1
  105. package/src/server/services/access.ts +2 -1
  106. package/src/services/account-category-policy.ts +1 -1
  107. package/src/services/account-lifecycle/audit.ts +1 -1
  108. package/src/services/account-lifecycle/index.ts +1 -1
  109. package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
  110. package/src/services/accounts/app.ts +1 -1
  111. package/src/services/accounts/avatar.ts +1 -1
  112. package/src/services/accounts/base-group.ts +1 -1
  113. package/src/services/accounts/directory-shares.ts +1 -1
  114. package/src/services/accounts/duplicate-emails.ts +1 -1
  115. package/src/services/accounts/entities.ts +2 -1
  116. package/src/services/accounts/group-sql.ts +2 -1
  117. package/src/services/accounts/groups.ts +1 -1
  118. package/src/services/accounts/identities.ts +13 -11
  119. package/src/services/accounts/ipa-data.ts +1 -1
  120. package/src/services/accounts/local-groups.ts +2 -1
  121. package/src/services/accounts/machine-group-management.ts +2 -1
  122. package/src/services/accounts/posix.ts +13 -12
  123. package/src/services/accounts/request-policy.ts +2 -1
  124. package/src/services/accounts/switching.ts +1 -1
  125. package/src/services/accounts/users.ts +2 -1
  126. package/src/services/announcements/index.ts +1 -1
  127. package/src/services/app-access-management.ts +3 -9
  128. package/src/services/app-access.ts +2 -1
  129. package/src/services/app-admin-facts.ts +160 -0
  130. package/src/services/app-approval.ts +34 -30
  131. package/src/services/app-bootstrap.ts +217 -0
  132. package/src/services/app-details.ts +3 -2
  133. package/src/services/app-domain-check.ts +152 -0
  134. package/src/services/app-domains.ts +172 -0
  135. package/src/services/app-events/accept.ts +5 -5
  136. package/src/services/app-events/admin.ts +15 -15
  137. package/src/services/app-events/declarations.ts +6 -3
  138. package/src/services/app-events/deliver.ts +6 -6
  139. package/src/services/app-events.ts +3 -2
  140. package/src/services/app-keys.ts +45 -11
  141. package/src/services/approvals/content.ts +1 -1
  142. package/src/services/approvals/index.ts +2 -1
  143. package/src/services/approvals/list.ts +1 -1
  144. package/src/services/approvals/migrate.ts +1 -1
  145. package/src/services/audit/index.ts +2 -1
  146. package/src/services/auth-flows/ipa.ts +1 -1
  147. package/src/services/auth-flows/magic-link.ts +1 -1
  148. package/src/services/auth-flows/password-reset.ts +1 -1
  149. package/src/services/auth-flows/proxy-return.ts +1 -1
  150. package/src/services/downstream-budget.ts +2 -2
  151. package/src/services/geo/admin.ts +13 -12
  152. package/src/services/geo/identity.ts +46 -0
  153. package/src/services/geo/ledger.ts +118 -106
  154. package/src/services/geo/migrate.ts +2 -1
  155. package/src/services/geo/route-chain.ts +3 -3
  156. package/src/services/geo/service.ts +4 -24
  157. package/src/services/geoip/index.ts +5 -5
  158. package/src/services/geoip/store.ts +24 -21
  159. package/src/services/help/index.ts +2 -1
  160. package/src/services/help/store.ts +2 -1
  161. package/src/services/identity/app-request.ts +44 -0
  162. package/src/services/identity/constants.ts +1 -0
  163. package/src/services/identity/index.ts +1 -0
  164. package/src/services/identity/invocation-actor.ts +2 -1
  165. package/src/services/identity/invocation-token.ts +112 -0
  166. package/src/services/identity/key-ring.ts +2 -2
  167. package/src/services/identity/resource-readers.ts +1 -1
  168. package/src/services/identity/routes.ts +11 -3
  169. package/src/services/identity/runtime-config.ts +14 -3
  170. package/src/services/identity/workload-auth.ts +2 -0
  171. package/src/services/identity/workload-runtime.ts +3 -3
  172. package/src/services/index.ts +4 -0
  173. package/src/services/ipa/groups.ts +1 -1
  174. package/src/services/ipa/profile.ts +1 -1
  175. package/src/services/ipa/search.ts +1 -1
  176. package/src/services/ipa/sync.ts +1 -1
  177. package/src/services/ipa/users.ts +1 -1
  178. package/src/services/known-browsers.ts +2 -1
  179. package/src/services/legal-consent.ts +1 -1
  180. package/src/services/links/index.ts +16 -0
  181. package/src/services/links/runtime.ts +44 -0
  182. package/src/services/links/store.ts +105 -0
  183. package/src/services/logging/index.ts +13 -8
  184. package/src/services/logging/redaction.ts +6 -1
  185. package/src/services/logging/trace.ts +9 -9
  186. package/src/services/logging/web-vitals.ts +1 -1
  187. package/src/services/mandates/index.ts +2 -1
  188. package/src/services/notifications/batches.ts +1 -1
  189. package/src/services/notifications/browser.ts +1 -1
  190. package/src/services/notifications/catalog.ts +1 -1
  191. package/src/services/notifications/dispatcher.ts +1 -1
  192. package/src/services/notifications/index.ts +1 -1
  193. package/src/services/notifications/observability.ts +2 -1
  194. package/src/services/notifications/platform.ts +1 -1
  195. package/src/services/notifications/quiet.ts +1 -1
  196. package/src/services/notifications/user.ts +1 -1
  197. package/src/services/oauth-tokens.ts +1 -1
  198. package/src/services/outbox.ts +15 -14
  199. package/src/services/outgoing-mail/admin.ts +1 -1
  200. package/src/services/outgoing-mail/dispatcher.ts +1 -1
  201. package/src/services/outgoing-mail/drain.ts +2 -1
  202. package/src/services/outgoing-mail/enqueue.ts +2 -1
  203. package/src/services/outgoing-mail/messages.ts +2 -1
  204. package/src/services/outgoing-mail/retention.ts +1 -1
  205. package/src/services/outgoing-mail/send.ts +1 -1
  206. package/src/services/outgoing-mail/store.ts +2 -1
  207. package/src/services/panel-context-consent.ts +32 -28
  208. package/src/services/providers/local/auth.ts +1 -1
  209. package/src/services/providers/local/users.ts +1 -1
  210. package/src/services/pwa-devices.ts +2 -1
  211. package/src/services/rail-preferences.ts +16 -12
  212. package/src/services/rail-shortcuts.ts +12 -10
  213. package/src/services/request-cache-redis.ts +3 -3
  214. package/src/services/service-account-credentials.ts +2 -1
  215. package/src/services/service-accounts.ts +2 -1
  216. package/src/services/session/index.ts +2 -1
  217. package/src/services/session/user.ts +2 -1
  218. package/src/services/settings/core-settings.ts +20 -0
  219. package/src/services/settings/crypto.ts +2 -2
  220. package/src/services/settings/index.ts +1 -1
  221. package/src/services/settings/store.ts +6 -1
  222. package/src/services/settings/versions.ts +2 -1
  223. package/src/services/weather/forecast.ts +2 -1
  224. package/src/services/weather/locations.ts +1 -1
  225. package/src/services/weather/migrate.ts +2 -1
  226. package/src/services/weather/short-id.ts +2 -1
  227. package/src/services/webauthn.ts +1 -1
  228. package/src/ssr/admin-navigation.ts +0 -1
  229. package/src/ssr/compact-shell.browser-harness.ts +2 -0
  230. package/src/ssr/platform-messages.ts +0 -2
  231. package/src/start.ts +35 -14
  232. package/src/workflows/ai/store.ts +2 -1
  233. package/src/workflows/store/budget.ts +2 -1
  234. package/src/workflows/store/definitions.ts +2 -1
  235. package/src/workflows/store/events.ts +2 -1
  236. package/src/workflows/store/observability.ts +2 -1
  237. package/src/workflows/store/retention.ts +2 -1
  238. package/src/workflows/store/runs.ts +2 -1
  239. package/src/workflows/store/transaction.ts +2 -1
  240. package/src/workflows/store/worker.ts +2 -1
@@ -1,6 +1,6 @@
1
1
  import { isPromise } from "node:util/types";
2
2
  import type { ReservedSQL, SQL, TransactionSQL } from "bun";
3
- import * as bun from "bun";
3
+ import { createSql } from "../runtime/clients";
4
4
  import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
5
5
  import { boundTransaction } from "./authority";
6
6
  import { AppPwaError } from "./contracts";
@@ -22,8 +22,9 @@ const cancellationPool = (db: SQL) => {
22
22
  const connection: Record<string, string | number> = {};
23
23
  for (let index = 0; index + 1 < parts.length; index += 2) connection[parts[index]!] = parts[index + 1]!;
24
24
  connection.statement_timeout = requestBudget;
25
- return new bun.SQL(`postgres://localhost?sslmode=${modes[sslMode]}`, {
25
+ return createSql({
26
26
  ...options,
27
+ adapter: "postgres",
27
28
  // Bun adds SNI to normalized prefer options. Passing that generated object
28
29
  // back as explicit TLS upgrades prefer to require. Recreate its default SNI
29
30
  // from hostname instead; explicit verification options remain unchanged.
@@ -35,6 +36,7 @@ const cancellationPool = (db: SQL) => {
35
36
  maxLifetime: REQUEST_IDLE_TIMEOUT_SECONDS,
36
37
  onconnect: undefined,
37
38
  onclose: undefined,
39
+ url: `postgres://localhost?sslmode=${modes[sslMode]}`,
38
40
  });
39
41
  };
40
42
 
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../runtime/clients";
2
+
2
3
  import { logger } from "../services/logging";
3
4
 
4
5
  const log = logger("capabilities:claims");
@@ -1,6 +1,6 @@
1
- import { sql } from "bun";
2
1
  import { createCapabilityDeadline } from "../_internal/capability-timeout";
3
2
  import type { CapabilityOrigin } from "../contracts/capabilities";
3
+ import { sql } from "../runtime/clients";
4
4
  import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
5
5
  import { pruneApprovalRequests } from "../services/approvals";
6
6
  import { logger } from "../services/logging";
@@ -65,6 +65,8 @@ export type CapabilityExecutionInput = {
65
65
  destructive: boolean;
66
66
  actorKind?: "user" | "service_account" | "app" | null;
67
67
  actorId?: string | null;
68
+ actorAppId?: string | null;
69
+ actorKeyId?: string | null;
68
70
  userId?: string | null;
69
71
  /** Recorded only when the access subject differs from the acting principal. */
70
72
  accessSubject?: { type: "user" | "service_account"; id: string } | null;
@@ -92,6 +94,8 @@ export type CapabilityExecution = {
92
94
  destructive: boolean;
93
95
  actorKind: "user" | "service_account" | "app" | null;
94
96
  actorId: string | null;
97
+ actorAppId?: string | null;
98
+ actorKeyId?: string | null;
95
99
  userId: string | null;
96
100
  accessSubjectType: "user" | "service_account" | null;
97
101
  accessSubjectId: string | null;
@@ -122,12 +126,12 @@ export const recordCapabilityExecution = async (input: CapabilityExecutionInput)
122
126
  const [row] = await db<{ id: string }[]>`
123
127
  INSERT INTO capabilities.executions (
124
128
  request_id, invocation_id, parent_invocation_id, via_app_id, origin, app_id, capability, kind, destructive,
125
- actor_kind, actor_id, user_id, access_subject_type, access_subject_id,
129
+ actor_kind, actor_id, actor_app_id, actor_key_id, user_id, access_subject_type, access_subject_id,
126
130
  status, error_code, input_meta, output_meta, idempotency_key, replayed,
127
131
  started_at, completed_at, duration_ms
128
132
  ) VALUES (
129
133
  ${input.requestId}, ${input.invocationId ?? null}, ${input.parentInvocationId ?? null}, ${input.viaAppId ?? null}, ${input.origin}, ${input.appId}, ${input.capability}, ${input.kind}, ${input.destructive},
130
- ${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.userId ?? null},
134
+ ${input.actorKind ?? null}, ${input.actorId ?? null}, ${input.actorAppId ?? null}, ${input.actorKeyId ?? null}::uuid, ${input.userId ?? null},
131
135
  ${input.accessSubject?.type ?? null}, ${input.accessSubject?.id ?? null},
132
136
  ${input.status}, ${input.errorCode ?? null},
133
137
  ${input.inputMeta ? JSON.stringify(input.inputMeta) : null}::text::jsonb,
@@ -177,7 +181,7 @@ const columns = () => sql`
177
181
  execution.id::text AS id, execution.request_id AS "requestId",
178
182
  execution.invocation_id::text AS "invocationId", execution.parent_invocation_id::text AS "parentInvocationId", execution.via_app_id AS "viaAppId", execution.origin, execution.app_id AS "appId",
179
183
  execution.capability, execution.kind, execution.destructive,
180
- execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
184
+ execution.actor_app_id AS "actorAppId", execution.actor_key_id::text AS "actorKeyId", execution.actor_kind AS "actorKind", execution.actor_id::text AS "actorId", execution.user_id::text AS "userId",
181
185
  execution.access_subject_type AS "accessSubjectType", execution.access_subject_id::text AS "accessSubjectId",
182
186
  execution.status, execution.error_code AS "errorCode", execution.input_meta AS "inputMeta",
183
187
  execution.output_meta AS "outputMeta", execution.idempotency_key AS "idempotencyKey", execution.replayed,
@@ -4,7 +4,7 @@ import { geoMapContract } from "../contracts/geo-map";
4
4
  import { geoRouteContract } from "../contracts/geo-route";
5
5
  import { type CapabilityCaller, invokeCapabilityWithDataSchema } from "./server";
6
6
 
7
- /** Calls Core using the caller's interactive identity, invocation transport, or durable mandate. */
7
+ /** Calls Core using the caller's interactive identity, invocation transport, app authority, or durable mandate. */
8
8
  export const geoClient = {
9
9
  mapStyle: (input: z.input<typeof geoMapContract.style.input>, caller: CapabilityCaller) =>
10
10
  invokeCapabilityWithDataSchema(
@@ -1,4 +1,4 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../runtime/clients";
2
2
 
3
3
  /**
4
4
  * Platform-owned capability execution history.
@@ -58,6 +58,8 @@ export const migrateCloudCapabilities = async (): Promise<void> => {
58
58
  await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS invocation_id UUID`.simple();
59
59
  await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS parent_invocation_id UUID`.simple();
60
60
  await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS via_app_id TEXT`.simple();
61
+ await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS actor_app_id TEXT`.simple();
62
+ await sql`ALTER TABLE capabilities.executions ADD COLUMN IF NOT EXISTS actor_key_id UUID`.simple();
61
63
 
62
64
  await sql`DO $$ BEGIN
63
65
  IF EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid = 'capabilities.executions'::regclass
@@ -33,7 +33,7 @@ export const readResourcePrincipals = async (options: {
33
33
  const ref = CloudResourceRefSchema.parse(options.ref);
34
34
  // Core issues the credential automatically; an explicit CLOUD_APP_CREDENTIAL override wins.
35
35
  const credential = getAppWorkloadCredential();
36
- if (!credential) return { state: "not_configured", reason: "no_credential" };
36
+ if (!env.CLOUD_APP_KEY && !credential) return { state: "not_configured", reason: "no_credential" };
37
37
  if (!env.CLOUD_CORE_INTERNAL_ORIGIN) return { state: "not_configured", reason: "no_internal_origin" };
38
38
  const callerAppId = getProcessApplicationId();
39
39
  if (!callerAppId) return { state: "not_configured", reason: "no_app_identity" };
@@ -54,7 +54,7 @@ export const readResourcePrincipals = async (options: {
54
54
  ResourceReadableDataSchema,
55
55
  {
56
56
  workload: { appId: callerAppId },
57
- authorization: `Bearer ${credential}`,
57
+ ...(credential ? { authorization: `Bearer ${credential}` } : {}),
58
58
  signal: options.signal,
59
59
  locale: options.locale,
60
60
  requestId: options.requestId,
@@ -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,
@@ -1,14 +1,18 @@
1
1
  import { z } from "zod";
2
+ import { getProcessApplicationId } from "../_internal/process-identity";
2
3
  import { refreshAppWorkloadCredential } from "../_internal/workload-credential";
3
4
  import { dispatchCapability, loadCapabilityCatalogPage } from "../api/capabilities";
5
+ import { env } from "../config/env";
4
6
  import {
5
7
  CAPABILITY_FRAMEWORK_ERROR_CODES,
6
8
  CAPABILITY_MAX_CATALOG_BYTES,
9
+ CAPABILITY_MAX_RESULT_BYTES,
7
10
  CapabilityCatalogSchema,
8
11
  type CapabilityOrigin,
9
12
  capabilityResultSchema,
10
13
  } from "../contracts/capabilities";
11
14
  import type { RequestAuthority } from "../server";
15
+ import { sendAppRequest } from "../services/identity/app-request";
12
16
  import { coreOrigin } from "../services/identity/core-origin";
13
17
  import { capabilityMessages } from "../shared/capability-messages";
14
18
  import { LOCALE_HEADER } from "../shared/locale";
@@ -36,12 +40,24 @@ export type CapabilityCaller = {
36
40
  /** BCP 47 locale of the originating request, forwarded as invocation metadata. */
37
41
  locale?: string | null;
38
42
  signal?: AbortSignal;
39
- /** App-only query authority. The workload credential goes only to Core. */
43
+ /** Request-local aggregate response budget; called before result validation for every received chunk. */
44
+ consumeResponseBytes?: (bytes: number) => void;
45
+ /** App-only query authority. The platform authenticates each transmission to Core. */
40
46
  workload?: { appId: string };
41
- /** Durable background authority. `authorization` must be the owning app's workload credential. */
47
+ /** App authority authenticated only by the configured Ed25519 app key. */
48
+ app?: { appId: string };
49
+ /** Durable background authority. The platform authenticates each transmission to Core. */
42
50
  mandate?: { id: string; revision: number; callingAppId: string };
43
51
  };
44
52
 
53
+ /** Call as the started application, without a person, service account token, or mandate. */
54
+ export const appCaller = (options: Pick<CapabilityCaller, "locale" | "requestId" | "signal"> = {}): CapabilityCaller => {
55
+ const appId = getProcessApplicationId();
56
+ if (!appId) throw new Error("Start the application before creating its capability caller");
57
+ if (!env.CLOUD_APP_KEY) throw new Error("App capability calls require CLOUD_APP_KEY");
58
+ return { ...options, app: { appId }, origin: "app" };
59
+ };
60
+
45
61
  export { coreOrigin } from "../services/identity/core-origin";
46
62
 
47
63
  export const callerRequest = async (
@@ -52,7 +68,7 @@ export const callerRequest = async (
52
68
  ): Promise<Request> => {
53
69
  const headers = new Headers();
54
70
  if (caller.authorization) headers.set("authorization", caller.authorization);
55
- if (!caller.mandate && !caller.workload) {
71
+ if (!caller.mandate && !caller.workload && !caller.app) {
56
72
  if (caller.cookie) headers.set("cookie", caller.cookie);
57
73
  if (caller.requestId) headers.set("x-request-id", caller.requestId);
58
74
  if (caller.traceparent) headers.set("traceparent", caller.traceparent);
@@ -60,14 +76,17 @@ export const callerRequest = async (
60
76
  if (caller.locale) headers.set(LOCALE_HEADER, caller.locale);
61
77
  if (idempotencyKey) headers.set("idempotency-key", idempotencyKey);
62
78
  } else {
63
- headers.set("x-cloud-app-id", caller.mandate?.callingAppId ?? caller.workload!.appId);
79
+ headers.set("x-cloud-app-id", caller.app?.appId ?? caller.mandate?.callingAppId ?? caller.workload!.appId);
64
80
  }
65
81
  const signal = combineCapabilitySignals(caller.signal, invocationSignal);
66
- return new Request(new URL(path, await coreOrigin(caller.mandate !== undefined || caller.workload !== undefined)), {
67
- method: "POST",
68
- headers,
69
- signal,
70
- });
82
+ return new Request(
83
+ new URL(path, await coreOrigin(caller.app !== undefined || caller.mandate !== undefined || caller.workload !== undefined)),
84
+ {
85
+ method: "POST",
86
+ headers,
87
+ signal,
88
+ },
89
+ );
71
90
  };
72
91
 
73
92
  export const unavailable = (
@@ -103,16 +122,23 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
103
122
  caller: CapabilityCaller,
104
123
  ): Promise<CapabilityClientResult<z.output<TDataSchema>>> => {
105
124
  try {
125
+ if (
126
+ caller.app &&
127
+ (!env.CLOUD_APP_KEY || caller.authority || caller.authorization || caller.cookie || caller.workload || caller.mandate)
128
+ )
129
+ throw new Error("App calls require one app-key authority source");
106
130
  if (caller.workload && (caller.mandate || caller.cookie || invocation.kind !== "query"))
107
131
  throw new Error("Workload authority permits only queries without a mandate or cookie");
108
132
  const path =
109
- caller.mandate || caller.workload
133
+ caller.app || caller.mandate || caller.workload
110
134
  ? "/api/_internal/identity/v1/invoke"
111
135
  : `/api/capabilities/v1/${invocation.kind === "query" ? "queries" : "actions"}/${encodeURIComponent(invocation.appId)}/${encodeURIComponent(invocation.capabilityId)}`;
112
136
  const request = await callerRequest(caller, path, invocation.idempotencyKey, invocation.signal);
113
137
  if (caller.authority) {
114
- if (caller.authorization || caller.cookie || caller.mandate || caller.workload || caller.transport)
138
+ if (caller.authorization || caller.cookie || caller.app || caller.mandate || caller.workload || caller.transport)
115
139
  throw new Error("Use exactly one capability authority source");
140
+ let charged = false;
141
+ const charge = caller.consumeResponseBytes;
116
142
  const outcome = await dispatchCapability({
117
143
  request,
118
144
  kind: invocation.kind === "query" ? "queries" : "actions",
@@ -122,13 +148,29 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
122
148
  authority: caller.authority,
123
149
  locale: caller.locale ?? undefined,
124
150
  origin: caller.origin ?? "app",
151
+ ...(charge
152
+ ? {
153
+ consumeResponseBytes: (bytes: number) => {
154
+ charged = true;
155
+ charge(bytes);
156
+ },
157
+ }
158
+ : {}),
125
159
  });
126
- return readCapabilityResponse(outcome, capabilityResultSchema(dataSchema, { consumer: true }));
160
+ return readCapabilityResponse(
161
+ outcome,
162
+ capabilityResultSchema(dataSchema, { consumer: true }),
163
+ CAPABILITY_MAX_RESULT_BYTES,
164
+ // Only a budgeted caller cancels reading a response that already arrived; others keep the received result.
165
+ charge ? combineCapabilitySignals(caller.signal, invocation.signal) : undefined,
166
+ charged ? undefined : charge,
167
+ );
127
168
  }
128
169
  const brokerRequest = new Request(request, {
129
170
  body: JSON.stringify(
130
- caller.mandate || caller.workload
171
+ caller.app || caller.mandate || caller.workload
131
172
  ? {
173
+ ...(caller.app ? { appCaller: true } : {}),
132
174
  kind: invocation.kind,
133
175
  targetApp: invocation.appId,
134
176
  capabilityId: invocation.capabilityId,
@@ -149,13 +191,15 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
149
191
  : { input: invocation.input },
150
192
  ),
151
193
  });
152
- const send = caller.transport ?? ((request: Request) => fetch(request, caller.workload ? { redirect: "error" } : undefined));
153
- // Only Core's pre-dispatch workload rejection is safe to retry, including a non-idempotent action.
154
- const workloadAppId = caller.mandate?.callingAppId ?? caller.workload?.appId;
194
+ const workloadAppId = caller.app?.appId ?? caller.mandate?.callingAppId ?? caller.workload?.appId;
195
+ const send = (request: Request) =>
196
+ workloadAppId ? sendAppRequest(request, { appId: workloadAppId, transport: caller.transport }) : (caller.transport ?? fetch)(request);
197
+ // Only Core's pre-dispatch legacy rejection is safe to refresh and retry.
198
+ // A configured app key is never downgraded to a reusable credential.
155
199
  const retryRequest = workloadAppId ? brokerRequest.clone() : undefined;
156
200
  let response = await send(brokerRequest);
157
201
  const credentialRejected = (reply: Response) => reply.status === 401 && reply.headers.get("x-cloud-workload-auth") === "required";
158
- if (workloadAppId && retryRequest && credentialRejected(response)) {
202
+ if (!caller.app && workloadAppId && retryRequest && !env.CLOUD_APP_KEY && credentialRejected(response)) {
159
203
  await response.body?.cancel();
160
204
  let token: string | undefined;
161
205
  try {
@@ -170,7 +214,13 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
170
214
  return unavailable(undefined, undefined, caller.locale ?? undefined);
171
215
  }
172
216
  }
173
- return readCapabilityResponse(response, capabilityResultSchema(dataSchema, { consumer: true }));
217
+ return readCapabilityResponse(
218
+ response,
219
+ capabilityResultSchema(dataSchema, { consumer: true }),
220
+ CAPABILITY_MAX_RESULT_BYTES,
221
+ caller.consumeResponseBytes ? combineCapabilitySignals(caller.signal, invocation.signal) : undefined,
222
+ caller.consumeResponseBytes,
223
+ );
174
224
  } catch (cause) {
175
225
  return unavailable(cause, invocation, caller.locale ?? undefined);
176
226
  }
@@ -202,14 +252,17 @@ export const listCapabilityCatalog = async (
202
252
  if (options.caller) {
203
253
  const caller = options.caller;
204
254
  if (options.authority) throw new Error("Use one catalog authority source");
205
- if (caller.authority && (caller.authorization || caller.cookie || caller.mandate || caller.workload || caller.transport))
255
+ if (
256
+ caller.authority &&
257
+ (caller.authorization || caller.cookie || caller.app || caller.mandate || caller.workload || caller.transport)
258
+ )
206
259
  throw new Error("Use exactly one capability authority source");
207
260
  if (caller.authority)
208
261
  return {
209
262
  ok: true,
210
263
  data: await loadCapabilityCatalogPage({ cursor: options.cursor, limit }, {}, caller.locale ?? options.locale, caller.authority),
211
264
  };
212
- if (caller.mandate || caller.workload) throw new Error("Calendar discovery requires request credentials");
265
+ if (caller.app || caller.mandate || caller.workload) throw new Error("Calendar discovery requires request credentials");
213
266
  const query = new URLSearchParams({ limit: String(limit) });
214
267
  if (options.cursor) query.set("cursor", options.cursor);
215
268
  const request = new Request(await callerRequest(caller, `/api/capabilities/v1/catalog?${query}`), { method: "GET" });
@@ -19,6 +19,7 @@ import type { CapabilityCatalogAppClientResult, CapabilityInvocation, Capability
19
19
 
20
20
  export type { CapabilityActionWording } from "../_internal/capability-sentences";
21
21
  export {
22
+ appCaller,
22
23
  type CapabilityCaller,
23
24
  invokeCapability,
24
25
  invokeCapabilityWithDataSchema,
@@ -89,7 +90,7 @@ export const reviewCapabilityAction = async <TInput = unknown>(
89
90
  caller: CapabilityCaller,
90
91
  ): Promise<CapabilityReviewClientResult> => {
91
92
  try {
92
- if (caller.workload) throw new Error("App query authority cannot review Actions");
93
+ if (caller.app || caller.workload) throw new Error("App query authority cannot review Actions");
93
94
  if (caller.authority) {
94
95
  if (caller.authorization || caller.cookie || caller.mandate || caller.transport)
95
96
  throw new Error("Use exactly one capability authority source");
@@ -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
+ };
@@ -78,7 +78,8 @@ export const enrollAppKey = async (input: {
78
78
  format: AppKeyArtifactFormat;
79
79
  uid?: number;
80
80
  name?: string;
81
- register: (body: { publicKey: string; name?: string }) => Promise<unknown>;
81
+ grants?: "oauth:issue"[];
82
+ register: (body: { publicKey: string; name?: string; grants?: "oauth:issue"[] }) => Promise<unknown>;
82
83
  }) => {
83
84
  validateAppKeyAppId(input.appId);
84
85
  if (input.uid !== undefined && process.getuid?.() !== 0 && process.getuid?.() !== input.uid)
@@ -113,7 +114,11 @@ export const enrollAppKey = async (input: {
113
114
  if ((await deriveAppPublicKey(state.seed)) !== state.publicKey) throw new Error("Enrollment public key does not match its seed");
114
115
  await owner(provisional, input.uid);
115
116
  const registered = registrationSchema.parse(
116
- await input.register({ publicKey: state.publicKey, ...(input.name ? { name: input.name } : {}) }),
117
+ await input.register({
118
+ publicKey: state.publicKey,
119
+ ...(input.name ? { name: input.name } : {}),
120
+ ...(input.grants ? { grants: input.grants } : {}),
121
+ }),
117
122
  );
118
123
  if (registered.fingerprint !== (await appKeyFingerprint(state.publicKey)))
119
124
  throw new Error("Core returned a different app-key fingerprint");
@@ -161,8 +166,11 @@ export const appKeyCommands = [
161
166
  format: flag.enum(["file", "env", "compose", "k8s-secret"] as const, { default: "file" }),
162
167
  uid: flag.int({ min: 0, description: "File owner UID; requires root or the current UID" }),
163
168
  name: flag.string({ description: "Human-readable key name" }),
169
+ grant: flag.enum(["oauth:issue"] as const, { description: "Grant OAuth issuance to each new key; repeat on rotation" }),
170
+ noGrant: flag.boolean({ name: "no-grant", description: "Enroll new keys without grants (the default)" }),
164
171
  },
165
172
  async run({ ctx, args, flags }) {
173
+ if (flags.grant && flags.noGrant) throw new Error("Choose either --grant or --no-grant");
166
174
  const apps = await resolveAppKeyApps(args.apps, flags.all, flags.apps);
167
175
  const results = [];
168
176
  for (const appId of apps)
@@ -174,6 +182,7 @@ export const appKeyCommands = [
174
182
  format: flags.format ?? "file",
175
183
  uid: flags.uid,
176
184
  name: flags.name,
185
+ grants: flags.grant ? [flags.grant] : flags.noGrant ? [] : undefined,
177
186
  register: (body) => apiJson(ctx, "POST", keyPath(appId), body),
178
187
  }),
179
188
  );
@@ -194,6 +203,14 @@ export const appKeyCommands = [
194
203
  const result = z
195
204
  .object({
196
205
  installationId: z.uuid(),
206
+ lastRejectedStart: z
207
+ .object({
208
+ at: z.iso.datetime(),
209
+ reason: z.enum(["revoked", "expired", "disabled", "connections_not_permitted"]),
210
+ keyId: z.uuid().nullable(),
211
+ })
212
+ .nullable()
213
+ .optional(),
197
214
  items: z.array(
198
215
  z
199
216
  .object({ keyId: z.string(), name: z.string(), fingerprint: z.string(), status: z.string(), accountStatus: z.string() })
@@ -202,7 +219,11 @@ export const appKeyCommands = [
202
219
  })
203
220
  .passthrough()
204
221
  .parse(await apiGet(ctx, `${keyPath(args.app)}?page=${flags.page}&perPage=${flags.perPage}`));
205
- if (!printStructured(ctx, result))
222
+ if (!printStructured(ctx, result)) {
223
+ if (result.lastRejectedStart) {
224
+ const rejection = result.lastRejectedStart;
225
+ ctx.print(`Last rejected start: ${rejection.at} · ${rejection.reason} · key ${rejection.keyId ?? "unknown"}`);
226
+ }
206
227
  ctx.table(result.items, [
207
228
  { key: "keyId", label: "Key ID" },
208
229
  { key: "name", label: "Name" },
@@ -210,6 +231,7 @@ export const appKeyCommands = [
210
231
  { key: "status", label: "Status" },
211
232
  { key: "accountStatus", label: "Account" },
212
233
  ]);
234
+ }
213
235
  },
214
236
  }),
215
237
  command("apps keys revoke", {