@k2b/cloud 0.7.0 → 0.8.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 (182) hide show
  1. package/package.json +5 -4
  2. package/scripts/README.md +10 -0
  3. package/scripts/build.ts +6 -1
  4. package/scripts/preload.ts +7 -0
  5. package/scripts/sync-recovery-smoke.ts +21 -6
  6. package/src/_internal/build-metadata.ts +8 -1
  7. package/src/_internal/capabilities.ts +35 -12
  8. package/src/_internal/define-app.ts +28 -11
  9. package/src/_internal/page-responses.ts +1 -1
  10. package/src/_internal/readiness.ts +4 -1
  11. package/src/_internal/runtime-context.ts +1 -1
  12. package/src/access/PermissionEditor.tsx +15 -9
  13. package/src/access/PrincipalPicker.tsx +9 -3
  14. package/src/access/ResourceApiKeys.tsx +1 -1
  15. package/src/access/ui.ts +1 -2
  16. package/src/ai/admin.ts +2 -2
  17. package/src/ai/assistant-models.ts +1 -1
  18. package/src/ai/audio-tool.ts +1 -1
  19. package/src/ai/browser-code-contracts.ts +174 -52
  20. package/src/ai/browser.ts +4 -2
  21. package/src/ai/capabilities.ts +3 -3
  22. package/src/ai/chat/blocks.tsx +127 -50
  23. package/src/ai/chat/builtin-tools.tsx +2 -4
  24. package/src/ai/chat/capability-table.tsx +1 -1
  25. package/src/ai/chat/composer-adapter.ts +48 -13
  26. package/src/ai/chat/message-utils.ts +18 -5
  27. package/src/ai/chat/presentation.tsx +24 -11
  28. package/src/ai/chat/tool-disclosure.tsx +6 -2
  29. package/src/ai/chat/tool-groups.ts +8 -2
  30. package/src/ai/chat-quotas.ts +2 -2
  31. package/src/ai/chat-task-contracts.ts +9 -13
  32. package/src/ai/chat-tasks.ts +39 -18
  33. package/src/ai/client/controller.ts +87 -38
  34. package/src/ai/client/projection.ts +13 -6
  35. package/src/ai/client/transport.ts +4 -2
  36. package/src/ai/code-capability-routes.ts +22 -27
  37. package/src/ai/code-capability-transport.ts +2 -1
  38. package/src/ai/code-execution.ts +53 -0
  39. package/src/ai/code-mode-skill.ts +9 -5
  40. package/src/ai/code-runtime-tools.ts +16 -3
  41. package/src/ai/code-source-contracts.ts +100 -32
  42. package/src/ai/code-source-tools.ts +49 -6
  43. package/src/ai/compaction.ts +3 -3
  44. package/src/ai/default-tools.ts +87 -14
  45. package/src/ai/dictation-runtime.ts +1 -1
  46. package/src/ai/draft-content.ts +27 -12
  47. package/src/ai/enrich.ts +1 -1
  48. package/src/ai/executor.ts +8 -12
  49. package/src/ai/fetch-file-tool.ts +2 -2
  50. package/src/ai/file-content-version.ts +6 -1
  51. package/src/ai/file-tools.ts +8 -2
  52. package/src/ai/files-store.ts +55 -17
  53. package/src/ai/grids-skill.ts +2 -0
  54. package/src/ai/http.ts +18 -7
  55. package/src/ai/index.ts +24 -25
  56. package/src/ai/inference-calls.ts +3 -4
  57. package/src/ai/live.ts +1 -1
  58. package/src/ai/markdown-pdf-tool.ts +2 -2
  59. package/src/ai/memories.ts +30 -27
  60. package/src/ai/memory-learning-runs.ts +1 -1
  61. package/src/ai/memory-learning.ts +5 -5
  62. package/src/ai/memory-tool.ts +1 -5
  63. package/src/ai/memory-workflow-evidence.ts +9 -4
  64. package/src/ai/message-queue.ts +23 -12
  65. package/src/ai/migrate.ts +6 -4
  66. package/src/ai/model-pricing.ts +4 -4
  67. package/src/ai/pdf-render-worker.ts +3 -2
  68. package/src/ai/projects-routes.ts +15 -40
  69. package/src/ai/projects.ts +18 -13
  70. package/src/ai/quota-provider.ts +6 -5
  71. package/src/ai/quota-report.ts +5 -5
  72. package/src/ai/quotas.ts +3 -3
  73. package/src/ai/routes.ts +60 -34
  74. package/src/ai/run-timeout.ts +3 -1
  75. package/src/ai/runtime-tools.ts +6 -7
  76. package/src/ai/runtime.ts +38 -20
  77. package/src/ai/settings.ts +1 -1
  78. package/src/ai/short-id.ts +4 -5
  79. package/src/ai/skill-search.ts +21 -10
  80. package/src/ai/skill-seeds.ts +16 -7
  81. package/src/ai/skill-tool.ts +6 -1
  82. package/src/ai/skills-routes.ts +32 -15
  83. package/src/ai/skills.ts +54 -28
  84. package/src/ai/store.ts +80 -47
  85. package/src/ai/structured.ts +6 -2
  86. package/src/ai/system-prompt.ts +7 -5
  87. package/src/ai/task-contracts.ts +84 -0
  88. package/src/ai/task-execution.ts +92 -0
  89. package/src/ai/todo-contracts.ts +24 -12
  90. package/src/ai/todo-tool.ts +16 -13
  91. package/src/ai/tools.ts +1 -1
  92. package/src/ai/transcription.ts +4 -4
  93. package/src/ai/turn-timing.ts +1 -1
  94. package/src/ai/ui.tsx +2 -1
  95. package/src/ai/usage.ts +1 -1
  96. package/src/ai/vision-tool.ts +38 -17
  97. package/src/api/admin-ai-quotas.ts +14 -8
  98. package/src/api/admin-ai-skills.ts +3 -3
  99. package/src/api/admin-linux-identities.ts +10 -4
  100. package/src/api/admin-rail.ts +2 -2
  101. package/src/api/app-approval.ts +2 -2
  102. package/src/api/auth.ts +1 -1
  103. package/src/api/capabilities.ts +45 -13
  104. package/src/api/index.ts +5 -5
  105. package/src/api/mcp.ts +5 -12
  106. package/src/browser/CloudResourceSearch.tsx +1 -1
  107. package/src/browser/command-bridge.ts +1 -1
  108. package/src/browser/command-shortcuts.ts +1 -1
  109. package/src/browser/commands.ts +8 -7
  110. package/src/browser/testing.ts +2 -1
  111. package/src/capabilities/client.ts +1 -1
  112. package/src/capabilities/command-link.ts +1 -1
  113. package/src/capabilities/server.ts +2 -1
  114. package/src/cli/admin/ai-quotas.ts +5 -5
  115. package/src/cli/admin/app-credentials.ts +7 -9
  116. package/src/cli/admin/index.ts +3 -3
  117. package/src/cli/admin/instance.ts +2 -2
  118. package/src/cli/admin/linux.ts +4 -2
  119. package/src/cli/admin/sync.ts +1 -1
  120. package/src/cli/capabilities.ts +42 -29
  121. package/src/config/define-env.ts +97 -0
  122. package/src/config/env.ts +147 -54
  123. package/src/config/index.ts +3 -1
  124. package/src/contracts/capabilities.ts +8 -1
  125. package/src/contracts/index.ts +5 -7
  126. package/src/contracts/registry.ts +2 -0
  127. package/src/contracts/shared.ts +12 -3
  128. package/src/desktop/index.ts +1 -0
  129. package/src/desktop/solid.tsx +1 -1
  130. package/src/index.ts +8 -2
  131. package/src/server/index.ts +2 -2
  132. package/src/server/ratelimit.ts +1 -1
  133. package/src/server/services/access-revision.ts +3 -3
  134. package/src/server/services/access.ts +3 -1
  135. package/src/services/accounts/notification-sender.ts +12 -2
  136. package/src/services/announcements/index.ts +4 -4
  137. package/src/services/app-approval.ts +11 -10
  138. package/src/services/identity/key-config.ts +6 -4
  139. package/src/services/identity/runtime-config.ts +6 -5
  140. package/src/services/ipa/posix.ts +1 -1
  141. package/src/services/ipa/sync.ts +1 -1
  142. package/src/services/logging/index.ts +1 -1
  143. package/src/services/logging/trace.ts +1 -1
  144. package/src/services/mandates/index.ts +5 -2
  145. package/src/services/mandates/policy.ts +106 -6
  146. package/src/services/notifications/batches.ts +1 -0
  147. package/src/services/pdf/gotenberg.ts +50 -14
  148. package/src/services/pdf/index.ts +11 -11
  149. package/src/services/public-http.ts +1 -1
  150. package/src/services/rail-shortcuts.ts +1 -1
  151. package/src/services/rail-snapshot.ts +1 -1
  152. package/src/services/request-cache-redis.ts +2 -1
  153. package/src/services/session/user.ts +1 -1
  154. package/src/services/settings/app.ts +5 -1
  155. package/src/services/settings/core-settings.ts +6 -8
  156. package/src/services/settings/index.ts +3 -3
  157. package/src/services/settings/store.ts +3 -3
  158. package/src/services/webauthn.ts +1 -1
  159. package/src/shared/index.ts +3 -6
  160. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  161. package/src/ssr/AdminSidebar.tsx +2 -2
  162. package/src/ssr/AppLaunchpad.island.tsx +12 -2
  163. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  164. package/src/ssr/GlobalSearchDialog.tsx +6 -6
  165. package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
  166. package/src/ssr/Layout.tsx +2 -2
  167. package/src/ssr/LayoutHeader.tsx +1 -1
  168. package/src/ssr/LayoutHelp.tsx +4 -6
  169. package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
  170. package/src/ssr/MinimalLayout.tsx +1 -1
  171. package/src/ssr/MobileNavigation.tsx +2 -2
  172. package/src/ssr/ProfilePreferences.island.tsx +1 -1
  173. package/src/ssr/admin-navigation.ts +6 -2
  174. package/src/ssr/islands/index.ts +2 -2
  175. package/src/ssr/mobile-menu-history.ts +1 -1
  176. package/src/workflows/ai/index.ts +2 -2
  177. package/src/workflows/ai/runtime.ts +16 -91
  178. package/src/workflows/ai/store.ts +1 -1
  179. package/src/workflows/ai/types.ts +6 -82
  180. package/src/workflows/store/index.ts +2 -2
  181. package/src/server/services/freeipa/test-certificates.ts +0 -44
  182. package/src/services/session/test-fixture.ts +0 -18
package/src/config/env.ts CHANGED
@@ -1,57 +1,150 @@
1
1
  /**
2
- * Environment variable parsing.
3
- * All env vars are parsed and validated here, then exported as typed `env` object.
2
+ * Platform environment registry.
3
+ * Every variable a Cloud process reads is declared here or in an application's
4
+ * `src/env.ts`; values are parsed lazily on access (see `define-env.ts`).
4
5
  */
6
+ import { z } from "zod";
7
+ import { defineEnv, envBoolean, envList } from "./define-env";
5
8
 
6
- const str = (key: string, fallback: string = ""): string => process.env[key] ?? fallback;
7
-
8
- const optional = (key: string): string | undefined => process.env[key]?.trim() || undefined;
9
-
10
- const bool = (key: string, fallback: boolean): boolean => {
11
- const value = process.env[key]?.trim().toLowerCase();
12
- if (!value) return fallback;
13
- return value === "true" || value === "1";
14
- };
15
-
16
- const syncReplicas = (): 1 | 3 | 5 => {
17
- const value = str("SYNC_REPLICAS", "3").trim();
18
- if (value === "1") return 1;
19
- if (value === "3") return 3;
20
- if (value === "5") return 5;
21
- throw new Error("SYNC_REPLICAS must be 1, 3, or 5");
22
- };
23
-
24
- const list = (key: string): string[] =>
25
- (process.env[key] ?? "")
26
- .split(",")
27
- .map((entry) => entry.trim())
28
- .filter(Boolean);
29
-
30
- export const env = {
31
- // Application (infrastructure — not changeable at runtime)
32
- APP_SECRET: str("APP_SECRET"),
33
- IS_DEVELOPMENT: process.env.NODE_ENV === "development",
34
-
35
- // Admin login (temporary first access or recovery — Core only, no IPA required)
36
- ADMIN_LOGIN_TOKEN: str("ADMIN_LOGIN_TOKEN"),
37
-
38
- // NATS / @k2b/sync (Postgres and Redis are read by Bun from DATABASE_URL / REDIS_URL)
39
- /** Bootstrap servers, comma-separated `nats://host:port` URLs. Required by `connectNats()`. */
40
- NATS_SERVERS: list("NATS_SERVERS"),
41
- /** Sync namespace shared by every app of one Cloud installation (e.g. `dev`, `prod`). Required. */
42
- SYNC_NAMESPACE: str("SYNC_NAMESPACE"),
43
- /** Persistent stream replica count; consistent across the installation. */
44
- SYNC_REPLICAS: syncReplicas(),
45
- /** Path to a mounted NATS `.creds` file (JWT + NKey). Unset = server-side no-auth. */
46
- NATS_CREDS_FILE: optional("NATS_CREDS_FILE"),
47
- /** Path to a mounted CA certificate; enables TLS with that CA. Unset = plain TCP. */
48
- NATS_TLS_CA_FILE: optional("NATS_TLS_CA_FILE"),
49
- /**
50
- * Default true: only dial the servers listed in NATS_SERVERS. Clustered
51
- * nodes gossip their peers' advertised addresses, which are Docker-internal
52
- * hostnames (`ipa_nats_2`) that host-side clients (tests, `dev:cld` tooling)
53
- * cannot resolve. Set to `false` when clients can reach every advertised
54
- * address and should learn new nodes at runtime.
55
- */
56
- NATS_IGNORE_CLUSTER_UPDATES: bool("NATS_IGNORE_CLUSTER_UPDATES", true),
57
- } as const;
9
+ const syncReplicas = z
10
+ .enum(["1", "3", "5"], { error: "must be 1, 3, or 5" })
11
+ .transform((value): 1 | 3 | 5 => (value === "1" ? 1 : value === "3" ? 3 : 5));
12
+
13
+ const registry = defineEnv({
14
+ // Process
15
+ NODE_ENV: {
16
+ schema: z.string(),
17
+ doc: "Release images set `production`; `development` enables development-only behavior such as insecure cookies and HTTP issuers.",
18
+ },
19
+ HOSTNAME: {
20
+ schema: z.string(),
21
+ doc: "Container or host name; the default instance identity in worker leases and gateway registration.",
22
+ },
23
+
24
+ // Infrastructure
25
+ DATABASE_URL: {
26
+ schema: z.string(),
27
+ example: "postgresql://ipa:ipa@localhost:5432/ipa",
28
+ doc: "Postgres connection read by Bun SQL and by application-owned pools.",
29
+ },
30
+ REDIS_URL: {
31
+ schema: z.string(),
32
+ aliases: ["VALKEY_URL"],
33
+ example: "redis://localhost:6379",
34
+ doc: "Valkey connection used by Bun Redis for caches and rate limits.",
35
+ },
36
+ NATS_SERVERS: {
37
+ schema: envList,
38
+ default: [],
39
+ example: "nats://localhost:4222",
40
+ doc: "Comma-separated NATS JetStream bootstrap servers; the client dials all of them.",
41
+ },
42
+ SYNC_NAMESPACE: {
43
+ schema: z.string(),
44
+ default: "",
45
+ example: "dev",
46
+ doc: "Sync namespace shared by every app of one installation; isolates its NATS resources from other installations on the same cluster.",
47
+ },
48
+ SYNC_REPLICAS: {
49
+ schema: syncReplicas,
50
+ default: 3,
51
+ example: "1",
52
+ doc: "Persistent JetStream replica count (1, 3 or 5); keep it consistent across the installation and migrate existing resources before changing it.",
53
+ },
54
+ NATS_CREDS_FILE: {
55
+ schema: z.string(),
56
+ doc: "Path to a mounted NATS `.creds` file (JWT and NKey); unset means server-side no-auth.",
57
+ },
58
+ NATS_TLS_CA_FILE: {
59
+ schema: z.string(),
60
+ doc: "Path to a mounted CA certificate that enables TLS to NATS; unset means plain TCP.",
61
+ },
62
+ NATS_IGNORE_CLUSTER_UPDATES: {
63
+ schema: envBoolean,
64
+ default: true,
65
+ example: "true",
66
+ doc: "Only dial the listed servers and ignore cluster-advertised peer addresses; set to `false` when every advertised address is reachable.",
67
+ },
68
+
69
+ // Installation
70
+ APP_URL: {
71
+ schema: z.string(),
72
+ example: "localhost:3000",
73
+ doc: "Public Cloud URL used to bootstrap the `app.url` setting; the stored setting wins once written.",
74
+ },
75
+ APP_SECRET: {
76
+ schema: z.string(),
77
+ default: "",
78
+ scope: "secret",
79
+ example: "change-me-generate-with-openssl-rand-hex-32",
80
+ doc: "Deployment-wide secret for at-rest settings encryption and signed cursors; generate once with `openssl rand -hex 32` and never change it after data has been written.",
81
+ },
82
+ ADMIN_LOGIN_TOKEN: {
83
+ schema: z.string(),
84
+ default: "",
85
+ scope: "secret",
86
+ doc: "Core only: temporary first-administrator or recovery login token; remove it after normal administrator sign-in works.",
87
+ },
88
+
89
+ // Identity and OAuth
90
+ CLOUD_IDENTITY_KEY_ENCRYPTION_KEY: {
91
+ schema: z.string(),
92
+ scope: "secret",
93
+ example: "change-me-generate-with-openssl-rand-hex-32",
94
+ doc: "Core only: 64-hex KEK for platform identity signing keys stored in Postgres; generate once with `openssl rand -hex 32` and never distribute it to other apps.",
95
+ },
96
+ CLOUD_IDENTITY_NEXT_KEY: {
97
+ schema: z.string(),
98
+ scope: "secret",
99
+ doc: "Core only: pre-distributed next KEK before promoting it to current; cannot be combined with `CLOUD_IDENTITY_PREVIOUS_KEY`.",
100
+ },
101
+ CLOUD_IDENTITY_PREVIOUS_KEY: {
102
+ schema: z.string(),
103
+ scope: "secret",
104
+ doc: "Core only: old KEK set during an explicit rolling rewrap of stored identity signing keys, then removed.",
105
+ },
106
+ CLOUD_OAUTH_BROKER_SECRET: {
107
+ schema: z.string(),
108
+ scope: "secret",
109
+ doc: "Core and OAuth only: 64-hex secret that lets the OAuth app request token signatures from Core; generate independently with `openssl rand -hex 32`.",
110
+ },
111
+ CLOUD_CORE_INTERNAL_ORIGIN: {
112
+ schema: z.string(),
113
+ doc: "Direct Core listener origin for OAuth issuance, mandate-backed background work and internal callbacks; host-run callers use the Core port, not the gateway.",
114
+ },
115
+ CLOUD_APP_CREDENTIAL: {
116
+ schema: z.string(),
117
+ scope: "secret",
118
+ doc: "App-bound workload credential provisioned through Core's admin workload API; background apps such as Mail need one with `identity:invoke`.",
119
+ },
120
+ CLOUD_IDENTITY_JWKS_ORIGIN: {
121
+ schema: z.string(),
122
+ doc: "Optional private origin of the Core listener for session and invocation JWKS; unset fetches keys from the public issuer origin.",
123
+ },
124
+ CLOUD_OAUTH_JWKS_ORIGIN: {
125
+ schema: z.string(),
126
+ doc: "Optional private origin of the OAuth listener for OAuth JWKS; unset fetches keys from the public issuer origin.",
127
+ },
128
+
129
+ // Build and standalone development
130
+ APP_DIR: {
131
+ schema: z.string(),
132
+ scope: "development",
133
+ doc: "Application directory for standalone consumers running `@k2b/cloud/scripts/build.ts` or the SSR dev server outside the monorepo; defaults to the package directory.",
134
+ },
135
+
136
+ // Desktop
137
+ CLOUD_DESKTOP_SQLITE_PATH: {
138
+ schema: z.string(),
139
+ scope: "development",
140
+ doc: "SQLite file used by `desktop.sql` in the Bun desktop process; defaults to `.local/desktop.sqlite` in the working directory.",
141
+ },
142
+ });
143
+
144
+ type PlatformEnv = typeof registry & { readonly IS_DEVELOPMENT: boolean };
145
+
146
+ /** Platform environment; `IS_DEVELOPMENT` derives from `NODE_ENV === "development"`. */
147
+ export const env: PlatformEnv = Object.defineProperty(registry as PlatformEnv, "IS_DEVELOPMENT", {
148
+ enumerable: true,
149
+ get: () => registry.NODE_ENV === "development",
150
+ });
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Main config export.
3
- * Re-exports env vars. SSR config is per-app via defineApp().
3
+ * Re-exports the platform environment registry and the registry factory used
4
+ * by application `src/env.ts` files. SSR config is per-app via defineApp().
4
5
  */
5
6
 
7
+ export { defineEnv, type EnvRegistry, type EnvScope, type EnvSpec, type EnvSpecs, envBoolean, envList, envString } from "./define-env";
6
8
  export { env } from "./env";
@@ -1,5 +1,12 @@
1
- import { CapabilityStreamSchema, CapabilityStreamPolicySchema, type CapabilityStream, type CapabilityStreamDefinition } from "./capability-streams";
1
+ import {
2
+ type CapabilityStream,
3
+ type CapabilityStreamDefinition,
4
+ CapabilityStreamPolicySchema,
5
+ CapabilityStreamSchema,
6
+ } from "./capability-streams";
7
+
2
8
  export * from "./capability-streams";
9
+
3
10
  import { z } from "zod";
4
11
  import { CommandPathSchema } from "./commands";
5
12
  import type { AccessSubject, RequestActor, User } from "./shared";
@@ -1,19 +1,17 @@
1
1
  export * from "./account-categories";
2
2
  export * from "./announcements";
3
3
  export * from "./app";
4
+ export * from "./app-approval";
4
5
  export * from "./capabilities";
6
+ export * from "./commands";
5
7
  export * from "./notification-types";
6
8
  export * from "./posix";
7
9
  export * from "./profile";
10
+ // Core app bar administration and shared shortcut values.
11
+ export { RailAccessSchema, type RailAdminEntry, RailAdminInputSchema, RailAdminSchema, type RailAdminState } from "./rail-admin";
12
+ export type { RailShortcut } from "./rail-preferences";
8
13
  export * from "./registry";
9
14
  export * from "./resource-clipboard";
10
15
  export * from "./shared";
11
16
  export * from "./user-notifications";
12
17
  export * from "./widgets";
13
- export * from "./app-approval";
14
-
15
- // Core app bar administration and shared shortcut values.
16
- export { RailAccessSchema, RailAdminInputSchema, RailAdminSchema, type RailAdminEntry, type RailAdminState } from "./rail-admin";
17
- export type { RailShortcut } from "./rail-preferences";
18
-
19
- export * from "./commands";
@@ -51,6 +51,8 @@ export type AppRegistryWidget = {
51
51
  };
52
52
 
53
53
  export type AppRuntimeMetadata = {
54
+ /** Cloud version of the running app. Missing on apps built before versions were baked in. */
55
+ version?: string;
54
56
  release: string;
55
57
  syncVersion: string;
56
58
  };
@@ -289,9 +289,18 @@ export const AccountActivityListResponseSchema = z.object({
289
289
  export type AccountActivityListResponse = z.infer<typeof AccountActivityListResponseSchema>;
290
290
 
291
291
  export const AuthenticatedPrincipalSchema = z.discriminatedUnion("type", [
292
- z.object({ type: z.literal("user").describe("One user account."), userId: z.uuid().describe("Exact user ID returned by the Accounts directory.") }),
293
- z.object({ type: z.literal("group").describe("Members of one group."), groupId: z.uuid().describe("Exact group ID returned by the Accounts directory.") }),
294
- z.object({ type: z.literal("service_account").describe("One service account."), serviceAccountId: z.uuid().describe("Exact service account ID returned by the Accounts directory.") }),
292
+ z.object({
293
+ type: z.literal("user").describe("One user account."),
294
+ userId: z.uuid().describe("Exact user ID returned by the Accounts directory."),
295
+ }),
296
+ z.object({
297
+ type: z.literal("group").describe("Members of one group."),
298
+ groupId: z.uuid().describe("Exact group ID returned by the Accounts directory."),
299
+ }),
300
+ z.object({
301
+ type: z.literal("service_account").describe("One service account."),
302
+ serviceAccountId: z.uuid().describe("Exact service account ID returned by the Accounts directory."),
303
+ }),
295
304
  z.object({ type: z.literal("authenticated").describe("Every authenticated identity; no directory ID is needed.") }),
296
305
  ]);
297
306
 
@@ -311,6 +311,7 @@ const getSql = (): DesktopSql => {
311
311
  const { Database } = requireFn("bun:sqlite") as { Database: new (path: string) => any };
312
312
  const { dirname, resolve } = requireFn("node:path") as typeof import("node:path");
313
313
  const { mkdirSync } = requireFn("node:fs") as typeof import("node:fs");
314
+ // Declared in the platform registry; this browser-safe module must not import zod-backed config.
314
315
  const dbPath = process.env.CLOUD_DESKTOP_SQLITE_PATH ?? resolve(process.cwd(), ".local", "desktop.sqlite");
315
316
  mkdirSync(dirname(dbPath), { recursive: true });
316
317
  const db = new Database(dbPath);
@@ -1,6 +1,6 @@
1
1
  import { deserialize as deserializeProps, serialize as serializeProps } from "seroval";
2
2
  import type { Component, JSX, ParentProps } from "solid-js";
3
- import { children, createContext, createEffect, createMemo, createSignal, For, onCleanup, onMount, Show, useContext } from "solid-js";
3
+ import { children, createContext, createMemo, createSignal, For, onCleanup, onMount, Show, useContext } from "solid-js";
4
4
  import { Dynamic, Portal } from "solid-js/web";
5
5
  import {
6
6
  type DesktopEnvironment,
package/src/index.ts CHANGED
@@ -10,13 +10,19 @@ export {
10
10
  unbindProcessSync,
11
11
  } from "./_internal/process-sync";
12
12
  export { APP_READINESS_PATH } from "./_internal/readiness";
13
- export type { AppRegistryDetail, AppRegistryIssue, AppRegistrySnapshot, CapabilityRegistryRecord, DashboardWidget } from "./_internal/registry";
13
+ export type {
14
+ AppRegistryDetail,
15
+ AppRegistryIssue,
16
+ AppRegistrySnapshot,
17
+ CapabilityRegistryRecord,
18
+ DashboardWidget,
19
+ } from "./_internal/registry";
14
20
  export {
15
21
  APP_REGISTRY_CONFIG,
16
22
  APP_REGISTRY_PREFIX,
17
23
  APP_REGISTRY_TTL_MS,
18
- CAPABILITY_REGISTRY_CONFIG,
19
24
  appRegistry,
25
+ CAPABILITY_REGISTRY_CONFIG,
20
26
  capabilityRegistry,
21
27
  getApp,
22
28
  listApps,
@@ -8,8 +8,8 @@ export { defineHelp, defineHelpCollection, type HelpCollection, type HelpDefinit
8
8
  export { DEFAULT_LOCALE, getLocale, LOCALE_COOKIE, LOCALE_HEADER, locale, preferredLocale, resolveLocale } from "./locale";
9
9
  export type {
10
10
  AuthContext,
11
- InvocationProvenance,
12
11
  InvocationExpectation,
12
+ InvocationProvenance,
13
13
  RateLimitConfig,
14
14
  RateLimitRouteOverride,
15
15
  RequestActor,
@@ -26,8 +26,8 @@ export {
26
26
  openApiMeta,
27
27
  rateLimit,
28
28
  rejectReservedWorkloadCredential,
29
- requireInvocation,
30
29
  requestLogger,
30
+ requireInvocation,
31
31
  requiresAdmin,
32
32
  requiresAuth,
33
33
  requiresIpa,
@@ -86,7 +86,7 @@ export class RateLimitError extends Error {
86
86
  // ==========================
87
87
 
88
88
  export const ratelimit = (config: RateLimiterConfig): RateLimiter => {
89
- // windowSecs: 0 — e.g. Number(process.env.RL_WINDOW) on an unset var — made
89
+ // windowSecs: 0 — e.g. `Number()` of an unset variable — made
90
90
  // both window keys Infinity and collapse to one, elapsedRatio NaN, and
91
91
  // `EXPIRE key 0` delete the counter on every first increment, so the
92
92
  // weighted count never exceeded the limit and the limiter allowed
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import type { Principal, PermissionLevel } from "./access";
2
+ import type { PermissionLevel, Principal } from "./access";
3
3
 
4
4
  /** Compare the grants that were reviewed with those held under a resource lock. */
5
5
  export function accessRevision(entries: ReadonlyArray<{ id: string; principal: Principal; permission: PermissionLevel }>): string {
6
- const state = entries.map(entry => [entry.id, entry.permission,
7
- Object.entries(entry.principal).sort(([left], [right]) => left.localeCompare(right))])
6
+ const state = entries
7
+ .map((entry) => [entry.id, entry.permission, Object.entries(entry.principal).sort(([left], [right]) => left.localeCompare(right))])
8
8
  .sort((left, right) => String(left[0]).localeCompare(String(right[0])));
9
9
  return createHash("sha256").update(JSON.stringify(state)).digest("hex");
10
10
  }
@@ -568,7 +568,9 @@ export const listUsersWithAccess = async (params: {
568
568
  * Resolve display names for access entries.
569
569
  * Populates the displayName field based on principal type.
570
570
  */
571
- export const resolveDisplayNames = async <T extends { principal: Principal }>(entries: T[]): Promise<(T & { displayName: string; avatarHash?: string | null })[]> => {
571
+ export const resolveDisplayNames = async <T extends { principal: Principal }>(
572
+ entries: T[],
573
+ ): Promise<(T & { displayName: string; avatarHash?: string | null })[]> => {
572
574
  const userIds = entries.filter((e) => e.principal.type === "user").map((e) => (e.principal as { type: "user"; userId: string }).userId);
573
575
 
574
576
  const groupIds = entries
@@ -3,7 +3,12 @@ import type { TypedNotificationSendResult } from "../notifications/platform";
3
3
  export type AccountNotificationDeliveryResult = Pick<TypedNotificationSendResult, "id" | "status">;
4
4
 
5
5
  export type AccountsNotificationSender = {
6
- sendLoginLink: (input: { email: string; token: string; magicLink: string; locale?: string }) => Promise<AccountNotificationDeliveryResult>;
6
+ sendLoginLink: (input: {
7
+ email: string;
8
+ token: string;
9
+ magicLink: string;
10
+ locale?: string;
11
+ }) => Promise<AccountNotificationDeliveryResult>;
7
12
  sendFreeIpaWelcome: (input: {
8
13
  userId: string;
9
14
  uid: string;
@@ -11,7 +16,12 @@ export type AccountsNotificationSender = {
11
16
  accountExpires: string | null;
12
17
  locale?: string;
13
18
  }) => Promise<AccountNotificationDeliveryResult>;
14
- sendLocalWelcome: (input: { userId: string; email: string; accountExpires: string | null; locale?: string }) => Promise<AccountNotificationDeliveryResult>;
19
+ sendLocalWelcome: (input: {
20
+ userId: string;
21
+ email: string;
22
+ accountExpires: string | null;
23
+ locale?: string;
24
+ }) => Promise<AccountNotificationDeliveryResult>;
15
25
  sendRequestDenied: (input: {
16
26
  requestId: string;
17
27
  userId: string;
@@ -1,11 +1,7 @@
1
- import { requestCacheRedis } from "../request-cache-redis";
2
1
  import { err, fail, ok, type Result } from "@k2b/stdlib";
3
2
  import { sql } from "bun";
4
3
  import { HTTPException } from "hono/http-exception";
5
- import { hasRole, type User } from "../../contracts/shared";
6
4
  import { z } from "zod";
7
- import { AnnouncementDisplayEntrySchema } from "../../contracts/announcements";
8
- import { claimCacheFill, completeCacheFill } from "../cache-fill";
9
5
  import type {
10
6
  AnnouncementCookieState,
11
7
  AnnouncementDisplayEntry,
@@ -13,8 +9,12 @@ import type {
13
9
  CreateAnnouncement,
14
10
  UpdateAnnouncement,
15
11
  } from "../../contracts/announcements";
12
+ import { AnnouncementDisplayEntrySchema } from "../../contracts/announcements";
13
+ import { hasRole, type User } from "../../contracts/shared";
16
14
  import { markdown } from "../../shared/markdown";
15
+ import { claimCacheFill, completeCacheFill } from "../cache-fill";
17
16
  import { logger } from "../logging";
17
+ import { requestCacheRedis } from "../request-cache-redis";
18
18
 
19
19
  const log = logger("announcements");
20
20
  const ACTIVE_CACHE_KEY = "appglobalcache:shared:announcements:v1";
@@ -1,23 +1,24 @@
1
1
  import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
2
- import { sql, type SQL } from "bun";
2
+ import { type SQL, sql } from "bun";
3
+ import { env } from "../config/env";
4
+ import { type AccountCategory, accountCategory } from "../contracts/account-categories";
3
5
  import {
4
- APP_APPROVAL_LIMITS as limits,
5
6
  APP_APPROVAL_PATH,
6
7
  APP_APPROVAL_PROTOCOL,
7
- AppDevicePublicKeySchema,
8
- appDeviceProofMessage,
9
- appPairingProofMessage,
10
8
  type AppDevicePublicKey,
9
+ AppDevicePublicKeySchema,
11
10
  type AppDeviceRequest,
12
11
  type AppDeviceView,
13
12
  type AppPendingLogin,
13
+ appDeviceProofMessage,
14
+ appPairingProofMessage,
15
+ APP_APPROVAL_LIMITS as limits,
14
16
  } from "../contracts/app-approval";
15
- import { accountCategory, type AccountCategory } from "../contracts/account-categories";
17
+ import { publicCloudOrigin } from "../shared/app-url";
16
18
  import { isAccountCategoryAllowed } from "./account-category-policy";
17
19
  import { audit } from "./audit";
18
- import { decryptValue } from "./settings/crypto";
19
- import { publicCloudOrigin } from "../shared/app-url";
20
20
  import { CORE_SETTINGS } from "./settings/core-settings";
21
+ import { decryptValue } from "./settings/crypto";
21
22
 
22
23
  export class AppApprovalError extends Error {
23
24
  constructor(
@@ -47,13 +48,13 @@ export const readAppApprovalConfig = async (db: SQL = sql, requireAppOrigin = tr
47
48
  const adminPairing = values.get("user.app_approval.admin_pairing") ?? false;
48
49
  if (typeof enabled !== "boolean" || typeof adminPairing !== "boolean") return reject("UNAVAILABLE", 503);
49
50
  // The issuer is the operator's canonical URL, never the request Host header.
50
- const rawIssuer = values.get("app.url") ?? process.env.APP_URL ?? "localhost:3000";
51
+ const rawIssuer = values.get("app.url") ?? env.APP_URL ?? "localhost:3000";
51
52
  const rawOrigin = values.get("user.app_approval.origin") ?? CORE_SETTINGS["user.app_approval.origin"].default;
52
53
  const origin = (value: unknown) => {
53
54
  if (typeof value !== "string") return reject("UNAVAILABLE", 503);
54
55
  try {
55
56
  const url = new URL(value);
56
- const development = process.env.NODE_ENV !== "production" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
57
+ const development = env.NODE_ENV !== "production" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
57
58
  if (
58
59
  (url.protocol !== "https:" && !(url.protocol === "http:" && development)) ||
59
60
  url.username ||
@@ -1,5 +1,6 @@
1
1
  import { crypto } from "@k2b/stdlib";
2
2
  import { getProcessApplicationId } from "../../_internal/process-identity";
3
+ import { env } from "../../config/env";
3
4
 
4
5
  const KEY_PATTERN = /^[0-9a-f]{64}$/i;
5
6
 
@@ -27,9 +28,9 @@ export const readIdentityKeyEncryptionConfig = async (): Promise<IdentityKeyEncr
27
28
  if (getProcessApplicationId() !== "core") {
28
29
  throw new Error("Private Cloud identity keys may only be loaded by the Core application");
29
30
  }
30
- const current = parseKey("CLOUD_IDENTITY_KEY_ENCRYPTION_KEY", process.env.CLOUD_IDENTITY_KEY_ENCRYPTION_KEY, true)!;
31
- const previous = parseKey("CLOUD_IDENTITY_PREVIOUS_KEY", process.env.CLOUD_IDENTITY_PREVIOUS_KEY, false);
32
- const next = parseKey("CLOUD_IDENTITY_NEXT_KEY", process.env.CLOUD_IDENTITY_NEXT_KEY, false);
31
+ const current = parseKey("CLOUD_IDENTITY_KEY_ENCRYPTION_KEY", env.CLOUD_IDENTITY_KEY_ENCRYPTION_KEY, true)!;
32
+ const previous = parseKey("CLOUD_IDENTITY_PREVIOUS_KEY", env.CLOUD_IDENTITY_PREVIOUS_KEY, false);
33
+ const next = parseKey("CLOUD_IDENTITY_NEXT_KEY", env.CLOUD_IDENTITY_NEXT_KEY, false);
33
34
  const currentIdentified = await identify(current);
34
35
  const previousIdentified = previous ? await identify(previous) : null;
35
36
  const nextIdentified = next ? await identify(next) : null;
@@ -37,6 +38,7 @@ export const readIdentityKeyEncryptionConfig = async (): Promise<IdentityKeyEncr
37
38
  if (new Set(ids).size !== ids.length) {
38
39
  throw new Error("Configured Cloud identity KEKs must be distinct");
39
40
  }
40
- if (previousIdentified && nextIdentified) throw new Error("Configure either CLOUD_IDENTITY_PREVIOUS_KEY or CLOUD_IDENTITY_NEXT_KEY, not both");
41
+ if (previousIdentified && nextIdentified)
42
+ throw new Error("Configure either CLOUD_IDENTITY_PREVIOUS_KEY or CLOUD_IDENTITY_NEXT_KEY, not both");
41
43
  return { current: currentIdentified, previous: previousIdentified, next: nextIdentified };
42
44
  };
@@ -1,3 +1,4 @@
1
+ import { env } from "../../config/env";
1
2
  import { publicCloudOrigin } from "../../shared/app-url";
2
3
  import { get as getSetting } from "../settings";
3
4
  import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_OAUTH_JWKS_PATH, CLOUD_SESSION_JWKS_PATH } from "./constants";
@@ -28,7 +29,7 @@ const normalizeGroups = (value: unknown): string[] => {
28
29
  return groups.length > 0 ? groups : ["admins"];
29
30
  };
30
31
 
31
- const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL => {
32
+ const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL => {
32
33
  if (!transportOrigin) return new URL(path, issuer);
33
34
  const origin = new URL(transportOrigin);
34
35
  if (origin.protocol !== "http:" && origin.protocol !== "https:") {
@@ -37,13 +38,13 @@ const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = proc
37
38
  return new URL(path, origin);
38
39
  };
39
40
 
40
- export const resolveSessionJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
41
+ export const resolveSessionJwksUrl = (issuer: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL =>
41
42
  resolveCoreJwksUrl(issuer, CLOUD_SESSION_JWKS_PATH, transportOrigin);
42
43
 
43
- export const resolveInvocationJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
44
+ export const resolveInvocationJwksUrl = (issuer: string, transportOrigin = env.CLOUD_IDENTITY_JWKS_ORIGIN): URL =>
44
45
  resolveCoreJwksUrl(issuer, CLOUD_INVOCATION_JWKS_PATH, transportOrigin);
45
46
 
46
- export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_OAUTH_JWKS_ORIGIN?.trim()): URL => {
47
+ export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = env.CLOUD_OAUTH_JWKS_ORIGIN): URL => {
47
48
  if (!transportOrigin) return new URL(CLOUD_OAUTH_JWKS_PATH, issuer);
48
49
 
49
50
  const url = new URL(CLOUD_OAUTH_JWKS_PATH, transportOrigin);
@@ -56,7 +57,7 @@ export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = process.en
56
57
  const load = async (): Promise<IdentityRuntimeConfig> => {
57
58
  const [appUrl, rawGroupsAdmin] = await Promise.all([getSetting<string>("app.url"), getSetting<unknown>("freeipa.groups.admin")]);
58
59
  const issuer = publicCloudOrigin(appUrl);
59
- if (process.env.NODE_ENV === "production" && new URL(issuer).protocol !== "https:") {
60
+ if (env.NODE_ENV === "production" && new URL(issuer).protocol !== "https:") {
60
61
  throw new Error("Cloud identity issuer must use HTTPS in production");
61
62
  }
62
63
  return {
@@ -1,4 +1,4 @@
1
- import { sql } from "bun";
1
+ import type { sql } from "bun";
2
2
 
3
3
  /** Mirror directory-owned attributes, including missing values; never apply local defaults. */
4
4
  export const mirrorIpaPosix = async (
@@ -6,9 +6,9 @@ import { applyIpaAccountTransitionPolicy } from "../accounts/switching";
6
6
  import { getFreeIpaConfig } from "../freeipa-config";
7
7
  import { logger } from "../logging";
8
8
  import { session } from "../session";
9
- import { mirrorIpaPosix } from "./posix";
10
9
  import * as settings from "../settings";
11
10
  import { buildEffectiveIpaGroupsByUid } from "./effective-groups";
11
+ import { mirrorIpaPosix } from "./posix";
12
12
  import { calculateIpaProfileFromEffectiveProjection, getEffectiveUserGroups } from "./profile";
13
13
  import { createIpaRuntimeCheckpoint, type IpaSyncRuntime } from "./runtime";
14
14
  import { assessIpaDestructionGuard, formatIpaDestructionGuardFailure, readCompleteIpaList, selectStaleLocalIpaRows } from "./sync-planning";
@@ -1,10 +1,10 @@
1
- import { readWebVitals } from "./web-vitals";
2
1
  import { sql } from "bun";
3
2
  import type { PaginationParams } from "../../contracts/shared";
4
3
  import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
5
4
  import { registerSettings } from "../settings/defaults";
6
5
  import { redactMetadata } from "./redaction";
7
6
  import { observeSyncEvent, TRACE_STUCK_AFTER_MS, trace } from "./trace";
7
+ import { readWebVitals } from "./web-vitals";
8
8
 
9
9
  export type {
10
10
  TraceAttributes,
@@ -1,7 +1,7 @@
1
- import { getProcessApplicationId } from "../../_internal/process-identity";
2
1
  import { createHash, randomBytes } from "node:crypto";
3
2
  import type { SyncEvent } from "@k2b/sync";
4
3
  import { sql } from "bun";
4
+ import { getProcessApplicationId } from "../../_internal/process-identity";
5
5
  import type { PaginationParams } from "../../contracts/shared";
6
6
  import { escapeLikePattern, parsePgJsonRecord, toPgTextArray } from "../postgres";
7
7
  import { isSensitiveMetadataKey, REDACTED, redactMetadata } from "./redaction";
@@ -9,9 +9,9 @@ import { parsePgJsonValue } from "../postgres";
9
9
  import { isMandatePolicyNarrowing, type MandatePolicyV1, MandatePolicyV1Schema, mandatePolicyAllows, parseMandatePolicy } from "./policy";
10
10
 
11
11
  export {
12
- CapabilityGrantsSchema,
13
- CapabilityGrantSchema,
14
12
  type CapabilityGrant,
13
+ CapabilityGrantSchema,
14
+ CapabilityGrantsSchema,
15
15
  capabilityGrantAllows,
16
16
  isMandatePolicyNarrowing,
17
17
  MANDATE_POLICY_MAX_IDENTIFIERS,
@@ -20,6 +20,9 @@ export {
20
20
  MandatePolicyV1Schema,
21
21
  mandatePolicyAllows,
22
22
  parseMandatePolicy,
23
+ type TaskGrant,
24
+ TaskGrantSchema,
25
+ TaskGrantsSchema,
23
26
  } from "./policy";
24
27
 
25
28
  export type MandateState = "active" | "paused" | "revoked";