@k2b/cloud 0.43.0 → 0.45.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 (308) hide show
  1. package/package.json +11 -2
  2. package/src/_internal/app-access-request.ts +13 -0
  3. package/src/_internal/app-access-resources.ts +141 -0
  4. package/src/_internal/app-event-catalog.ts +40 -0
  5. package/src/_internal/app-event-schemas.ts +127 -0
  6. package/src/_internal/application-package.ts +40 -0
  7. package/src/_internal/built-in-app-ids.json +27 -0
  8. package/src/_internal/capabilities.ts +158 -12
  9. package/src/_internal/capability-origin.ts +4 -0
  10. package/src/_internal/capability-streams.ts +7 -5
  11. package/src/_internal/define-app.ts +57 -6
  12. package/src/_internal/object-store-area.ts +9 -0
  13. package/src/_internal/object-store-config.ts +101 -0
  14. package/src/_internal/object-store-key.ts +14 -0
  15. package/src/_internal/process-identity.ts +7 -2
  16. package/src/_internal/registry-validation.ts +11 -1
  17. package/src/_internal/registry.ts +8 -1
  18. package/src/_internal/runtime-context.ts +8 -1
  19. package/src/access/AppAccessEditor.tsx +2 -0
  20. package/src/access/GroupCoverage.tsx +20 -5
  21. package/src/access/PermissionEditor.tsx +20 -5
  22. package/src/access/PrincipalAvatar.tsx +30 -21
  23. package/src/access/PrincipalPicker.tsx +14 -4
  24. package/src/access/ResourceApiKeys.tsx +3 -4
  25. package/src/access/app-access-messages.ts +6 -0
  26. package/src/access/messages.ts +4 -4
  27. package/src/account/EntitySearch.tsx +12 -0
  28. package/src/account/ProfilePictureDialog.tsx +699 -0
  29. package/src/account/profile-picture-messages.ts +269 -0
  30. package/src/account/ui.ts +7 -0
  31. package/src/ai/admin.ts +3 -0
  32. package/src/ai/app-frame.ts +12 -0
  33. package/src/ai/application-tasks.ts +101 -0
  34. package/src/ai/assistant-models.ts +126 -64
  35. package/src/ai/capabilities.ts +19 -4
  36. package/src/ai/chart-block.ts +63 -30
  37. package/src/ai/chat/chart-block.tsx +12 -3
  38. package/src/ai/chat/composer-adapter.ts +8 -7
  39. package/src/ai/chat/message-actions.tsx +1 -1
  40. package/src/ai/chat/message-utils.ts +2 -2
  41. package/src/ai/chat/presentation.tsx +22 -1
  42. package/src/ai/chat-quotas.ts +5 -4
  43. package/src/ai/chat-tasks.ts +2 -2
  44. package/src/ai/client/controller.ts +11 -10
  45. package/src/ai/client/projection.ts +3 -3
  46. package/src/ai/compaction.ts +3 -3
  47. package/src/ai/credentials.ts +13 -114
  48. package/src/ai/default-tool-contracts.ts +4 -3
  49. package/src/ai/dictation-runtime.ts +10 -6
  50. package/src/ai/enrich.ts +2 -2
  51. package/src/ai/executor.ts +40 -25
  52. package/src/ai/http.ts +17 -29
  53. package/src/ai/index.ts +12 -5
  54. package/src/ai/inference-calls.ts +5 -4
  55. package/src/ai/maintenance.ts +1 -1
  56. package/src/ai/memory-learning-runs.ts +5 -5
  57. package/src/ai/memory-learning.ts +3 -3
  58. package/src/ai/message-queue.ts +1 -2
  59. package/src/ai/migrate.ts +75 -42
  60. package/src/ai/model-access-migrate.ts +3 -17
  61. package/src/ai/model-access.ts +44 -85
  62. package/src/ai/model-card.tsx +134 -0
  63. package/src/ai/model-catalog.ts +359 -0
  64. package/src/ai/model-chooser-messages.ts +88 -0
  65. package/src/ai/model-chooser.tsx +577 -0
  66. package/src/ai/model-delete.ts +234 -0
  67. package/src/ai/model-fixture.ts +39 -0
  68. package/src/ai/model-icon.tsx +36 -0
  69. package/src/ai/model-overview.ts +53 -0
  70. package/src/ai/model-usage.ts +45 -0
  71. package/src/ai/openai-api-messages.ts +0 -4
  72. package/src/ai/openai-api.ts +26 -9
  73. package/src/ai/personal-agent.ts +1 -1
  74. package/src/ai/prefs.ts +12 -10
  75. package/src/ai/project-tool.ts +4 -4
  76. package/src/ai/projects-authorization.ts +53 -0
  77. package/src/ai/projects-routes.ts +58 -47
  78. package/src/ai/projects.ts +110 -88
  79. package/src/ai/protocol.ts +2 -2
  80. package/src/ai/provider-connections.ts +161 -0
  81. package/src/ai/provider-errors.ts +55 -6
  82. package/src/ai/provider-fetch.ts +4 -0
  83. package/src/ai/provider.ts +1 -1
  84. package/src/ai/quota-limits.ts +36 -0
  85. package/src/ai/quota-provider.ts +4 -2
  86. package/src/ai/quota-report.ts +7 -4
  87. package/src/ai/quotas.ts +19 -8
  88. package/src/ai/recent-resources.ts +9 -3
  89. package/src/ai/request-headers.ts +14 -87
  90. package/src/ai/resource-refs.ts +6 -2
  91. package/src/ai/routes.ts +47 -48
  92. package/src/ai/runtime.ts +50 -22
  93. package/src/ai/settings.ts +93 -580
  94. package/src/ai/skill-tool.ts +4 -4
  95. package/src/ai/skills-routes.ts +27 -26
  96. package/src/ai/skills.ts +120 -81
  97. package/src/ai/store.ts +45 -23
  98. package/src/ai/stream.ts +1 -1
  99. package/src/ai/structured-runs.ts +2 -2
  100. package/src/ai/structured.ts +21 -44
  101. package/src/ai/task-contracts.ts +25 -5
  102. package/src/ai/task-execution.ts +15 -6
  103. package/src/ai/task-model.ts +9 -0
  104. package/src/ai/transcription.ts +27 -22
  105. package/src/ai/types.ts +50 -62
  106. package/src/ai/ui.tsx +13 -0
  107. package/src/ai/usage.ts +8 -8
  108. package/src/ai/validate.ts +8 -7
  109. package/src/ai/view-block.ts +2 -2
  110. package/src/api/accounts-entities.ts +22 -3
  111. package/src/api/admin-ai-quotas.ts +34 -56
  112. package/src/api/admin-ai-skills.ts +4 -1
  113. package/src/api/admin-ai-usage.ts +1 -1
  114. package/src/api/admin-ai.ts +173 -0
  115. package/src/api/admin-core-settings.ts +68 -244
  116. package/src/api/app-approval.ts +3 -1
  117. package/src/api/app-event-delivery.ts +51 -0
  118. package/src/api/app-events.ts +123 -0
  119. package/src/api/app-resource-access.ts +48 -0
  120. package/src/api/auth/account-mismatch.ts +24 -0
  121. package/src/api/auth.ts +35 -5
  122. package/src/api/capabilities.ts +28 -4
  123. package/src/api/index.ts +7 -1
  124. package/src/api/mcp.ts +175 -2
  125. package/src/api/me.ts +6 -4
  126. package/src/api/pwa-phone.ts +75 -26
  127. package/src/api/pwa.ts +34 -8
  128. package/src/api/resource-links.ts +83 -6
  129. package/src/api/resource-resolve.ts +50 -0
  130. package/src/api/search/schemas.ts +3 -1
  131. package/src/api/search.ts +9 -7
  132. package/src/app-pwa/contracts.ts +4 -0
  133. package/src/app-pwa/core-query-error.ts +20 -0
  134. package/src/app-pwa/index.ts +1 -0
  135. package/src/app-pwa/service.ts +91 -0
  136. package/src/app-pwa/transport.ts +22 -0
  137. package/src/approvals/messages.ts +2 -2
  138. package/src/browser/cloud-id.ts +86 -0
  139. package/src/browser/resource-clipboard.ts +4 -0
  140. package/src/browser/resource-links.ts +52 -0
  141. package/src/browser/session.ts +39 -0
  142. package/src/browser/shared-text-history-validation.ts +151 -0
  143. package/src/browser/shared-text-history.ts +464 -0
  144. package/src/browser/shared-text-offline-authorized.ts +29 -0
  145. package/src/browser/shared-text-offline.ts +351 -0
  146. package/src/browser/shared-text-provider.ts +714 -0
  147. package/src/browser/shared-text-table-contract.ts +150 -0
  148. package/src/browser/shared-text.ts +19 -0
  149. package/src/capabilities/server-invoke.ts +2 -11
  150. package/src/capabilities/testing.ts +5 -2
  151. package/src/cli/access.ts +6 -5
  152. package/src/cli/admin/ai-models.ts +199 -0
  153. package/src/cli/admin/ai-quotas.ts +6 -102
  154. package/src/cli/admin/ai-usage.ts +4 -4
  155. package/src/cli/admin/app-credentials.ts +5 -3
  156. package/src/cli/admin/index.ts +5 -2
  157. package/src/cli/apps.ts +89 -26
  158. package/src/cli/capabilities.ts +22 -1
  159. package/src/cli/events.ts +320 -0
  160. package/src/config/env.ts +40 -0
  161. package/src/contracts/app-access.ts +26 -2
  162. package/src/contracts/app-details.ts +89 -0
  163. package/src/contracts/app-events.ts +51 -0
  164. package/src/contracts/app.ts +21 -5
  165. package/src/contracts/capabilities.ts +143 -3
  166. package/src/contracts/index.ts +2 -0
  167. package/src/contracts/notification-types.ts +36 -9
  168. package/src/contracts/pwa.ts +17 -1
  169. package/src/contracts/registry.ts +6 -0
  170. package/src/contracts/resource-clipboard.ts +35 -0
  171. package/src/contracts/resource-pages.ts +140 -0
  172. package/src/contracts/resource-resolution.ts +13 -0
  173. package/src/contracts/shared.ts +6 -1
  174. package/src/contracts/user-notifications.ts +2 -0
  175. package/src/events/app-event-delivery.ts +100 -0
  176. package/src/events/app-events.testing.ts +65 -0
  177. package/src/events/app-events.ts +134 -0
  178. package/src/events/core-user-created.ts +24 -0
  179. package/src/events/index.ts +9 -0
  180. package/src/events/testing.ts +2 -0
  181. package/src/index.ts +1 -1
  182. package/src/server/api/respond.ts +17 -4
  183. package/src/server/index.ts +2 -0
  184. package/src/server/middleware/app-access.ts +1 -1
  185. package/src/server/middleware/auth.ts +21 -7
  186. package/src/server/middleware/index.ts +1 -0
  187. package/src/server/middleware/invocation.ts +2 -0
  188. package/src/server/public-storage.ts +106 -0
  189. package/src/server/services/access.ts +31 -10
  190. package/src/server/services/index.ts +1 -0
  191. package/src/services/accounts/app.ts +36 -13
  192. package/src/services/accounts/entities.ts +43 -19
  193. package/src/services/accounts/group-sql.ts +23 -0
  194. package/src/services/accounts/groups.ts +19 -14
  195. package/src/services/accounts/identities.ts +70 -11
  196. package/src/services/accounts/identity-reconciliation.ts +3 -1
  197. package/src/services/accounts/local-groups.ts +69 -4
  198. package/src/services/accounts/users.ts +45 -14
  199. package/src/services/app-access.ts +25 -7
  200. package/src/services/app-details.ts +224 -0
  201. package/src/services/app-discovery.ts +2 -1
  202. package/src/services/app-event-shapes.ts +139 -0
  203. package/src/services/app-events/accept.ts +116 -0
  204. package/src/services/app-events/admin.ts +250 -0
  205. package/src/services/app-events/declarations.ts +187 -0
  206. package/src/services/app-events/deliver.ts +218 -0
  207. package/src/services/app-events.ts +92 -0
  208. package/src/services/config-conflict.ts +11 -0
  209. package/src/services/core-user-events.ts +1 -0
  210. package/src/services/freeipa-config.ts +9 -0
  211. package/src/services/geo/ledger.ts +28 -10
  212. package/src/services/geo/service.ts +43 -24
  213. package/src/services/identity/core-origin.ts +12 -0
  214. package/src/services/identity/invocation-actor.ts +20 -2
  215. package/src/services/identity/invocation-authority.ts +4 -0
  216. package/src/services/identity/invocation-token.ts +238 -56
  217. package/src/services/identity/issue-invocation.ts +6 -1
  218. package/src/services/image-metadata.ts +65 -1
  219. package/src/services/index.ts +13 -1
  220. package/src/services/ipa/sync.ts +6 -2
  221. package/src/services/ipa/users.ts +4 -1
  222. package/src/services/mandates/index.ts +2 -2
  223. package/src/services/mandates/policy.ts +20 -12
  224. package/src/services/notifications/browser.ts +4 -0
  225. package/src/services/notifications/catalog.ts +5 -2
  226. package/src/services/notifications/platform.ts +7 -2
  227. package/src/services/notifications/user.ts +18 -7
  228. package/src/services/oauth-tokens.ts +28 -2
  229. package/src/services/object-store.ts +217 -0
  230. package/src/services/providers/local/users.ts +5 -0
  231. package/src/services/pwa-devices.ts +147 -37
  232. package/src/services/session/index.ts +26 -3
  233. package/src/services/settings/core-settings.ts +71 -43
  234. package/src/services/settings/index.ts +8 -0
  235. package/src/services/settings/versions.ts +26 -0
  236. package/src/shared/ai-models.ts +274 -0
  237. package/src/shared/ai-quotas.ts +2 -2
  238. package/src/shared/ai-usage.ts +2 -2
  239. package/src/shared/email-html.ts +1 -0
  240. package/src/shared/index.ts +2 -0
  241. package/src/shared/markdown/extensions/katex.ts +8 -1
  242. package/src/shared/markdown/extensions/tables.ts +14 -1
  243. package/src/shared/markdown/index.ts +22 -11
  244. package/src/shared-text/anchors.ts +29 -0
  245. package/src/shared-text/awareness.ts +87 -0
  246. package/src/shared-text/changes.ts +27 -0
  247. package/src/shared-text/documents.ts +433 -65
  248. package/src/shared-text/events.ts +12 -2
  249. package/src/shared-text/index.ts +16 -1
  250. package/src/shared-text/log.ts +24 -5
  251. package/src/shared-text/snapshots.ts +69 -23
  252. package/src/shared-text/structural-history.ts +377 -0
  253. package/src/shared-text/table-actions.ts +432 -0
  254. package/src/shared-text/text-diff.ts +75 -0
  255. package/src/shared-text/types.ts +24 -5
  256. package/src/ssr/AdminArea.tsx +131 -0
  257. package/src/ssr/AdminLayout.tsx +9 -3
  258. package/src/ssr/AdminSidebar.tsx +7 -7
  259. package/src/ssr/AppAccessDenied.tsx +2 -0
  260. package/src/ssr/AppBadge.tsx +14 -0
  261. package/src/ssr/AppLaunchpadPanel.tsx +2 -2
  262. package/src/ssr/CloudShell.island.tsx +44 -7
  263. package/src/ssr/Layout.tsx +9 -7
  264. package/src/ssr/MobileProfileActions.tsx +2 -2
  265. package/src/ssr/ProfilePreferences.tsx +2 -1
  266. package/src/ssr/PwaLayout.tsx +16 -1
  267. package/src/ssr/PwaPartCount.island.tsx +23 -0
  268. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  269. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  270. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  271. package/src/ssr/admin-active-link.ts +16 -0
  272. package/src/ssr/admin-navigation.ts +26 -31
  273. package/src/ssr/app-badges.ts +31 -7
  274. package/src/ssr/app-launchpad.tsx +6 -1
  275. package/src/ssr/index.ts +8 -0
  276. package/src/ssr/page-trail-runtime.ts +3 -1
  277. package/src/ssr/platform-messages.ts +11 -1
  278. package/src/ssr/profile-actions.ts +3 -6
  279. package/src/ssr/pwa-messages.ts +2 -2
  280. package/src/ssr/rail-navigation.ts +5 -1
  281. package/src/ssr/settings-draft.ts +153 -0
  282. package/src/ssr/shell-data.tsx +3 -1
  283. package/src/styles/global.css +1 -0
  284. package/src/styles/profile-picture.css +270 -0
  285. package/src/styles/utilities-navigation.css +6 -0
  286. package/src/workflows/ai/policy.ts +49 -0
  287. package/src/workflows/ai/runtime.ts +131 -21
  288. package/src/workflows/ai/store.ts +283 -35
  289. package/src/workflows/ai/types.ts +8 -2
  290. package/src/workflows/ai-actions.ts +148 -120
  291. package/src/workflows/authoring.ts +31 -0
  292. package/src/workflows/contracts.ts +1 -0
  293. package/src/workflows/definition.ts +2 -0
  294. package/src/workflows/editor.ts +15 -0
  295. package/src/workflows/index.ts +2 -1
  296. package/src/workflows/language/compiler.ts +34 -3
  297. package/src/workflows/language/expressions.ts +3 -2
  298. package/src/workflows/runtime/executor.ts +134 -34
  299. package/src/workflows/runtime/index.ts +2 -0
  300. package/src/workflows/runtime/ports.ts +5 -1
  301. package/src/workflows/store/actions.ts +1 -1
  302. package/src/workflows/store/definitions.ts +5 -0
  303. package/src/workflows/store/runs.ts +5 -3
  304. package/src/workflows/store/worker.ts +7 -2
  305. package/src/ai/model-pricing.ts +0 -43
  306. package/src/ai/model-request-settings.ts +0 -98
  307. package/src/ai/settings-messages.ts +0 -129
  308. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -0,0 +1,274 @@
1
+ import { z } from "zod";
2
+ import { PrincipalSchema } from "../contracts/shared";
3
+ import { AiModelPricingSchema } from "./ai-costs";
4
+ import { AiExtraBodySchema, AiReasoningEffortSchema } from "./ai-request-options";
5
+
6
+ export const AI_MODEL_CLASSES = ["fast", "balanced", "thorough", "maximum"] as const;
7
+ export type AiModelClass = (typeof AI_MODEL_CLASSES)[number];
8
+ export const AI_MODEL_SLOTS = [...AI_MODEL_CLASSES, "vision", "audio"] as const;
9
+ export type AiModelSlot = (typeof AI_MODEL_SLOTS)[number];
10
+ export const AiModelRequestSchema = z.union([
11
+ z.object({ class: z.enum(AI_MODEL_CLASSES) }).strict(),
12
+ z.object({ id: z.string().trim().min(1).max(241) }).strict(),
13
+ ]);
14
+ export type AiModelRequest = z.infer<typeof AiModelRequestSchema>;
15
+ export const AiResolvedModelRefSchema = z
16
+ .object({
17
+ requestedClass: z.enum(AI_MODEL_CLASSES).optional(),
18
+ class: z.enum(AI_MODEL_CLASSES).optional(),
19
+ id: z.string().min(1).max(241),
20
+ })
21
+ .strict();
22
+ export type AiResolvedModelRef = { requestedClass?: AiModelClass; class?: AiModelClass; id: string };
23
+ /** Only empty assignments fall back; policy and budget failures never do. */
24
+ export const aiModelClassFallbackChain = (modelClass: AiModelClass): AiModelClass[] => {
25
+ const index = AI_MODEL_CLASSES.indexOf(modelClass);
26
+ return [...AI_MODEL_CLASSES.slice(0, index).reverse(), ...AI_MODEL_CLASSES.slice(index + 1)];
27
+ };
28
+ export const parseAiModelChoice = (value: string): AiModelRequest => {
29
+ const modelClass = z.enum(AI_MODEL_CLASSES).safeParse(value);
30
+ return modelClass.success ? { class: modelClass.data } : AiModelRequestSchema.parse({ id: value });
31
+ };
32
+
33
+ const slug = z
34
+ .string()
35
+ .trim()
36
+ .regex(/^[a-z0-9][a-z0-9._-]*$/)
37
+ .max(120);
38
+ const image = z
39
+ .string()
40
+ .trim()
41
+ .regex(/^data:image\//)
42
+ .max(28_000);
43
+ const httpUrl = z
44
+ .string()
45
+ .url()
46
+ .refine((value) => ["http:", "https:"].includes(new URL(value).protocol));
47
+ export const AiProviderTypeSchema = z.enum([
48
+ "openai",
49
+ "openrouter",
50
+ "anthropic",
51
+ "mistral",
52
+ "gemini",
53
+ "ollama",
54
+ "vllm",
55
+ "openai-compatible",
56
+ ]);
57
+ export type AiProviderType = z.infer<typeof AiProviderTypeSchema>;
58
+ /** Hosted APIs refuse requests without a key; self-hosted endpoints may run without one. */
59
+ export const providerRequiresCredential = (provider: AiProviderType): boolean =>
60
+ ["openai", "openrouter", "anthropic", "mistral", "gemini"].includes(provider);
61
+ export const providerSupportsCredential = (provider: AiProviderType): boolean =>
62
+ providerRequiresCredential(provider) || provider === "vllm" || provider === "openai-compatible";
63
+ export const AiProviderStatusSchema = z.enum(["unknown", "ok", "unreachable", "key_rejected", "error"]);
64
+ export const AiProviderInputSchema = z
65
+ .object({
66
+ id: slug,
67
+ type: AiProviderTypeSchema,
68
+ label: z.string().trim().min(1),
69
+ baseUrl: httpUrl.nullable().default(null),
70
+ dataBoundary: z.enum(["hosted", "private"]),
71
+ enabled: z.boolean().default(true),
72
+ logo: image.nullable().default(null),
73
+ })
74
+ .strict();
75
+ export const AiModelIconSchema = z.discriminatedUnion("kind", [
76
+ z.object({ kind: z.literal("provider") }).strict(),
77
+ z.object({ kind: z.literal("emoji"), emoji: z.string().min(1).max(64) }).strict(),
78
+ z.object({ kind: z.literal("image"), dataUrl: image }).strict(),
79
+ ]);
80
+ export type AiModelIcon = z.infer<typeof AiModelIconSchema>;
81
+ export const AiModelInputSchema = z
82
+ .object({
83
+ id: slug.refine((value) => !AI_MODEL_CLASSES.some((modelClass) => modelClass === value), "Model IDs cannot be class names."),
84
+ providerModel: z.string().trim().min(1),
85
+ label: z.string().trim().min(1),
86
+ description: z.string().default(""),
87
+ kind: z.enum(["chat", "transcription"]).default("chat"),
88
+ capabilities: z.array(z.enum(["tools", "vision", "transcription"])).default([]),
89
+ contextWindow: z.number().int().positive().nullable().default(null),
90
+ maxOutputTokens: z.number().int().positive().nullable().default(null),
91
+ temperature: z.number().min(0).max(2).nullable().default(null),
92
+ extraBody: AiExtraBodySchema.nullable().default(null),
93
+ maxLoadedTools: z.number().int().nullable().default(null),
94
+ maxToolRounds: z.number().int().nullable().default(null),
95
+ pricing: AiModelPricingSchema.nullable().default(null),
96
+ icon: AiModelIconSchema.default({ kind: "provider" }),
97
+ enabled: z.boolean().default(true),
98
+ })
99
+ .strict();
100
+ export const AiVariantInputSchema = z
101
+ .object({
102
+ slug,
103
+ label: z.string().trim().min(1),
104
+ hint: z.string().default(""),
105
+ reasoningEffort: AiReasoningEffortSchema.nullable().default(null),
106
+ maxOutputTokens: z.number().int().positive().nullable().default(null),
107
+ pricing: AiModelPricingSchema.nullable().default(null),
108
+ extraBody: AiExtraBodySchema.nullable().default(null),
109
+ showInPicker: z.boolean().default(true),
110
+ position: z.number().int().default(0),
111
+ })
112
+ .strict();
113
+ export const AiProviderPatchSchema = AiProviderInputSchema.omit({ id: true })
114
+ .partial()
115
+ .extend({
116
+ baseUrl: AiProviderInputSchema.shape.baseUrl.removeDefault().optional(),
117
+ enabled: AiProviderInputSchema.shape.enabled.removeDefault().optional(),
118
+ logo: AiProviderInputSchema.shape.logo.removeDefault().optional(),
119
+ })
120
+ .strict();
121
+ export const AiModelPatchSchema = AiModelInputSchema.omit({ id: true })
122
+ .partial()
123
+ .extend({
124
+ description: AiModelInputSchema.shape.description.removeDefault().optional(),
125
+ kind: AiModelInputSchema.shape.kind.removeDefault().optional(),
126
+ capabilities: AiModelInputSchema.shape.capabilities.removeDefault().optional(),
127
+ contextWindow: AiModelInputSchema.shape.contextWindow.removeDefault().optional(),
128
+ maxOutputTokens: AiModelInputSchema.shape.maxOutputTokens.removeDefault().optional(),
129
+ temperature: AiModelInputSchema.shape.temperature.removeDefault().optional(),
130
+ extraBody: AiModelInputSchema.shape.extraBody.removeDefault().optional(),
131
+ maxLoadedTools: AiModelInputSchema.shape.maxLoadedTools.removeDefault().optional(),
132
+ maxToolRounds: AiModelInputSchema.shape.maxToolRounds.removeDefault().optional(),
133
+ pricing: AiModelInputSchema.shape.pricing.removeDefault().optional(),
134
+ icon: AiModelInputSchema.shape.icon.removeDefault().optional(),
135
+ enabled: AiModelInputSchema.shape.enabled.removeDefault().optional(),
136
+ })
137
+ .strict();
138
+ export const AiVariantPatchSchema = AiVariantInputSchema.omit({ slug: true })
139
+ .partial()
140
+ .extend({
141
+ hint: AiVariantInputSchema.shape.hint.removeDefault().optional(),
142
+ reasoningEffort: AiVariantInputSchema.shape.reasoningEffort.removeDefault().optional(),
143
+ maxOutputTokens: AiVariantInputSchema.shape.maxOutputTokens.removeDefault().optional(),
144
+ pricing: AiVariantInputSchema.shape.pricing.removeDefault().optional(),
145
+ extraBody: AiVariantInputSchema.shape.extraBody.removeDefault().optional(),
146
+ showInPicker: AiVariantInputSchema.shape.showInPicker.removeDefault().optional(),
147
+ position: AiVariantInputSchema.shape.position.removeDefault().optional(),
148
+ })
149
+ .strict();
150
+ const revisionFields = {
151
+ revision: z.number().int(),
152
+ createdAt: z.string(),
153
+ updatedAt: z.string(),
154
+ updatedBy: z.string().nullable(),
155
+ };
156
+ export const AiProviderSchema = AiProviderInputSchema.extend({
157
+ ...revisionFields,
158
+ status: AiProviderStatusSchema,
159
+ statusCode: z.string().nullable(),
160
+ statusMessage: z.string().nullable(),
161
+ statusCheckedAt: z.string().nullable(),
162
+ credentialSet: z.boolean(),
163
+ requestHeaderNames: z.array(z.string()),
164
+ });
165
+ export const AiModelSchema = AiModelInputSchema.extend({ ...revisionFields, providerId: z.string() });
166
+ export const AiVariantSchema = AiVariantInputSchema.extend({ ...revisionFields, id: z.string(), modelId: z.string() });
167
+ export const AiAssignmentSchema = z.object({
168
+ slot: z.enum(AI_MODEL_SLOTS),
169
+ variantId: z.string().nullable(),
170
+ revision: z.number().int(),
171
+ updatedAt: z.string(),
172
+ updatedBy: z.string().nullable(),
173
+ });
174
+ export type AiProvider = z.infer<typeof AiProviderSchema>;
175
+ export type AiModel = z.infer<typeof AiModelSchema>;
176
+ export type AiModelVariant = z.infer<typeof AiVariantSchema>;
177
+ export type AiModelAssignment = z.infer<typeof AiAssignmentSchema>;
178
+ export type AiModelUnavailableReason =
179
+ | { code: "budget_exhausted"; resetsAt: string }
180
+ | { code: "provider_unreachable" | "key_rejected" | "access_revoked" };
181
+ export type AiPeopleModelIcon =
182
+ | { kind: "emoji"; emoji: string }
183
+ | { kind: "image"; src: string }
184
+ | { kind: "mark"; text: string; color: string };
185
+ /** Neutral provider marks: Cloud ships no brand logos, so a provider shows its initials on one of these colours. */
186
+ export const AI_PROVIDER_MARK_COLORS = ["#0f766e", "#6d28d9", "#475569", "#15803d", "#1d4ed8", "#c2410c", "#be123c", "#a16207"] as const;
187
+ /** The initials and colour a provider shows wherever it has no logo, the same in the AI area and in every chooser. */
188
+ export const aiProviderMark = (provider: { id: string; label: string }): { text: string; color: string } => {
189
+ const words = provider.label.split(/\s+/).filter(Boolean);
190
+ const capitals = words[0]?.match(/\p{Lu}/gu) ?? [];
191
+ const text =
192
+ words.length > 1
193
+ ? `${words[0]![0]}${words[1]![0]}`
194
+ : capitals.length >= 2
195
+ ? capitals.slice(0, 2).join("")
196
+ : (words[0] ?? "").slice(0, 2);
197
+ return {
198
+ text: text.toUpperCase() || "AI",
199
+ color:
200
+ AI_PROVIDER_MARK_COLORS[
201
+ Array.from(provider.id).reduce((sum, letter) => (sum * 31 + letter.charCodeAt(0)) >>> 0, 0) % AI_PROVIDER_MARK_COLORS.length
202
+ ]!,
203
+ };
204
+ };
205
+ export type AiPeopleModels = {
206
+ status: "ok";
207
+ selection: AiModelRequest | null;
208
+ effectiveDefault: AiModelRequest;
209
+ classes: { class: AiModelClass; variantId: string; unavailable?: AiModelUnavailableReason }[];
210
+ options: {
211
+ id: string;
212
+ label: string;
213
+ variantLabel?: string;
214
+ description: string;
215
+ icon: AiPeopleModelIcon;
216
+ classes: AiModelClass[];
217
+ unavailable?: AiModelUnavailableReason;
218
+ }[];
219
+ };
220
+ export const AiDeleteModelSchema = z
221
+ .object({
222
+ revision: z.number().int(),
223
+ replacements: z.partialRecord(z.enum(AI_MODEL_SLOTS), z.string().nullable()),
224
+ retarget: z.boolean(),
225
+ retargetTo: z.string().nullable(),
226
+ })
227
+ .strict();
228
+ export type AiDeleteModelInput = z.infer<typeof AiDeleteModelSchema>;
229
+ export type AiModelUsage = {
230
+ assignments: AiModelSlot[];
231
+ projects: { id: string; name: string }[];
232
+ workflows: { id: string; name: string }[];
233
+ scheduledTasks: { id: string; name: string }[];
234
+ chats: number;
235
+ budgetRules: { id: string; label: string }[];
236
+ peopleAtLimit: number;
237
+ stats: { answers14d: number; avgCostPerAnswer: number | null; avgSecondsPerAnswer: number | null };
238
+ };
239
+
240
+ const AiModelGrantSchema = z
241
+ .object({
242
+ principal: PrincipalSchema.refine((principal) => principal.type !== "public", "Assistant models cannot be public."),
243
+ permission: z.literal("read"),
244
+ })
245
+ .strict();
246
+ export const AiModelAccessDraftSchema = z
247
+ .object({
248
+ expectedRevision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
249
+ entries: z.array(AiModelGrantSchema),
250
+ })
251
+ .strict();
252
+ export type AiModelAccessDraft = z.infer<typeof AiModelAccessDraftSchema>;
253
+
254
+ /** Whether a model supports a catalog assignment; safe to use in browser pickers. */
255
+ export const aiModelSupportsSlot = (slot: AiModelSlot, model: Pick<AiModel, "kind" | "capabilities">): boolean =>
256
+ slot === "audio" ? model.kind === "transcription" : model.kind === "chat" && (slot !== "vision" || model.capabilities.includes("vision"));
257
+
258
+ /** The same replacement requirements apply to deletion previews and committed retargeting. */
259
+ export const aiReplacementProblem = (
260
+ source: Pick<AiProvider, "dataBoundary">,
261
+ target: Pick<AiProvider, "dataBoundary" | "enabled">,
262
+ original: Pick<AiModel, "kind" | "capabilities">,
263
+ replacement: Pick<AiModel, "kind" | "capabilities" | "enabled">,
264
+ ): "private_boundary" | "incompatible_model" | null => {
265
+ if (source.dataBoundary === "private" && target.dataBoundary !== "private") return "private_boundary";
266
+ if (
267
+ !target.enabled ||
268
+ !replacement.enabled ||
269
+ original.kind !== replacement.kind ||
270
+ original.capabilities.some((capability) => !replacement.capabilities.includes(capability))
271
+ )
272
+ return "incompatible_model";
273
+ return null;
274
+ };
@@ -143,7 +143,7 @@ export type AiQuotaReport = {
143
143
  perPage: number;
144
144
  };
145
145
  export const aiQuotaHref = (query: Partial<AiQuotaReportQuery>) => {
146
- const params = new URLSearchParams({ tab: "ai-quotas" });
146
+ const params = new URLSearchParams({ sec: "budgets", ...(query.view === "rules" ? {} : { rule: "people" }) });
147
147
  for (const [key, value] of Object.entries(query)) if (value !== undefined && value !== "") params.set(key, String(value));
148
- return `/admin/settings?${params}`;
148
+ return `/admin/ai?${params}`;
149
149
  };
@@ -8,7 +8,7 @@ export const AiUsageQuerySchema = z.object({
8
8
  range: z.enum(AI_USAGE_RANGES).default("30d"),
9
9
  until: z.iso.datetime({ offset: true }).optional(),
10
10
  userId: z.union([z.uuid(), z.literal("unassigned")]).optional(),
11
- modelProfileId: text,
11
+ modelId: text,
12
12
  providerModel: text,
13
13
  appId: text,
14
14
  workflowId: z.uuid().optional(),
@@ -36,4 +36,4 @@ export const aiUsageSearchParams = (query: Partial<AiUsageQuery>): URLSearchPara
36
36
  for (const [key, value] of Object.entries(query)) if (value !== undefined && value !== "") params.set(key, String(value));
37
37
  return params;
38
38
  };
39
- export const aiUsageHref = (query: Partial<AiUsageQuery>) => `/admin/settings?tab=ai-usage&${aiUsageSearchParams(query)}`;
39
+ export const aiUsageHref = (query: Partial<AiUsageQuery>) => `/admin/ai?sec=usage&${aiUsageSearchParams(query)}`;
@@ -6,6 +6,7 @@ export const EMAIL_HTML_TAGS = [
6
6
  "br",
7
7
  "code",
8
8
  "div",
9
+ "del",
9
10
  "em",
10
11
  "h1",
11
12
  "h2",
@@ -13,6 +13,8 @@ export {
13
13
  export * from "./account-display";
14
14
  export * from "./account-session";
15
15
  export * from "./ai-costs";
16
+ export * from "./ai-models";
17
+ export * from "./ai-models";
16
18
  export type { AiPromptContextInput, AiToolPromptHint } from "./ai-platform-prompt";
17
19
  export {
18
20
  AI_LOCAL_CLIENT_DEFAULT_INSTRUCTIONS,
@@ -8,6 +8,7 @@
8
8
 
9
9
  import katex from "katex";
10
10
  import type { MarkedExtension, Tokens } from "marked";
11
+ import { escapeHtml } from "../shared";
11
12
 
12
13
  /**
13
14
  * Token shape produced by the math tokenizers below. The marked extension API
@@ -40,7 +41,7 @@ function renderInlineMath(latex: string): string {
40
41
  }
41
42
  }
42
43
 
43
- export function katexExtension(): MarkedExtension {
44
+ export function katexExtension(literal = false): MarkedExtension {
44
45
  return {
45
46
  extensions: [
46
47
  // ```math code blocks
@@ -63,6 +64,10 @@ export function katexExtension(): MarkedExtension {
63
64
  return undefined;
64
65
  },
65
66
  renderer(token: Tokens.Generic) {
67
+ if (literal) {
68
+ const source = token.raw.slice("```math\n".length, -"\n```".length);
69
+ return `<pre>${escapeHtml(source)}</pre>\n`;
70
+ }
66
71
  const latex = (token as MathToken).latex;
67
72
  return renderBlockMath(latex);
68
73
  },
@@ -87,6 +92,7 @@ export function katexExtension(): MarkedExtension {
87
92
  return undefined;
88
93
  },
89
94
  renderer(token: Tokens.Generic) {
95
+ if (literal) return `<p>${escapeHtml(token.raw)}</p>\n`;
90
96
  const latex = (token as MathToken).latex;
91
97
  return renderBlockMath(latex);
92
98
  },
@@ -111,6 +117,7 @@ export function katexExtension(): MarkedExtension {
111
117
  return undefined;
112
118
  },
113
119
  renderer(token: Tokens.Generic) {
120
+ if (literal) return escapeHtml(token.raw);
114
121
  const latex = (token as MathToken).latex;
115
122
  return renderInlineMath(latex);
116
123
  },
@@ -66,11 +66,24 @@ const renderCell = (alignCls: string, originalText: string, ctx: EvalContext, ht
66
66
  return `<td><span class="md-table-cell md-formula-error${alignCls}" title="${escapeHtml(tooltip)}">⚠ ${escapeHtml(originalText)}</span></td>`;
67
67
  };
68
68
 
69
- export function tablesExtension(): MarkedExtension {
69
+ export function tablesExtension(plain = false): MarkedExtension {
70
70
  return {
71
71
  renderer: {
72
72
  table(token: Tokens.Table): string {
73
73
  const align = token.align ?? [];
74
+ // Email has no Cloud stylesheet or formula widgets. Keep alignment on the
75
+ // structural cells and escape formula source before Markdown can interpret it.
76
+ if (plain) {
77
+ const cellHtml = (cell: Tokens.TableCell, tag: "th" | "td", column: number): string => {
78
+ const alignment = align[column];
79
+ const attribute = alignment ? ` align="${alignment}"` : "";
80
+ const content = isFormula(cell.text) ? escapeHtml(cell.text) : this.parser.parseInline(cell.tokens);
81
+ return `<${tag}${attribute}>${content}</${tag}>`;
82
+ };
83
+ const header = token.header.map((cell, column) => cellHtml(cell, "th", column)).join("");
84
+ const rows = token.rows.map((row) => `<tr>${row.map((cell, column) => cellHtml(cell, "td", column)).join("")}</tr>`).join("");
85
+ return `<table><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`;
86
+ }
74
87
 
75
88
  // Build the EvalContext once per table — formulas reference
76
89
  // raw cell text, not other formulas' results, so a single pass
@@ -25,8 +25,15 @@ import { escapeHtml } from "./shared";
25
25
  // Create a configured marked instance
26
26
  type MarkdownProfile = "content" | "help";
27
27
  type LinkStyle = "widget" | "plain";
28
+ type MarkdownOutput = "cloud" | "email";
28
29
 
29
- const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: MarkedExtension, locale?: string) => {
30
+ const createMarked = (
31
+ profile: MarkdownProfile,
32
+ links: LinkStyle,
33
+ infoBlocks: MarkedExtension,
34
+ locale?: string,
35
+ output: MarkdownOutput = "cloud",
36
+ ) => {
30
37
  const marked = new Marked();
31
38
 
32
39
  marked.use({
@@ -38,12 +45,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
38
45
  // Note: katexExtension must come before codeExtension to handle ```math blocks
39
46
  marked.use(infoBlocks);
40
47
  marked.use(taskListExtension());
41
- marked.use(tablesExtension());
48
+ marked.use(tablesExtension(output === "email"));
42
49
  // Plain output keeps marked's own link renderer and renders code blocks as a bare `pre`.
43
50
  // Cloud links are calm reference pills and quiet web links from `@k2b/ui`.
44
51
  if (links === "widget") marked.use(linksExtension({ internalTarget: profile === "help" ? "_self" : "_blank", locale }));
45
52
  marked.use(imagesExtension());
46
- marked.use(katexExtension());
53
+ marked.use(katexExtension(output === "email"));
47
54
  marked.use(links === "widget" ? codeExtension() : plainCodeExtension());
48
55
  // Inline-style decorators come last so they run after structural tokenizers.
49
56
  marked.use(markExtension());
@@ -57,12 +64,12 @@ const createMarked = (profile: MarkdownProfile, links: LinkStyle, infoBlocks: Ma
57
64
  // headers choose the locale, and `markdownInfoBlocks()` returns one extension
58
65
  // for every locale that shares a catalog, so the cache stays bounded.
59
66
  const instances = new Map<MarkedExtension, Map<string, Marked>>();
60
- const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string): Marked => {
67
+ const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale?: string, output: MarkdownOutput = "cloud"): Marked => {
61
68
  const infoBlocks = markdownInfoBlocks({ locale });
62
69
  const byStyle = instances.get(infoBlocks) ?? new Map<string, Marked>();
63
70
  instances.set(infoBlocks, byStyle);
64
- const key = `${profile}:${links}`;
65
- const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks, locale);
71
+ const key = `${profile}:${links}:${output}`;
72
+ const instance = byStyle.get(key) ?? createMarked(profile, links, infoBlocks, locale, output);
66
73
  byStyle.set(key, instance);
67
74
  return instance;
68
75
  };
@@ -70,18 +77,21 @@ const markedFor = (profile: MarkdownProfile, links: LinkStyle = "widget", locale
70
77
  const marked = markedFor("content");
71
78
 
72
79
  export type MarkdownRenderOptions = {
80
+ /** `"email"` keeps strikethrough and structural table alignment, and renders table formulas and math as literal source. Defaults to Cloud's styled output with formula evaluation. */
81
+ output?: MarkdownOutput;
73
82
  /** `"plain"` renders each link as an ordinary anchor around its text, for HTML read outside Cloud such as email. It also renders a code block, fenced or indented and including a Mermaid fence, as its source in a `pre` without the language label. The default renders references to Cloud content as calm pills and web links as quiet text links. */
74
83
  links?: LinkStyle;
75
84
  /** Request locale for the screen-reader names of untitled info blocks. Defaults to English on the server and the page language in the browser. */
76
85
  locale?: string;
77
86
  };
78
87
 
79
- const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"): string => {
88
+ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content", output: MarkdownOutput = "cloud"): string => {
80
89
  const callouts = markdownCalloutSanitizeOptions();
81
90
  return sanitizeHtml(html, {
82
91
  ...callouts,
83
92
  allowedTags: [
84
93
  ...sanitizeHtml.defaults.allowedTags,
94
+ ...(output === "email" ? ["del"] : []),
85
95
  "annotation",
86
96
  "aside",
87
97
  "br",
@@ -137,6 +147,7 @@ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"
137
147
  input: ["checked", "class", "disabled", "type"],
138
148
  math: ["xmlns"],
139
149
  pre: ["class"],
150
+ ...(output === "email" ? { th: ["align"], td: ["align"] } : {}),
140
151
  ...callouts.allowedAttributes,
141
152
  span: ["aria-hidden", "class", "style", "title"],
142
153
  },
@@ -192,10 +203,10 @@ const sanitizeRenderedHtml = (html: string, profile: MarkdownProfile = "content"
192
203
  export function renderMarkdown(content: string, options: MarkdownRenderOptions = {}): string {
193
204
  if (!content || typeof content !== "string") return "";
194
205
 
195
- const html = markedFor("content", options.links, options.locale).parse(content);
206
+ const html = markedFor("content", options.links, options.locale, options.output).parse(content);
196
207
  if (typeof html !== "string") return "";
197
208
 
198
- return sanitizeRenderedHtml(html);
209
+ return sanitizeRenderedHtml(html, "content", options.output);
199
210
  }
200
211
 
201
212
  /**
@@ -204,10 +215,10 @@ export function renderMarkdown(content: string, options: MarkdownRenderOptions =
204
215
  export function renderMarkdownSync(content: string, options: MarkdownRenderOptions = {}): string {
205
216
  if (!content || typeof content !== "string") return "";
206
217
 
207
- const html = markedFor("content", options.links, options.locale).parse(content);
218
+ const html = markedFor("content", options.links, options.locale, options.output).parse(content);
208
219
  if (typeof html !== "string") return "";
209
220
 
210
- return sanitizeRenderedHtml(html);
221
+ return sanitizeRenderedHtml(html, "content", options.output);
211
222
  }
212
223
 
213
224
  /**
@@ -0,0 +1,29 @@
1
+ import * as Y from "yjs";
2
+
3
+ /** Capture an existing character, never a boundary that could drift to a neighbour. */
4
+ export const captureTextAnchor = (text: Y.Text, index: number): Uint8Array => {
5
+ if (!text.doc || !Number.isSafeInteger(index) || index < 0 || index >= text.length)
6
+ throw new RangeError("A text anchor must identify an existing character in an attached Y.Text");
7
+ return Y.encodeRelativePosition(Y.createRelativePositionFromTypeIndex(text, index, 0));
8
+ };
9
+
10
+ /**
11
+ * Resolve only the original live character. Ordinary relative positions also
12
+ * resolve beside deleted characters and follow undo replacements; neither is
13
+ * a safe target for a delayed structural action.
14
+ */
15
+ export const resolveTextAnchor = (text: Y.Text, bytes: Uint8Array): number | null => {
16
+ if (!text.doc) return null;
17
+ try {
18
+ const anchor = Y.decodeRelativePosition(bytes);
19
+ if (!anchor.item || anchor.assoc !== 0) return null;
20
+ const position = Y.createAbsolutePositionFromRelativePosition(anchor, text.doc, false);
21
+ if (!position || position.type !== text || position.index < 0 || position.index >= text.length) return null;
22
+ // Item encodings omit the root type name. Compare two decoded positions,
23
+ // rather than a decoded anchor with a fresh position containing that name.
24
+ const live = Y.decodeRelativePosition(captureTextAnchor(text, position.index));
25
+ return Y.compareRelativePositions(anchor, live) ? position.index : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ };
@@ -0,0 +1,87 @@
1
+ import * as decoding from "lib0/decoding";
2
+ import * as encoding from "lib0/encoding";
3
+ import { z } from "zod";
4
+
5
+ const unsigned = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
6
+ const positionId = z.strictObject({ client: unsigned, clock: unsigned });
7
+ const relativePosition = z
8
+ .strictObject({
9
+ type: positionId.nullable().optional(),
10
+ tname: z.string().nullable().optional(),
11
+ item: positionId.nullable().optional(),
12
+ assoc: z.number().int().min(Number.MIN_SAFE_INTEGER).max(Number.MAX_SAFE_INTEGER).optional(),
13
+ })
14
+ .refine((position) => position.type != null || position.tname != null);
15
+ const stateSchema = z
16
+ .strictObject({
17
+ // Identity is supplied by the authenticated transport, never copied from here.
18
+ user: z.unknown().optional(),
19
+ cursor: z.strictObject({ anchor: relativePosition, head: relativePosition }).nullable().optional(),
20
+ })
21
+ .nullable();
22
+
23
+ export class InvalidSharedTextAwarenessError extends Error {
24
+ constructor() {
25
+ super("Invalid shared text awareness update");
26
+ this.name = "InvalidSharedTextAwarenessError";
27
+ }
28
+ }
29
+
30
+ const encode = (clientId: number, clock: number, state: unknown): Uint8Array => {
31
+ const encoder = encoding.createEncoder();
32
+ encoding.writeVarUint(encoder, 1);
33
+ encoding.writeVarUint(encoder, clientId);
34
+ encoding.writeVarUint(encoder, clock);
35
+ encoding.writeVarString(encoder, JSON.stringify(state));
36
+ return encoding.toUint8Array(encoder);
37
+ };
38
+
39
+ /** One authorized connection, with an ID allocated uniquely by its transport. */
40
+ export const createTrustedTextAwareness = (config: {
41
+ clientId: number;
42
+ textName: string;
43
+ user: { userId: string; name: string; color: string };
44
+ }) => {
45
+ if (!Number.isSafeInteger(config.clientId) || config.clientId < 2 ** 32) throw new RangeError("Invalid server awareness ID");
46
+ const user = { userId: config.user.userId, name: config.user.name, color: config.user.color };
47
+ let localClientId: number | undefined;
48
+ let clock = 0;
49
+ let active = false;
50
+ const next = (state: unknown): Uint8Array => {
51
+ if (clock === Number.MAX_SAFE_INTEGER) throw new RangeError("Awareness clock exhausted");
52
+ return encode(config.clientId, ++clock, state);
53
+ };
54
+ return {
55
+ /** Validate one local state and replace its envelope and identity. Throws on invalid input. */
56
+ publish(bytes: Uint8Array): Uint8Array {
57
+ try {
58
+ const decoder = decoding.createDecoder(bytes);
59
+ if (decoding.readVarUint(decoder) !== 1) throw new InvalidSharedTextAwarenessError();
60
+ const clientId = decoding.readVarUint(decoder);
61
+ const clientClock = decoding.readVarUint(decoder);
62
+ if (!Number.isSafeInteger(clientId) || !Number.isSafeInteger(clientClock)) throw new InvalidSharedTextAwarenessError();
63
+ const state = stateSchema.parse(JSON.parse(decoding.readVarString(decoder)));
64
+ if (decoder.pos !== bytes.length || (localClientId !== undefined && localClientId !== clientId))
65
+ throw new InvalidSharedTextAwarenessError();
66
+ if (
67
+ state?.cursor &&
68
+ [state.cursor.anchor, state.cursor.head].some((position) => position.tname !== config.textName || position.type != null)
69
+ )
70
+ throw new InvalidSharedTextAwarenessError();
71
+ const result = next(state === null ? null : { user, writerId: clientId, cursor: state.cursor ?? null });
72
+ localClientId = clientId;
73
+ active = state !== null;
74
+ return result;
75
+ } catch {
76
+ throw new InvalidSharedTextAwarenessError();
77
+ }
78
+ },
79
+ /** Publish on leave/close, including when the browser cannot send its own null state. */
80
+ remove(): Uint8Array | null {
81
+ if (!active) return null;
82
+ const result = next(null);
83
+ active = false;
84
+ return result;
85
+ },
86
+ };
87
+ };
@@ -0,0 +1,27 @@
1
+ import type { SharedTextChange } from "./types";
2
+
3
+ export const validateChanges = (text: string, changes: readonly SharedTextChange[], maxLength: number): SharedTextChange[] => {
4
+ const sorted = [...changes].sort((a, b) => a.from - b.from || a.to - b.to);
5
+ let length = text.length;
6
+ const splitsCharacter = (at: number) =>
7
+ at > 0 && at < text.length && /[\uD800-\uDBFF]/.test(text[at - 1]!) && /[\uDC00-\uDFFF]/.test(text[at]!);
8
+ for (let index = 0; index < sorted.length; index++) {
9
+ const change = sorted[index]!;
10
+ const previous = sorted[index - 1];
11
+ if (
12
+ !Number.isSafeInteger(change.from) ||
13
+ !Number.isSafeInteger(change.to) ||
14
+ change.from < 0 ||
15
+ change.to < change.from ||
16
+ change.to > text.length ||
17
+ (change.recreate === true && change.to === change.from) ||
18
+ (previous && (change.from < previous.to || change.from === previous.from))
19
+ )
20
+ throw new RangeError("Shared text changes must have distinct, non-overlapping original-source ranges");
21
+ if (splitsCharacter(change.from) || splitsCharacter(change.to) || !change.insert.isWellFormed())
22
+ throw new RangeError("Shared text changes must preserve Unicode characters");
23
+ length += change.insert.length - (change.to - change.from);
24
+ }
25
+ if (length > maxLength) throw new RangeError("Shared text changes exceed maxLength");
26
+ return sorted.filter((change) => change.recreate === true || text.slice(change.from, change.to) !== change.insert);
27
+ };