@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,86 @@
1
+ import { i18n } from "@k2b/stdlib";
2
+ import { type DropdownItem, toast, useLocale } from "@k2b/ui";
3
+ import { createMemo, onMount } from "solid-js";
4
+ import type { CloudResourceRef } from "../contracts/capabilities";
5
+ import { cloudResourceHref } from "../contracts/resource-href";
6
+ import { cloudOrigin } from "./resource-links";
7
+
8
+ export const cloudIdMessages = i18n.define({
9
+ baseLocale: "en",
10
+ messages: {
11
+ en: {
12
+ copy: "Copy Cloud ID",
13
+ copied: "Cloud ID copied",
14
+ copyFailed: "Could not copy the Cloud ID",
15
+ },
16
+ de: {
17
+ copy: "Cloud-ID kopieren",
18
+ copied: "Cloud-ID kopiert",
19
+ copyFailed: "Cloud-ID konnte nicht kopiert werden",
20
+ },
21
+ },
22
+ });
23
+
24
+ type ClipboardModule = typeof import("./resource-clipboard");
25
+ // The clipboard contract validates with Zod, which stays out of an island's first load. It loads once the page is
26
+ // idle, so a copy usually writes the structured reference inside the click that asked for it.
27
+ let clipboardModule: ClipboardModule | undefined;
28
+ const loadClipboard = () => import("./resource-clipboard").then((module) => (clipboardModule = module));
29
+
30
+ /** One entry of the menu; `label` names the resource when a menu offers several, such as a message and its conversation. */
31
+ export type CloudIdTarget = { ref: CloudResourceRef; label?: string };
32
+
33
+ /**
34
+ * "Copy Cloud ID" for the "…" menu of a resource. It copies the structured reference and its `/open` permalink as
35
+ * text: Cloud paste surfaces recognize the resource, and anywhere else the text opens it after sign-in. Without a
36
+ * configured Cloud URL the menu shows nothing.
37
+ */
38
+ export const useCloudIdCopy = () => {
39
+ const locale = useLocale();
40
+ const t = createMemo(() => cloudIdMessages.resolve([locale()]).t);
41
+ onMount(() => {
42
+ if (clipboardModule || !cloudOrigin()) return;
43
+ const warm = () => void loadClipboard().catch(() => undefined);
44
+ if (typeof requestIdleCallback === "function") requestIdleCallback(warm, { timeout: 2000 });
45
+ else setTimeout(warm, 0);
46
+ });
47
+
48
+ const copy = (ref: CloudResourceRef) => {
49
+ const cloudUrl = cloudOrigin();
50
+ if (!cloudUrl) return;
51
+ const failed = () => void toast.error(t().copyFailed);
52
+ const write = (module: ClipboardModule) => {
53
+ try {
54
+ const { cloudResourceClipboard } = module;
55
+ return cloudResourceClipboard
56
+ .write({ cloudUrl, ref, fallbackText: cloudResourceClipboard.url(ref, cloudUrl) })
57
+ .then(() => void toast.success(t().copied), failed);
58
+ } catch {
59
+ failed();
60
+ }
61
+ };
62
+ if (clipboardModule) return void write(clipboardModule);
63
+ // Before the clipboard code loads, copy the permalink alone: WebKit allows a write only while the click is handled.
64
+ try {
65
+ void navigator.clipboard.writeText(new URL(cloudResourceHref(ref), cloudUrl).href).then(() => void toast.success(t().copied), failed);
66
+ } catch {
67
+ failed();
68
+ }
69
+ void loadClipboard().catch(() => undefined);
70
+ };
71
+
72
+ return {
73
+ available: () => Boolean(cloudOrigin()),
74
+ copy,
75
+ /** The last section of a resource's "…" menu; empty without a resource or a configured Cloud URL. */
76
+ section: (...targets: readonly (CloudResourceRef | CloudIdTarget | null | undefined | false)[]): DropdownItem[] => {
77
+ if (!cloudOrigin()) return [];
78
+ const items = targets.flatMap((target) => {
79
+ if (!target) return [];
80
+ const { ref, label } = "ref" in target ? target : { ref: target, label: undefined };
81
+ return [{ label: label ?? t().copy, icon: "ti ti-id", immediate: true, action: () => copy(ref) }];
82
+ });
83
+ return items.length > 0 ? [{ items }] : [];
84
+ },
85
+ };
86
+ };
@@ -3,7 +3,9 @@ import {
3
3
  CLOUD_RESOURCE_CLIPBOARD_MAX_BYTES,
4
4
  CLOUD_RESOURCE_CLIPBOARD_MIME_TYPE,
5
5
  CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT,
6
+ formatCloudResourceUrl,
6
7
  parseCloudResourceClipboard,
8
+ parseCloudResourceUrl,
7
9
  serializeCloudResourceClipboard,
8
10
  } from "../contracts/resource-clipboard";
9
11
 
@@ -65,6 +67,8 @@ export const cloudResourceClipboard = {
65
67
  webFormat: CLOUD_RESOURCE_CLIPBOARD_WEB_FORMAT,
66
68
  parse: parseCloudResourceClipboard,
67
69
  serialize: serializeCloudResourceClipboard,
70
+ url: formatCloudResourceUrl,
71
+ parseUrl: parseCloudResourceUrl,
68
72
  write: writeCloudResourceClipboard,
69
73
  read: readCloudResourceClipboard,
70
74
  } as const;
@@ -0,0 +1,52 @@
1
+ import type { CoreApiType } from "../api";
2
+ import { localCloudResourceUrl } from "../contracts/resource-pages";
3
+ import type { ResolvedCloudResourceUrl } from "../contracts/resource-resolution";
4
+ import { createApiClient } from "../server/api-client";
5
+ import { readShellData } from "../ssr/shell-data";
6
+
7
+ export type { ResolvedCloudResourceUrl } from "../contracts/resource-resolution";
8
+
9
+ /** Available on Layout and AdminLayout pages; MinimalLayout does not install shell data. */
10
+ export const cloudOrigin = (): string | null => readShellData()?.cloudOrigin ?? null;
11
+
12
+ /** Recognize one pasted Cloud link. Unsupported, unreadable, and unavailable links all return null. */
13
+ export const resolveCloudUrl = async (text: string, options: { signal?: AbortSignal } = {}): Promise<ResolvedCloudResourceUrl | null> => {
14
+ const url = text.trim();
15
+ if (!localCloudResourceUrl(url, cloudOrigin())) return null;
16
+ try {
17
+ const response = await createApiClient<CoreApiType>().resources.resolve.$post({ json: { url } }, { init: { signal: options.signal } });
18
+ if (!response.ok) return null;
19
+ // Validate only after a successful response; Zod stays out of the initial browser graph.
20
+ const { ResolvedCloudResourceUrlSchema } = await import("../contracts/resource-resolution");
21
+ const result = ResolvedCloudResourceUrlSchema.safeParse(await response.json());
22
+ return result.success ? result.data : null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ };
27
+
28
+ // Ctrl+Shift+V (Cmd+Shift+V, also with Option, which changes the key's character) asks for plain text; that paste
29
+ // keeps the link as it is.
30
+ let plainPasteAt = -Infinity;
31
+ if (typeof window !== "undefined")
32
+ window.addEventListener(
33
+ "keydown",
34
+ (event) => {
35
+ if (event.shiftKey && (event.ctrlKey || event.metaKey) && (event.code === "KeyV" || event.key.toLowerCase() === "v"))
36
+ plainPasteAt = event.timeStamp;
37
+ },
38
+ true,
39
+ );
40
+
41
+ /**
42
+ * The Cloud link a paste carries: one absolute URL on this Cloud, as copied from the address bar or "Copy Cloud ID",
43
+ * and nothing else. Returns null for other text, other origins, and a paste as plain text. Recognize the link with
44
+ * `resolveCloudUrl()`.
45
+ */
46
+ export const pastedCloudLink = (event: ClipboardEvent): string | null => {
47
+ const plain = event.timeStamp - plainPasteAt < 1_000;
48
+ plainPasteAt = -Infinity;
49
+ const text = (event.clipboardData?.getData("text/plain") ?? "").trim();
50
+ if (plain || text.startsWith("/")) return null;
51
+ return localCloudResourceUrl(text, cloudOrigin()) ? text : null;
52
+ };
@@ -0,0 +1,39 @@
1
+ import { apiClient } from "../clients/core";
2
+
3
+ /** Fence and remove application-owned entries in the authenticated user's browser namespace. */
4
+ const clearUserStorage = (userId: string): void => {
5
+ const prefix = `cloud:user:${userId}:`;
6
+ localStorage.setItem(`cloud:user-epoch:${userId}`, crypto.randomUUID());
7
+ if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("cloud:user-storage-invalidated", { detail: { userId } }));
8
+ const keys = Array.from({ length: localStorage.length }, (_, index) => localStorage.key(index));
9
+ for (const key of keys) if (key?.startsWith(prefix)) localStorage.removeItem(key);
10
+ };
11
+
12
+ /** Revoke this browser's credential even if local storage cleanup fails. The caller keeps a visible retry until both succeed. */
13
+ export const signOutBrowserSession = async (userId: string | undefined, errorMessage: string): Promise<void> => {
14
+ await import("./notifications").then(({ browserNotificationClient }) => browserNotificationClient.disable()).catch(() => undefined);
15
+ let cleanupFailed = false;
16
+ try {
17
+ if (userId) {
18
+ clearUserStorage(userId);
19
+ const { logoutSharedTextOffline } = await import("./shared-text-offline");
20
+ await logoutSharedTextOffline(userId);
21
+ }
22
+ } catch {
23
+ cleanupFailed = true;
24
+ }
25
+ const response = await apiClient.auth.logout.$post({}, { headers: { "x-cloud-logout-user": userId ?? "none" } });
26
+ // Close the interval between the first local fence and credential revocation:
27
+ // a tab that authenticated during it must not retain or recreate device data.
28
+ if (response.ok && userId) {
29
+ try {
30
+ clearUserStorage(userId);
31
+ const { logoutSharedTextOffline } = await import("./shared-text-offline");
32
+ await logoutSharedTextOffline(userId);
33
+ cleanupFailed = false;
34
+ } catch {
35
+ cleanupFailed = true;
36
+ }
37
+ }
38
+ if (!response.ok || cleanupFailed) throw new Error(errorMessage);
39
+ };
@@ -0,0 +1,151 @@
1
+ import * as T from "@k2b/ui/text-editor/model";
2
+ import * as Y from "yjs";
3
+
4
+ type NativeFrame = Y.UndoManager["undoStack"][number];
5
+ type Span = { from: number; to: number };
6
+ type Piece = Span & { original: number | null };
7
+
8
+ export class SharedTextNativeHistoryError extends Error {
9
+ readonly code = "shared_text_history_target";
10
+ constructor(readonly reason: "dependency" | "unavailable") {
11
+ super("The input history target is unavailable");
12
+ }
13
+ }
14
+
15
+ const intersect = (set: NativeFrame["insertions"], item: Y.Item): Span[] =>
16
+ (set.clients.get(item.id.client) ?? []).flatMap((run) => {
17
+ const from = Math.max(run.clock, item.id.clock) - item.id.clock;
18
+ const to = Math.min(run.clock + run.len, item.id.clock + item.length) - item.id.clock;
19
+ return from < to ? [{ from, to }] : [];
20
+ });
21
+ const subtract = (wanted: Span[], excluded: Span[]): Span[] =>
22
+ wanted.flatMap((span) => {
23
+ const result: Span[] = [];
24
+ let from = span.from;
25
+ for (const cut of excluded) {
26
+ if (cut.to <= from || cut.from >= span.to) continue;
27
+ if (cut.from > from) result.push({ from, to: cut.from });
28
+ from = Math.max(from, cut.to);
29
+ }
30
+ if (from < span.to) result.push({ from, to: span.to });
31
+ return result;
32
+ });
33
+
34
+ type Region = Span & { key: string | undefined };
35
+ const regions = (source: string, original: (position: number) => number | null): Region[] =>
36
+ T.findTables(source).flatMap((table) =>
37
+ table.lines.flatMap((line, row) =>
38
+ line.cells.map((_cell, column) => {
39
+ const left = line.leadingPipe ? line.pipes[column]! + 1 : column === 0 ? line.from : line.pipes[column - 1]! + 1;
40
+ const right = (line.leadingPipe ? line.pipes[column + 1] : line.pipes[column]) ?? line.to;
41
+ const separator = table.lines[1]!.cells[column];
42
+ const hyphen = separator ? original(separator.from + separator.text.indexOf("-")) : null;
43
+ const header = original(table.lines[0]!.to);
44
+ const rowIdentity = row < 2 ? row : original(line.from - 1);
45
+ // Extra physical cells have no separator hyphen. Their surviving left
46
+ // delimiter distinguishes them even when columns are inserted/removed.
47
+ // An unknown identity is never evidence that two targets are the same.
48
+ const columnIdentity = separator ? hyphen : original(left - 1);
49
+ return {
50
+ from: left,
51
+ to: right,
52
+ key:
53
+ header === null || rowIdentity === null || columnIdentity === null
54
+ ? undefined
55
+ : `${header}:${rowIdentity}:${separator ? "column" : "extra"}:${columnIdentity}`,
56
+ };
57
+ }),
58
+ ),
59
+ );
60
+ const locate = (regions: Region[], position: number, end: number) => {
61
+ let low = 0,
62
+ high = regions.length;
63
+ while (low < high) {
64
+ const middle = (low + high) >>> 1;
65
+ if (regions[middle]!.to <= position) low = middle + 1;
66
+ else high = middle;
67
+ }
68
+ const region = regions[low];
69
+ return region && region.from <= position ? { key: region.key, to: region.to } : { key: null, to: region?.from ?? end };
70
+ };
71
+
72
+ /** Project root plain-text native effects without a second manager or live mutation.
73
+ * Identity sets and linked Item/content fields are exported Yjs contracts. Native
74
+ * restoration occupies the original deleted item's gap; redone items already
75
+ * have their effect and are not copied again. Unknown content refuses safely.
76
+ */
77
+ export const validateNativeHistory = (
78
+ text: Y.Text,
79
+ frame: NativeFrame,
80
+ liveInsertions: NativeFrame["insertions"],
81
+ baseline: Uint8Array | null,
82
+ ) => {
83
+ const known = baseline ? Y.decodeStateVector(baseline) : new Map<number, number>();
84
+ const chunks: string[] = [];
85
+ const pieces: Piece[] = [];
86
+ const protectedSpans: { before: number; after: number; length: number }[] = [];
87
+ let before = 0,
88
+ after = 0;
89
+ const append = (value: string, original: number | null) => {
90
+ if (!value.length) return;
91
+ chunks.push(value);
92
+ pieces.push({ from: after, to: after + value.length, original });
93
+ after += value.length;
94
+ };
95
+ for (const item of Y.getTypeChildren(text)) {
96
+ // Adopted frames lack correspondence captured by this controller. Native
97
+ // Undo follows pre-existing redone chains for insertions; projecting only
98
+ // the original IDs would omit their live effects. Leave such frames intact.
99
+ if (baseline === null && item.redone !== null && intersect(frame.insertions, item).length)
100
+ throw new SharedTextNativeHistoryError("unavailable");
101
+ if (item.deleted) {
102
+ const restore = subtract(intersect(frame.deletions, item), intersect(frame.insertions, item));
103
+ if (!restore.length || item.redone !== null) continue;
104
+ const content = item.content;
105
+ if (!(content instanceof Y.ContentString)) throw new SharedTextNativeHistoryError("unavailable");
106
+ for (const span of restore) append(content.str.slice(span.from, span.to), null);
107
+ continue;
108
+ }
109
+ if (!item.countable) continue;
110
+ const content = item.content;
111
+ if (!(content instanceof Y.ContentString)) throw new SharedTextNativeHistoryError("unavailable");
112
+ const keep = subtract([{ from: 0, to: item.length }], intersect(liveInsertions, item));
113
+ for (const span of keep) {
114
+ const newer = Math.max(span.from, (known.get(item.id.client) ?? 0) - item.id.clock);
115
+ if (newer < span.to) protectedSpans.push({ before: before + newer, after: after + newer - span.from, length: span.to - newer });
116
+ append(content.str.slice(span.from, span.to), before + span.from);
117
+ }
118
+ before += item.length;
119
+ }
120
+ const source = text.toString();
121
+ const candidate = chunks.join("");
122
+ if (before !== source.length) throw new SharedTextNativeHistoryError("unavailable");
123
+ const original = (position: number) => {
124
+ let low = 0,
125
+ high = pieces.length;
126
+ while (low < high) {
127
+ const middle = (low + high) >>> 1;
128
+ if (pieces[middle]!.to <= position) low = middle + 1;
129
+ else high = middle;
130
+ }
131
+ const piece = pieces[low];
132
+ return piece?.original != null ? piece.original + position - piece.from : null;
133
+ };
134
+ const beforeRegions = regions(source, (position) => position);
135
+ const afterRegions = regions(candidate, original);
136
+ for (const span of protectedSpans) {
137
+ let offset = 0;
138
+ while (offset < span.length) {
139
+ const oldAt = span.before + offset,
140
+ newAt = span.after + offset;
141
+ const oldTarget = locate(beforeRegions, oldAt, source.length);
142
+ const newTarget = locate(afterRegions, newAt, candidate.length);
143
+ if (oldTarget.key === undefined || newTarget.key === undefined || oldTarget.key !== newTarget.key)
144
+ throw new SharedTextNativeHistoryError("dependency");
145
+ const length = Math.min(span.length - offset, oldTarget.to - oldAt, newTarget.to - newAt);
146
+ if (length <= 0) throw new SharedTextNativeHistoryError("unavailable");
147
+ offset += length;
148
+ }
149
+ }
150
+ return candidate;
151
+ };