@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,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import { Hono, type MiddlewareHandler } from "hono";
3
2
  import { z } from "zod";
4
3
  import { setAiCredential } from "../ai/credentials";
@@ -15,6 +14,7 @@ import {
15
14
  testAiProvider,
16
15
  } from "../ai/provider-connections";
17
16
  import { getAiRequestHeaders, setAiRequestHeaders } from "../ai/request-headers";
17
+ import { sql } from "../runtime/clients";
18
18
  import { type AuthContext, auth, v } from "../server";
19
19
  import { ConfigConflict } from "../services/config-conflict";
20
20
  import { ImageMetadataError } from "../services/image-metadata";
@@ -3,7 +3,7 @@ import { Hono } from "hono";
3
3
  import { describeRoute } from "hono-openapi";
4
4
  import { z } from "zod";
5
5
  import { validateAppKeyAppId } from "../app-key";
6
- import { type AuthContext, auth, respond, v } from "../server";
6
+ import { type AuthContext, auth, jsonResponse, respond, v } from "../server";
7
7
  import { noTransform } from "../server/middleware/no-transform";
8
8
  import { appKeys } from "../services/app-keys";
9
9
 
@@ -21,9 +21,49 @@ const pagination = z.object({
21
21
  perPage: z.coerce.number().int().min(1).max(500).default(100),
22
22
  });
23
23
  const input = z
24
- .object({ publicKey: z.string().length(43), name: z.string().trim().min(1).max(120).optional(), expiresAt: z.iso.datetime().optional() })
24
+ .object({
25
+ publicKey: z.string().length(43),
26
+ name: z.string().trim().min(1).max(120).optional(),
27
+ expiresAt: z.iso.datetime().optional(),
28
+ grants: z.array(z.literal("oauth:issue")).max(1).optional(),
29
+ })
25
30
  .strict();
26
31
 
32
+ const keyListResponse = z.object({
33
+ installationId: z.uuid(),
34
+ items: z.array(
35
+ z.object({
36
+ keyId: z.uuid(),
37
+ appId: z.string(),
38
+ serviceAccountId: z.uuid(),
39
+ kind: z.literal("app_key"),
40
+ name: z.string(),
41
+ publicKey: z.string(),
42
+ grants: z.array(z.literal("oauth:issue")),
43
+ fingerprint: z.string(),
44
+ status: z.enum(["active", "expired", "revoked"]),
45
+ accountStatus: z.enum(["active", "disabled"]),
46
+ createdAt: z.iso.datetime(),
47
+ createdBy: z.uuid().nullable(),
48
+ expiresAt: z.iso.datetime().nullable(),
49
+ revokedAt: z.iso.datetime().nullable(),
50
+ revokedBy: z.uuid().nullable(),
51
+ lastUsedAt: z.iso.datetime().nullable(),
52
+ }),
53
+ ),
54
+ lastRejectedStart: z
55
+ .object({
56
+ at: z.iso.datetime(),
57
+ reason: z.enum(["revoked", "expired", "disabled", "connections_not_permitted"]),
58
+ keyId: z.uuid().nullable(),
59
+ })
60
+ .nullable(),
61
+ page: z.number().int(),
62
+ perPage: z.number().int(),
63
+ total: z.number().int(),
64
+ hasNext: z.boolean(),
65
+ });
66
+
27
67
  export const createAdminAppKeyRoutes = (
28
68
  authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
29
69
  service: typeof appKeys = appKeys,
@@ -44,7 +84,11 @@ export const createAdminAppKeyRoutes = (
44
84
  )
45
85
  .get(
46
86
  "/:appId/keys",
47
- describeRoute({ tags: ["App keys"], summary: "List app public keys and lifecycle metadata" }),
87
+ describeRoute({
88
+ tags: ["App keys"],
89
+ summary: "List app public keys, lifecycle metadata and last rejected start",
90
+ responses: { 200: jsonResponse(keyListResponse, "App keys and last rejected start") },
91
+ }),
48
92
  v("param", params),
49
93
  v("query", pagination),
50
94
  (c) => respond(c, () => service.list({ actor: c.get("user"), ...c.req.valid("param"), ...c.req.valid("query") })),
@@ -6,13 +6,13 @@
6
6
  * core app mounts it under `/api/admin/core/settings`.
7
7
  */
8
8
 
9
- import { sql } from "bun";
10
9
  import { Hono } from "hono";
11
10
  import { describeRoute } from "hono-openapi";
12
11
  import { z } from "zod";
13
12
  import { ObjectStoreConfigurationError, ObjectStoreConnectionError, testObjectStoreConnection } from "../_internal/object-store-config";
14
13
  import { listApps } from "../_internal/registry";
15
14
  import { enrichDirtyAiConversations } from "../ai/enrich";
15
+ import { sql } from "../runtime/clients";
16
16
  import { type AuthContext, auth, getLocale, jsonResponse, requiresAdmin, v } from "../server";
17
17
  import { settingsDeleteLegacyKeys, settingsListLegacyKeys, settingsService } from "../services";
18
18
  import { readAccountCategoryPolicy } from "../services/account-category-policy";
@@ -1,4 +1,3 @@
1
- import { sql } from "bun";
2
1
  import { type Context, Hono, type MiddlewareHandler } from "hono";
3
2
  import { describeRoute, validator } from "hono-openapi";
4
3
  import { z } from "zod";
@@ -13,6 +12,7 @@ import {
13
12
  MailRetentionSchema,
14
13
  MailStatusSchema,
15
14
  } from "../contracts/outgoing-mail";
15
+ import { sql } from "../runtime/clients";
16
16
  import { type AuthContext, auth, jsonResponse, requiresAdmin } from "../server";
17
17
  import { noTransform } from "../server/middleware/no-transform";
18
18
  import { audit } from "../services/audit";
@@ -11,8 +11,17 @@ import {
11
11
  import { connections } from "../runtime/connections";
12
12
  import { clientIpBucket } from "../server/client-ip";
13
13
  import { type RateLimitResult, ratelimit } from "../server/ratelimit";
14
- import { authenticateBootstrap, BootstrapProofError, bootstrapRateLimit, consumeBootstrapProof } from "../services/app-bootstrap";
14
+ import {
15
+ authenticateBootstrap,
16
+ BootstrapProofError,
17
+ bootstrapRateLimit,
18
+ clearRejectedBootstrapStart,
19
+ consumeBootstrapProof,
20
+ RejectedBootstrapStart,
21
+ recordRejectedBootstrapStart,
22
+ } from "../services/app-bootstrap";
15
23
  import { touchAppKey } from "../services/app-keys";
24
+ import { logger } from "../services/logging";
16
25
 
17
26
  type BootstrapContext = { Variables: { bootstrapBody: Uint8Array<ArrayBuffer> } };
18
27
 
@@ -110,6 +119,14 @@ export const createAppBootstrapRoutes = (dependencies: Dependencies = {}) => {
110
119
  try {
111
120
  verified = await authenticateBootstrap(header.slice(7), { method: c.req.method, path: url.pathname, body }, dependencies.db);
112
121
  } catch (error) {
122
+ if (error instanceof RejectedBootstrapStart) {
123
+ try {
124
+ const rate = dependencies.admit ? await dependencies.admit(error.identity.keyId) : await admit("key", error.identity.keyId);
125
+ if (!rate.limited) await recordRejectedBootstrapStart(error, dependencies.db);
126
+ } catch {
127
+ logger("app-bootstrap").warn("Could not record rejected app start", { appId: error.identity.appId });
128
+ }
129
+ }
113
130
  if (error instanceof BootstrapProofError) return c.json({ code: error.code }, error.code === "invalid_proof" ? 401 : 403);
114
131
  throw error;
115
132
  }
@@ -124,6 +141,11 @@ export const createAppBootstrapRoutes = (dependencies: Dependencies = {}) => {
124
141
  await touchAppKey(identity, dependencies.db);
125
142
  const response = JSON.stringify(BootstrapResponseSchema.parse({ identity, connections: snapshot }));
126
143
  if (Buffer.byteLength(response) > BOOTSTRAP_RESPONSE_BYTES) throw new Error("Bootstrap response exceeds protocol budget");
144
+ try {
145
+ await clearRejectedBootstrapStart(identity.appId, dependencies.db);
146
+ } catch {
147
+ logger("app-bootstrap").warn("Could not clear rejected app start", { appId: identity.appId });
148
+ }
127
149
  c.header("Content-Type", "application/json");
128
150
  return c.body(response);
129
151
  },
@@ -1,10 +1,10 @@
1
- import { sql } from "bun";
2
1
  import { Hono, type MiddlewareHandler } from "hono";
3
2
  import { describeRoute } from "hono-openapi";
4
3
  import { z } from "zod";
5
4
  import { AppDetailsSchema } from "../contracts/app-details";
6
5
  import { AppEventEnvelopeSchema } from "../contracts/app-events";
7
6
  import { CapabilityQualifiedIdSchema } from "../contracts/capabilities";
7
+ import { sql } from "../runtime/clients";
8
8
  import { type AuthContext, auth, expectUserBackedActor, getLocale, v } from "../server";
9
9
  import { AppAccessManagementError } from "../services/app-access-management";
10
10
  import { appDetails } from "../services/app-details";
package/src/api/auth.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { sql } from "bun";
2
1
  import { type Context, Hono } from "hono";
3
2
  import { describeRoute } from "hono-openapi";
4
3
  import { z } from "zod";
5
4
  import { env } from "../config";
6
5
  import { ChangeExpiredPasswordSchema } from "../contracts";
7
6
  import { coreUserEventPublisher } from "../events/core-user-created";
7
+ import { sql } from "../runtime/clients";
8
8
  import { type AuthContext, auth, getLocale, jsonResponse, rateLimit, respond, v } from "../server";
9
9
  import { accounts, audit, authFlows, getFreeIpaConfig, logger, webauthn } from "../services";
10
10
  import { isAccountCategoryAllowed } from "../services/account-category-policy";
@@ -0,0 +1,129 @@
1
+ import type { SQL } from "bun";
2
+ import type { Context } from "hono";
3
+ import { getConnInfo } from "hono/bun";
4
+ import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
5
+ import { clientIpBucket } from "../server/client-ip";
6
+ import { type RateLimitResult, ratelimit } from "../server/ratelimit";
7
+ import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
8
+ import {
9
+ authenticateAppProof,
10
+ BootstrapProofError,
11
+ bootstrapRateLimit,
12
+ consumeBootstrapProof,
13
+ inspectAppProof,
14
+ } from "../services/app-bootstrap";
15
+ import { touchAppKey } from "../services/app-keys";
16
+ import type { AuthenticatedWorkload } from "../services/identity/workload-auth";
17
+
18
+ type Dependencies = {
19
+ db?: SQL;
20
+ ingress?: (peer: string) => Promise<RateLimitResult>;
21
+ admit?: (keyId: string) => Promise<RateLimitResult>;
22
+ };
23
+ const admission = async (kind: "ingress" | "key", id: string) =>
24
+ ratelimit({ id: `app-bootstrap-${kind}`, limit: await bootstrapRateLimit() }).check(id);
25
+
26
+ /** Core-only request authentication; the body remains available to the domain handler. */
27
+ export const authenticateSignedAppRequest = async (
28
+ c: Context,
29
+ scope: "identity:invoke" | "oauth:issue",
30
+ dependencies: Dependencies = {},
31
+ ): Promise<AuthenticatedWorkload | Response> => {
32
+ c.header("Cache-Control", "no-store");
33
+ const deadline = AbortSignal.timeout(REQUEST_IDLE_TIMEOUT_SECONDS * 1000);
34
+ const signal = AbortSignal.any([c.req.raw.signal, deadline]);
35
+ const reject = (status: 401 | 403 | 413 | 429 | 503 | 504, code: string, message: string) => c.json({ code, message }, status);
36
+ const limited = (rate: RateLimitResult) => {
37
+ c.header("Retry-After", String(Math.max(1, Math.ceil(rate.resetIn / 1000))));
38
+ return reject(429, "RATE_LIMITED", "App request admission limit reached");
39
+ };
40
+ try {
41
+ let peer = "unknown";
42
+ try {
43
+ peer = getConnInfo(c).remote.address ?? peer;
44
+ } catch {
45
+ /* In-process requests have no socket. */
46
+ }
47
+ const ingress = await (dependencies.ingress ?? ((id) => admission("ingress", id)))(clientIpBucket(peer));
48
+ if (ingress.limited) return limited(ingress);
49
+ const header = c.req.header("Authorization") ?? "";
50
+ if (!header.startsWith("Bearer ")) return reject(401, "invalid_proof", "A valid app proof is required");
51
+ inspectAppProof(header.slice(7));
52
+ const tooLarge = () => reject(413, "body_too_large", "App request exceeds the byte limit");
53
+ if (Number(c.req.header("Content-Length")) > CAPABILITY_MAX_REQUEST_BYTES) {
54
+ void c.req.raw.body?.cancel().catch(() => undefined);
55
+ return tooLarge();
56
+ }
57
+ signal.throwIfAborted();
58
+ const copy = c.req.raw.clone();
59
+ const reader = copy.body?.getReader();
60
+ const bytes = new Uint8Array(CAPABILITY_MAX_REQUEST_BYTES);
61
+ let total = 0;
62
+ if (reader) {
63
+ const cancel = () => {
64
+ // A tee cancels its source only after both branches cancel. Never await a stalled source.
65
+ void reader.cancel(signal.reason).catch(() => undefined);
66
+ void c.req.raw.body?.cancel(signal.reason).catch(() => undefined);
67
+ };
68
+ signal.addEventListener("abort", cancel, { once: true });
69
+ try {
70
+ signal.throwIfAborted();
71
+ while (true) {
72
+ const chunk = await reader.read();
73
+ if (chunk.done) break;
74
+ if (total + chunk.value.byteLength > bytes.byteLength) {
75
+ cancel();
76
+ return tooLarge();
77
+ }
78
+ bytes.set(chunk.value, total);
79
+ total += chunk.value.byteLength;
80
+ }
81
+ signal.throwIfAborted();
82
+ } catch (error) {
83
+ cancel();
84
+ if (signal.aborted) throw signal.reason;
85
+ throw error;
86
+ } finally {
87
+ signal.removeEventListener("abort", cancel);
88
+ reader.releaseLock();
89
+ }
90
+ }
91
+ const url = new URL(c.req.url);
92
+ const verified = await authenticateAppProof(
93
+ header.slice(7),
94
+ { method: c.req.method, path: url.pathname + url.search, body: bytes.slice(0, total) },
95
+ dependencies.db,
96
+ );
97
+ // Recheck the operator-owned key grant on every transmission.
98
+ if (scope === "oauth:issue" && !verified.key.oauthIssueAllowed)
99
+ return reject(403, "scope_not_permitted", "App is not permitted to issue OAuth credentials");
100
+ const declaredApp = c.req.header("X-Cloud-App-Id");
101
+ if (declaredApp && declaredApp !== verified.identity.appId) return reject(403, "wrong_app", "App identity does not match the proof");
102
+ const rate = await (dependencies.admit ?? ((id) => admission("key", id)))(verified.identity.keyId);
103
+ if (rate.limited) return limited(rate);
104
+ if (!(await consumeBootstrapProof(verified.identity.keyId, verified.jti, verified.exp, dependencies.db)))
105
+ return reject(403, "replayed_proof", "App proof has already been used");
106
+ await touchAppKey(verified.identity, dependencies.db);
107
+ return {
108
+ appId: verified.identity.appId,
109
+ keyId: verified.identity.keyId,
110
+ serviceAccountId: verified.key.serviceAccountId,
111
+ credentialId: verified.identity.keyId,
112
+ scope: "identity:invoke",
113
+ };
114
+ } catch (error) {
115
+ if (signal.aborted && error === signal.reason) {
116
+ void c.req.raw.body?.cancel(signal.reason).catch(() => undefined);
117
+ return c.req.raw.signal.aborted
118
+ ? Response.json(
119
+ { code: "REQUEST_CANCELLED", message: "App request was cancelled" },
120
+ { status: 499, headers: { "Cache-Control": "no-store" } },
121
+ )
122
+ : reject(504, "DEADLINE_EXCEEDED", "App request body deadline exceeded");
123
+ }
124
+ if (error instanceof BootstrapProofError)
125
+ return reject(error.code === "invalid_proof" ? 401 : 403, error.code, "App proof was rejected");
126
+ c.header("Retry-After", "1");
127
+ return reject(503, "app_proof_unavailable", "App request authentication is temporarily unavailable");
128
+ }
129
+ };
@@ -61,11 +61,12 @@ import { noTransform } from "../server/middleware/no-transform";
61
61
  import { hasOAuthScope } from "../server/oauth-scope";
62
62
  import { ratelimit } from "../server/ratelimit";
63
63
  import { appAccessAuthority, canUseApp } from "../services/app-access";
64
+ import { bootstrapRateLimit } from "../services/app-bootstrap";
64
65
  import { discoverableApps } from "../services/app-discovery";
65
66
  import { authorityFromParentInvocation, invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
66
67
  import { capabilityInvocationOperation } from "../services/identity/invocation-operations";
67
68
  import type { CloudInvocationClaims, InvocationAuthority } from "../services/identity/invocation-token";
68
- import { normalizeInvocationRequestId, signAppQueryInvocation } from "../services/identity/invocation-token";
69
+ import { normalizeInvocationRequestId, signAppCapabilityInvocation, signAppQueryInvocation } from "../services/identity/invocation-token";
69
70
  import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
70
71
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
71
72
  import {
@@ -128,6 +129,7 @@ export type CapabilityRouteDependencies = {
128
129
  withMandateIssueAuthority?: typeof withMandateIssueAuthority;
129
130
  queryTimeoutMs?: number;
130
131
  actionTimeoutMs?: number;
132
+ checkAppCapabilityRate?: (appId: string) => Promise<{ limited: boolean; resetIn: number }>;
131
133
  checkAppQueryRate?: (appId: string) => Promise<{ limited: boolean }>;
132
134
  reviewTimeoutMs?: number;
133
135
  appUrl?: () => Promise<string>;
@@ -135,6 +137,7 @@ export type CapabilityRouteDependencies = {
135
137
 
136
138
  export type CapabilityDispatchDependencies = Pick<
137
139
  CapabilityRouteDependencies,
140
+ | "checkAppCapabilityRate"
138
141
  | "checkAppQueryRate"
139
142
  | "getApp"
140
143
  | "getCapability"
@@ -293,6 +296,8 @@ export type CapabilityDispatchParams = {
293
296
  deadline?: number;
294
297
  /** Core-only authenticated workload. Never populated from request JSON. */
295
298
  workload?: AuthenticatedWorkload;
299
+ /** Verified Ed25519 app identity, separate from person and legacy workload authority. */
300
+ appCaller?: { appId: string; keyId: string };
296
301
  mandate?: MandatedCapabilityAuthority;
297
302
  callingAppId?: string;
298
303
  /** Internal observation only: called immediately before attempting the app request. */
@@ -363,6 +368,39 @@ const runCapabilityDispatch = async (
363
368
  }
364
369
  if ("appOnly" in operation && operation.appOnly !== undefined && !params.workload)
365
370
  return errorResponse("FORBIDDEN", "App authority required for this query", 403);
371
+ if (params.appCaller) {
372
+ if (
373
+ params.authority ||
374
+ params.workload ||
375
+ params.mandate ||
376
+ params.parentInvocation ||
377
+ params.review ||
378
+ !operation.appPermission ||
379
+ operation.stream
380
+ )
381
+ return errorResponse("FORBIDDEN", "This capability does not accept app authority", 403);
382
+ try {
383
+ const app = params.appCaller;
384
+ const caller = await deadline.wait(() => (params.dependencies?.getApp ?? getApp)(app.appId));
385
+ if (!caller) return errorResponse("APP_UNAVAILABLE", "Calling app registry entry unavailable", 503);
386
+ if (!caller.platformPermissions?.includes(operation.appPermission))
387
+ return errorResponse("APP_CAPABILITY_NOT_ADMITTED", `App caller does not declare ${operation.appPermission}`, 403);
388
+ // Same operator-owned per-second budget as signed app ingress; shared by all keys of the app.
389
+ const rate = await deadline.wait(() =>
390
+ (
391
+ params.dependencies?.checkAppCapabilityRate ??
392
+ (async (appId) => ratelimit({ id: "app-capabilities", limit: await bootstrapRateLimit() }).check(appId))
393
+ )(app.appId),
394
+ );
395
+ if (rate.limited)
396
+ return errorResponse("RATE_LIMITED", "App capability rate limit reached", 429, {
397
+ retryAfter: Math.max(1, Math.ceil(rate.resetIn / 1000)),
398
+ });
399
+ } catch {
400
+ deadline.check();
401
+ return errorResponse("APP_UNAVAILABLE", "App capability admission unavailable", 503);
402
+ }
403
+ }
366
404
  if (params.workload) {
367
405
  if (
368
406
  params.authority ||
@@ -544,6 +582,36 @@ const runCapabilityDispatch = async (
544
582
  });
545
583
  })()
546
584
  : await (async () => {
585
+ if (params.appCaller && operation.appPermission) {
586
+ const app = params.appCaller;
587
+ const permission = operation.appPermission;
588
+ return deadline.wait(() =>
589
+ (params.dependencies?.withActiveSigner ?? withActiveIdentitySigner)(
590
+ "invocation",
591
+ async (signer) => {
592
+ deadline.check();
593
+ const signed = await signAppCapabilityInvocation({
594
+ app,
595
+ permission,
596
+ kind: params.kind === "queries" ? "query" : "action",
597
+ targetAppId: params.appId,
598
+ capabilityId: params.capabilityId,
599
+ schemaHash: operation.schemaHash,
600
+ input: input.data,
601
+ requestId,
602
+ signer,
603
+ });
604
+ observed.invocationId = signed.claims.jti;
605
+ return new Headers({
606
+ "content-type": "application/json",
607
+ accept: "application/json",
608
+ authorization: `Bearer ${signed.token}`,
609
+ });
610
+ },
611
+ { signal, timeoutMs: deadline.remainingMs() },
612
+ ),
613
+ );
614
+ }
547
615
  if (params.workload) {
548
616
  const workload = params.workload;
549
617
  return deadline.wait(() =>
@@ -675,7 +743,11 @@ const runCapabilityDispatch = async (
675
743
  ? {
676
744
  appId: params.appId,
677
745
  capability: `${params.appId}.${params.capabilityId}`,
678
- principal: claimPrincipal ? `${claimPrincipal.type}:${claimPrincipal.id}` : "anonymous",
746
+ principal: params.appCaller
747
+ ? `app:${params.appCaller.appId}`
748
+ : claimPrincipal
749
+ ? `${claimPrincipal.type}:${claimPrincipal.id}`
750
+ : "anonymous",
679
751
  keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
680
752
  }
681
753
  : null;
@@ -749,7 +821,7 @@ const runCapabilityDispatch = async (
749
821
  params.onUpstreamAttempt?.();
750
822
  const fetchResponse = () =>
751
823
  fetchUpstream(
752
- `${entry.endpoint}/${params.workload ? "app-queries" : params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
824
+ `${entry.endpoint}/${params.appCaller ? `app-capabilities/${params.kind}` : params.workload ? "app-queries" : params.kind}/${encodeURIComponent(params.capabilityId)}${params.review ? "/review" : ""}`,
753
825
  {
754
826
  method: "POST",
755
827
  headers,
@@ -986,13 +1058,15 @@ export const dispatchCapability = async (params: CapabilityDispatchParams): Prom
986
1058
  origin: params.origin,
987
1059
  invocationId: observed.invocationId,
988
1060
  parentInvocationId: params.parentInvocation?.jti,
989
- viaAppId: params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
1061
+ viaAppId: params.appCaller?.appId ?? params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
990
1062
  appId: params.appId,
991
1063
  capability: `${params.appId}.${params.capabilityId}`,
992
1064
  kind: params.kind === "queries" ? "query" : "action",
993
1065
  destructive: observed.destructive,
994
- actorKind: params.workload ? "app" : (principal?.type ?? null),
1066
+ actorKind: params.appCaller || params.workload ? "app" : (principal?.type ?? null),
995
1067
  actorId: principal?.id ?? null,
1068
+ actorAppId: params.appCaller?.appId ?? params.workload?.appId,
1069
+ actorKeyId: params.appCaller?.keyId,
996
1070
  userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
997
1071
  accessSubject: subject && subject.id !== principal?.id ? subject : null,
998
1072
  status: executionStatus(outcome.status, errorCode),
package/src/api/index.ts CHANGED
@@ -142,6 +142,8 @@ export { createAdminAppKeyRoutes } from "./admin-app-keys";
142
142
  export { createAppAccessRoutes } from "./app-access";
143
143
  /** Core-owned internal startup protocol; applications use @k2b/cloud/start. */
144
144
  export { createAppBootstrapRoutes } from "./app-bootstrap";
145
+ /** Core-owned app-key request authentication. */
146
+ export { authenticateSignedAppRequest } from "./authenticate-app-request";
145
147
  export {
146
148
  CLOUD_MCP_PATH,
147
149
  CLOUD_MCP_PROTECTED_RESOURCE_PATH,
package/src/api/me.ts CHANGED
@@ -5,7 +5,6 @@
5
5
  */
6
6
 
7
7
  import { err, fail, ok, type Result } from "@k2b/stdlib";
8
- import { sql } from "bun";
9
8
  import { type Context, Hono, type MiddlewareHandler } from "hono";
10
9
  import { deleteCookie } from "hono/cookie";
11
10
  import { createMiddleware } from "hono/factory";
@@ -42,6 +41,7 @@ import {
42
41
  WebAuthnPasskeySchema,
43
42
  } from "../contracts";
44
43
  import { CapabilityAppIdSchema } from "../contracts/capabilities";
44
+ import { sql } from "../runtime/clients";
45
45
  import { type AuthContext, auth, getLocale, getTimeZone, jsonResponse, rateLimit, requiresAuth, respond, v } from "../server";
46
46
  import { APP_SESSION_FORBIDDEN } from "../server/middleware/auth";
47
47
  import { noTransform } from "../server/middleware/no-transform";
@@ -1,10 +1,10 @@
1
1
  import { ok } from "@k2b/stdlib";
2
- import { sql } from "bun";
3
2
  import type { MiddlewareHandler } from "hono";
4
3
  import { generateKeyPair, jwtVerify, SignJWT } from "jose";
5
4
  import { compileCapabilities } from "../_internal/capabilities";
6
5
  import { defineCapabilities, UniversalSearchDataSchema, UniversalSearchInputSchema } from "../contracts/capabilities";
7
6
  import type { CapabilityRegistryEntry } from "../contracts/registry";
7
+ import { sql } from "../runtime/clients";
8
8
  import type { AuthContext } from "../server";
9
9
  import type { signInvocationToken } from "../services/identity/invocation-token";
10
10
  import type { withActiveIdentitySigner } from "../services/identity/key-ring";
@@ -1,8 +1,9 @@
1
- import { sql, type TransactionSQL } from "bun";
1
+ import type { TransactionSQL } from "bun";
2
2
  import { z } from "zod";
3
3
  import { GeoAutocompleteInputSchema } from "../contracts/geo";
4
4
  import { isInstallablePath } from "../contracts/installable";
5
5
  import { PWA_LIMITS } from "../contracts/pwa";
6
+ import { sql } from "../runtime/clients";
6
7
  import { ratelimit } from "../server/ratelimit";
7
8
  import { REQUEST_IDLE_TIMEOUT_SECONDS } from "../server/request-timeout";
8
9
  import { getEffectivePermission, hasPermission } from "../server/services/access";
@@ -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,