@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
@@ -0,0 +1,351 @@
1
+ import * as Y from "yjs";
2
+ import { compareStreamCursor, parseStreamCursor } from "../shared-text/cursor";
3
+
4
+ /** Opaque IDs from the authenticated server, never a locally inferred generation. */
5
+ export interface SharedTextOfflineIdentity {
6
+ readonly userId: string;
7
+ readonly draftId: string;
8
+ readonly generation: string;
9
+ }
10
+
11
+ export interface SharedTextDurableSnapshot {
12
+ readonly identity: SharedTextOfflineIdentity;
13
+ /** Complete Yjs V1 state read from the application's committed durable store. */
14
+ readonly state: Uint8Array;
15
+ /** Caller attestation: a transport echo or a replay-only read is not durable. */
16
+ readonly durability: "durable";
17
+ }
18
+
19
+ export interface SharedTextOfflineSession {
20
+ /** Aborts on logout, close, or a lost database/channel. Observe it to stop the editor. */
21
+ readonly signal: AbortSignal;
22
+ load(): Promise<Uint8Array | null>;
23
+ /** State and its oldest unconfirmed input base, read in one transaction. */
24
+ loadPending(): Promise<{ state: Uint8Array; baseCursor: string | null } | null>;
25
+ /** Save Y.encodeStateAsUpdate(doc), including unconfirmed edits. Returns the merged tab state. */
26
+ save(state: Uint8Array, baseCursor?: string | null): Promise<Uint8Array>;
27
+ /** True when no saved state remains. Deletes only when the snapshot covers all saved structs AND deletes. */
28
+ acknowledgeSnapshot(snapshot: SharedTextDurableSnapshot): Promise<boolean>;
29
+ /** Explicit discard of this draft generation. Does not revoke other handles; retire the generation server-side. */
30
+ forget(): Promise<void>;
31
+ /** Idempotent; aborts pending operations and releases the database and channel. */
32
+ close(): void;
33
+ }
34
+
35
+ export class SharedTextOfflineLoggedOutError extends Error {
36
+ constructor() {
37
+ super("The shared text offline session was logged out.");
38
+ this.name = "SharedTextOfflineLoggedOutError";
39
+ }
40
+ }
41
+
42
+ const databaseName = "cloud-shared-text-offline";
43
+ const channelName = "cloud-shared-text-offline-logout";
44
+ const stores = ["users", "drafts"];
45
+
46
+ const abort = (tx: IDBTransaction) => {
47
+ try {
48
+ tx.abort();
49
+ } catch (error) {
50
+ // An aborted transaction can still be awaiting its abort event.
51
+ if (!(error instanceof DOMException && error.name === "InvalidStateError")) throw error;
52
+ }
53
+ };
54
+
55
+ const openDatabase = (): Promise<IDBDatabase> =>
56
+ new Promise((resolve, reject) => {
57
+ const request = indexedDB.open(databaseName, 1);
58
+ let blocked = false;
59
+ request.onupgradeneeded = () => {
60
+ const db = request.result;
61
+ db.createObjectStore("users");
62
+ db.createObjectStore("drafts", { keyPath: ["userId", "draftId", "generation"] }).createIndex("userId", "userId");
63
+ };
64
+ request.onerror = () => reject(request.error);
65
+ request.onblocked = () => {
66
+ blocked = true;
67
+ reject(new Error("Shared text offline database upgrade is blocked. Close older tabs and retry."));
68
+ };
69
+ request.onsuccess = () => {
70
+ const db = request.result;
71
+ db.onversionchange = () => db.close();
72
+ if (blocked) db.close();
73
+ else resolve(db);
74
+ };
75
+ });
76
+
77
+ /** Discover pending generations from the same store that owns their bytes, never a separate browser marker. */
78
+ export const listSharedTextOfflineGenerations = async (
79
+ identity: Pick<SharedTextOfflineIdentity, "userId" | "draftId">,
80
+ ): Promise<string[]> => {
81
+ const db = await openDatabase();
82
+ try {
83
+ return await transact<string[]>(db, "readonly", (tx, result, read) => {
84
+ const { userId, draftId } = identity;
85
+ // Array-key bounds include every string generation for this exact draft.
86
+ read(tx.objectStore("drafts").getAllKeys(IDBKeyRange.bound([userId, draftId], [userId, draftId, []])), (keys) => {
87
+ result(
88
+ keys.map((key) => {
89
+ if (!Array.isArray(key) || key.length !== 3 || key[0] !== userId || key[1] !== draftId || typeof key[2] !== "string")
90
+ throw new Error("Invalid shared text offline identity.");
91
+ return key[2];
92
+ }),
93
+ );
94
+ });
95
+ });
96
+ } finally {
97
+ db.close();
98
+ }
99
+ };
100
+
101
+ /** All callbacks stay in IDB request events; no asynchronous gap inside a live transaction. */
102
+ const transact = <T>(
103
+ db: IDBDatabase,
104
+ mode: IDBTransactionMode,
105
+ body: (tx: IDBTransaction, result: (value: T) => void, read: <V>(request: IDBRequest<V>, next: (value: V) => void) => void) => void,
106
+ active?: Map<IDBTransaction, (error: unknown) => void>,
107
+ ): Promise<T> =>
108
+ new Promise((resolve, reject) => {
109
+ const tx = db.transaction(stores, mode, mode === "readwrite" ? { durability: "strict" } : undefined);
110
+ active?.set(tx, reject);
111
+ let result: { value: T } | undefined;
112
+ let failure: { error: unknown } | undefined;
113
+ const fail = (error: unknown) => {
114
+ failure = { error };
115
+ abort(tx);
116
+ };
117
+ tx.oncomplete = () => {
118
+ active?.delete(tx);
119
+ if (result) resolve(result.value);
120
+ else reject(new Error("Shared text offline transaction completed without a result."));
121
+ };
122
+ tx.onabort = () => {
123
+ active?.delete(tx);
124
+ reject(failure ? failure.error : (tx.error ?? new DOMException("Shared text offline transaction aborted.", "AbortError")));
125
+ };
126
+ const read = <V>(request: IDBRequest<V>, next: (value: V) => void) => {
127
+ request.onsuccess = () => {
128
+ try {
129
+ next(request.result);
130
+ } catch (error) {
131
+ fail(error);
132
+ }
133
+ };
134
+ };
135
+ try {
136
+ body(
137
+ tx,
138
+ (value) => {
139
+ result = { value };
140
+ },
141
+ read,
142
+ );
143
+ } catch (error) {
144
+ fail(error);
145
+ }
146
+ });
147
+
148
+ const readEpoch = (value: unknown): number => {
149
+ if (value === undefined) return 0;
150
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error("Invalid shared text offline user epoch.");
151
+ return value;
152
+ };
153
+
154
+ const readState = (value: unknown): Uint8Array | null => {
155
+ if (value === undefined) return null;
156
+ if (typeof value !== "object" || value === null || !("state" in value) || !(value.state instanceof Uint8Array)) {
157
+ throw new Error("Invalid shared text offline draft record.");
158
+ }
159
+ // Do not mistake corrupt storage for an absent draft.
160
+ Y.decodeUpdate(value.state);
161
+ return value.state;
162
+ };
163
+
164
+ const inputBase = (value: unknown): string | null => {
165
+ if (value === undefined || value === null) return null;
166
+ if (typeof value !== "string" || value.length > 256 || !parseStreamCursor(value))
167
+ throw new Error("Invalid shared text offline input base.");
168
+ return value;
169
+ };
170
+ const recordBase = (value: unknown): string | null =>
171
+ inputBase(typeof value === "object" && value !== null && "baseCursor" in value ? value.baseCursor : null);
172
+
173
+ /**
174
+ * Open only after authentication, with the server's current generation. To avoid
175
+ * remembering this device, the consumer keeps its Y.Doc in memory and never opens
176
+ * this store (and explicitly forgets any earlier persisted state if switching).
177
+ * Storage failures reject; this API never falls back to memory.
178
+ */
179
+ export const openSharedTextOffline = async (identity: SharedTextOfflineIdentity): Promise<SharedTextOfflineSession> => {
180
+ const { userId, draftId, generation } = identity;
181
+ const key = [userId, draftId, generation];
182
+ const db = await openDatabase();
183
+ const controller = new AbortController();
184
+ const active = new Map<IDBTransaction, (error: unknown) => void>();
185
+ let channel: BroadcastChannel | undefined;
186
+ let epoch: number | undefined;
187
+ let observedLogout = 0;
188
+
189
+ const close = (reason: Error) => {
190
+ if (controller.signal.aborted) return;
191
+ // WebKit may never fire `abort` for a transaction still queued behind another connection; settle it here.
192
+ for (const [tx, reject] of active) {
193
+ abort(tx);
194
+ reject(new DOMException("Shared text offline transaction aborted.", "AbortError"));
195
+ }
196
+ active.clear();
197
+ db.close();
198
+ channel?.close();
199
+ controller.abort(reason);
200
+ };
201
+ db.onversionchange = () => close(new Error("Shared text offline database version changed. Reopen the session."));
202
+ db.onclose = () => close(new Error("Shared text offline database connection closed unexpectedly."));
203
+
204
+ try {
205
+ channel = new BroadcastChannel(channelName);
206
+ channel.onmessageerror = () => close(new Error("Shared text offline logout notification could not be read."));
207
+ channel.onmessage = (event: MessageEvent<unknown>) => {
208
+ const message = event.data;
209
+ if (
210
+ typeof message === "object" &&
211
+ message !== null &&
212
+ "userId" in message &&
213
+ message.userId === userId &&
214
+ "epoch" in message &&
215
+ typeof message.epoch === "number" &&
216
+ Number.isSafeInteger(message.epoch)
217
+ ) {
218
+ observedLogout = Math.max(observedLogout, message.epoch);
219
+ if (epoch !== undefined && observedLogout > epoch) close(new SharedTextOfflineLoggedOutError());
220
+ }
221
+ };
222
+ epoch = await transact<number>(
223
+ db,
224
+ "readonly",
225
+ (tx, result, read) => {
226
+ read(tx.objectStore("users").get(userId), (value: unknown) => result(readEpoch(value)));
227
+ },
228
+ active,
229
+ );
230
+ if (observedLogout > epoch) throw new SharedTextOfflineLoggedOutError();
231
+ controller.signal.throwIfAborted();
232
+ } catch (error) {
233
+ close(error instanceof Error ? error : new Error(String(error)));
234
+ throw error;
235
+ }
236
+
237
+ const run = async <T>(
238
+ mode: IDBTransactionMode,
239
+ action: (drafts: IDBObjectStore, state: Uint8Array | null, baseCursor: string | null) => T,
240
+ discard = false,
241
+ ): Promise<T> => {
242
+ controller.signal.throwIfAborted();
243
+ try {
244
+ return await transact<T>(
245
+ db,
246
+ mode,
247
+ (tx, result, read) => {
248
+ read(tx.objectStore("users").get(userId), (value: unknown) => {
249
+ // The epoch check and the draft access share one transaction. A delayed
250
+ // save cannot recreate data after logout, even without a broadcast.
251
+ if (readEpoch(value) !== epoch) throw new SharedTextOfflineLoggedOutError();
252
+ const drafts = tx.objectStore("drafts");
253
+ if (discard) result(action(drafts, null, null));
254
+ else read(drafts.get(key), (record: unknown) => result(action(drafts, readState(record), recordBase(record))));
255
+ });
256
+ },
257
+ active,
258
+ );
259
+ } catch (error) {
260
+ if (error instanceof SharedTextOfflineLoggedOutError) close(error);
261
+ throw error;
262
+ }
263
+ };
264
+
265
+ return {
266
+ signal: controller.signal,
267
+ load: () => run("readonly", (_drafts, state) => state),
268
+ loadPending: () => run("readonly", (_drafts, state, baseCursor) => (state ? { state, baseCursor } : null)),
269
+ save: async (state, baseCursor = null) => {
270
+ const base = inputBase(baseCursor);
271
+ const input = state.slice();
272
+ Y.decodeUpdate(input);
273
+ return run("readwrite", (drafts, saved, oldBase) => {
274
+ const merged = saved ? Y.mergeUpdates([saved, input]) : input;
275
+ // Preserve the earliest source position across tabs. A newly read server
276
+ // head must never masquerade as the base of older offline input.
277
+ const mergedBase = !saved
278
+ ? base
279
+ : oldBase === null || base === null
280
+ ? null
281
+ : compareStreamCursor(oldBase, base) <= 0
282
+ ? oldBase
283
+ : base;
284
+ drafts.put({ userId, draftId, generation, state: merged, baseCursor: mergedBase });
285
+ return merged;
286
+ });
287
+ },
288
+ acknowledgeSnapshot: async (snapshot) => {
289
+ if (
290
+ snapshot.durability !== "durable" ||
291
+ snapshot.identity.userId !== userId ||
292
+ snapshot.identity.draftId !== draftId ||
293
+ snapshot.identity.generation !== generation
294
+ )
295
+ throw new Error("A durable snapshot for this exact shared text identity is required.");
296
+ const doc = new Y.Doc();
297
+ try {
298
+ const state = snapshot.state.slice();
299
+ Y.applyUpdate(doc, state);
300
+ const coverage = Y.snapshot(doc);
301
+ if (!Y.snapshotContainsUpdate(coverage, state)) throw new Error("The durable snapshot must be complete Yjs state.");
302
+ return await run("readwrite", (drafts, saved) => {
303
+ if (saved && !Y.snapshotContainsUpdate(coverage, saved)) return false;
304
+ drafts.delete(key);
305
+ return true;
306
+ });
307
+ } finally {
308
+ doc.destroy();
309
+ }
310
+ },
311
+ forget: () =>
312
+ run(
313
+ "readwrite",
314
+ (drafts) => {
315
+ drafts.delete(key);
316
+ },
317
+ true,
318
+ ),
319
+ close: () => close(new Error("Shared text offline session is closed.")),
320
+ };
321
+ };
322
+
323
+ /**
324
+ * Erase every draft/generation for this user and fence existing handles in one
325
+ * transaction. Keep the epoch tombstone: deleting it would revalidate old tabs.
326
+ * The consumer must finish logout before opening a freshly authenticated session.
327
+ */
328
+ export const logoutSharedTextOffline = async (userId: string): Promise<void> => {
329
+ const db = await openDatabase();
330
+ let channel: BroadcastChannel | undefined;
331
+ try {
332
+ channel = new BroadcastChannel(channelName);
333
+ const epoch = await transact<number>(db, "readwrite", (tx, result, read) => {
334
+ read(tx.objectStore("users").get(userId), (value: unknown) => {
335
+ const next = readEpoch(value) + 1;
336
+ if (!Number.isSafeInteger(next)) throw new Error("Shared text offline user epoch exhausted.");
337
+ tx.objectStore("users").put(next, userId);
338
+ read(tx.objectStore("drafts").index("userId").openCursor(IDBKeyRange.only(userId)), (cursor) => {
339
+ if (cursor) {
340
+ cursor.delete();
341
+ cursor.continue();
342
+ } else result(next);
343
+ });
344
+ });
345
+ });
346
+ channel.postMessage({ userId, epoch });
347
+ } finally {
348
+ channel?.close();
349
+ db.close();
350
+ }
351
+ };