@k2b/cloud 0.48.0 → 0.50.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 (206) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/app-access-resources.ts +2 -1
  3. package/src/_internal/app-capabilities.ts +73 -0
  4. package/src/_internal/app-event-catalog.ts +1 -1
  5. package/src/_internal/app-queries.ts +1 -1
  6. package/src/_internal/application-package.ts +2 -1
  7. package/src/_internal/capabilities.ts +17 -2
  8. package/src/_internal/define-app.ts +2 -0
  9. package/src/ai/app-skill-store.ts +2 -1
  10. package/src/ai/app-skills.ts +2 -1
  11. package/src/ai/approvals.ts +2 -1
  12. package/src/ai/chat-tasks.ts +2 -1
  13. package/src/ai/credentials.ts +2 -1
  14. package/src/ai/dictation-runtime.ts +1 -1
  15. package/src/ai/dictations.ts +1 -1
  16. package/src/ai/files-store.ts +2 -1
  17. package/src/ai/inference-calls.ts +2 -1
  18. package/src/ai/map-store.ts +1 -1
  19. package/src/ai/map-tool.ts +1 -1
  20. package/src/ai/memories.ts +1 -1
  21. package/src/ai/memory-learning-runs.ts +1 -1
  22. package/src/ai/memory-learning.ts +1 -1
  23. package/src/ai/memory-workflow-evidence.ts +1 -1
  24. package/src/ai/message-queue.ts +2 -1
  25. package/src/ai/migrate.ts +1 -1
  26. package/src/ai/model-access-migrate.ts +1 -1
  27. package/src/ai/model-access.ts +1 -1
  28. package/src/ai/model-catalog.ts +1 -1
  29. package/src/ai/model-delete.ts +1 -1
  30. package/src/ai/model-usage.ts +2 -1
  31. package/src/ai/open-questions.ts +1 -1
  32. package/src/ai/openai-api-slots.ts +2 -1
  33. package/src/ai/prefs.ts +2 -1
  34. package/src/ai/projects.ts +2 -1
  35. package/src/ai/provider-connections.ts +1 -1
  36. package/src/ai/quota-limits.ts +2 -1
  37. package/src/ai/quota-provider.ts +2 -1
  38. package/src/ai/quota-report.ts +2 -1
  39. package/src/ai/quotas-migrate.ts +2 -1
  40. package/src/ai/quotas.ts +2 -1
  41. package/src/ai/request-headers.ts +2 -1
  42. package/src/ai/review-content.ts +2 -1
  43. package/src/ai/skill-search.ts +2 -1
  44. package/src/ai/skills.ts +2 -1
  45. package/src/ai/store.ts +2 -1
  46. package/src/ai/structured-runs.ts +2 -1
  47. package/src/ai/tool-audit.ts +2 -1
  48. package/src/ai/turn-timing.ts +1 -1
  49. package/src/ai/usage.ts +2 -1
  50. package/src/ai/web-provenance.ts +2 -1
  51. package/src/api/admin-ai-quotas.ts +1 -1
  52. package/src/api/admin-ai.ts +1 -1
  53. package/src/api/admin-app-keys.ts +47 -3
  54. package/src/api/admin-core-settings.ts +1 -1
  55. package/src/api/admin-outgoing-mail.ts +1 -1
  56. package/src/api/app-bootstrap.ts +23 -1
  57. package/src/api/app-events.ts +1 -1
  58. package/src/api/auth.ts +1 -1
  59. package/src/api/authenticate-app-request.ts +129 -0
  60. package/src/api/capabilities.ts +79 -5
  61. package/src/api/index.ts +2 -0
  62. package/src/api/me.ts +1 -1
  63. package/src/api/search.bench.ts +1 -1
  64. package/src/app-pwa/service.ts +2 -1
  65. package/src/app-pwa/transaction.ts +4 -2
  66. package/src/capabilities/claims.ts +2 -1
  67. package/src/capabilities/executions.ts +8 -4
  68. package/src/capabilities/geo-client.ts +1 -1
  69. package/src/capabilities/migrate.ts +3 -1
  70. package/src/capabilities/resource-readers.ts +2 -2
  71. package/src/capabilities/server-invoke.ts +45 -18
  72. package/src/capabilities/server.ts +2 -1
  73. package/src/cli/admin/app-keys.ts +25 -3
  74. package/src/config/env.ts +4 -4
  75. package/src/contracts/capabilities.ts +15 -2
  76. package/src/contracts/outgoing-mail.ts +1 -1
  77. package/src/events/app-event-delivery.ts +1 -1
  78. package/src/events/live-delivery.ts +2 -1
  79. package/src/runtime/bootstrap-protocol.ts +11 -4
  80. package/src/runtime/clients.ts +6 -2
  81. package/src/server/index.ts +1 -0
  82. package/src/server/ratelimit.ts +1 -1
  83. package/src/server/services/access.ts +2 -1
  84. package/src/services/account-category-policy.ts +1 -1
  85. package/src/services/account-lifecycle/audit.ts +1 -1
  86. package/src/services/account-lifecycle/index.ts +1 -1
  87. package/src/services/account-lifecycle/ipa-backfill.ts +2 -1
  88. package/src/services/accounts/app.ts +1 -1
  89. package/src/services/accounts/avatar.ts +1 -1
  90. package/src/services/accounts/base-group.ts +1 -1
  91. package/src/services/accounts/directory-shares.ts +1 -1
  92. package/src/services/accounts/duplicate-emails.ts +1 -1
  93. package/src/services/accounts/entities.ts +2 -1
  94. package/src/services/accounts/group-sql.ts +2 -1
  95. package/src/services/accounts/groups.ts +1 -1
  96. package/src/services/accounts/identities.ts +13 -11
  97. package/src/services/accounts/ipa-data.ts +1 -1
  98. package/src/services/accounts/local-groups.ts +2 -1
  99. package/src/services/accounts/machine-group-management.ts +2 -1
  100. package/src/services/accounts/posix.ts +13 -12
  101. package/src/services/accounts/request-policy.ts +2 -1
  102. package/src/services/accounts/switching.ts +1 -1
  103. package/src/services/accounts/users.ts +2 -1
  104. package/src/services/announcements/index.ts +1 -1
  105. package/src/services/app-access-management.ts +2 -1
  106. package/src/services/app-access.ts +2 -1
  107. package/src/services/app-admin-facts.ts +11 -3
  108. package/src/services/app-approval.ts +34 -30
  109. package/src/services/app-bootstrap.ts +104 -21
  110. package/src/services/app-details.ts +3 -2
  111. package/src/services/app-events/accept.ts +5 -5
  112. package/src/services/app-events/admin.ts +15 -15
  113. package/src/services/app-events/declarations.ts +6 -3
  114. package/src/services/app-events/deliver.ts +6 -6
  115. package/src/services/app-events.ts +3 -2
  116. package/src/services/app-keys.ts +42 -11
  117. package/src/services/approvals/content.ts +1 -1
  118. package/src/services/approvals/index.ts +2 -1
  119. package/src/services/approvals/list.ts +1 -1
  120. package/src/services/approvals/migrate.ts +1 -1
  121. package/src/services/audit/index.ts +2 -1
  122. package/src/services/auth-flows/ipa.ts +1 -1
  123. package/src/services/auth-flows/magic-link.ts +1 -1
  124. package/src/services/auth-flows/password-reset.ts +1 -1
  125. package/src/services/auth-flows/proxy-return.ts +1 -1
  126. package/src/services/geo/admin.ts +13 -12
  127. package/src/services/geo/identity.ts +46 -0
  128. package/src/services/geo/ledger.ts +118 -106
  129. package/src/services/geo/migrate.ts +2 -1
  130. package/src/services/geo/route-chain.ts +3 -3
  131. package/src/services/geo/service.ts +4 -24
  132. package/src/services/geoip/index.ts +5 -5
  133. package/src/services/geoip/store.ts +24 -21
  134. package/src/services/help/index.ts +2 -1
  135. package/src/services/help/store.ts +2 -1
  136. package/src/services/identity/app-request.ts +44 -0
  137. package/src/services/identity/constants.ts +1 -0
  138. package/src/services/identity/index.ts +1 -0
  139. package/src/services/identity/invocation-actor.ts +2 -1
  140. package/src/services/identity/invocation-token.ts +112 -0
  141. package/src/services/identity/key-ring.ts +2 -2
  142. package/src/services/identity/resource-readers.ts +1 -1
  143. package/src/services/identity/routes.ts +11 -3
  144. package/src/services/identity/runtime-config.ts +14 -3
  145. package/src/services/identity/workload-auth.ts +2 -0
  146. package/src/services/identity/workload-runtime.ts +1 -1
  147. package/src/services/ipa/groups.ts +1 -1
  148. package/src/services/ipa/profile.ts +1 -1
  149. package/src/services/ipa/search.ts +1 -1
  150. package/src/services/ipa/sync.ts +1 -1
  151. package/src/services/ipa/users.ts +1 -1
  152. package/src/services/known-browsers.ts +2 -1
  153. package/src/services/legal-consent.ts +1 -1
  154. package/src/services/logging/index.ts +6 -3
  155. package/src/services/logging/trace.ts +1 -1
  156. package/src/services/logging/web-vitals.ts +1 -1
  157. package/src/services/mandates/index.ts +2 -1
  158. package/src/services/notifications/batches.ts +1 -1
  159. package/src/services/notifications/browser.ts +1 -1
  160. package/src/services/notifications/catalog.ts +1 -1
  161. package/src/services/notifications/dispatcher.ts +1 -1
  162. package/src/services/notifications/index.ts +1 -1
  163. package/src/services/notifications/observability.ts +2 -1
  164. package/src/services/notifications/platform.ts +1 -1
  165. package/src/services/notifications/quiet.ts +1 -1
  166. package/src/services/notifications/user.ts +1 -1
  167. package/src/services/oauth-tokens.ts +1 -1
  168. package/src/services/outbox.ts +15 -14
  169. package/src/services/outgoing-mail/admin.ts +1 -1
  170. package/src/services/outgoing-mail/dispatcher.ts +1 -1
  171. package/src/services/outgoing-mail/drain.ts +2 -1
  172. package/src/services/outgoing-mail/enqueue.ts +2 -1
  173. package/src/services/outgoing-mail/messages.ts +2 -1
  174. package/src/services/outgoing-mail/retention.ts +1 -1
  175. package/src/services/outgoing-mail/send.ts +1 -1
  176. package/src/services/outgoing-mail/store.ts +2 -1
  177. package/src/services/panel-context-consent.ts +32 -28
  178. package/src/services/providers/local/auth.ts +1 -1
  179. package/src/services/providers/local/users.ts +1 -1
  180. package/src/services/pwa-devices.ts +2 -1
  181. package/src/services/rail-preferences.ts +16 -12
  182. package/src/services/rail-shortcuts.ts +12 -10
  183. package/src/services/request-cache-redis.ts +3 -3
  184. package/src/services/service-account-credentials.ts +2 -1
  185. package/src/services/service-accounts.ts +2 -1
  186. package/src/services/session/index.ts +2 -1
  187. package/src/services/session/user.ts +2 -1
  188. package/src/services/settings/index.ts +1 -1
  189. package/src/services/settings/store.ts +1 -1
  190. package/src/services/settings/versions.ts +2 -1
  191. package/src/services/weather/forecast.ts +2 -1
  192. package/src/services/weather/locations.ts +1 -1
  193. package/src/services/weather/migrate.ts +2 -1
  194. package/src/services/weather/short-id.ts +2 -1
  195. package/src/services/webauthn.ts +1 -1
  196. package/src/shared-text/documents.ts +2 -2
  197. package/src/ssr/compact-shell.browser-harness.ts +2 -0
  198. package/src/workflows/ai/store.ts +2 -1
  199. package/src/workflows/store/budget.ts +2 -1
  200. package/src/workflows/store/definitions.ts +2 -1
  201. package/src/workflows/store/events.ts +2 -1
  202. package/src/workflows/store/observability.ts +2 -1
  203. package/src/workflows/store/retention.ts +2 -1
  204. package/src/workflows/store/runs.ts +2 -1
  205. package/src/workflows/store/transaction.ts +2 -1
  206. package/src/workflows/store/worker.ts +2 -1
@@ -1,6 +1,8 @@
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,
@@ -10,6 +12,7 @@ import {
10
12
  capabilityResultSchema,
11
13
  } from "../contracts/capabilities";
12
14
  import type { RequestAuthority } from "../server";
15
+ import { sendAppRequest } from "../services/identity/app-request";
13
16
  import { coreOrigin } from "../services/identity/core-origin";
14
17
  import { capabilityMessages } from "../shared/capability-messages";
15
18
  import { LOCALE_HEADER } from "../shared/locale";
@@ -39,12 +42,22 @@ export type CapabilityCaller = {
39
42
  signal?: AbortSignal;
40
43
  /** Request-local aggregate response budget; called before result validation for every received chunk. */
41
44
  consumeResponseBytes?: (bytes: number) => void;
42
- /** App-only query authority. The workload credential goes only to Core. */
45
+ /** App-only query authority. The platform authenticates each transmission to Core. */
43
46
  workload?: { appId: string };
44
- /** 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. */
45
50
  mandate?: { id: string; revision: number; callingAppId: string };
46
51
  };
47
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
+
48
61
  export { coreOrigin } from "../services/identity/core-origin";
49
62
 
50
63
  export const callerRequest = async (
@@ -55,7 +68,7 @@ export const callerRequest = async (
55
68
  ): Promise<Request> => {
56
69
  const headers = new Headers();
57
70
  if (caller.authorization) headers.set("authorization", caller.authorization);
58
- if (!caller.mandate && !caller.workload) {
71
+ if (!caller.mandate && !caller.workload && !caller.app) {
59
72
  if (caller.cookie) headers.set("cookie", caller.cookie);
60
73
  if (caller.requestId) headers.set("x-request-id", caller.requestId);
61
74
  if (caller.traceparent) headers.set("traceparent", caller.traceparent);
@@ -63,14 +76,17 @@ export const callerRequest = async (
63
76
  if (caller.locale) headers.set(LOCALE_HEADER, caller.locale);
64
77
  if (idempotencyKey) headers.set("idempotency-key", idempotencyKey);
65
78
  } else {
66
- 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);
67
80
  }
68
81
  const signal = combineCapabilitySignals(caller.signal, invocationSignal);
69
- return new Request(new URL(path, await coreOrigin(caller.mandate !== undefined || caller.workload !== undefined)), {
70
- method: "POST",
71
- headers,
72
- signal,
73
- });
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
+ );
74
90
  };
75
91
 
76
92
  export const unavailable = (
@@ -106,15 +122,20 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
106
122
  caller: CapabilityCaller,
107
123
  ): Promise<CapabilityClientResult<z.output<TDataSchema>>> => {
108
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");
109
130
  if (caller.workload && (caller.mandate || caller.cookie || invocation.kind !== "query"))
110
131
  throw new Error("Workload authority permits only queries without a mandate or cookie");
111
132
  const path =
112
- caller.mandate || caller.workload
133
+ caller.app || caller.mandate || caller.workload
113
134
  ? "/api/_internal/identity/v1/invoke"
114
135
  : `/api/capabilities/v1/${invocation.kind === "query" ? "queries" : "actions"}/${encodeURIComponent(invocation.appId)}/${encodeURIComponent(invocation.capabilityId)}`;
115
136
  const request = await callerRequest(caller, path, invocation.idempotencyKey, invocation.signal);
116
137
  if (caller.authority) {
117
- 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)
118
139
  throw new Error("Use exactly one capability authority source");
119
140
  let charged = false;
120
141
  const charge = caller.consumeResponseBytes;
@@ -147,8 +168,9 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
147
168
  }
148
169
  const brokerRequest = new Request(request, {
149
170
  body: JSON.stringify(
150
- caller.mandate || caller.workload
171
+ caller.app || caller.mandate || caller.workload
151
172
  ? {
173
+ ...(caller.app ? { appCaller: true } : {}),
152
174
  kind: invocation.kind,
153
175
  targetApp: invocation.appId,
154
176
  capabilityId: invocation.capabilityId,
@@ -169,13 +191,15 @@ const invokeCapabilityWithResultSchema = async <TDataSchema extends z.ZodType, T
169
191
  : { input: invocation.input },
170
192
  ),
171
193
  });
172
- const send = caller.transport ?? ((request: Request) => fetch(request, caller.workload ? { redirect: "error" } : undefined));
173
- // Only Core's pre-dispatch workload rejection is safe to retry, including a non-idempotent action.
174
- 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.
175
199
  const retryRequest = workloadAppId ? brokerRequest.clone() : undefined;
176
200
  let response = await send(brokerRequest);
177
201
  const credentialRejected = (reply: Response) => reply.status === 401 && reply.headers.get("x-cloud-workload-auth") === "required";
178
- if (workloadAppId && retryRequest && credentialRejected(response)) {
202
+ if (!caller.app && workloadAppId && retryRequest && !env.CLOUD_APP_KEY && credentialRejected(response)) {
179
203
  await response.body?.cancel();
180
204
  let token: string | undefined;
181
205
  try {
@@ -228,14 +252,17 @@ export const listCapabilityCatalog = async (
228
252
  if (options.caller) {
229
253
  const caller = options.caller;
230
254
  if (options.authority) throw new Error("Use one catalog authority source");
231
- 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
+ )
232
259
  throw new Error("Use exactly one capability authority source");
233
260
  if (caller.authority)
234
261
  return {
235
262
  ok: true,
236
263
  data: await loadCapabilityCatalogPage({ cursor: options.cursor, limit }, {}, caller.locale ?? options.locale, caller.authority),
237
264
  };
238
- 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");
239
266
  const query = new URLSearchParams({ limit: String(limit) });
240
267
  if (options.cursor) query.set("cursor", options.cursor);
241
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");
@@ -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", {
package/src/config/env.ts CHANGED
@@ -164,13 +164,13 @@ const registry = defineEnv({
164
164
  schema: z.string(),
165
165
  scope: "secret",
166
166
  secretFile: true,
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`.",
167
+ doc: "Deprecated: Core and OAuth legacy 64-hex broker secret, used only when OAuth has no app key. Configure an OAuth app key instead; retained for this release.",
168
168
  },
169
169
  CLOUD_APP_KEY: {
170
170
  schema: z.string(),
171
171
  scope: "secret",
172
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.",
173
+ doc: "App Ed25519 private key for startup bootstrap, background calls and OAuth issuance; 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
174
  },
175
175
  CLOUD_CORE_INTERNAL_ORIGIN: {
176
176
  schema: z.string(),
@@ -180,7 +180,7 @@ const registry = defineEnv({
180
180
  schema: z.string(),
181
181
  scope: "secret",
182
182
  secretFile: true,
183
- doc: "Optional host-run app override for the automatically delivered app workload credential; provision through Core's admin workload API with `identity:invoke`.",
183
+ doc: "Deprecated: host-run override for the automatically delivered workload credential when no app key is configured; use CLOUD_APP_KEY or CLOUD_APP_KEY_FILE instead. Retained for this release.",
184
184
  },
185
185
  CLOUD_IDENTITY_JWKS_ORIGIN: {
186
186
  schema: z.string(),
@@ -188,7 +188,7 @@ const registry = defineEnv({
188
188
  },
189
189
  CLOUD_OAUTH_JWKS_ORIGIN: {
190
190
  schema: z.string(),
191
- doc: "Optional private origin of the OAuth listener for OAuth JWKS; unset fetches keys from the public issuer origin.",
191
+ doc: "Deprecated: explicit legacy OAuth JWKS origin, honored for this release. Unset uses Core internal OAuth JWKS through CLOUD_CORE_INTERNAL_ORIGIN, then the public issuer origin.",
192
192
  },
193
193
 
194
194
  // Build and standalone development
@@ -12,6 +12,7 @@ import { z } from "zod";
12
12
  import type { RequestAuthority } from "../server/middleware/auth";
13
13
  import type { AppEventDefinition, AppEventSubscriptionDefinition } from "./app-events";
14
14
  import { CommandPathSchema } from "./commands";
15
+ import { type PlatformPermission, PlatformPermissionSchema } from "./outgoing-mail";
15
16
  import type { AccessSubject, RequestActor, User } from "./shared";
16
17
  import { AuthenticatedPrincipalSchema } from "./shared";
17
18
 
@@ -955,12 +956,12 @@ export type CapabilityPresentationCatalog = {
955
956
 
956
957
  /** App authority has no grant-bearing subject, user, nested exchange, or interactive credential. */
957
958
  export type CapabilityAppExecutionContext = {
958
- actor: { kind: "app"; appId: string };
959
+ actor: { kind: "app"; appId: string; keyId?: string };
959
960
  accessSubject: { type: "none" };
960
961
  user: null;
961
962
  authority?: never;
962
963
  query?: never;
963
- idempotencyKey?: never;
964
+ idempotencyKey?: string;
964
965
  locale: string;
965
966
  requestId: string;
966
967
  origin: CapabilityOrigin;
@@ -968,8 +969,18 @@ export type CapabilityAppExecutionContext = {
968
969
  signal: AbortSignal;
969
970
  };
970
971
 
972
+ /** Explicit provider entry for calls paid for and authorized as the calling application. */
973
+ export type CapabilityAppHandler<Input extends z.ZodType, Data extends z.ZodType> = {
974
+ permission: PlatformPermission;
975
+ run: (
976
+ input: z.output<Input>,
977
+ context: CapabilityAppExecutionContext,
978
+ ) => CapabilityInvocationResult<z.output<Data>> | Promise<CapabilityInvocationResult<z.output<Data>>>;
979
+ };
980
+
971
981
  export type CapabilityQueryDefinition<Input extends z.ZodType = z.ZodType<any>, Data extends z.ZodType = z.ZodType<any>> = {
972
982
  appOnly?: never;
983
+ app?: CapabilityAppHandler<Input, Data>;
973
984
  title: string;
974
985
  description: string;
975
986
  input: Input;
@@ -988,6 +999,7 @@ export type CapabilityActionApprovalPolicy = "rememberable" | "none";
988
999
 
989
1000
  export type CapabilityActionDefinition<Input extends z.ZodType = z.ZodType<any>, Data extends z.ZodType = z.ZodType<any>> = {
990
1001
  appOnly?: never;
1002
+ app?: CapabilityAppHandler<Input, Data>;
991
1003
  title: string;
992
1004
  description: string;
993
1005
  input: Input;
@@ -1104,6 +1116,7 @@ const CapabilityOperationManifestBaseSchema = z
1104
1116
  inputSchema: z.record(z.string(), z.unknown()),
1105
1117
  dataSchema: z.record(z.string(), z.unknown()),
1106
1118
  stream: CapabilityStreamPolicySchema.optional(),
1119
+ appPermission: PlatformPermissionSchema.optional(),
1107
1120
  schemaHash: z.string().regex(/^[a-f0-9]{64}$/),
1108
1121
  })
1109
1122
  .strict();
@@ -2,7 +2,7 @@ import type { PageParams, Paginated, ServiceError } from "@k2b/stdlib";
2
2
  import { z } from "zod";
3
3
  import { type RequestActor, ServiceAccountSchema, UserSchema } from "./shared";
4
4
 
5
- export const PlatformPermissionSchema = z.enum(["mail:send", "mail:read", "resources:readers"]);
5
+ export const PlatformPermissionSchema = z.enum(["mail:send", "mail:read", "resources:readers", "geo:query"]);
6
6
  export type PlatformPermission = z.infer<typeof PlatformPermissionSchema>;
7
7
  export const MailRetentionSchema = z.object({ contentDays: z.int().min(1).max(36500), recordDays: z.int().min(1).max(36500) }).strict();
8
8
  export type MailRetention = z.infer<typeof MailRetentionSchema> & {
@@ -1,8 +1,8 @@
1
1
  import type { Topic, TopicConfig } from "@k2b/sync";
2
- import { sql } from "bun";
3
2
  import { assertAppEventSize } from "../_internal/app-event-schemas";
4
3
  import { lazySync } from "../_internal/process-sync";
5
4
  import { type AppEventEnvelope, AppEventEnvelopeSchema } from "../contracts/app-events";
5
+ import { sql } from "../runtime/clients";
6
6
  import { logger } from "../services/logging";
7
7
  import { createPgOutbox } from "../services/outbox";
8
8
 
@@ -1,7 +1,8 @@
1
1
  import type { Topic, TopicConfig } from "@k2b/sync";
2
- import { type SQL, sql } from "bun";
2
+ import type { SQL } from "bun";
3
3
  import type { z } from "zod";
4
4
  import { lazySync } from "../_internal/process-sync";
5
+ import { sql } from "../runtime/clients";
5
6
  import { logger } from "../services/logging";
6
7
  import { createPgOutbox } from "../services/outbox";
7
8
  import type { LiveEnvelope } from "./live-engine";
@@ -42,12 +42,16 @@ export const bootstrapBodyDigest = async (body: Uint8Array<ArrayBuffer>): Promis
42
42
  Buffer.from(await crypto.subtle.digest("SHA-256", body)).toString("base64url");
43
43
 
44
44
  /** Call at the HTTP transmission seam, including every retry. */
45
- export const signBootstrapProof = async (key: AppKey, now = Date.now()): Promise<string> => {
45
+ export const signAppProof = async (
46
+ key: AppKey,
47
+ request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
48
+ now = Date.now(),
49
+ ): Promise<string> => {
46
50
  const iat = Math.floor(now / 1000);
47
51
  return new SignJWT({
48
- htm: "POST",
49
- htu: BOOTSTRAP_PATH,
50
- bdh: await bootstrapBodyDigest(new TextEncoder().encode(BOOTSTRAP_BODY)),
52
+ htm: request.method,
53
+ htu: request.path,
54
+ bdh: await bootstrapBodyDigest(request.body),
51
55
  })
52
56
  .setProtectedHeader({ alg: "EdDSA", typ: BOOTSTRAP_TYPE, kid: key.keyId })
53
57
  .setIssuer(key.appId)
@@ -58,3 +62,6 @@ export const signBootstrapProof = async (key: AppKey, now = Date.now()): Promise
58
62
  .setJti(crypto.randomUUID())
59
63
  .sign(await importAppKeySeed(key.seed));
60
64
  };
65
+
66
+ export const signBootstrapProof = (key: AppKey, now = Date.now()): Promise<string> =>
67
+ signAppProof(key, { method: "POST", path: BOOTSTRAP_PATH, body: new TextEncoder().encode(BOOTSTRAP_BODY) }, now);
@@ -1,6 +1,6 @@
1
1
  import { RedisClient, type RedisOptions, SQL } from "bun";
2
2
  import { postgresApplicationOptions } from "../_internal/postgres-application-name";
3
- import { connections, hasConnections } from "./connections";
3
+ import { connections } from "./connections";
4
4
 
5
5
  export type SqlPoolOptions = SQL.PostgresOrMySQLOptions;
6
6
 
@@ -28,6 +28,8 @@ const notStarted = <T>(name: string): T => {
28
28
 
29
29
  export let sql: SQL = notStarted("sql");
30
30
  export let redis: RedisClient = notStarted("redis");
31
+ // Whether `sql` and `redis` hold native clients; teardown must not touch the placeholders.
32
+ let live = false;
31
33
 
32
34
  /** Own pool and lifecycle; defaults to the installation URL and application name. */
33
35
  export const createSql = (options: SqlPoolOptions = {}): SQL => {
@@ -55,6 +57,7 @@ export const initializeClients = async (): Promise<void> => {
55
57
  const valkey = createRedis();
56
58
  sql = database;
57
59
  redis = valkey;
60
+ live = true;
58
61
  } catch {
59
62
  await database.close();
60
63
  throw new Error("Valkey client configuration is invalid; check REDIS_URL and native Redis options");
@@ -63,7 +66,8 @@ export const initializeClients = async (): Promise<void> => {
63
66
 
64
67
  /** Internal test-target teardown, before a fixture selects a different database. */
65
68
  export const closePlatformClients = async (): Promise<void> => {
66
- if (hasConnections()) {
69
+ if (live) {
70
+ live = false;
67
71
  await sql.close();
68
72
  redis.close();
69
73
  }
@@ -1,6 +1,7 @@
1
1
  export { buildMetadata } from "../_internal/build-metadata";
2
2
  export { loadPendingApprovals } from "../api/approvals-loader";
3
3
  export { geoClient } from "../capabilities/geo-client";
4
+ export { appCaller } from "../capabilities/server-invoke";
4
5
  export { expectUserBackedActor, getUserBackedActor, isAppSessionActor, userFromActor } from "./actor";
5
6
  export type { ApiErrorBody, ApiErrorResponse, ApiErrorStatus } from "./api";
6
7
  export { api, respond, respondMessage } from "./api";
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { redis } from "bun";
2
+ import { redis } from "../runtime/clients";
3
3
 
4
4
  /**
5
5
  * Sliding-window rate limiter on the Cloud Redis.
@@ -1,7 +1,8 @@
1
1
  import { err, fail, ok, type Result } from "@k2b/stdlib";
2
- import { type SQLQuery, sql } from "bun";
2
+ import type { SQLQuery } from "bun";
3
3
  import { type ManagerCandidate, removesLastManager } from "../../access/managers";
4
4
  import { accessMessages } from "../../access/messages";
5
+ import { sql } from "../../runtime/clients";
5
6
  import { recursiveGroupIdsSubquery, recursiveServiceAccountGroupIdsSubquery } from "../../services/accounts/group-sql";
6
7
  import { toPgUuidArray } from "../../services/postgres";
7
8
  import type { ServiceAccountKind } from "../../services/service-accounts";
@@ -1,10 +1,10 @@
1
- import { sql } from "bun";
2
1
  import {
3
2
  type AccountCategory,
4
3
  type AccountCategoryPolicy,
5
4
  accountCategory,
6
5
  DEFAULT_ACCOUNT_CATEGORY_POLICY,
7
6
  } from "../contracts/account-categories";
7
+ import { sql } from "../runtime/clients";
8
8
  import { decryptValue } from "./settings/crypto";
9
9
 
10
10
  const prefix = "user.category.";
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { UserProfile, UserProvider } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
 
4
4
  export type DeletedAccountReason =
5
5
  | "ipa_expired_demoted"
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { User } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { err, fail, freeipa, ok, type Result } from "../../server/services";
4
4
  import { dates } from "../../shared";
5
5
  import { getConfiguredExpiryDays, parseIpaAccountTransitionPolicy } from "../account-model";
@@ -1,5 +1,6 @@
1
1
  import type { JobContext, PumpConfig, Sync } from "@k2b/sync";
2
- import { sql } from "bun";
2
+ import { sql } from "../../runtime/clients";
3
+
3
4
  import { freeipa } from "../../server/services/freeipa";
4
5
  import { getConfiguredExpiryDays } from "../account-model";
5
6
  import { getFreeIpaConfig } from "../freeipa-config";
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import { accountCategory } from "../../contracts/account-categories";
3
2
  import type { PersonAvatar } from "../../contracts/avatar";
4
3
  import type {
@@ -14,6 +13,7 @@ import type {
14
13
  UserProfile,
15
14
  UserProvider,
16
15
  } from "../../contracts/shared";
16
+ import { sql } from "../../runtime/clients";
17
17
  import type { ServiceAccountRequestActor } from "../../server/middleware/auth";
18
18
  import {
19
19
  err,
@@ -1,5 +1,4 @@
1
1
  import { createHash } from "node:crypto";
2
- import { sql } from "bun";
3
2
  import {
4
3
  type AvatarConfig,
5
4
  type AvatarKind,
@@ -12,6 +11,7 @@ import {
12
11
  } from "../../contracts/avatar";
13
12
  import { MAX_AVATAR_BYTES, MAX_AVATAR_DATA_URL_LENGTH } from "../../contracts/profile";
14
13
  import type { MutationResult } from "../../contracts/shared";
14
+ import { sql } from "../../runtime/clients";
15
15
  import { AVATAR_RENDERER_REVISION, renderAvatarSvg } from "../../shared/avatar-art";
16
16
  import { stripImageMetadata } from "../image-metadata";
17
17
 
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { BaseGroup, UserProvider } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
 
4
4
  type DbRow = Record<string, unknown>;
5
5
 
@@ -1,7 +1,7 @@
1
1
  import { err, fail, ok, type Result } from "@k2b/stdlib";
2
- import { sql } from "bun";
3
2
  import { z } from "zod";
4
3
  import type { User } from "../../contracts/shared";
4
+ import { sql } from "../../runtime/clients";
5
5
  import type { RequestActor } from "../../server/middleware/auth";
6
6
  import { audit } from "../audit";
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { err, fail, ok, type PageParams, type Paginated, paginate, type Result } from "@k2b/stdlib";
2
- import { sql } from "bun";
3
2
  import type { User } from "../../contracts/shared";
3
+ import { sql } from "../../runtime/clients";
4
4
  import { type AccountsActor, isAdminActor } from "./authz";
5
5
 
6
6
  export type DuplicateEmailUser = Pick<
@@ -1,5 +1,6 @@
1
- import { type SQLQuery, sql } from "bun";
1
+ import type { SQLQuery } from "bun";
2
2
  import type { EntityKind, EntityListItem, UserProfile, UserProvider } from "../../contracts/shared";
3
+ import { sql } from "../../runtime/clients";
3
4
  import { getFreeIpaConfig } from "../freeipa-config";
4
5
  import { escapeLikePattern, toPgTextArray, toPgUuidArray } from "../postgres";
5
6
  import type { ServiceAccountKind, ServiceAccountStatus } from "../service-accounts";
@@ -1,4 +1,5 @@
1
- import { type SQLQuery, sql } from "bun";
1
+ import type { SQLQuery } from "bun";
2
+ import { sql } from "../../runtime/clients";
2
3
 
3
4
  type SqlFragment = SQLQuery;
4
5
  type SqlValue = SQLQuery | string;
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
  import { freeipa } from "../../server/services";
4
4
  import { getServiceIpaSession } from "../ipa/service-account";
5
5
  import { toPgUuidArray } from "../postgres";