@k2b/cloud 0.39.0 → 0.41.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.
- package/README.md +8 -0
- package/package.json +11 -4
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +217 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +49 -18
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +2 -2
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/background-review.ts +72 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +464 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +680 -0
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +456 -0
- package/src/ai/chat/turn-view.tsx +10 -160
- package/src/ai/chat/view-block.tsx +15 -5
- package/src/ai/chat-task-contracts.ts +4 -1
- package/src/ai/chat-task-reasons-migrate.ts +7 -0
- package/src/ai/chat-task-reasons.ts +1 -1
- package/src/ai/chat-tasks.ts +55 -25
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +198 -39
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/index.ts +3 -0
- package/src/ai/migrate.ts +26 -2
- package/src/ai/open-questions.ts +172 -0
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/routes.ts +195 -5
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +245 -24
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +36 -8
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +43 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +2 -0
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/app-approval.ts +46 -5
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +2 -0
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +67 -12
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +6 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +30 -3
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +159 -36
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +166 -262
- package/src/approvals/approval-card.ts +68 -0
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/app-approval.ts +54 -5
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +45 -3
- package/src/browser/composer-insert.ts +10 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +11 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +412 -10
- package/src/browser/panel.ts +37 -0
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +123 -16
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +3 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +13 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/users.ts +12 -1
- package/src/services/app-approval.ts +382 -59
- package/src/services/approvals/device.ts +49 -31
- package/src/services/approvals/index.ts +7 -5
- package/src/services/approvals/list.ts +27 -6
- package/src/services/approvals/question.ts +68 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +13 -0
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +296 -41
- package/src/services/session/recent.ts +13 -0
- package/src/services/webauthn.ts +12 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +10 -2
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +25 -2
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +222 -419
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +135 -0
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +9 -1
- package/src/ssr/PanelRuntime.island.tsx +7 -0
- package/src/ssr/PanelSlot.tsx +53 -3
- package/src/ssr/ShellRuntime.island.tsx +11 -2
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/app-color.ts +14 -9
- package/src/ssr/app-navigation.ts +16 -3
- package/src/ssr/compact-shell.browser-harness.ts +30 -22
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +21 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +53 -4
- package/src/ssr/platform-messages.ts +18 -0
- package/src/ssr/shell-data.tsx +2 -1
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +213 -9
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +49 -12
- package/src/styles/panel.css +47 -4
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +17 -11
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +17 -22
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Topic } from "@k2b/sync";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { compareStreamCursor } from "./cursor";
|
|
4
|
+
import { SHARED_TEXT_NAME, sharedTextNodeId, toBase64 } from "./events";
|
|
5
|
+
import { minimalTextDiff } from "./text-diff";
|
|
6
|
+
import type {
|
|
7
|
+
SharedTextActor,
|
|
8
|
+
SharedTextContributor,
|
|
9
|
+
SharedTextEditResult,
|
|
10
|
+
SharedTextSnapshot,
|
|
11
|
+
SharedTextStore,
|
|
12
|
+
SharedTextStoredState,
|
|
13
|
+
SharedTextSyncEvent,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
type DocumentLog = {
|
|
17
|
+
topic(): Pick<Topic<SharedTextSyncEvent>, "latestCursor" | "publish">;
|
|
18
|
+
replay(config: {
|
|
19
|
+
docId: string;
|
|
20
|
+
after: string | null;
|
|
21
|
+
until: string;
|
|
22
|
+
doc: Y.Doc;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
}): Promise<SharedTextContributor[]>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fallback text becomes the same Yjs items in every copy: an update published
|
|
29
|
+
* against one copy then applies to the next one, which a random client ID
|
|
30
|
+
* would leave pending and silently drop from the saved state.
|
|
31
|
+
*/
|
|
32
|
+
const FALLBACK_CLIENT_ID = 0;
|
|
33
|
+
|
|
34
|
+
const createDoc = (stored: SharedTextStoredState): Y.Doc => {
|
|
35
|
+
const doc = new Y.Doc({ gc: true });
|
|
36
|
+
try {
|
|
37
|
+
if (stored.state) Y.applyUpdate(doc, stored.state, "snapshot");
|
|
38
|
+
else if (stored.text) {
|
|
39
|
+
const clientID = doc.clientID;
|
|
40
|
+
doc.clientID = FALLBACK_CLIENT_ID;
|
|
41
|
+
doc.getText(SHARED_TEXT_NAME).insert(0, stored.text);
|
|
42
|
+
doc.clientID = clientID;
|
|
43
|
+
}
|
|
44
|
+
return doc;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
doc.destroy();
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const bindDocuments = <Saved, Context>({ topic, replay }: DocumentLog, store: SharedTextStore<Saved, Context>) => {
|
|
52
|
+
return {
|
|
53
|
+
async read(docId: string, options: { signal?: AbortSignal } = {}): Promise<SharedTextSnapshot | null> {
|
|
54
|
+
const stored = await store.load(docId, options);
|
|
55
|
+
if (!stored) return null;
|
|
56
|
+
const latest = await topic().latestCursor({ tenantId: docId });
|
|
57
|
+
if (!latest || (stored.cursor && compareStreamCursor(latest, stored.cursor) <= 0))
|
|
58
|
+
return { state: stored.state, text: stored.text, cursor: stored.cursor };
|
|
59
|
+
const doc = createDoc(stored);
|
|
60
|
+
try {
|
|
61
|
+
await replay({ docId, after: stored.cursor, until: latest, doc, signal: options.signal });
|
|
62
|
+
return { state: Y.encodeStateAsUpdate(doc), text: doc.getText(SHARED_TEXT_NAME).toString(), cursor: latest };
|
|
63
|
+
} finally {
|
|
64
|
+
doc.destroy();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
async edit(
|
|
68
|
+
docId: string,
|
|
69
|
+
options: { actor: SharedTextActor | null; context?: Context; signal?: AbortSignal },
|
|
70
|
+
update: (current: { text: string; contributors: SharedTextContributor[]; state(): Uint8Array }) => string | Promise<string>,
|
|
71
|
+
): Promise<SharedTextEditResult<Saved>> {
|
|
72
|
+
const requestedAt = Date.now();
|
|
73
|
+
const stored = await store.load(docId, { signal: options.signal });
|
|
74
|
+
if (!stored) return { status: "notFound" };
|
|
75
|
+
const handle = topic();
|
|
76
|
+
const target = await handle.latestCursor({ tenantId: docId });
|
|
77
|
+
const doc = createDoc(stored);
|
|
78
|
+
try {
|
|
79
|
+
let contributors: SharedTextContributor[] = [];
|
|
80
|
+
try {
|
|
81
|
+
if (target) contributors = await replay({ docId, after: stored.cursor, until: target, doc, signal: options.signal });
|
|
82
|
+
} catch (error) {
|
|
83
|
+
// A cancelled request is not a sync failure: it throws like every other abort before publication.
|
|
84
|
+
options.signal?.throwIfAborted();
|
|
85
|
+
return { status: "syncFailed", error };
|
|
86
|
+
}
|
|
87
|
+
options.signal?.throwIfAborted();
|
|
88
|
+
const ytext = doc.getText(SHARED_TEXT_NAME);
|
|
89
|
+
const text = ytext.toString();
|
|
90
|
+
const next = await update({ text, contributors, state: () => Y.encodeStateAsUpdate(doc) });
|
|
91
|
+
if (next === text) return { status: "unchanged", text };
|
|
92
|
+
// A request cancelled while the callback ran must not publish a write.
|
|
93
|
+
options.signal?.throwIfAborted();
|
|
94
|
+
const diff = minimalTextDiff(text, next);
|
|
95
|
+
const beforeEditState = Y.encodeStateVector(doc);
|
|
96
|
+
doc.transact(() => {
|
|
97
|
+
if (diff.deleteCount > 0) ytext.delete(diff.index, diff.deleteCount);
|
|
98
|
+
if (diff.insert.length > 0) ytext.insert(diff.index, diff.insert);
|
|
99
|
+
}, "cli-edit");
|
|
100
|
+
const actor = options.actor;
|
|
101
|
+
const published = await handle.publish({
|
|
102
|
+
tenantId: docId,
|
|
103
|
+
data: {
|
|
104
|
+
kind: "sync",
|
|
105
|
+
payload: toBase64(Y.encodeStateAsUpdate(doc, beforeEditState)),
|
|
106
|
+
originNodeId: sharedTextNodeId,
|
|
107
|
+
originPeerId: null,
|
|
108
|
+
...(actor ? { actor } : {}),
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
let persistedDoc: Y.Doc | undefined;
|
|
112
|
+
try {
|
|
113
|
+
persistedDoc = createDoc(stored);
|
|
114
|
+
await replay({ docId, after: stored.cursor, until: published.cursor, doc: persistedDoc, signal: options.signal });
|
|
115
|
+
const persistedText = persistedDoc.getText(SHARED_TEXT_NAME).toString();
|
|
116
|
+
const credited = new Map(contributors.map((contributor) => [`${contributor.kind}:${contributor.id}`, contributor]));
|
|
117
|
+
if (actor) credited.set(`${actor.kind}:${actor.id}`, { ...actor, lastContributedAt: new Date(requestedAt) });
|
|
118
|
+
const saved = await store.save(
|
|
119
|
+
docId,
|
|
120
|
+
{
|
|
121
|
+
state: Y.encodeStateAsUpdate(persistedDoc),
|
|
122
|
+
text: persistedText,
|
|
123
|
+
cursor: published.cursor,
|
|
124
|
+
restoreRevision: stored.restoreRevision,
|
|
125
|
+
requestedAt,
|
|
126
|
+
actor,
|
|
127
|
+
contributors: [...credited.values()],
|
|
128
|
+
},
|
|
129
|
+
options.context,
|
|
130
|
+
);
|
|
131
|
+
return { status: "edited", text: persistedText, cursor: published.cursor, saved };
|
|
132
|
+
} catch (error) {
|
|
133
|
+
return { status: "persistFailed", cursor: published.cursor, error };
|
|
134
|
+
} finally {
|
|
135
|
+
persistedDoc?.destroy();
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
doc.destroy();
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { fromBase64Strict } from "@k2b/stdlib";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import type { SharedTextEvent } from "./types";
|
|
4
|
+
|
|
5
|
+
export const SHARED_TEXT_NAME = "codemirror";
|
|
6
|
+
export const sharedTextNodeId = crypto.randomUUID();
|
|
7
|
+
export const toBase64 = (data: Uint8Array): string => Buffer.from(data).toString("base64");
|
|
8
|
+
export const fromBase64 = fromBase64Strict;
|
|
9
|
+
|
|
10
|
+
/** A retained event cannot be decoded as a Yjs update: retrying never helps, no replay can pass it. */
|
|
11
|
+
export class MalformedSharedTextEventError extends Error {
|
|
12
|
+
constructor(
|
|
13
|
+
readonly docId: string,
|
|
14
|
+
readonly cursor: string,
|
|
15
|
+
cause: unknown,
|
|
16
|
+
) {
|
|
17
|
+
super(`Malformed sync event for document ${docId} at cursor ${cursor}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
18
|
+
this.name = "MalformedSharedTextEventError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Ingress check: a publish is accepted only when it is base64 of a decodable Yjs update. */
|
|
23
|
+
export const isValidSharedTextUpdate = (payload: string): boolean => {
|
|
24
|
+
try {
|
|
25
|
+
Y.decodeUpdate(fromBase64(payload));
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const applySharedTextEvent = (doc: Y.Doc, event: { cursor: string; data: SharedTextEvent }, docId: string): void => {
|
|
33
|
+
if (event.data.kind !== "sync") return;
|
|
34
|
+
try {
|
|
35
|
+
const update = fromBase64(event.data.payload);
|
|
36
|
+
// Decode completely before mutating the document: malformed bytes must not
|
|
37
|
+
// partially integrate state before recovery skips the invalid event.
|
|
38
|
+
Y.decodeUpdate(update);
|
|
39
|
+
Y.applyUpdate(doc, update, `replay:${event.cursor}`);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new MalformedSharedTextEventError(docId, event.cursor, error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { compareStreamCursor, maxStreamCursor, parseStreamCursor } from "./cursor";
|
|
2
|
+
export {
|
|
3
|
+
applySharedTextEvent,
|
|
4
|
+
fromBase64,
|
|
5
|
+
isValidSharedTextUpdate,
|
|
6
|
+
MalformedSharedTextEventError,
|
|
7
|
+
SHARED_TEXT_NAME,
|
|
8
|
+
sharedTextNodeId,
|
|
9
|
+
toBase64,
|
|
10
|
+
} from "./events";
|
|
11
|
+
export { sharedText } from "./log";
|
|
12
|
+
export * from "./types";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { TopicConfig } from "@k2b/sync";
|
|
2
|
+
import type * as Y from "yjs";
|
|
3
|
+
import { getProcessSync } from "../_internal/process-sync";
|
|
4
|
+
import { compareStreamCursor } from "./cursor";
|
|
5
|
+
import { bindDocuments } from "./documents";
|
|
6
|
+
import { applySharedTextEvent } from "./events";
|
|
7
|
+
import type { SharedTextContributor, SharedTextStore, SharedTextSyncEvent } from "./types";
|
|
8
|
+
|
|
9
|
+
const define = (topicConfig: Readonly<Pick<TopicConfig, "id" | "retention" | "deadLetterRetention" | "maxPayloadBytes">>) => {
|
|
10
|
+
const topic = () => getProcessSync().topic<SharedTextSyncEvent>(topicConfig);
|
|
11
|
+
const replay = async (config: {
|
|
12
|
+
docId: string;
|
|
13
|
+
after: string | null;
|
|
14
|
+
until: string;
|
|
15
|
+
doc: Y.Doc;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}): Promise<SharedTextContributor[]> => {
|
|
18
|
+
const contributors = new Map<string, SharedTextContributor>();
|
|
19
|
+
const handle = topic();
|
|
20
|
+
const after = config.after ?? handle.cursorAt(0);
|
|
21
|
+
if (compareStreamCursor(after, config.until) >= 0) return [];
|
|
22
|
+
let reachedTarget = false;
|
|
23
|
+
for await (const event of handle.replay({ tenantId: config.docId, after, until: config.until, signal: config.signal })) {
|
|
24
|
+
applySharedTextEvent(config.doc, event, config.docId);
|
|
25
|
+
if (event.data.actor)
|
|
26
|
+
contributors.set(`${event.data.actor.kind}:${event.data.actor.id}`, {
|
|
27
|
+
...event.data.actor,
|
|
28
|
+
lastContributedAt: event.publishedAt,
|
|
29
|
+
});
|
|
30
|
+
reachedTarget = event.cursor === config.until;
|
|
31
|
+
}
|
|
32
|
+
if (!reachedTarget) throw new Error(`Target cursor "${config.until}" was not reached during replay`);
|
|
33
|
+
return [...contributors.values()];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id: topicConfig.id,
|
|
38
|
+
topicConfig,
|
|
39
|
+
topic,
|
|
40
|
+
replay,
|
|
41
|
+
withStore<Saved, Context>(store: SharedTextStore<Saved, Context>) {
|
|
42
|
+
return bindDocuments({ topic, replay }, store);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Server-only document logs. Applications authorize callers before read/edit. */
|
|
48
|
+
export const sharedText = { define };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const splitsSurrogatePair = (text: string, index: number): boolean => {
|
|
2
|
+
const before = text.charCodeAt(index - 1);
|
|
3
|
+
const after = text.charCodeAt(index);
|
|
4
|
+
return before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/** One replacement retaining the shared prefix and suffix at Unicode boundaries. */
|
|
8
|
+
export const minimalTextDiff = (before: string, after: string): { index: number; deleteCount: number; insert: string } => {
|
|
9
|
+
let index = 0;
|
|
10
|
+
while (index < before.length && index < after.length && before[index] === after[index]) index++;
|
|
11
|
+
if (splitsSurrogatePair(before, index) || splitsSurrogatePair(after, index)) index--;
|
|
12
|
+
|
|
13
|
+
let suffix = 0;
|
|
14
|
+
while (
|
|
15
|
+
suffix < before.length - index &&
|
|
16
|
+
suffix < after.length - index &&
|
|
17
|
+
before[before.length - suffix - 1] === after[after.length - suffix - 1]
|
|
18
|
+
)
|
|
19
|
+
suffix++;
|
|
20
|
+
if (splitsSurrogatePair(before, before.length - suffix) || splitsSurrogatePair(after, after.length - suffix)) suffix--;
|
|
21
|
+
|
|
22
|
+
return { index, deleteCount: before.length - index - suffix, insert: after.slice(index, after.length - suffix) };
|
|
23
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type SharedTextActor = { kind: "user" | "service_account"; id: string };
|
|
2
|
+
|
|
3
|
+
/** Actor is optional so events retained from older deployments remain readable. */
|
|
4
|
+
export type SharedTextEvent = {
|
|
5
|
+
kind: "sync" | "awareness";
|
|
6
|
+
payload: string;
|
|
7
|
+
originNodeId: string;
|
|
8
|
+
originPeerId: string | null;
|
|
9
|
+
actor?: SharedTextActor;
|
|
10
|
+
};
|
|
11
|
+
export type SharedTextSyncEvent = SharedTextEvent & { kind: "sync" };
|
|
12
|
+
export type SharedTextContributor = SharedTextActor & { lastContributedAt: Date };
|
|
13
|
+
|
|
14
|
+
/** The store anchors legacy cursors to this log before returning a snapshot. */
|
|
15
|
+
export type SharedTextStoredState = {
|
|
16
|
+
state: Uint8Array | null;
|
|
17
|
+
cursor: string | null;
|
|
18
|
+
restoreRevision: string;
|
|
19
|
+
text: string | null;
|
|
20
|
+
};
|
|
21
|
+
export type SharedTextSaveInput = {
|
|
22
|
+
state: Uint8Array;
|
|
23
|
+
text: string;
|
|
24
|
+
cursor: string;
|
|
25
|
+
restoreRevision: string;
|
|
26
|
+
requestedAt: number;
|
|
27
|
+
actor: SharedTextActor | null;
|
|
28
|
+
contributors: SharedTextContributor[];
|
|
29
|
+
};
|
|
30
|
+
export type SharedTextSnapshot = { state: Uint8Array | null; text: string | null; cursor: string | null };
|
|
31
|
+
export type SharedTextEditResult<Saved> =
|
|
32
|
+
| { status: "notFound" }
|
|
33
|
+
| { status: "unchanged"; text: string }
|
|
34
|
+
| { status: "syncFailed"; error: unknown }
|
|
35
|
+
| { status: "persistFailed"; cursor: string; error: unknown }
|
|
36
|
+
| { status: "edited"; text: string; cursor: string; saved: Saved };
|
|
37
|
+
|
|
38
|
+
export type SharedTextStore<Saved, Context> = {
|
|
39
|
+
load(docId: string, options: { signal?: AbortSignal }): Promise<SharedTextStoredState | null>;
|
|
40
|
+
save(docId: string, input: SharedTextSaveInput, context: Context | undefined): Promise<Saved>;
|
|
41
|
+
};
|
package/src/ssr/AdminLayout.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AppWorkspace } from "@k2b/ui";
|
|
2
2
|
import type { JSX } from "solid-js/jsx-runtime";
|
|
3
|
+
import type { PanelConsentSnapshot } from "../contracts/panel-context";
|
|
3
4
|
import type { RailSnapshot } from "../contracts/rail-preferences";
|
|
4
5
|
import { getLocale } from "../server/locale";
|
|
5
6
|
import type { LayoutAnnouncementsState } from "../server/middleware/settings";
|
|
@@ -10,6 +11,7 @@ import { getLocalizedRuntimeContext, type RuntimeContext } from "./runtime";
|
|
|
10
11
|
|
|
11
12
|
type Breadcrumb = { title: string; href?: string };
|
|
12
13
|
type AdminLayoutContext = {
|
|
14
|
+
get(key: "panelConsent"): PanelConsentSnapshot | undefined;
|
|
13
15
|
get(key: "railPreferences"): RailSnapshot | undefined;
|
|
14
16
|
get(key: "user"): any;
|
|
15
17
|
get(key: "page"): any;
|
|
@@ -143,7 +143,7 @@ export const AppLaunchpadPanel = (
|
|
|
143
143
|
<i class={shortcut.iconClass} aria-hidden="true" />
|
|
144
144
|
<AppBadge endpoint={shortcut.badge} appId={shortcut.appId} />
|
|
145
145
|
</span>
|
|
146
|
-
<span class="max-w-full truncate text-[11px] font-medium text-primary dark:text-white">
|
|
146
|
+
<span class="max-w-full k2b-truncate-fade text-[11px] font-medium text-primary dark:text-white">
|
|
147
147
|
{shortcut.label}
|
|
148
148
|
<Show when={badge()}>
|
|
149
149
|
<span class="sr-only">, {badge()}</span>
|
|
@@ -173,7 +173,7 @@ export const AppLaunchpadPanel = (
|
|
|
173
173
|
<i class={app.iconClass} />
|
|
174
174
|
<AppBadge endpoint={app.badge} appId={app.id} />
|
|
175
175
|
</span>
|
|
176
|
-
<span class="max-w-full truncate text-[11px] font-medium text-primary sm:text-xs dark:text-white">
|
|
176
|
+
<span class="max-w-full k2b-truncate-fade text-[11px] font-medium text-primary sm:text-xs dark:text-white">
|
|
177
177
|
{app.label}
|
|
178
178
|
<Show when={badge()}>
|
|
179
179
|
<span class="sr-only">, {badge()}</span>
|
|
@@ -95,7 +95,11 @@ function RailNavigation(props: { currentPath: string; panelOpen?: boolean }) {
|
|
|
95
95
|
<div class="mt-auto shrink-0 flex flex-col items-center gap-1">
|
|
96
96
|
{(data.panel?.providers.length ?? 0) > 1 && <PanelTrigger variant="rail" open={props.panelOpen} />}
|
|
97
97
|
<GlobalSearchTrigger variant="rail" />
|
|
98
|
-
<LayoutHelpTrigger
|
|
98
|
+
<LayoutHelpTrigger
|
|
99
|
+
variant="rail"
|
|
100
|
+
panel={data.panel?.providers.some((provider) => provider.id === "core")}
|
|
101
|
+
helpOpen={!!props.panelOpen && data.panel?.provider === "core"}
|
|
102
|
+
/>
|
|
99
103
|
<ProfilePreferences
|
|
100
104
|
appHref={data.profile.appHref}
|
|
101
105
|
avatarSrc={data.profile.avatarSrc}
|
|
@@ -148,7 +152,11 @@ function HeaderControls(props: {
|
|
|
148
152
|
<div class="flex items-center gap-1 lg:hidden">
|
|
149
153
|
{(data.panel?.providers.length ?? 0) > 1 && <PanelTrigger variant="header" open={props.panelOpen} />}
|
|
150
154
|
{props.authenticated && <GlobalSearchTrigger variant="header" />}
|
|
151
|
-
<LayoutHelpTrigger
|
|
155
|
+
<LayoutHelpTrigger
|
|
156
|
+
variant="header"
|
|
157
|
+
panel={data.panel?.providers.some((provider) => provider.id === "core")}
|
|
158
|
+
helpOpen={!!props.panelOpen && data.panel?.provider === "core"}
|
|
159
|
+
/>
|
|
152
160
|
{props.launchpad && (
|
|
153
161
|
<AppLaunchpadButton
|
|
154
162
|
profile={
|