@k2b/cloud 0.7.0 → 0.9.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 (190) hide show
  1. package/package.json +5 -4
  2. package/scripts/README.md +10 -0
  3. package/scripts/build.ts +14 -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/schemas.ts +7 -1
  103. package/src/api/auth.ts +4 -3
  104. package/src/api/capabilities.ts +45 -13
  105. package/src/api/index.ts +5 -5
  106. package/src/api/mcp.ts +5 -12
  107. package/src/browser/CloudResourceSearch.tsx +1 -1
  108. package/src/browser/command-bridge.ts +1 -1
  109. package/src/browser/command-shortcuts.ts +1 -1
  110. package/src/browser/commands.ts +8 -7
  111. package/src/browser/testing.ts +2 -1
  112. package/src/capabilities/client.ts +1 -1
  113. package/src/capabilities/command-link.ts +1 -1
  114. package/src/capabilities/server.ts +2 -1
  115. package/src/cli/admin/ai-quotas.ts +5 -5
  116. package/src/cli/admin/app-credentials.ts +7 -9
  117. package/src/cli/admin/index.ts +3 -3
  118. package/src/cli/admin/instance.ts +2 -2
  119. package/src/cli/admin/linux.ts +4 -2
  120. package/src/cli/admin/sync.ts +1 -1
  121. package/src/cli/capabilities.ts +42 -29
  122. package/src/config/define-env.ts +97 -0
  123. package/src/config/env.ts +147 -54
  124. package/src/config/index.ts +3 -1
  125. package/src/contracts/capabilities.ts +8 -1
  126. package/src/contracts/index.ts +5 -7
  127. package/src/contracts/registry.ts +2 -0
  128. package/src/contracts/shared.ts +12 -3
  129. package/src/desktop/index.ts +1 -0
  130. package/src/desktop/solid.tsx +1 -1
  131. package/src/index.ts +8 -2
  132. package/src/server/app-assets.ts +27 -0
  133. package/src/server/index.ts +3 -2
  134. package/src/server/ratelimit.ts +1 -1
  135. package/src/server/services/access-revision.ts +3 -3
  136. package/src/server/services/access.ts +3 -1
  137. package/src/services/accounts/notification-sender.ts +12 -2
  138. package/src/services/announcements/index.ts +4 -4
  139. package/src/services/app-approval.ts +11 -10
  140. package/src/services/auth-flows/magic-link.ts +13 -1
  141. package/src/services/identity/key-config.ts +6 -4
  142. package/src/services/identity/runtime-config.ts +6 -5
  143. package/src/services/ipa/posix.ts +1 -1
  144. package/src/services/ipa/sync.ts +1 -1
  145. package/src/services/logging/index.ts +1 -1
  146. package/src/services/logging/trace.ts +1 -1
  147. package/src/services/mandates/index.ts +5 -2
  148. package/src/services/mandates/policy.ts +106 -6
  149. package/src/services/notifications/batches.ts +1 -0
  150. package/src/services/pdf/gotenberg.ts +50 -14
  151. package/src/services/pdf/index.ts +11 -11
  152. package/src/services/public-http.ts +1 -1
  153. package/src/services/rail-shortcuts.ts +1 -1
  154. package/src/services/rail-snapshot.ts +1 -1
  155. package/src/services/request-cache-redis.ts +2 -1
  156. package/src/services/session/user.ts +1 -1
  157. package/src/services/settings/app.ts +5 -1
  158. package/src/services/settings/core-settings.ts +6 -8
  159. package/src/services/settings/index.ts +3 -3
  160. package/src/services/settings/store.ts +3 -3
  161. package/src/services/webauthn.ts +1 -1
  162. package/src/shared/index.ts +3 -6
  163. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  164. package/src/ssr/AdminSidebar.tsx +2 -2
  165. package/src/ssr/AppLaunchpad.island.tsx +12 -2
  166. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  167. package/src/ssr/GlobalSearchDialog.tsx +6 -6
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
  169. package/src/ssr/Layout.tsx +4 -3
  170. package/src/ssr/LayoutHeader.tsx +1 -1
  171. package/src/ssr/LayoutHelp.tsx +4 -6
  172. package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
  173. package/src/ssr/LayoutRail.tsx +3 -2
  174. package/src/ssr/MinimalLayout.tsx +1 -1
  175. package/src/ssr/MobileNavigation.tsx +2 -2
  176. package/src/ssr/ProfilePreferences.island.tsx +1 -1
  177. package/src/ssr/RailApps.island.tsx +7 -7
  178. package/src/ssr/admin-navigation.ts +6 -2
  179. package/src/ssr/index.ts +1 -0
  180. package/src/ssr/islands/index.ts +2 -2
  181. package/src/ssr/mobile-menu-history.ts +1 -1
  182. package/src/ssr/rail-navigation.ts +3 -2
  183. package/src/ssr/request-path.ts +12 -0
  184. package/src/workflows/ai/index.ts +2 -2
  185. package/src/workflows/ai/runtime.ts +16 -91
  186. package/src/workflows/ai/store.ts +1 -1
  187. package/src/workflows/ai/types.ts +6 -82
  188. package/src/workflows/store/index.ts +2 -2
  189. package/src/server/services/freeipa/test-certificates.ts +0 -44
  190. package/src/services/session/test-fixture.ts +0 -18
@@ -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";
@@ -12,6 +12,8 @@ const identifierList = <T extends z.ZodType<string>>(schema: T) =>
12
12
 
13
13
  const operationSchema = z.union([
14
14
  z.literal("search.query"),
15
+ z.literal("runtime.http"),
16
+ z.literal("runtime.database"),
15
17
  z.string().refine((value) => {
16
18
  const [prefix, localId, extra] = value.split(":");
17
19
  return (
@@ -36,6 +38,79 @@ export const CapabilityGrantSchema = z
36
38
  export const CapabilityGrantsSchema = z.array(CapabilityGrantSchema).max(MANDATE_POLICY_MAX_IDENTIFIERS / 2);
37
39
  export type CapabilityGrant = z.output<typeof CapabilityGrantSchema>;
38
40
 
41
+ const httpsUrl = z
42
+ .string()
43
+ .url()
44
+ .max(2000)
45
+ .refine((value) => {
46
+ const url = new URL(value);
47
+ return url.protocol === "https:" && !url.username && !url.password && !url.hash;
48
+ }, "Use HTTPS without credentials or fragment");
49
+ export const TaskGrantSchema = z.union([
50
+ CapabilityGrantSchema,
51
+ z
52
+ .object({
53
+ kind: z.literal("http").describe("Authorize scheduled Code Mode HTTP requests."),
54
+ fixedInput: z
55
+ .object({
56
+ origin: httpsUrl
57
+ .refine((value) => new URL(value).origin === value, "Use an exact origin without path")
58
+ .optional()
59
+ .describe("Exact HTTPS origin, without path, wildcard or trailing slash."),
60
+ url: httpsUrl.optional().describe("Exact full URL, including query parameters."),
61
+ method: z
62
+ .enum(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
63
+ .optional()
64
+ .describe("Exact HTTP method; omit to permit every supported method."),
65
+ })
66
+ .strict()
67
+ .default({})
68
+ .describe("Fixed input values. Omitted fields are unrestricted; empty input permits every supported target and operation."),
69
+ })
70
+ .strict(),
71
+ z
72
+ .object({
73
+ kind: z.literal("database").describe("Authorize scheduled Code Mode RSQL operations."),
74
+ fixedInput: z
75
+ .object({
76
+ resourceId: z.string().min(1).max(80).optional().describe("Exact Studio app ID whose database may be used."),
77
+ operation: z
78
+ .enum([
79
+ "connect",
80
+ "status",
81
+ "export",
82
+ "clear",
83
+ "reset",
84
+ "tables.list",
85
+ "tables.create",
86
+ "tables.update",
87
+ "tables.delete",
88
+ "schema.get",
89
+ "rows.list",
90
+ "rows.get",
91
+ "rows.insert",
92
+ "rows.update",
93
+ "rows.delete",
94
+ "query",
95
+ ])
96
+ .optional()
97
+ .describe("Exact database operation; connect needs its own grant when operation is fixed."),
98
+ table: z
99
+ .string()
100
+ .min(1)
101
+ .max(63)
102
+ .optional()
103
+ .describe("Exact table; applies to row and schema operations, not arbitrary SQL queries."),
104
+ })
105
+ .strict()
106
+ .default({})
107
+ .describe("Fixed input values. Omitted fields are unrestricted; empty input permits every supported target and operation."),
108
+ })
109
+ .strict(),
110
+ ]);
111
+ export const TaskGrantsSchema = z.array(TaskGrantSchema).max(MANDATE_POLICY_MAX_IDENTIFIERS / 2);
112
+ export type TaskGrant = z.output<typeof TaskGrantSchema>;
113
+
39
114
  const equalJson = (a: unknown, b: unknown): boolean => {
40
115
  if (a === b) return true;
41
116
  if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
@@ -66,7 +141,7 @@ export const MandatePolicyV1Schema = z
66
141
  apps: z.union([z.literal("*"), identifierList(CapabilityAppIdSchema)]),
67
142
  operations: z.union([z.literal("*"), identifierList(operationSchema)]),
68
143
  actions: z.enum(["deny", "require_approval", "preapproved"]),
69
- grants: CapabilityGrantsSchema.optional(),
144
+ grants: TaskGrantsSchema.optional(),
70
145
  })
71
146
  .strict()
72
147
  .superRefine((policy, context) => {
@@ -107,11 +182,17 @@ export const isMandatePolicyNarrowing = (current: MandatePolicyV1, next: Mandate
107
182
  setAllows(current.apps, next.apps) &&
108
183
  setAllows(current.operations, next.operations) &&
109
184
  ACTION_AUTHORITY[next.actions] <= ACTION_AUTHORITY[current.actions] &&
110
- (current.grants === undefined ||
111
- (next.grants !== undefined &&
185
+ (current.grants === undefined
186
+ ? !next.grants?.some((grant) => grant.kind === "http" || grant.kind === "database")
187
+ : next.grants !== undefined &&
112
188
  next.grants.every((grant) =>
113
- current.grants!.some((existing) => capabilityGrantAllows(existing, { ...grant, input: grant.fixedInput })),
114
- )));
189
+ current.grants!.some((existing) => {
190
+ if (existing.kind !== grant.kind) return false;
191
+ if ("appId" in existing && "appId" in grant && (existing.appId !== grant.appId || existing.capabilityId !== grant.capabilityId))
192
+ return false;
193
+ return Object.entries(existing.fixedInput).every(([key, value]) => equalJson(value, Reflect.get(grant.fixedInput, key)));
194
+ }),
195
+ ));
115
196
 
116
197
  export const mandatePolicyAllows = (
117
198
  policy: MandatePolicyV1,
@@ -125,6 +206,23 @@ export const mandatePolicyAllows = (
125
206
  ): boolean => {
126
207
  if (policy.apps !== "*" && !policy.apps.includes(input.appId)) return false;
127
208
  if (policy.operations !== "*" && !policy.operations.includes(input.operation)) return false;
209
+ if (input.operation === "runtime.http" || input.operation === "runtime.database") {
210
+ const kind = input.operation === "runtime.http" ? "http" : "database";
211
+ return (
212
+ input.appId === "assistant" &&
213
+ !!policy.grants?.some(
214
+ (grant) =>
215
+ grant.kind === kind &&
216
+ Object.entries(grant.fixedInput).every(
217
+ ([key, value]) =>
218
+ input.input !== null &&
219
+ typeof input.input === "object" &&
220
+ Object.hasOwn(input.input, key) &&
221
+ equalJson(value, Reflect.get(input.input, key)),
222
+ ),
223
+ )
224
+ );
225
+ }
128
226
  if (policy.grants !== undefined) {
129
227
  const [operation, capabilityId] = input.operation.split(":");
130
228
  const kind =
@@ -136,7 +234,9 @@ export const mandatePolicyAllows = (
136
234
  if (
137
235
  !kind ||
138
236
  !capabilityId ||
139
- !policy.grants.some((grant) => capabilityGrantAllows(grant, { appId: input.appId, capabilityId, kind, input: input.input }))
237
+ !policy.grants.some(
238
+ (grant) => "appId" in grant && capabilityGrantAllows(grant, { appId: input.appId, capabilityId, kind, input: input.input }),
239
+ )
140
240
  )
141
241
  return false;
142
242
  if (kind === "action" && input.capabilityApproval !== "none" && input.capabilityApproval !== "rememberable") return false;
@@ -786,4 +786,5 @@ export const __notificationBatchTest = {
786
786
  selectionHash,
787
787
  recipientHash,
788
788
  resolveCandidates,
789
+ processBatchChunk,
789
790
  };
@@ -126,8 +126,9 @@ const fileName = (name: string) => {
126
126
  };
127
127
 
128
128
  function htmlForm(input: RenderHtmlToPdfInput, config: GotenbergConfig): FormData {
129
- const total = [input.html, input.headerHtml ?? "", input.footerHtml ?? ""].reduce((sum, value) => sum + byteLength(value), 0)
130
- + (input.assets ?? []).reduce((sum, asset) => sum + asset.data.size, 0);
129
+ const total =
130
+ [input.html, input.headerHtml ?? "", input.footerHtml ?? ""].reduce((sum, value) => sum + byteLength(value), 0) +
131
+ (input.assets ?? []).reduce((sum, asset) => sum + asset.data.size, 0);
131
132
  if (total > config.maxHtmlBytes) throw new GotenbergRenderError("html_too_large", "HTML and assets exceed the configured input budget.");
132
133
  const form = new FormData();
133
134
  form.append("files", new Blob([input.html], { type: "text/html" }), "index.html");
@@ -136,7 +137,8 @@ function htmlForm(input: RenderHtmlToPdfInput, config: GotenbergConfig): FormDat
136
137
  const names = new Set(["index.html", "header.html", "footer.html", "factur-x.xml"]);
137
138
  for (const asset of input.assets ?? []) {
138
139
  const name = fileName(asset.name);
139
- if (names.has(name.toLowerCase())) throw new GotenbergRenderError("bad_input", "Asset filenames must be unique and cannot replace HTML documents.");
140
+ if (names.has(name.toLowerCase()))
141
+ throw new GotenbergRenderError("bad_input", "Asset filenames must be unique and cannot replace HTML documents.");
140
142
  names.add(name.toLowerCase());
141
143
  form.append("files", asset.data, name);
142
144
  }
@@ -151,7 +153,8 @@ function htmlForm(input: RenderHtmlToPdfInput, config: GotenbergConfig): FormDat
151
153
  form.append("landscape", String(input.page.landscape ?? false));
152
154
  for (const side of ["top", "right", "bottom", "left"] as const) {
153
155
  const value = input.page.margin?.[side] ?? 15;
154
- if (!Number.isFinite(value) || value < 0) throw new GotenbergRenderError("bad_input", "Page margins must be nonnegative millimeters.");
156
+ if (!Number.isFinite(value) || value < 0)
157
+ throw new GotenbergRenderError("bad_input", "Page margins must be nonnegative millimeters.");
155
158
  form.append(`margin${side[0]!.toUpperCase()}${side.slice(1)}`, String(value / 25.4));
156
159
  }
157
160
  }
@@ -159,14 +162,24 @@ function htmlForm(input: RenderHtmlToPdfInput, config: GotenbergConfig): FormDat
159
162
  }
160
163
 
161
164
  /** Bounded transport shared by HTML conversion and attachment embedding. */
162
- async function requestPdf(route: string, form: FormData, config: GotenbergConfig, options: RenderHtmlToPdfOptions): Promise<RenderHtmlToPdfResult> {
165
+ async function requestPdf(
166
+ route: string,
167
+ form: FormData,
168
+ config: GotenbergConfig,
169
+ options: RenderHtmlToPdfOptions,
170
+ ): Promise<RenderHtmlToPdfResult> {
163
171
  const headers = new Headers();
164
172
  const auth = basicAuthHeader(config);
165
173
  if (auth) headers.set("Authorization", auth);
166
174
  const signal = options.signal ? AbortSignal.any([options.signal, abortSignal(config.timeoutMs)]) : abortSignal(config.timeoutMs);
167
175
  let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
168
176
  try {
169
- const response = await (options.fetch ?? fetch)(`${normalizeBaseUrl(config.url)}${route}`, { method: "POST", headers, body: form, signal });
177
+ const response = await (options.fetch ?? fetch)(`${normalizeBaseUrl(config.url)}${route}`, {
178
+ method: "POST",
179
+ headers,
180
+ body: form,
181
+ signal,
182
+ });
170
183
  if (!response.ok) {
171
184
  await response.body?.cancel();
172
185
  throw new GotenbergRenderError("bad_response", `Gotenberg returned HTTP ${response.status}.`, response.status);
@@ -190,20 +203,36 @@ async function requestPdf(route: string, form: FormData, config: GotenbergConfig
190
203
  }
191
204
  const pdf = new Uint8Array(size);
192
205
  let offset = 0;
193
- for (const chunk of chunks) { pdf.set(chunk, offset); offset += chunk.byteLength; }
206
+ for (const chunk of chunks) {
207
+ pdf.set(chunk, offset);
208
+ offset += chunk.byteLength;
209
+ }
194
210
  return { pdf, contentType };
195
211
  } catch (error) {
196
212
  await reader?.cancel().catch(() => {});
197
213
  throw sanitizeFetchError(error);
198
- } finally { reader?.releaseLock(); }
214
+ } finally {
215
+ reader?.releaseLock();
216
+ }
199
217
  }
200
218
 
201
- export const renderHtmlToPdfWithConfig = async (input: RenderHtmlToPdfInput, config: GotenbergConfig, options: RenderHtmlToPdfOptions = {}): Promise<RenderHtmlToPdfResult> =>
202
- requestPdf("/forms/chromium/convert/html", htmlForm(input, config), config, options);
219
+ export const renderHtmlToPdfWithConfig = async (
220
+ input: RenderHtmlToPdfInput,
221
+ config: GotenbergConfig,
222
+ options: RenderHtmlToPdfOptions = {},
223
+ ): Promise<RenderHtmlToPdfResult> => requestPdf("/forms/chromium/convert/html", htmlForm(input, config), config, options);
203
224
 
204
- export const renderFacturXHtmlToPdfWithConfig = async (input: RenderFacturXHtmlToPdfInput, config: GotenbergConfig, options: RenderHtmlToPdfOptions = {}): Promise<RenderHtmlToPdfResult> => {
225
+ export const renderFacturXHtmlToPdfWithConfig = async (
226
+ input: RenderFacturXHtmlToPdfInput,
227
+ config: GotenbergConfig,
228
+ options: RenderHtmlToPdfOptions = {},
229
+ ): Promise<RenderHtmlToPdfResult> => {
205
230
  const xmlBytes = byteLength(input.xml);
206
- if (!xmlBytes || xmlBytes > config.maxHtmlBytes) throw new GotenbergRenderError(xmlBytes ? "html_too_large" : "bad_input", "Invoice XML is empty or exceeds the configured input budget.");
231
+ if (!xmlBytes || xmlBytes > config.maxHtmlBytes)
232
+ throw new GotenbergRenderError(
233
+ xmlBytes ? "html_too_large" : "bad_input",
234
+ "Invoice XML is empty or exceeds the configured input budget.",
235
+ );
207
236
  const form = htmlForm(input, { ...config, maxHtmlBytes: config.maxHtmlBytes - xmlBytes });
208
237
  form.append("facturxXml", new Blob([input.xml], { type: "application/xml" }), "factur-x.xml");
209
238
  form.append("facturxConformanceLevel", input.conformanceLevel ?? "EN 16931");
@@ -213,7 +242,11 @@ export const renderFacturXHtmlToPdfWithConfig = async (input: RenderFacturXHtmlT
213
242
  return requestPdf("/forms/chromium/convert/html", form, config, options);
214
243
  };
215
244
 
216
- export const attachPdfFilesWithConfig = async (input: AttachPdfFilesInput, config: GotenbergConfig, options: RenderHtmlToPdfOptions = {}): Promise<RenderHtmlToPdfResult> => {
245
+ export const attachPdfFilesWithConfig = async (
246
+ input: AttachPdfFilesInput,
247
+ config: GotenbergConfig,
248
+ options: RenderHtmlToPdfOptions = {},
249
+ ): Promise<RenderHtmlToPdfResult> => {
217
250
  if (!input.attachments.length) throw new GotenbergRenderError("bad_input", "At least one attachment is required.");
218
251
  if (input.document.size + input.attachments.reduce((sum, file) => sum + file.data.size, 0) > config.maxPdfBytes)
219
252
  throw new GotenbergRenderError("pdf_too_large", "PDF and attachments exceed the configured input budget.");
@@ -226,7 +259,10 @@ export const attachPdfFilesWithConfig = async (input: AttachPdfFilesInput, confi
226
259
  if (names.has(name.toLowerCase())) throw new GotenbergRenderError("bad_input", "Attachment filenames must be unique.");
227
260
  names.add(name.toLowerCase());
228
261
  form.append("embeds", attachment.data, name);
229
- metadata[name] = { mimeType: attachment.data.type || "application/octet-stream", relationship: attachment.relationship ?? "Unspecified" };
262
+ metadata[name] = {
263
+ mimeType: attachment.data.type || "application/octet-stream",
264
+ relationship: attachment.relationship ?? "Unspecified",
265
+ };
230
266
  }
231
267
  form.append("embedsMetadata", JSON.stringify(metadata));
232
268
  return requestPdf("/forms/pdfengines/embed", form, config, options);
@@ -1,6 +1,6 @@
1
1
  export type {
2
- GotenbergConfig,
3
2
  AttachPdfFilesInput,
3
+ GotenbergConfig,
4
4
  GotenbergRenderErrorCode,
5
5
  MergePdfsInput,
6
6
  RenderFacturXHtmlToPdfInput,
@@ -9,10 +9,10 @@ export type {
9
9
  RenderHtmlToPdfResult,
10
10
  } from "./gotenberg";
11
11
  export {
12
- GotenbergRenderError,
13
- getGotenbergConfig,
14
12
  attachPdfFiles,
15
13
  attachPdfFilesWithConfig,
14
+ GotenbergRenderError,
15
+ getGotenbergConfig,
16
16
  mergePdfs,
17
17
  mergePdfsWithConfig,
18
18
  renderFacturXHtmlToPdf,
@@ -21,14 +21,6 @@ export {
21
21
  renderHtmlToPdfWithConfig,
22
22
  testGotenberg,
23
23
  } from "./gotenberg";
24
- export type {
25
- RenderTemplatePdfPreviewInput,
26
- RenderTemplatePdfPreviewOptions,
27
- TemplatePdfPreviewError,
28
- TemplatePdfPreviewPhase,
29
- TemplatePdfPreviewResult,
30
- } from "./template-preview";
31
- export { renderTemplatePdfPreview } from "./template-preview";
32
24
  export type {
33
25
  MarkdownPdfErrorCode,
34
26
  MarkdownPdfTemplateId,
@@ -44,3 +36,11 @@ export {
44
36
  renderMarkdownToPdf,
45
37
  renderMarkdownToPdfWithConfig,
46
38
  } from "./markdown";
39
+ export type {
40
+ RenderTemplatePdfPreviewInput,
41
+ RenderTemplatePdfPreviewOptions,
42
+ TemplatePdfPreviewError,
43
+ TemplatePdfPreviewPhase,
44
+ TemplatePdfPreviewResult,
45
+ } from "./template-preview";
46
+ export { renderTemplatePdfPreview } from "./template-preview";
@@ -1,5 +1,5 @@
1
- import { request, type RequestOptions } from "node:https";
2
1
  import type { ClientRequest, IncomingMessage } from "node:http";
2
+ import { type RequestOptions, request } from "node:https";
3
3
  import { resolvePublicNetworkAddresses } from "./network-security";
4
4
 
5
5
  export type PublicHttpInput = {
@@ -1,5 +1,5 @@
1
1
  import { type SQL, sql } from "bun";
2
- import { RailAdminSchema, RailAdminInputSchema, type RailAdminInput, type RailAdminState } from "../contracts/rail-admin";
2
+ import { type RailAdminInput, RailAdminInputSchema, RailAdminSchema, type RailAdminState } from "../contracts/rail-admin";
3
3
  import { RailShortcutSchema } from "../contracts/rail-preferences";
4
4
  import { hasRole, type User } from "../contracts/shared";
5
5
  import { buildAccessPrincipalCondition, createAccess } from "../server/services/access";
@@ -1,8 +1,8 @@
1
- import { requestCacheRedis } from "./request-cache-redis";
2
1
  import { type RailSnapshot, RailSnapshotSchema } from "../contracts/rail-preferences";
3
2
  import type { User } from "../contracts/shared";
4
3
  import { railPreferences } from "./rail-preferences";
5
4
  import { railShortcuts } from "./rail-shortcuts";
5
+ import { requestCacheRedis } from "./request-cache-redis";
6
6
 
7
7
  // Private request metadata: never serialized into the public User contract.
8
8
  const versions = new WeakMap<User, string>();
@@ -1,4 +1,5 @@
1
1
  import { RedisClient } from "bun";
2
+ import { env } from "../config/env";
2
3
 
3
4
  // Cache commands must not queue across a disconnect: delayed fills/invalidations
4
5
  // are not useful, and reads must be able to fall back to Postgres. Keep this
@@ -9,7 +10,7 @@ let reconnecting: Promise<void> | undefined;
9
10
 
10
11
  export const requestCacheRedis = async (): Promise<RedisClient> => {
11
12
  if (!client) {
12
- client = new RedisClient(process.env.VALKEY_URL || process.env.REDIS_URL, { enableOfflineQueue: false });
13
+ client = new RedisClient(env.REDIS_URL, { enableOfflineQueue: false });
13
14
  // The first operation waits for normal connection establishment (Bun's
14
15
  // connection timeout applies). Later disconnected operations fail promptly.
15
16
  initialConnection = client.connect().catch(() => {});
@@ -6,8 +6,8 @@ import { resolveProviderProfile } from "../accounts/base-user";
6
6
  import { managedGroupIdsSubquery } from "../accounts/group-sql";
7
7
  import { buildIpaUserData, emptyIpaUserData, userIpaDataColumns, userIpaDataJoin } from "../accounts/ipa-data";
8
8
  import { resolveAccountExpires } from "../accounts/model";
9
- import { setRailCacheVersion } from "../rail-snapshot";
10
9
  import { toPgTextArray } from "../postgres";
10
+ import { setRailCacheVersion } from "../rail-snapshot";
11
11
 
12
12
  type DbRow = Record<string, unknown>;
13
13
 
@@ -30,7 +30,11 @@ const redactSecretValue = (entry: SettingEntry): SettingEntry => {
30
30
 
31
31
  export const settingsService = {
32
32
  entry: {
33
- list: async (config?: { pagination?: PageParams; filter?: { query?: string; group?: string }; locale?: string }): Promise<Paginated<SettingEntry>> => {
33
+ list: async (config?: {
34
+ pagination?: PageParams;
35
+ filter?: { query?: string; group?: string };
36
+ locale?: string;
37
+ }): Promise<Paginated<SettingEntry>> => {
34
38
  const entries = await settingsPrimitives.getAll(config?.locale);
35
39
  const query = config?.filter?.query?.trim().toLowerCase();
36
40
  const group = config?.filter?.group?.trim().toLowerCase();
@@ -1,3 +1,5 @@
1
+ import { env } from "../../config/env";
2
+
1
3
  /**
2
4
  * Platform-owned settings, declared in the `defineApp({ settings: ... })` shape.
3
5
  *
@@ -14,11 +16,6 @@
14
16
  * - keep `description`, `label`, `placeholder`, `default`, `kind` complete
15
17
  */
16
18
 
17
- const envString = (key: string): string | undefined => {
18
- const value = process.env[key]?.trim();
19
- return value && value.length > 0 ? value : undefined;
20
- };
21
-
22
19
  const IPA_MATCH_MODE_OPTIONS = [
23
20
  { value: "ignore", label: "Ignore local match" },
24
21
  { value: "migrate", label: "Migrate matching local account" },
@@ -124,8 +121,8 @@ export const CORE_SETTINGS = {
124
121
  description:
125
122
  "Public-facing application URL used for links in emails, OAuth redirects, and WebSocket connections (with or without scheme)",
126
123
  placeholder: "e.g. https://cloud.example.org",
127
- envFallback: () => envString("APP_URL"),
128
- envBootstrap: () => envString("APP_URL"),
124
+ envFallback: () => env.APP_URL,
125
+ envBootstrap: () => env.APP_URL,
129
126
  },
130
127
  "app.home_path": {
131
128
  kind: "string",
@@ -294,7 +291,8 @@ export const CORE_SETTINGS = {
294
291
  integer: true,
295
292
  default: 30,
296
293
  min: 0,
297
- description: "Maximum running time per AI turn. Zero or an empty field disables this limit. Existing turns keep their original budget; request and tool timeouts still apply.",
294
+ description:
295
+ "Maximum running time per AI turn. Zero or an empty field disables this limit. Existing turns keep their original budget; request and tool timeouts still apply.",
298
296
  },
299
297
  "ai.max_tool_result_chars": {
300
298
  kind: "number",
@@ -11,8 +11,8 @@
11
11
 
12
12
  import { redis, sql } from "bun";
13
13
  import { decryptValue, encryptValue, getAppSecret } from "./crypto";
14
- import { SETTINGS, SETTINGS_MAP, resolveSettingPresentation, type SettingDef, validateSettingValue } from "./defaults";
15
- import { bulkRead, deleteKey, invalidateSettingsCache, readKey, writeKey } from "./store";
14
+ import { resolveSettingPresentation, SETTINGS, SETTINGS_MAP, type SettingDef, validateSettingValue } from "./defaults";
15
+ import { bulkRead, deleteKey, readKey, writeKey } from "./store";
16
16
 
17
17
  type SqlClient = typeof sql;
18
18
 
@@ -175,8 +175,8 @@ export async function remove(key: string, db?: SqlClient): Promise<void> {
175
175
 
176
176
  import type { SettingEntry } from "../../contracts/shared";
177
177
 
178
- export { invalidateSettingsCache } from "./store";
179
178
  export type { SettingEntry } from "../../contracts/shared";
179
+ export { invalidateSettingsCache } from "./store";
180
180
 
181
181
  export async function getAll(locale?: string): Promise<SettingEntry[]> {
182
182
  // Determine which keys have a custom row in Postgres (vs. falling back to
@@ -13,12 +13,12 @@
13
13
  * snapshot exposed via `c.get("settings")` (built by snapshot.ts middleware).
14
14
  */
15
15
 
16
+ import { sql } from "bun";
16
17
  import { HTTPException } from "hono/http-exception";
17
18
  import { hasRole, type User } from "../../contracts/shared";
18
- import { sql } from "bun";
19
- import { requestCacheRedis } from "../request-cache-redis";
20
- import { toPgTextArray } from "../postgres";
21
19
  import { claimCacheFill, completeCacheFill, MISSING_SETTING } from "../cache-fill";
20
+ import { toPgTextArray } from "../postgres";
21
+ import { requestCacheRedis } from "../request-cache-redis";
22
22
  import { decryptValue, encryptValue } from "./crypto";
23
23
  import { SETTINGS, SETTINGS_MAP, type SettingDef, validateSettingValue } from "./defaults";
24
24
 
@@ -1,3 +1,4 @@
1
+ import { err, fail, ok, type Result } from "@k2b/stdlib";
1
2
  import {
2
3
  type AuthenticationResponseJSON,
3
4
  type AuthenticatorTransportFuture,
@@ -10,7 +11,6 @@ import {
10
11
  verifyRegistrationResponse,
11
12
  type WebAuthnCredential,
12
13
  } from "@simplewebauthn/server";
13
- import { err, fail, ok, type Result } from "@k2b/stdlib";
14
14
  import { redis, sql } from "bun";
15
15
  import type { User, WebAuthnPasskey } from "../contracts/shared";
16
16
  import { accounts } from "./accounts";
@@ -11,8 +11,11 @@ export {
11
11
  } from "./account-action-notice";
12
12
  export * from "./account-display";
13
13
  export * from "./account-session";
14
+ export * from "./ai-costs";
14
15
  export type { AiPromptContextInput, AiToolPromptHint } from "./ai-platform-prompt";
15
16
  export { AI_PLATFORM_PROMPT_TEMPLATE, aiPromptContext, renderAiPlatformPrompt } from "./ai-platform-prompt";
17
+ export type { AiChatQuotaSnapshot } from "./ai-quotas";
18
+ export * from "./ai-quotas";
16
19
  export {
17
20
  AI_USAGE_RANGES,
18
21
  AI_USAGE_REASONS,
@@ -52,9 +55,3 @@ export {
52
55
  } from "./template-rendering";
53
56
  export * from "./theme";
54
57
  export * from "./time";
55
-
56
- export * from "./ai-quotas";
57
-
58
- export type { AiChatQuotaSnapshot } from "./ai-quotas";
59
-
60
- export * from "./ai-costs";
@@ -13,8 +13,8 @@
13
13
  * Supported types: note, info, success, warning, danger
14
14
  */
15
15
 
16
- import type { MarkedExtension, Tokens } from "marked";
17
16
  import { NOTICE_CARD_CLASSES, NOTICE_CARD_ICONS, type NoticeTone } from "@k2b/ui";
17
+ import type { MarkedExtension, Tokens } from "marked";
18
18
  import { escapeHtml } from "../shared";
19
19
 
20
20
  type BlockType = "note" | "info" | "success" | "warning" | "danger";
@@ -1,9 +1,9 @@
1
- import WorkspaceNavigation from "./WorkspaceNavigation.island";
2
1
  import { AppWorkspace, useLocale } from "@k2b/ui";
3
2
  import { activeAdminHref } from "./admin-active-link";
4
3
  import { type AdminLink, buildAdminGroups } from "./admin-navigation";
5
- import type { RuntimeContext } from "./runtime";
6
4
  import { platformMessages } from "./platform-messages";
5
+ import type { RuntimeContext } from "./runtime";
6
+ import WorkspaceNavigation from "./WorkspaceNavigation.island";
7
7
 
8
8
  const AdminNavigation = (props: { currentPath: string; groups: ReturnType<typeof buildAdminGroups> }) => {
9
9
  const activeHref = activeAdminHref(
@@ -1,6 +1,8 @@
1
- import { AppLaunchpadPanel, type AppLaunchpadApp, type AppLaunchpadLegalLink, type AppLaunchpadContext } from "./AppLaunchpadPanel";
1
+ import { type AppLaunchpadApp, type AppLaunchpadContext, type AppLaunchpadLegalLink, AppLaunchpadPanel } from "./AppLaunchpadPanel";
2
2
  import { openCloudMobileMenu } from "./MobileNavigation";
3
+
3
4
  export type { AppLaunchpadApp, AppLaunchpadLegalLink } from "./AppLaunchpadPanel";
5
+
4
6
  import { dialogCore, IconButton, Tooltip, useLocale } from "@k2b/ui";
5
7
  import { createEffect } from "solid-js";
6
8
  import { platformMessages } from "./platform-messages";
@@ -117,7 +119,15 @@ export function AppLaunchpad(props: AppLaunchpadContext & { variant?: "provider"
117
119
  return <AppLaunchpadProvider apps={props.apps} legalLinks={props.legalLinks} profile={props.profile} />;
118
120
  }
119
121
 
120
- return <AppLaunchpadButton apps={props.apps} legalLinks={props.legalLinks} profile={props.profile} variant={props.variant} label={props.label} />;
122
+ return (
123
+ <AppLaunchpadButton
124
+ apps={props.apps}
125
+ legalLinks={props.legalLinks}
126
+ profile={props.profile}
127
+ variant={props.variant}
128
+ label={props.label}
129
+ />
130
+ );
121
131
  }
122
132
 
123
133
  export default AppLaunchpad;
@@ -1,10 +1,10 @@
1
+ import { documentNavigate } from "@k2b/ssr/nav";
2
+ import { IconButton, ScrollArea, TextInput, useLocale } from "@k2b/ui";
3
+ import { createSignal, For, type JSX, Show } from "solid-js";
4
+ import { Dynamic } from "solid-js/web";
1
5
  import type { CloudTheme } from "../shared/theme";
2
6
  import { MobileProfileActions } from "./MobileProfileActions";
3
- import { Dynamic } from "solid-js/web";
4
7
  import { platformMessages } from "./platform-messages";
5
- import { IconButton, ScrollArea, TextInput, useLocale } from "@k2b/ui";
6
- import { createSignal, For, Show, type JSX } from "solid-js";
7
- import { documentNavigate } from "@k2b/ssr/nav";
8
8
  import { openRailEditor } from "./RailEditor";
9
9
  import { readRailContext } from "./rail-context";
10
10
  import { railMessages } from "./rail-messages";