@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
@@ -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";
@@ -0,0 +1,153 @@
1
+ import type { SQL } from "bun";
2
+ import { Hono, type MiddlewareHandler } from "hono";
3
+ import { getConnInfo } from "hono/bun";
4
+ import {
5
+ BOOTSTRAP_BODY,
6
+ BOOTSTRAP_PATH,
7
+ BOOTSTRAP_RESPONSE_BYTES,
8
+ BootstrapConnectionsSchema,
9
+ BootstrapResponseSchema,
10
+ } from "../runtime/bootstrap-protocol";
11
+ import { connections } from "../runtime/connections";
12
+ import { clientIpBucket } from "../server/client-ip";
13
+ import { type RateLimitResult, ratelimit } from "../server/ratelimit";
14
+ import {
15
+ authenticateBootstrap,
16
+ BootstrapProofError,
17
+ bootstrapRateLimit,
18
+ clearRejectedBootstrapStart,
19
+ consumeBootstrapProof,
20
+ RejectedBootstrapStart,
21
+ recordRejectedBootstrapStart,
22
+ } from "../services/app-bootstrap";
23
+ import { touchAppKey } from "../services/app-keys";
24
+ import { logger } from "../services/logging";
25
+
26
+ type BootstrapContext = { Variables: { bootstrapBody: Uint8Array<ArrayBuffer> } };
27
+
28
+ /** Count actual bytes even when Content-Length is missing or forged. Never await cancellation. */
29
+ const limitBody: MiddlewareHandler<BootstrapContext> = async (c, next) => {
30
+ const max = Buffer.byteLength(BOOTSTRAP_BODY);
31
+ const reject = () => c.json({ code: "body_too_large" }, 413);
32
+ if (Number(c.req.header("Content-Length")) > max) {
33
+ void c.req.raw.body?.cancel().catch(() => undefined);
34
+ return reject();
35
+ }
36
+ const reader = c.req.raw.body?.getReader();
37
+ const bytes = new Uint8Array(max);
38
+ let total = 0;
39
+ if (reader) {
40
+ try {
41
+ while (true) {
42
+ const chunk = await reader.read();
43
+ if (chunk.done) break;
44
+ if (total + chunk.value.byteLength > max) {
45
+ void reader.cancel().catch(() => undefined);
46
+ return reject();
47
+ }
48
+ bytes.set(chunk.value, total);
49
+ total += chunk.value.byteLength;
50
+ }
51
+ } finally {
52
+ reader.releaseLock();
53
+ }
54
+ }
55
+ c.set("bootstrapBody", bytes.slice(0, total));
56
+ await next();
57
+ };
58
+
59
+ type Dependencies = {
60
+ db?: SQL;
61
+ ingress?: MiddlewareHandler;
62
+ admit?: (keyId: string) => Promise<RateLimitResult>;
63
+ };
64
+ const admit = async (kind: "ingress" | "key", identifier: string) => {
65
+ // Use the installation's existing per-second security budget, in separate ingress and authenticated-key buckets.
66
+ const limit = await bootstrapRateLimit();
67
+ return ratelimit({ id: `app-bootstrap-${kind}`, limit }).check(identifier);
68
+ };
69
+
70
+ const ingress: MiddlewareHandler<BootstrapContext> = async (c, next) => {
71
+ // Bootstrap uses the direct Core socket. Unknown peers share one bounded bucket.
72
+ let peer = "unknown";
73
+ try {
74
+ peer = getConnInfo(c).remote.address ?? peer;
75
+ } catch {
76
+ /* In-process requests have no socket. */
77
+ }
78
+ const rate = await admit("ingress", clientIpBucket(peer));
79
+ if (rate.limited) {
80
+ c.header("Retry-After", String(Math.max(1, Math.ceil(rate.resetIn / 1000))));
81
+ return c.json({ code: "bootstrap_ingress_limited" }, 429);
82
+ }
83
+ await next();
84
+ };
85
+
86
+ /** Core-only mount. Capture the startup snapshot once, including NATS file contents. */
87
+ export const createAppBootstrapRoutes = (dependencies: Dependencies = {}) => {
88
+ const { database, valkey, nats, appSecret } = connections();
89
+ const snapshot = BootstrapConnectionsSchema.parse({ database, valkey, nats, appSecret });
90
+ // Refuse an undeliverable Core configuration at startup, using the largest valid identity.
91
+ const largestIdentity = {
92
+ installationId: "11111111-1111-4111-8111-111111111111",
93
+ appId: "a".repeat(80),
94
+ keyId: "22222222-2222-4222-8222-222222222222",
95
+ };
96
+ if (Buffer.byteLength(JSON.stringify({ identity: largestIdentity, connections: snapshot })) > BOOTSTRAP_RESPONSE_BYTES)
97
+ throw new Error("Bootstrap response exceeds protocol budget");
98
+ // The startup snapshot includes appId/sources; only the delivery contract goes on the wire.
99
+ return new Hono<BootstrapContext>()
100
+ .onError(() =>
101
+ Response.json({ code: "bootstrap_unavailable" }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }),
102
+ )
103
+ .post(
104
+ BOOTSTRAP_PATH,
105
+ async (c, next) => {
106
+ c.header("Cache-Control", "no-store");
107
+ await next();
108
+ },
109
+ // The v1 request is exactly two UTF-8 bytes. This runs before proof parsing, even for invalid proofs.
110
+ limitBody,
111
+ dependencies.ingress ?? ingress,
112
+ async (c) => {
113
+ const url = new URL(c.req.url);
114
+ if (url.search) return c.json({ code: "invalid_request" }, 400);
115
+ const body = c.get("bootstrapBody");
116
+ const header = c.req.header("Authorization") ?? "";
117
+ if (!header.startsWith("Bearer ")) return c.json({ code: "invalid_proof" }, 401);
118
+ let verified: Awaited<ReturnType<typeof authenticateBootstrap>>;
119
+ try {
120
+ verified = await authenticateBootstrap(header.slice(7), { method: c.req.method, path: url.pathname, body }, dependencies.db);
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
+ }
130
+ if (error instanceof BootstrapProofError) return c.json({ code: error.code }, error.code === "invalid_proof" ? 401 : 403);
131
+ throw error;
132
+ }
133
+ if (new TextDecoder().decode(body) !== BOOTSTRAP_BODY) return c.json({ code: "invalid_request" }, 400);
134
+ const { identity, jti, exp } = verified;
135
+ const rate = dependencies.admit ? await dependencies.admit(identity.keyId) : await admit("key", identity.keyId);
136
+ if (rate.limited) {
137
+ c.header("Retry-After", String(Math.max(1, Math.ceil(rate.resetIn / 1000))));
138
+ return c.json({ code: "bootstrap_rate_limited" }, 429);
139
+ }
140
+ if (!(await consumeBootstrapProof(identity.keyId, jti, exp, dependencies.db))) return c.json({ code: "replayed_proof" }, 403);
141
+ await touchAppKey(identity, dependencies.db);
142
+ const response = JSON.stringify(BootstrapResponseSchema.parse({ identity, connections: snapshot }));
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
+ }
149
+ c.header("Content-Type", "application/json");
150
+ return c.body(response);
151
+ },
152
+ );
153
+ };
@@ -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,10 +296,14 @@ 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. */
299
304
  onUpstreamAttempt?: () => void;
305
+ /** Core-only aggregate budget, charged on upstream bytes before parsing or validation. */
306
+ consumeResponseBytes?: (bytes: number) => void;
300
307
  dependencies?: CapabilityDispatchDependencies;
301
308
  };
302
309
 
@@ -361,6 +368,39 @@ const runCapabilityDispatch = async (
361
368
  }
362
369
  if ("appOnly" in operation && operation.appOnly !== undefined && !params.workload)
363
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
+ }
364
404
  if (params.workload) {
365
405
  if (
366
406
  params.authority ||
@@ -542,6 +582,36 @@ const runCapabilityDispatch = async (
542
582
  });
543
583
  })()
544
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
+ }
545
615
  if (params.workload) {
546
616
  const workload = params.workload;
547
617
  return deadline.wait(() =>
@@ -673,7 +743,11 @@ const runCapabilityDispatch = async (
673
743
  ? {
674
744
  appId: params.appId,
675
745
  capability: `${params.appId}.${params.capabilityId}`,
676
- 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",
677
751
  keyHash: capabilityIdempotencyKeyHash(idempotencyKey.data),
678
752
  }
679
753
  : null;
@@ -747,7 +821,7 @@ const runCapabilityDispatch = async (
747
821
  params.onUpstreamAttempt?.();
748
822
  const fetchResponse = () =>
749
823
  fetchUpstream(
750
- `${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" : ""}`,
751
825
  {
752
826
  method: "POST",
753
827
  headers,
@@ -789,7 +863,12 @@ const runCapabilityDispatch = async (
789
863
  let upstreamBody: Awaited<ReturnType<typeof readBoundedJson>>;
790
864
  try {
791
865
  upstreamBody = await deadline.wait(() =>
792
- readBoundedJson(response, params.review ? CAPABILITY_MAX_REVIEW_RESPONSE_BYTES : CAPABILITY_MAX_RESULT_BYTES, signal),
866
+ readBoundedJson(
867
+ response,
868
+ params.review ? CAPABILITY_MAX_REVIEW_RESPONSE_BYTES : CAPABILITY_MAX_RESULT_BYTES,
869
+ signal,
870
+ params.consumeResponseBytes,
871
+ ),
793
872
  );
794
873
  } catch {
795
874
  if (actionWithoutRetrySafety) return settle(outcomeUnknown(), "uncertain");
@@ -979,13 +1058,15 @@ export const dispatchCapability = async (params: CapabilityDispatchParams): Prom
979
1058
  origin: params.origin,
980
1059
  invocationId: observed.invocationId,
981
1060
  parentInvocationId: params.parentInvocation?.jti,
982
- viaAppId: params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
1061
+ viaAppId: params.appCaller?.appId ?? params.workload?.appId ?? (params.parentInvocation ? params.callingAppId : undefined),
983
1062
  appId: params.appId,
984
1063
  capability: `${params.appId}.${params.capabilityId}`,
985
1064
  kind: params.kind === "queries" ? "query" : "action",
986
1065
  destructive: observed.destructive,
987
- actorKind: params.workload ? "app" : (principal?.type ?? null),
1066
+ actorKind: params.appCaller || params.workload ? "app" : (principal?.type ?? null),
988
1067
  actorId: principal?.id ?? null,
1068
+ actorAppId: params.appCaller?.appId ?? params.workload?.appId,
1069
+ actorKeyId: params.appCaller?.keyId,
989
1070
  userId: actor?.kind === "user" ? actor.user.id : (actor?.delegatedUser?.id ?? (principal?.type === "user" ? principal.id : null)),
990
1071
  accessSubject: subject && subject.id !== principal?.id ? subject : null,
991
1072
  status: executionStatus(outcome.status, errorCode),
package/src/api/index.ts CHANGED
@@ -26,6 +26,7 @@ import adminAiProjectsRoutes from "./admin-ai-projects";
26
26
  import adminAiQuotaRoutes from "./admin-ai-quotas";
27
27
  import adminAiSkillsRoutes from "./admin-ai-skills";
28
28
  import adminAiUsageRoutes from "./admin-ai-usage";
29
+ import adminAppDomainRoutes from "./admin-app-domains";
29
30
  import adminAppKeyRoutes from "./admin-app-keys";
30
31
  import adminCoreSettingsRoutes from "./admin-core-settings";
31
32
  import adminDirectorySharesRoutes from "./admin-directory-shares";
@@ -107,6 +108,7 @@ const buildCoreApi = (options: CoreApiOptions) => {
107
108
  .route("/admin/core/geo", adminGeoRoutes)
108
109
  .route("/admin/identity/directory-shares", adminDirectorySharesRoutes)
109
110
  .route("/admin/identity", adminIdentityRoutes)
111
+ .route("/admin/apps", adminAppDomainRoutes)
110
112
  .route("/admin/apps", adminAppKeyRoutes)
111
113
  .route("/admin/sync", createSyncOpsProxyRoutes())
112
114
  .route("/admin/lifecycle", adminLifecycleRoutes)
@@ -135,8 +137,13 @@ export const createCoreApiRouter = (options: CoreApiOptions) => {
135
137
  return { api };
136
138
  };
137
139
 
140
+ export { createAdminAppDomainRoutes } from "./admin-app-domains";
138
141
  export { createAdminAppKeyRoutes } from "./admin-app-keys";
139
142
  export { createAppAccessRoutes } from "./app-access";
143
+ /** Core-owned internal startup protocol; applications use @k2b/cloud/start. */
144
+ export { createAppBootstrapRoutes } from "./app-bootstrap";
145
+ /** Core-owned app-key request authentication. */
146
+ export { authenticateSignedAppRequest } from "./authenticate-app-request";
140
147
  export {
141
148
  CLOUD_MCP_PATH,
142
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";