@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
@@ -0,0 +1,195 @@
1
+ import type { AppKey } from "../app-key";
2
+ import {
3
+ BOOTSTRAP_ATTEMPT_MS,
4
+ BOOTSTRAP_BODY,
5
+ BOOTSTRAP_BUDGET_MS,
6
+ BOOTSTRAP_PATH,
7
+ BOOTSTRAP_RESPONSE_BYTES,
8
+ type BootstrapResponse,
9
+ BootstrapResponseSchema,
10
+ signBootstrapProof,
11
+ } from "./bootstrap-protocol";
12
+
13
+ export class BootstrapError extends Error {
14
+ constructor(
15
+ readonly kind: "transient" | "permanent",
16
+ readonly reason: string,
17
+ readonly retryAfterMs = 0,
18
+ ) {
19
+ super(`Core bootstrap ${kind}: ${reason}`);
20
+ this.name = "BootstrapError";
21
+ }
22
+ }
23
+
24
+ const originUrl = (origin: string | undefined): URL => {
25
+ try {
26
+ if (!origin) throw new Error();
27
+ const url = new URL(origin);
28
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash)
29
+ throw new Error();
30
+ return new URL(BOOTSTRAP_PATH, url);
31
+ } catch {
32
+ throw new BootstrapError("permanent", "CLOUD_CORE_INTERNAL_ORIGIN must be a direct http(s) Core origin");
33
+ }
34
+ };
35
+ const httpReason = (status: number): string => {
36
+ if (status === 401) return "HTTP 401: unknown, revoked, expired or invalid app key/proof";
37
+ if (status === 403) return "HTTP 403: connections_not_permitted, app/installation mismatch or replayed proof";
38
+ if (status === 404) return "HTTP 404: bootstrap route unavailable; check the direct Core origin and version";
39
+ return `HTTP ${status}`;
40
+ };
41
+ const retryAfter = (header: string | null, now: number): number => {
42
+ if (!header) return 0;
43
+ if (/^\d+$/.test(header)) return Number(header) * 1000;
44
+ const date = Date.parse(header);
45
+ return Number.isFinite(date) ? Math.max(0, date - now) : 0;
46
+ };
47
+ // Keep transport read failures separate from invalid JSON: only transport failures retry.
48
+ const readResponse = async (response: Response, signal: AbortSignal): Promise<unknown> => {
49
+ const tooLarge = () => new BootstrapError("permanent", "response too_large");
50
+ if (Number(response.headers.get("Content-Length")) > BOOTSTRAP_RESPONSE_BYTES) {
51
+ void response.body?.cancel().catch(() => undefined);
52
+ throw tooLarge();
53
+ }
54
+ const reader = response.body?.getReader();
55
+ if (!reader) throw new BootstrapError("permanent", "response invalid_json");
56
+ const abort = () => {
57
+ void reader.cancel().catch(() => undefined);
58
+ };
59
+ signal.addEventListener("abort", abort, { once: true });
60
+ if (signal.aborted) abort();
61
+ const bytes = new Uint8Array(BOOTSTRAP_RESPONSE_BYTES);
62
+ let length = 0;
63
+ try {
64
+ while (true) {
65
+ const chunk = await reader.read();
66
+ if (chunk.done) break;
67
+ if (length + chunk.value.byteLength > bytes.byteLength) {
68
+ void reader.cancel().catch(() => undefined);
69
+ throw tooLarge();
70
+ }
71
+ bytes.set(chunk.value, length);
72
+ length += chunk.value.byteLength;
73
+ }
74
+ } finally {
75
+ signal.removeEventListener("abort", abort);
76
+ reader.releaseLock();
77
+ }
78
+ signal.throwIfAborted();
79
+ try {
80
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
81
+ } catch {
82
+ throw new BootstrapError("permanent", "response invalid_json");
83
+ }
84
+ };
85
+ // Dependency injection keeps retry-budget tests fast and deterministic; not a public startup option.
86
+ type Dependencies = {
87
+ fetch: (url: URL, init: RequestInit) => Promise<Response>;
88
+ now: () => number;
89
+ wallNow: () => number;
90
+ sleep: (ms: number) => Promise<unknown>;
91
+ random: () => number;
92
+ log: (entry: { appId: string; attempt: number; errorClass: "transient" | "permanent" }) => void;
93
+ };
94
+ const defaults: Dependencies = {
95
+ fetch: (url, init) => fetch(url, init),
96
+ now: () => performance.now(),
97
+ wallNow: () => Date.now(),
98
+ sleep: (ms) => Bun.sleep(ms),
99
+ random: Math.random,
100
+ log: (entry) => console.warn("Core bootstrap attempt failed", entry),
101
+ };
102
+
103
+ /** No application imports or listeners exist while this bounded startup request waits. */
104
+ export const bootstrapConnections = async (
105
+ key: AppKey,
106
+ origin: string | undefined,
107
+ dependencies: Partial<Dependencies> = {},
108
+ ): Promise<BootstrapResponse> => {
109
+ const url = originUrl(origin);
110
+ const deps = { ...defaults, ...dependencies };
111
+ const deadline = deps.now() + BOOTSTRAP_BUDGET_MS;
112
+ let attempt = 0;
113
+ while (deps.now() < deadline) {
114
+ attempt++;
115
+ const controller = new AbortController();
116
+ let timer: ReturnType<typeof setTimeout> | undefined;
117
+ let failure: BootstrapError;
118
+ let httpFailure: BootstrapError | undefined;
119
+ try {
120
+ const transmission = async (): Promise<BootstrapResponse> => {
121
+ const proof = await signBootstrapProof(key, deps.wallNow()).catch(() => {
122
+ throw new BootstrapError("permanent", "could not create Ed25519 startup proof");
123
+ });
124
+ controller.signal.throwIfAborted();
125
+ const response = await deps.fetch(url, {
126
+ method: "POST",
127
+ headers: { Authorization: `Bearer ${proof}`, "Content-Type": "application/json", Accept: "application/json" },
128
+ body: BOOTSTRAP_BODY,
129
+ redirect: "error",
130
+ signal: controller.signal,
131
+ });
132
+ if (response.status !== 200) {
133
+ // A failed transmission may already have consumed its jti.
134
+ httpFailure = new BootstrapError(
135
+ response.status === 429 || response.status === 503 ? "transient" : "permanent",
136
+ httpReason(response.status),
137
+ retryAfter(response.headers.get("retry-after"), deps.wallNow()),
138
+ );
139
+ if (response.status === 403) {
140
+ try {
141
+ // Recognize only the stable permission code; never retain or log raw error payloads.
142
+ const body = await readResponse(response, controller.signal);
143
+ if (typeof body === "object" && body !== null && "code" in body && body.code === "connections_not_permitted")
144
+ httpFailure = new BootstrapError("permanent", "HTTP 403: connections_not_permitted; this key cannot receive connections");
145
+ } catch {
146
+ // A broken or stalled error body cannot turn a permanent HTTP status into a retry.
147
+ }
148
+ } else void response.body?.cancel().catch(() => undefined);
149
+ throw httpFailure;
150
+ }
151
+ const parsed = await readResponse(response, controller.signal);
152
+ const checked = BootstrapResponseSchema.safeParse(parsed);
153
+ if (!checked.success) throw new BootstrapError("permanent", "invalid response schema");
154
+ const { identity } = checked.data;
155
+ if (identity.installationId !== key.installationId) throw new BootstrapError("permanent", "wrong installation");
156
+ if (identity.appId !== key.appId) throw new BootstrapError("permanent", "wrong app");
157
+ if (identity.keyId !== key.keyId) throw new BootstrapError("permanent", "wrong key");
158
+ return checked.data;
159
+ };
160
+ return await Promise.race([
161
+ transmission(),
162
+ new Promise<never>((_, reject) => {
163
+ timer = setTimeout(
164
+ () => {
165
+ controller.abort();
166
+ reject(new BootstrapError("transient", "attempt deadline exceeded"));
167
+ },
168
+ Math.min(BOOTSTRAP_ATTEMPT_MS, Math.max(0, deadline - deps.now())),
169
+ );
170
+ }),
171
+ ]);
172
+ } catch (error) {
173
+ // Network/TLS/CA errors may embed credentials or destinations; never retain their text or cause.
174
+ const redirect = error instanceof TypeError && "code" in error && error.code === "UnexpectedRedirect";
175
+ failure =
176
+ httpFailure ??
177
+ (error instanceof BootstrapError
178
+ ? error
179
+ : redirect
180
+ ? new BootstrapError("permanent", "redirect refused; check the direct Core origin")
181
+ : new BootstrapError("transient", "network or TLS failure"));
182
+ deps.log({ appId: key.appId, attempt, errorClass: failure.kind });
183
+ if (failure.kind === "permanent") throw failure;
184
+ } finally {
185
+ clearTimeout(timer);
186
+ controller.abort();
187
+ }
188
+ // Full jitter, capped at the per-attempt deadline. Retry-After remains a minimum wait.
189
+ const delay = Math.max(failure.retryAfterMs, deps.random() * Math.min(BOOTSTRAP_ATTEMPT_MS, 250 * 2 ** Math.min(attempt - 1, 6)));
190
+ const remaining = deadline - deps.now();
191
+ if (delay >= remaining) break;
192
+ await deps.sleep(delay);
193
+ }
194
+ throw new BootstrapError("transient", "five-minute startup budget exhausted");
195
+ };
@@ -0,0 +1,67 @@
1
+ /** Bootstrap v1 has no platform barrel or application dependencies. */
2
+ import { SignJWT } from "jose";
3
+ import { z } from "zod";
4
+ import { type AppKey, importAppKeySeed } from "../app-key";
5
+ import type { PlatformConnectionValues } from "./connections";
6
+
7
+ export const BOOTSTRAP_PATH = "/_internal/apps/bootstrap";
8
+ export const BOOTSTRAP_TYPE = "cloud-app+jwt";
9
+ export const BOOTSTRAP_BODY = "{}";
10
+ export const BOOTSTRAP_RESPONSE_BYTES = 64 * 1024;
11
+ // Same compact-token budget as Cloud's identity protocols.
12
+ export const BOOTSTRAP_PROOF_BYTES = 4 * 1024;
13
+ export const BOOTSTRAP_BUDGET_MS = 5 * 60_000;
14
+ export const BOOTSTRAP_ATTEMPT_MS = 10_000;
15
+ export const BOOTSTRAP_PROOF_SECONDS = 30;
16
+ export const BOOTSTRAP_REPLAY_RETENTION_SECONDS = 5 * 60;
17
+
18
+ const nonempty = z.string().refine((value) => value.trim().length > 0);
19
+ export const BootstrapIdentitySchema = z.object({ installationId: z.uuid(), appId: z.string(), keyId: z.uuid() }).strict();
20
+ export type BootstrapIdentity = z.infer<typeof BootstrapIdentitySchema>;
21
+ export const BootstrapConnectionsSchema = z
22
+ .object({
23
+ database: z.object({ url: nonempty }).strict(),
24
+ valkey: z.object({ url: nonempty }).strict(),
25
+ nats: z
26
+ .object({
27
+ servers: z.array(nonempty).nonempty(),
28
+ namespace: nonempty,
29
+ replicas: z.union([z.literal(1), z.literal(3), z.literal(5)]),
30
+ ignoreClusterUpdates: z.boolean(),
31
+ creds: nonempty.optional(),
32
+ ca: nonempty.optional(),
33
+ })
34
+ .strict(),
35
+ appSecret: z.object({ current: nonempty, previous: nonempty.optional(), next: nonempty.optional() }).strict(),
36
+ })
37
+ .strict() satisfies z.ZodType<PlatformConnectionValues>;
38
+ export const BootstrapResponseSchema = z.object({ identity: BootstrapIdentitySchema, connections: BootstrapConnectionsSchema }).strict();
39
+ export type BootstrapResponse = z.infer<typeof BootstrapResponseSchema>;
40
+
41
+ export const bootstrapBodyDigest = async (body: Uint8Array<ArrayBuffer>): Promise<string> =>
42
+ Buffer.from(await crypto.subtle.digest("SHA-256", body)).toString("base64url");
43
+
44
+ /** Call at the HTTP transmission seam, including every retry. */
45
+ export const signAppProof = async (
46
+ key: AppKey,
47
+ request: { method: string; path: string; body: Uint8Array<ArrayBuffer> },
48
+ now = Date.now(),
49
+ ): Promise<string> => {
50
+ const iat = Math.floor(now / 1000);
51
+ return new SignJWT({
52
+ htm: request.method,
53
+ htu: request.path,
54
+ bdh: await bootstrapBodyDigest(request.body),
55
+ })
56
+ .setProtectedHeader({ alg: "EdDSA", typ: BOOTSTRAP_TYPE, kid: key.keyId })
57
+ .setIssuer(key.appId)
58
+ .setSubject(key.appId)
59
+ .setAudience(`cloud-core:${key.installationId}`)
60
+ .setIssuedAt(iat)
61
+ .setExpirationTime(iat + BOOTSTRAP_PROOF_SECONDS)
62
+ .setJti(crypto.randomUUID())
63
+ .sign(await importAppKeySeed(key.seed));
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
  }
@@ -17,9 +17,18 @@ export type PlatformConnectionValues = {
17
17
  export type PlatformConnectionSources = {
18
18
  [Group in keyof PlatformConnectionValues]: { [Key in keyof PlatformConnectionValues[Group]]: ConnectionSource };
19
19
  };
20
+ /** Credential-free capability metadata; absent on older processes means unknown. */
21
+ export type PlatformBootstrapMetadata = {
22
+ keyId?: string;
23
+ mode: "core" | "env";
24
+ sources: PlatformConnectionSources;
25
+ keyFormat: "cloud-app-key-v1";
26
+ protocol: 1;
27
+ };
20
28
  export type PlatformConnections = PlatformConnectionValues & {
21
29
  appId: string;
22
30
  sources: PlatformConnectionSources;
31
+ bootstrap?: PlatformBootstrapMetadata;
23
32
  };
24
33
 
25
34
  let current: PlatformConnections | undefined;
@@ -0,0 +1,14 @@
1
+ import { hkdfSync } from "node:crypto";
2
+ import { getProcessApplicationId } from "../_internal/process-identity";
3
+ import { validatePurposeName } from "../_internal/purpose-name";
4
+ import { connections } from "../runtime/connections";
5
+
6
+ /** Derive a purpose-specific signing key from this installation's current secret. */
7
+ export const appKey = (name: string): string => {
8
+ validatePurposeName(name);
9
+ const appId = getProcessApplicationId();
10
+ if (!appId) throw new Error("Start the application before using appKey()");
11
+ const secret = connections().appSecret.current;
12
+ if (!secret.trim()) throw new Error("APP_SECRET is required to derive application signing keys");
13
+ return Buffer.from(hkdfSync("sha256", secret, appId, name, 32)).toString("base64url");
14
+ };
@@ -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";
@@ -8,6 +9,7 @@ export type { CreateApiClientConfig } from "./api-client";
8
9
  export { api as apiClient } from "./api-client";
9
10
  export { appAssetPath } from "./app-assets";
10
11
  export type { AppContext } from "./app-context";
12
+ export { appKey } from "./app-key";
11
13
  export { clientIp, clientIpBucket } from "./client-ip";
12
14
  export { defineHelp, defineHelpCollection, type HelpCollection, type HelpDefinition, type HelpDefinitionDocument } from "./help";
13
15
  export { DEFAULT_LOCALE, getLocale, LOCALE_COOKIE, LOCALE_HEADER, locale, preferredLocale, resolveLocale } from "./locale";
@@ -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";
@@ -1,7 +1,8 @@
1
- import { type SQLQuery, sql } from "bun";
1
+ import type { SQLQuery } from "bun";
2
2
  import { z } from "zod";
3
3
  import { DEFAULT_LINUX_IDENTITY_CONFIGURATION, LinuxIdentityConfigurationSchema } from "../../contracts/posix";
4
4
  import type { RequestActor, UserProfile, UserProvider } from "../../contracts/shared";
5
+ import { sql } from "../../runtime/clients";
5
6
  import { userFromActor } from "../../server/actor";
6
7
  import { isAccountCategoryAllowed } from "../account-category-policy";
7
8
  import { toPgTextArray } from "../postgres";
@@ -66,9 +67,10 @@ const cursor = (after?: string): string | null => {
66
67
  };
67
68
 
68
69
  /** Server-side identity reads. Authentication and application resource authorization remain caller-owned. */
69
- export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdentity = readUpstreamIdentity) => {
70
+ export const createAccountIdentityService = (injectedDb?: typeof sql, upstreamIdentity = readUpstreamIdentity) => {
71
+ const db = () => injectedDb ?? sql;
70
72
  const setting = async (key: string, fallback: unknown): Promise<unknown> => {
71
- const [row] = await db<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
73
+ const [row] = await db()<{ value: string }[]>`SELECT value FROM settings.entries WHERE key = ${key}`;
72
74
  return row ? decryptValue(row.value) : fallback;
73
75
  };
74
76
  const availability = async (): Promise<AccountIdentityAvailability> => {
@@ -84,7 +86,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
84
86
  throw new AccountIdentityError("invalid_identity_configuration");
85
87
  }
86
88
  };
87
- const userRows = (filter: SQLQuery) => db<UserRow[]>`
89
+ const userRows = (filter: SQLQuery) => db()<UserRow[]>`
88
90
  SELECT u.id, u.provider, u.uid, u.profile, u.admin,
89
91
  (u.account_expires IS NOT NULL AND u.account_expires <= now()) AS expired,
90
92
  p.managed_by, p.uid_number, p.primary_gid_number
@@ -98,7 +100,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
98
100
  const [row] = await userRows(sql`u.id = ${supplied.id}::uuid`);
99
101
  if (!row || row.expired || row.provider !== supplied.provider || row.uid !== supplied.uid || row.profile !== supplied.profile)
100
102
  throw new AccountIdentityError("identity_unavailable");
101
- if (!(await isAccountCategoryAllowed(row, db))) throw new AccountIdentityError("identity_unavailable");
103
+ if (!(await isAccountCategoryAllowed(row, db()))) throw new AccountIdentityError("identity_unavailable");
102
104
  const state = await availability();
103
105
  if (row.provider === "ipa" && !state.freeipaEnabled) throw new AccountIdentityError("identity_unavailable");
104
106
  return { row, state, supplied };
@@ -113,14 +115,14 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
113
115
  if (!Array.isArray(configured) || !configured.every((value): value is string => typeof value === "string"))
114
116
  throw new AccountIdentityError("invalid_identity_configuration");
115
117
  const names = configured.length > 0 ? configured : ["admins"];
116
- const [membership] = await db`SELECT 1 FROM auth.ipa_user_effective_groups
118
+ const [membership] = await db()`SELECT 1 FROM auth.ipa_user_effective_groups
117
119
  WHERE user_id = ${current.row.id}::uuid AND group_name = ANY(${toPgTextArray(names)}::text[]) LIMIT 1`;
118
120
  if (!membership) throw new AccountIdentityError("admin_required");
119
121
  }
120
122
  return current;
121
123
  };
122
124
  const groupRows = async (filter: SQLQuery) => {
123
- const rows = await db<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
125
+ const rows = await db()<{ id: string; provider: UserProvider; name: string; gid_number: number | null; personal: boolean }[]>`
124
126
  SELECT g.id, g.provider, g.name, g.gid_number,
125
127
  EXISTS(SELECT 1 FROM auth.personal_groups p WHERE p.group_id = g.id) AS personal
126
128
  FROM auth.groups g
@@ -189,7 +191,7 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
189
191
  eligible: current.profile === "user",
190
192
  };
191
193
  }
192
- const [ownership] = await db<{ personal: boolean }[]>`SELECT EXISTS (
194
+ const [ownership] = await db()<{ personal: boolean }[]>`SELECT EXISTS (
193
195
  SELECT 1 FROM auth.personal_groups p
194
196
  WHERE (${input.identityId ?? null}::uuid IS NOT NULL AND p.group_id = ${input.identityId ?? null}::uuid)
195
197
  OR (${input.identityId ?? null}::uuid IS NULL AND p.group_name = ${input.name})
@@ -238,10 +240,10 @@ export const createAccountIdentityService = (db: typeof sql = sql, upstreamIdent
238
240
  const after = cursor(input.after);
239
241
  const state = await availability();
240
242
  const [localAllowed, ipaAllowed] = await Promise.all([
241
- isAccountCategoryAllowed({ provider: "local", profile: "user" }, db),
242
- isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db),
243
+ isAccountCategoryAllowed({ provider: "local", profile: "user" }, db()),
244
+ isAccountCategoryAllowed({ provider: "ipa", profile: "user" }, db()),
243
245
  ]);
244
- const rows = await db<
246
+ const rows = await db()<
245
247
  {
246
248
  id: string;
247
249
  provider: UserProvider;
@@ -1,5 +1,5 @@
1
- import { sql } from "bun";
2
1
  import type { IpaUserData } from "../../contracts/shared";
2
+ import { sql } from "../../runtime/clients";
3
3
 
4
4
  type DbRow = Record<string, unknown>;
5
5
 
@@ -1,5 +1,6 @@
1
- import { sql, type TransactionSQL } from "bun";
1
+ import type { TransactionSQL } from "bun";
2
2
  import type { BaseGroup, GroupMember, MutationResult, UserProvider } from "../../contracts/shared";
3
+ import { sql } from "../../runtime/clients";
3
4
  import { escapeLikePattern, isUniqueViolation } from "../postgres";
4
5
  import { buildBaseGroup, personalOwnerJoin } from "./base-group";
5
6
  import { recursiveServiceAccountGroupIdsSubquery } from "./group-sql";
@@ -1,4 +1,5 @@
1
- import { sql } from "bun";
1
+ import { sql } from "../../runtime/clients";
2
+
2
3
  import type { ServiceAccountRequestActor } from "../../server/middleware/auth";
3
4
  import { err, fail, ok, type Result } from "../../server/services";
4
5
  import { audit } from "../audit";