@k2b/cloud 0.44.0 → 0.46.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 (137) hide show
  1. package/package.json +9 -2
  2. package/src/_internal/app-queries.ts +73 -0
  3. package/src/_internal/capabilities.ts +137 -9
  4. package/src/_internal/define-app.ts +2 -0
  5. package/src/_internal/registry-validation.ts +17 -4
  6. package/src/_internal/registry.ts +32 -4
  7. package/src/_internal/resource-principals.ts +36 -0
  8. package/src/access/GroupCoverage.tsx +20 -5
  9. package/src/access/PermissionEditor.tsx +11 -2
  10. package/src/access/PrincipalAvatar.tsx +30 -21
  11. package/src/access/messages.ts +4 -4
  12. package/src/account/EntitySearch.tsx +12 -0
  13. package/src/ai/capabilities.ts +4 -2
  14. package/src/ai/chat/map-messages.ts +270 -0
  15. package/src/ai/chat/map-notebook.browser-harness.tsx +28 -0
  16. package/src/ai/chat/map-notebook.tsx +301 -0
  17. package/src/ai/chat/map-widget.browser-harness.tsx +71 -0
  18. package/src/ai/chat/map-widget.css +107 -0
  19. package/src/ai/chat/map-widget.tsx +1279 -0
  20. package/src/ai/chat/message-utils.ts +1 -0
  21. package/src/ai/chat/tool-names.ts +2 -0
  22. package/src/ai/chat/turn-layout.ts +2 -1
  23. package/src/ai/chat/turn-view.tsx +17 -1
  24. package/src/ai/default-tools.ts +3 -0
  25. package/src/ai/dictation-runtime.ts +6 -2
  26. package/src/ai/executor.ts +22 -4
  27. package/src/ai/index.ts +1 -0
  28. package/src/ai/map-contracts.ts +209 -0
  29. package/src/ai/map-execution.ts +14 -0
  30. package/src/ai/map-notebook-save.ts +276 -0
  31. package/src/ai/map-notebook.ts +32 -0
  32. package/src/ai/map-opening-hours.ts +40 -0
  33. package/src/ai/map-routes.ts +84 -0
  34. package/src/ai/map-service.ts +288 -0
  35. package/src/ai/map-store.ts +137 -0
  36. package/src/ai/map-tool.ts +73 -0
  37. package/src/ai/migrate.ts +3 -0
  38. package/src/ai/project-tool.ts +4 -4
  39. package/src/ai/projects-authorization.ts +53 -0
  40. package/src/ai/projects-routes.ts +56 -46
  41. package/src/ai/projects.ts +100 -79
  42. package/src/ai/protocol.ts +3 -1
  43. package/src/ai/recent-resources.ts +3 -3
  44. package/src/ai/routes.ts +32 -30
  45. package/src/ai/skill-tool.ts +4 -4
  46. package/src/ai/skills-routes.ts +27 -26
  47. package/src/ai/skills.ts +120 -81
  48. package/src/ai/store.ts +19 -6
  49. package/src/ai/tools.ts +14 -0
  50. package/src/ai/types.ts +3 -0
  51. package/src/api/accounts-entities.ts +6 -3
  52. package/src/api/admin-ai-skills.ts +4 -1
  53. package/src/api/auth.ts +9 -2
  54. package/src/api/capabilities.ts +90 -12
  55. package/src/api/mcp.ts +4 -2
  56. package/src/browser/notifications.ts +25 -8
  57. package/src/browser/session.ts +39 -0
  58. package/src/browser/shared-text-history-validation.ts +151 -0
  59. package/src/browser/shared-text-history.ts +464 -0
  60. package/src/browser/shared-text-offline-authorized.ts +29 -0
  61. package/src/browser/shared-text-offline.ts +351 -0
  62. package/src/browser/shared-text-provider.ts +714 -0
  63. package/src/browser/shared-text-table-contract.ts +150 -0
  64. package/src/browser/shared-text.ts +19 -0
  65. package/src/capabilities/discussion-providers.ts +99 -0
  66. package/src/capabilities/executions.ts +2 -2
  67. package/src/capabilities/migrate.ts +10 -1
  68. package/src/capabilities/resource-readers.ts +58 -0
  69. package/src/capabilities/server-invoke.ts +23 -13
  70. package/src/capabilities/server.ts +17 -6
  71. package/src/capabilities/testing.ts +13 -0
  72. package/src/contracts/capabilities.ts +137 -4
  73. package/src/contracts/discussions.ts +91 -0
  74. package/src/contracts/geo-document.ts +60 -0
  75. package/src/contracts/geo-snapshot.ts +86 -0
  76. package/src/contracts/geo.ts +14 -0
  77. package/src/contracts/index.ts +2 -0
  78. package/src/contracts/outgoing-mail.ts +1 -1
  79. package/src/contracts/resource-types.ts +21 -0
  80. package/src/contracts/shared.ts +2 -1
  81. package/src/geo/AddressAutocomplete.tsx +213 -0
  82. package/src/geo/messages.ts +29 -0
  83. package/src/geo/navigation.ts +25 -0
  84. package/src/geo/ui.ts +2 -0
  85. package/src/server/api/respond.ts +10 -2
  86. package/src/server/index.ts +1 -0
  87. package/src/server/services/access.ts +23 -10
  88. package/src/server/services/index.ts +1 -0
  89. package/src/services/accounts/app.ts +56 -17
  90. package/src/services/accounts/entities.ts +36 -16
  91. package/src/services/accounts/group-sql.ts +23 -0
  92. package/src/services/accounts/groups.ts +26 -17
  93. package/src/services/accounts/identities.ts +8 -1
  94. package/src/services/accounts/identity-reconciliation.ts +3 -1
  95. package/src/services/accounts/local-groups.ts +141 -27
  96. package/src/services/accounts/machine-group-management.ts +94 -0
  97. package/src/services/accounts/users.ts +38 -8
  98. package/src/services/geo/cache.ts +2 -1
  99. package/src/services/geo/geoapify.ts +3 -0
  100. package/src/services/identity/invocation-token.ts +110 -0
  101. package/src/services/mandates/index.ts +2 -2
  102. package/src/services/mandates/policy.ts +1 -0
  103. package/src/services/pdf/geo-snapshot.ts +107 -0
  104. package/src/services/pdf/index.ts +2 -0
  105. package/src/services/session/index.ts +12 -3
  106. package/src/shared/email-html.ts +1 -0
  107. package/src/shared/markdown/extensions/katex.ts +8 -1
  108. package/src/shared/markdown/extensions/tables.ts +14 -1
  109. package/src/shared/markdown/index.ts +22 -11
  110. package/src/shared-text/anchors.ts +29 -0
  111. package/src/shared-text/awareness.ts +87 -0
  112. package/src/shared-text/changes.ts +27 -0
  113. package/src/shared-text/documents.ts +428 -77
  114. package/src/shared-text/events.ts +12 -2
  115. package/src/shared-text/index.ts +16 -1
  116. package/src/shared-text/log.ts +24 -5
  117. package/src/shared-text/snapshots.ts +69 -23
  118. package/src/shared-text/structural-history.ts +377 -0
  119. package/src/shared-text/table-actions.ts +432 -0
  120. package/src/shared-text/types.ts +24 -5
  121. package/src/ssr/AdminArea.tsx +2 -2
  122. package/src/ssr/AdminLayout.tsx +1 -1
  123. package/src/ssr/AdminSidebar.tsx +1 -1
  124. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  125. package/src/ssr/CloudShell.island.tsx +19 -3
  126. package/src/ssr/Layout.tsx +1 -1
  127. package/src/ssr/MobileProfileActions.tsx +2 -2
  128. package/src/ssr/ProfilePreferences.tsx +2 -1
  129. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  130. package/src/ssr/admin-navigation.ts +1 -11
  131. package/src/ssr/app-launchpad.tsx +6 -1
  132. package/src/ssr/platform-messages.ts +2 -0
  133. package/src/ssr/profile-actions.ts +3 -6
  134. package/src/ssr/shell-data.tsx +1 -1
  135. package/src/styles/global.css +2 -0
  136. package/src/workflows/language/expressions.ts +3 -2
  137. package/src/workflows/runtime/executor.ts +2 -1
@@ -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
+ };