@ixo/oracle-runtime-workers 0.2.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 +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { tool } from '@langchain/core/tools';
|
|
2
|
+
import type { StructuredTool } from 'langchain';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { EDITOR_AGENT_TOOL_NAME } from './editor-agent';
|
|
5
|
+
import { GRANT_ACCESS_TOOL } from './failures';
|
|
6
|
+
import type { EditorUnavailableReason } from './prompts';
|
|
7
|
+
|
|
8
|
+
// Mirrors the real sub-agent tool's schema so a model that composed a task for
|
|
9
|
+
// the document assistant still produces a valid call — the answer is just the
|
|
10
|
+
// denial.
|
|
11
|
+
const taskSchema = z.object({
|
|
12
|
+
task: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe('The task you intended to send to the document assistant.'),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export interface EditorAccessDeniedToolOptions {
|
|
18
|
+
/** The document room the client reported open. */
|
|
19
|
+
editorRoomId: string;
|
|
20
|
+
/** Why the real document surface refused to bind. */
|
|
21
|
+
reason: EditorUnavailableReason;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Stand-in for `call_editor_agent` bound when a document is open but the real
|
|
26
|
+
* surface refused to attach (the user is not a verified member of the room, or
|
|
27
|
+
* the sub-agent build failed).
|
|
28
|
+
*
|
|
29
|
+
* Without it, the model is shown a document capability it cannot reach and
|
|
30
|
+
* either narrates its sub-agent task as user-facing text or hunts through
|
|
31
|
+
* `list_capabilities` / `load_capability` for a tool that will never appear.
|
|
32
|
+
*/
|
|
33
|
+
export function createEditorAccessDeniedTool(
|
|
34
|
+
options: EditorAccessDeniedToolOptions,
|
|
35
|
+
): StructuredTool {
|
|
36
|
+
const { editorRoomId, reason } = options;
|
|
37
|
+
|
|
38
|
+
// Membership is resolved with the oracle's admin identity, so a failed check
|
|
39
|
+
// can mean either side is missing — the user OR this oracle (the lookup is
|
|
40
|
+
// forbidden and fails closed). Keep the wording unified.
|
|
41
|
+
const message =
|
|
42
|
+
reason === 'not-member'
|
|
43
|
+
? `Document unavailable: membership of the document's room (${editorRoomId}) could not be verified — ` +
|
|
44
|
+
`either the user's account or this oracle is not a member, and BOTH must be for document access. ` +
|
|
45
|
+
`Tell the user the document can't be read or edited because either they or this oracle is missing from its room; ` +
|
|
46
|
+
`inviting this oracle to the document (or opening one they own) fixes it. ` +
|
|
47
|
+
`If they only need write access to a document you can already read, the \`${GRANT_ACCESS_TOOL}\` browser tool is what grants it. ` +
|
|
48
|
+
`Do not retry this tool.`
|
|
49
|
+
: `Document unavailable: the document service failed to attach to the room (${editorRoomId}). ` +
|
|
50
|
+
`Tell the user the document assistant is currently unavailable and they can retry shortly. Do not retry this tool.`;
|
|
51
|
+
|
|
52
|
+
return tool(async () => message, {
|
|
53
|
+
name: EDITOR_AGENT_TOOL_NAME,
|
|
54
|
+
description:
|
|
55
|
+
'Document assistant (unavailable for this request — calls return the denial reason).',
|
|
56
|
+
schema: taskSchema,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of the `call_editor_agent` tool, and the per-room provider config
|
|
3
|
+
* behind it. The tool itself lives in `standalone-editor-tool.ts`: one tool
|
|
4
|
+
* that targets the open document by default and any `room_id` on request.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computeSubAgentToolName } from '../../core/subagent-as-tool';
|
|
8
|
+
import type { BlocknoteToolsConfig } from './editor-config';
|
|
9
|
+
import type { AppConfig, MatrixRoomConfig } from './provider';
|
|
10
|
+
|
|
11
|
+
/** Display name the tool name is derived from. */
|
|
12
|
+
export const EDITOR_AGENT_NAME = 'Editor Agent';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The tool name the main agent sees for the document surface. Derived through
|
|
16
|
+
* `computeSubAgentToolName` so it stays stable for the prompt composer, which
|
|
17
|
+
* checks this name before injecting the document-mode prompts: telling the
|
|
18
|
+
* model a document is open without this tool bound makes it narrate its
|
|
19
|
+
* delegation as user-facing text instead of calling anything.
|
|
20
|
+
*/
|
|
21
|
+
export const EDITOR_AGENT_TOOL_NAME =
|
|
22
|
+
computeSubAgentToolName(EDITOR_AGENT_NAME);
|
|
23
|
+
|
|
24
|
+
/** Compose the per-room provider config from the plugin's boot-time config. */
|
|
25
|
+
export function buildAppConfig(
|
|
26
|
+
base: BlocknoteToolsConfig,
|
|
27
|
+
room: MatrixRoomConfig,
|
|
28
|
+
): AppConfig {
|
|
29
|
+
return {
|
|
30
|
+
matrix: { ...base.matrix, room },
|
|
31
|
+
provider: { ...base.provider },
|
|
32
|
+
blocknote: { ...base.blocknote },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matrix + provider configuration shared by every editor surface (and by the
|
|
3
|
+
* flows plugin, which opens the same Matrix-backed Y.Docs).
|
|
4
|
+
*
|
|
5
|
+
* The three Matrix env vars this reads are owned by the runtime's core base env
|
|
6
|
+
* schema, so the editor plugin declares no `configSchema` of its own — it only
|
|
7
|
+
* extracts typed values out of the already-validated `ctx.config`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { MatrixClient } from 'matrix-js-sdk';
|
|
11
|
+
|
|
12
|
+
/** Matrix admin credentials the editor uses to read/write Y.Docs. */
|
|
13
|
+
export interface BlocknoteToolsMatrixConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
accessToken: string;
|
|
16
|
+
userId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the provider/document config from Matrix credentials. Called once per
|
|
21
|
+
* request by the editor plugin and per call by the flows plugin.
|
|
22
|
+
*
|
|
23
|
+
* `writer.retryIfForbiddenInterval` / `maxForbiddenRetries` are deliberately
|
|
24
|
+
* tight: matrix-crdt defaults to retrying a rejected write every 30s up to 3
|
|
25
|
+
* times, which would keep a tool call open for ~90s before it could report the
|
|
26
|
+
* truth. Room power levels cannot change mid-call, so a rejected write is
|
|
27
|
+
* final — fail fast and let the caller surface `needs_access`.
|
|
28
|
+
*/
|
|
29
|
+
export function buildBlocknoteToolsConfig(matrix: BlocknoteToolsMatrixConfig) {
|
|
30
|
+
return {
|
|
31
|
+
matrix: {
|
|
32
|
+
baseUrl: matrix.baseUrl,
|
|
33
|
+
accessToken: matrix.accessToken,
|
|
34
|
+
userId: matrix.userId,
|
|
35
|
+
initialSyncTimeoutMs: 30_000,
|
|
36
|
+
},
|
|
37
|
+
provider: {
|
|
38
|
+
docName: 'document',
|
|
39
|
+
enableAwareness: false,
|
|
40
|
+
retryAttempts: 3,
|
|
41
|
+
retryDelayMs: 5_000,
|
|
42
|
+
flushInterval: 10,
|
|
43
|
+
retryIfForbiddenInterval: 1_000,
|
|
44
|
+
maxForbiddenRetries: 1,
|
|
45
|
+
},
|
|
46
|
+
blocknote: {
|
|
47
|
+
defaultBlockId: undefined,
|
|
48
|
+
blockNamespace: undefined,
|
|
49
|
+
mutableAttributeKeys: [] as string[],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The editor's config, optionally augmented with a host-provided
|
|
56
|
+
* `MatrixClient`. When `matrixClient` is set, `resolveEditorMatrixClient` uses
|
|
57
|
+
* it directly; when absent, the editor constructs its internal singleton from
|
|
58
|
+
* the Matrix credentials.
|
|
59
|
+
*/
|
|
60
|
+
export type BlocknoteToolsConfig = ReturnType<
|
|
61
|
+
typeof buildBlocknoteToolsConfig
|
|
62
|
+
> & {
|
|
63
|
+
matrixClient?: MatrixClient;
|
|
64
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createClient, type MatrixClient } from 'matrix-js-sdk';
|
|
2
|
+
import { createConsoleLogger } from '../../core/utils';
|
|
3
|
+
|
|
4
|
+
/** Matrix admin credentials needed to bootstrap the editor client. */
|
|
5
|
+
export interface EditorMatrixClientConfig {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
accessToken: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const logger = createConsoleLogger({ module: 'editor-mx' });
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-isolate singleton wrapper around a `matrix-js-sdk` `MatrixClient`. Used
|
|
15
|
+
* by `@ixo/matrix-crdt` to read/write BlockNote Y.js documents.
|
|
16
|
+
*
|
|
17
|
+
* No `startClient()` / sync is performed — matrix-crdt's `MatrixReader`
|
|
18
|
+
* explicitly tells consumers NOT to use the Sync API and polls the
|
|
19
|
+
* `/events` endpoint itself. No crypto is initialised either
|
|
20
|
+
* (`initRustCrypto` is never called): editor rooms are not E2EE at the doc
|
|
21
|
+
* level in this runtime — matrix-crdt stores updates in its own plain state
|
|
22
|
+
* events — so a crypto-less client is correct, and the crypto WASM stays
|
|
23
|
+
* untouched outside the gateway Durable Object.
|
|
24
|
+
*/
|
|
25
|
+
export class EditorMatrixClient {
|
|
26
|
+
private static instance: EditorMatrixClient | null = null;
|
|
27
|
+
|
|
28
|
+
private readonly cfg: EditorMatrixClientConfig;
|
|
29
|
+
private matrixClient: MatrixClient | null = null;
|
|
30
|
+
private isInitialized = false;
|
|
31
|
+
private initializationPromise: Promise<void> | null = null;
|
|
32
|
+
|
|
33
|
+
private constructor(cfg: EditorMatrixClientConfig) {
|
|
34
|
+
this.cfg = cfg;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Get the singleton instance. First caller supplies the Matrix admin
|
|
39
|
+
* config used for the lifetime of the isolate; subsequent callers
|
|
40
|
+
* receive the same instance and the config argument is ignored.
|
|
41
|
+
*/
|
|
42
|
+
public static getInstance(cfg: EditorMatrixClientConfig): EditorMatrixClient {
|
|
43
|
+
if (!EditorMatrixClient.instance) {
|
|
44
|
+
EditorMatrixClient.instance = new EditorMatrixClient(cfg);
|
|
45
|
+
}
|
|
46
|
+
return EditorMatrixClient.instance;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Test-only — reset the singleton so tests can swap the underlying client.
|
|
51
|
+
*/
|
|
52
|
+
public static resetForTesting(): void {
|
|
53
|
+
EditorMatrixClient.instance = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build the underlying `matrix-js-sdk` client. Idempotent — safe to call
|
|
58
|
+
* concurrently; only the first call does work.
|
|
59
|
+
*/
|
|
60
|
+
public async init(): Promise<void> {
|
|
61
|
+
if (this.isInitialized && this.matrixClient) return;
|
|
62
|
+
if (this.initializationPromise) return this.initializationPromise;
|
|
63
|
+
|
|
64
|
+
this.initializationPromise = this.performInitialization();
|
|
65
|
+
try {
|
|
66
|
+
await this.initializationPromise;
|
|
67
|
+
} finally {
|
|
68
|
+
this.initializationPromise = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async performInitialization(): Promise<void> {
|
|
73
|
+
const { baseUrl, userId, accessToken } = this.cfg;
|
|
74
|
+
if (!baseUrl || !userId || !accessToken) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'Missing Matrix configuration for the editor client (base URL, user id and the bot device token from the gateway).',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.matrixClient = createClient({
|
|
81
|
+
baseUrl,
|
|
82
|
+
accessToken,
|
|
83
|
+
userId,
|
|
84
|
+
timelineSupport: true,
|
|
85
|
+
// Never hand the SDK the bare global: workerd rejects `fetch` invoked
|
|
86
|
+
// with a foreign `this` ("Illegal invocation"), and the SDK calls
|
|
87
|
+
// `this.fetchFn(...)` on its own http-api object.
|
|
88
|
+
fetchFn: (input, init) => fetch(input, init),
|
|
89
|
+
});
|
|
90
|
+
this.isInitialized = true;
|
|
91
|
+
logger.log('EditorMatrixClient ready (no sync, polling-only mode)');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public getClient(): MatrixClient {
|
|
95
|
+
if (!this.isInitialized || !this.matrixClient) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'EditorMatrixClient not initialized. Call await init() first.',
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return this.matrixClient;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public isReady(): boolean {
|
|
104
|
+
return this.isInitialized && this.matrixClient !== null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async waitUntilReady(): Promise<void> {
|
|
108
|
+
if (this.isReady()) return;
|
|
109
|
+
await this.init();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the `MatrixClient` used by editor tools. Prefers the
|
|
115
|
+
* `matrixClient` from the plugin's runtime config (host/test-provided);
|
|
116
|
+
* otherwise lazily constructs the internal singleton.
|
|
117
|
+
*
|
|
118
|
+
* Centralised so every call site (standalone-editor-tool, the flows plugin's
|
|
119
|
+
* flow-doc) goes through one resolution path.
|
|
120
|
+
*/
|
|
121
|
+
export async function resolveEditorMatrixClient(
|
|
122
|
+
cfg: EditorMatrixClientConfig & { matrixClient?: MatrixClient },
|
|
123
|
+
): Promise<MatrixClient> {
|
|
124
|
+
if (cfg.matrixClient) return cfg.matrixClient;
|
|
125
|
+
const inst = EditorMatrixClient.getInstance(cfg);
|
|
126
|
+
await inst.init();
|
|
127
|
+
return inst.getClient();
|
|
128
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { MatrixClient } from 'matrix-js-sdk';
|
|
2
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
3
|
+
import type {
|
|
4
|
+
PluginManifest,
|
|
5
|
+
PluginTool,
|
|
6
|
+
RuntimeContext,
|
|
7
|
+
} from '../../plugin-api/types';
|
|
8
|
+
import { EDITOR_AGENT_TOOL_NAME } from './editor-agent';
|
|
9
|
+
import {
|
|
10
|
+
buildBlocknoteToolsConfig,
|
|
11
|
+
type BlocknoteToolsConfig,
|
|
12
|
+
} from './editor-config';
|
|
13
|
+
|
|
14
|
+
// This module must stay statically LIGHT: constructing `new EditorPlugin()`
|
|
15
|
+
// happens at Worker module scope (src/plugins/index.ts), and the tool's
|
|
16
|
+
// module graph (`standalone-editor-tool` → content-tools → provider →
|
|
17
|
+
// @ixo/matrix-crdt → vscode-lib, plus @blocknote/server-util) schedules a
|
|
18
|
+
// timer during module evaluation (vscode-lib's `IdleValue`/`runWhenIdle`),
|
|
19
|
+
// which workerd forbids in global scope. The chain is therefore reached only
|
|
20
|
+
// through `await import()` inside `getRequestTools`, which runs per turn
|
|
21
|
+
// inside a Durable Object request context where timers are allowed.
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Constructor options for `EditorPlugin`. Pass `matrixClient` when the host
|
|
25
|
+
* (or a test) already owns a long-lived matrix-js-sdk client — every editor
|
|
26
|
+
* surface will reuse it instead of constructing the internal singleton.
|
|
27
|
+
*/
|
|
28
|
+
export interface EditorPluginOptions {
|
|
29
|
+
matrixClient?: MatrixClient;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The editor's own polling client needs a device token. It never comes from
|
|
34
|
+
* the env: the gateway keeps a dedicated crypto-less device for plugins and
|
|
35
|
+
* hands its credentials out through `ctx.matrix.botCredentials()`. If that
|
|
36
|
+
* fails the plugin logs and contributes no tools rather than failing the
|
|
37
|
+
* whole request build.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
const manifest: PluginManifest = {
|
|
41
|
+
title: 'Documents',
|
|
42
|
+
summary:
|
|
43
|
+
"Reads and edits the content of the user's editor documents — prose, " +
|
|
44
|
+
'structure, and ordering.',
|
|
45
|
+
whenToUse: [
|
|
46
|
+
'User asks to read, summarize, or explain the document they have open.',
|
|
47
|
+
'User asks to write, rewrite, shorten, expand, or restructure document content.',
|
|
48
|
+
'User asks to fix wording, insert a section, reorder sections, or delete content.',
|
|
49
|
+
'User names a document in their workspace and wants it read or edited.',
|
|
50
|
+
],
|
|
51
|
+
whenNotToUse: [
|
|
52
|
+
'Building or configuring flows, action blocks, or forms — this surface edits content only.',
|
|
53
|
+
'Running or executing anything inside a document.',
|
|
54
|
+
'IXO entity lookups (use Domain Indexer) — documents are pages, not entities.',
|
|
55
|
+
'Web search or scraping (use Firecrawl); long-term user memory (use Memory).',
|
|
56
|
+
],
|
|
57
|
+
examples: [
|
|
58
|
+
{
|
|
59
|
+
user: 'Summarize this page.',
|
|
60
|
+
thought:
|
|
61
|
+
'Delegate to the document assistant with the open room. It reads the document and returns a summary.',
|
|
62
|
+
tool: EDITOR_AGENT_TOOL_NAME,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
user: 'Rename the "Introduction" heading to "Getting Started".',
|
|
66
|
+
thought:
|
|
67
|
+
'Content edit — delegate with the exact old and new text so the assistant can locate the block and replace the text.',
|
|
68
|
+
tool: EDITOR_AGENT_TOOL_NAME,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
tags: ['editor', 'documents', 'pages', 'blocknote', 'content'],
|
|
72
|
+
category: 'data',
|
|
73
|
+
// On-demand so ordinary chats carry no editor Tier-1 entry or gated tools.
|
|
74
|
+
visibility: 'on-demand',
|
|
75
|
+
stability: 'stable',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function toolsConfigFor(
|
|
79
|
+
creds: { baseUrl: string; userId: string; accessToken: string },
|
|
80
|
+
matrixClient?: MatrixClient,
|
|
81
|
+
): BlocknoteToolsConfig {
|
|
82
|
+
return {
|
|
83
|
+
...buildBlocknoteToolsConfig(creds),
|
|
84
|
+
matrixClient,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Document content plugin. Contributes one tool, `call_editor_agent`, on every
|
|
90
|
+
* request: it targets the document the user has open (`state.editorRoomId`)
|
|
91
|
+
* by default and any `room_id` the agent names per call.
|
|
92
|
+
*
|
|
93
|
+
* Access is enforced per call inside the tool with `isUserInRoom` (via the
|
|
94
|
+
* gateway Durable Object): the oracle acts with an admin Matrix identity, so
|
|
95
|
+
* "the user's documents" is always computed from the user's membership, never
|
|
96
|
+
* from what the admin can see.
|
|
97
|
+
*/
|
|
98
|
+
export class EditorPlugin extends OraclePlugin {
|
|
99
|
+
static readonly NAME = 'editor';
|
|
100
|
+
|
|
101
|
+
readonly name = EditorPlugin.NAME;
|
|
102
|
+
|
|
103
|
+
readonly version = '2.0.0';
|
|
104
|
+
|
|
105
|
+
readonly manifest = manifest;
|
|
106
|
+
|
|
107
|
+
// Intentionally no `configSchema` — the Matrix env vars the editor needs are
|
|
108
|
+
// owned by the core env schema. See `matrixConfigSchema` above.
|
|
109
|
+
|
|
110
|
+
private readonly matrixClient?: MatrixClient;
|
|
111
|
+
|
|
112
|
+
constructor(options: EditorPluginOptions = {}) {
|
|
113
|
+
super();
|
|
114
|
+
this.matrixClient = options.matrixClient;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override async getRequestTools(rtCtx: RuntimeContext): Promise<PluginTool[]> {
|
|
118
|
+
// Always bound. One tool covers every case: it targets the open document
|
|
119
|
+
// by default and any `room_id` the agent names, so a page created mid-turn
|
|
120
|
+
// (`create_page_room`) can be written into while another document is open.
|
|
121
|
+
//
|
|
122
|
+
// Not gated on `state.editorRoomId` or `state.spaceId` either — tools are
|
|
123
|
+
// resolved once per request, so a room that only exists after the turn
|
|
124
|
+
// starts is never in state. Access is enforced per call inside the tool
|
|
125
|
+
// (`isUserInRoom(room_id, user)`, fail closed); space membership never
|
|
126
|
+
// implied rights over a document, and the room need not live in that space.
|
|
127
|
+
try {
|
|
128
|
+
const toolsConfig = toolsConfigFor(
|
|
129
|
+
await rtCtx.matrix.botCredentials(),
|
|
130
|
+
this.matrixClient,
|
|
131
|
+
);
|
|
132
|
+
// Lazy on purpose — see the module header. The module registry caches
|
|
133
|
+
// the import, so only the first turn in an isolate pays for it.
|
|
134
|
+
const { createStandaloneEditorTool } =
|
|
135
|
+
await import('./standalone-editor-tool');
|
|
136
|
+
return [createStandaloneEditorTool({ toolsConfig })];
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
139
|
+
rtCtx.logger.error(`[editor] failed to build standalone tool: ${detail}`);
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failures the content tools return as *values*, never as thrown strings.
|
|
3
|
+
*
|
|
4
|
+
* The agent has to be able to branch on the reason: `needs_access` means "ask
|
|
5
|
+
* the user to grant access", `read_only_flow` means "describe, do not edit",
|
|
6
|
+
* `prop_not_editable` means "that prop is off limits — say which".
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The Portal browser tool the user runs to give the assistant write access. */
|
|
10
|
+
export const GRANT_ACCESS_TOOL = 'grant_assistant_access';
|
|
11
|
+
|
|
12
|
+
export type EditorFailureCode =
|
|
13
|
+
| 'needs_access'
|
|
14
|
+
| 'read_only_flow'
|
|
15
|
+
| 'prop_not_editable'
|
|
16
|
+
| 'block_not_found'
|
|
17
|
+
| 'not_a_member'
|
|
18
|
+
| 'no_document'
|
|
19
|
+
| 'flush_timeout'
|
|
20
|
+
| 'error';
|
|
21
|
+
|
|
22
|
+
export interface EditorFailure {
|
|
23
|
+
ok: false;
|
|
24
|
+
code: EditorFailureCode;
|
|
25
|
+
message: string;
|
|
26
|
+
/** The offending prop names, for `prop_not_editable`. */
|
|
27
|
+
props?: string[];
|
|
28
|
+
/** The block the failure is about, when there is one. */
|
|
29
|
+
blockId?: string;
|
|
30
|
+
roomId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isEditorFailure(value: unknown): value is EditorFailure {
|
|
34
|
+
return (
|
|
35
|
+
typeof value === 'object' &&
|
|
36
|
+
value !== null &&
|
|
37
|
+
'ok' in value &&
|
|
38
|
+
value.ok === false &&
|
|
39
|
+
'code' in value
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The assistant is in the document but cannot write to it. The user has to
|
|
45
|
+
* grant access from the Portal; naming the tool is what lets the agent say
|
|
46
|
+
* exactly what to do.
|
|
47
|
+
*/
|
|
48
|
+
export function needsAccess(roomId: string, detail?: string): EditorFailure {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
code: 'needs_access',
|
|
52
|
+
roomId,
|
|
53
|
+
message:
|
|
54
|
+
`This assistant does not have permission to write to this document` +
|
|
55
|
+
(detail ? ` (${detail})` : '') +
|
|
56
|
+
`. CALL the \`${GRANT_ACCESS_TOOL}\` browser tool yourself — do not ask ` +
|
|
57
|
+
`the user to run it, and do not tell them you cannot grant yourself ` +
|
|
58
|
+
`access. The tool prompts them to confirm in a dialog; if they confirm, ` +
|
|
59
|
+
`retry the edit once. If it returns \`granted: false\` or a failure, ` +
|
|
60
|
+
`report that plainly. Reads are unaffected.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The document is a live flow: readable, never writable. */
|
|
65
|
+
export function readOnlyFlow(roomId: string, alias?: string): EditorFailure {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
code: 'read_only_flow',
|
|
69
|
+
roomId,
|
|
70
|
+
message:
|
|
71
|
+
`This document is a live flow${alias ? ` (${alias})` : ''}. Flows are ` +
|
|
72
|
+
`read-only for this assistant — you can read and describe it, but edits ` +
|
|
73
|
+
`must be made in the flow builder. Do not retry the write.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One or more requested props are outside the block type's allowlist. */
|
|
78
|
+
export function propNotEditable(
|
|
79
|
+
blockId: string,
|
|
80
|
+
blockType: string,
|
|
81
|
+
rejected: Array<{ prop: string; reason: string }>,
|
|
82
|
+
): EditorFailure {
|
|
83
|
+
const detail = rejected
|
|
84
|
+
.map((entry) => `'${entry.prop}' — ${entry.reason}`)
|
|
85
|
+
.join('; ');
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
code: 'prop_not_editable',
|
|
89
|
+
blockId,
|
|
90
|
+
props: rejected.map((entry) => entry.prop),
|
|
91
|
+
message:
|
|
92
|
+
`Refused: ${detail}. Nothing was written to block ${blockId} ` +
|
|
93
|
+
`(type '${blockType}'). Tell the user which property could not be ` +
|
|
94
|
+
`changed and why.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The block id is not in the document. */
|
|
99
|
+
export function blockNotFound(blockId: string): EditorFailure {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
code: 'block_not_found',
|
|
103
|
+
blockId,
|
|
104
|
+
message:
|
|
105
|
+
`No block with id '${blockId}' exists in this document. Call ` +
|
|
106
|
+
`read_document to get current block ids — never guess or reuse an id ` +
|
|
107
|
+
`from an earlier turn.`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The user is not a member of the room, so it is not one of their documents. */
|
|
112
|
+
export function notAMember(roomId: string): EditorFailure {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
code: 'not_a_member',
|
|
116
|
+
roomId,
|
|
117
|
+
message:
|
|
118
|
+
`The user is not a member of document room ${roomId}, so it is not one ` +
|
|
119
|
+
`of their documents. Tell them they do not have access to it. Do not retry.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The write was made locally but could not be confirmed as sent. */
|
|
124
|
+
export function flushTimeout(roomId: string): EditorFailure {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
code: 'flush_timeout',
|
|
128
|
+
roomId,
|
|
129
|
+
message:
|
|
130
|
+
`The edit could not be confirmed as saved within the time limit — treat ` +
|
|
131
|
+
`it as NOT applied. Tell the user the document service is not responding ` +
|
|
132
|
+
`and they can retry shortly. Do not claim the edit succeeded.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Nothing to act on: no document is open and the call named no `room_id`.
|
|
138
|
+
* Distinct from `not_a_member` — there is no room to be a member of yet.
|
|
139
|
+
*/
|
|
140
|
+
export function noDocument(): EditorFailure {
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
code: 'no_document',
|
|
144
|
+
message:
|
|
145
|
+
'No document is open and no room_id was given. Pass the room id of the ' +
|
|
146
|
+
'document to work on (find it with list_workspace_pages), or create one ' +
|
|
147
|
+
'with create_page_room and pass the id it returns.',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Anything unexpected. Kept distinct so the agent does not misread it as a policy refusal. */
|
|
152
|
+
export function editorError(message: string, roomId?: string): EditorFailure {
|
|
153
|
+
return { ok: false, code: 'error', message, ...(roomId && { roomId }) };
|
|
154
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor plugin barrel — statically LIGHT by contract.
|
|
3
|
+
*
|
|
4
|
+
* This barrel is re-exported from `src/plugins/index.ts`, which is evaluated
|
|
5
|
+
* at Worker module scope. Nothing here may (transitively) import
|
|
6
|
+
* `@ixo/matrix-crdt` / `@blocknote/server-util` / `@ixo/editor`: vscode-lib
|
|
7
|
+
* (inside matrix-crdt) schedules a timer at module-evaluation time, which
|
|
8
|
+
* workerd forbids in global scope. The heavy tool chain
|
|
9
|
+
* (`standalone-editor-tool`, `content-tools`, `provider`, `blocknote-bridge`,
|
|
10
|
+
* `editor-mx`, `content-session`) is reached only via `await import()` inside
|
|
11
|
+
* `EditorPlugin.getRequestTools` — import those modules directly (not through
|
|
12
|
+
* this barrel) if you need them, and only from a request context.
|
|
13
|
+
*/
|
|
14
|
+
export { EditorPlugin, type EditorPluginOptions } from './editor.plugin';
|
|
15
|
+
export { EDITOR_AGENT_NAME, EDITOR_AGENT_TOOL_NAME } from './editor-agent';
|
|
16
|
+
export {
|
|
17
|
+
createEditorAccessDeniedTool,
|
|
18
|
+
type EditorAccessDeniedToolOptions,
|
|
19
|
+
} from './editor-access-denied-tool';
|
|
20
|
+
export {
|
|
21
|
+
buildBlocknoteToolsConfig,
|
|
22
|
+
type BlocknoteToolsConfig,
|
|
23
|
+
type BlocknoteToolsMatrixConfig,
|
|
24
|
+
} from './editor-config';
|
|
25
|
+
export {
|
|
26
|
+
GRANT_ACCESS_TOOL,
|
|
27
|
+
isEditorFailure,
|
|
28
|
+
type EditorFailure,
|
|
29
|
+
type EditorFailureCode,
|
|
30
|
+
} from './failures';
|
|
31
|
+
export { isUserInRoom, invalidateRoomMembership } from './room-membership';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-in replacement for the `jsdom` package on workerd, backed by linkedom.
|
|
3
|
+
*
|
|
4
|
+
* `@blocknote/server-util` does `import * as jsdom from 'jsdom'` and
|
|
5
|
+
* `new jsdom.JSDOM()` purely to get a `window`/`document` pair it can swap
|
|
6
|
+
* onto `globalThis` while BlockNote's markdown/HTML parsing runs. Real jsdom
|
|
7
|
+
* cannot load on workerd (it needs `node:vm` script contexts, which
|
|
8
|
+
* nodejs_compat only stubs), so the module specifier `jsdom` is aliased to
|
|
9
|
+
* this file — in `vitest.config.ts` (`resolve.alias`) and
|
|
10
|
+
* `test/wrangler.test.jsonc` (`alias`) for tests, and deploying hosts must
|
|
11
|
+
* carry the same `alias` entry in their wrangler config.
|
|
12
|
+
*
|
|
13
|
+
* linkedom implements the exact DOM surface the BlockNote parse path touches
|
|
14
|
+
* (`createElement`, `innerHTML`, `querySelectorAll` with `>`/`+` combinators,
|
|
15
|
+
* `insertAdjacentElement`, prosemirror-model's `DOMParser.parse` walk) with
|
|
16
|
+
* one gap: `document.implementation.createHTMLDocument`, which BlockNote's
|
|
17
|
+
* `nestedListsToBlockNoteStructure` uses to build a detached document. The
|
|
18
|
+
* constructor polyfills it with a fresh linkedom document.
|
|
19
|
+
*/
|
|
20
|
+
import { parseHTML } from 'linkedom';
|
|
21
|
+
|
|
22
|
+
const EMPTY_PAGE =
|
|
23
|
+
'<!doctype html><html><head><title>jsdom-shim</title></head><body></body></html>';
|
|
24
|
+
|
|
25
|
+
interface DomImplementationLike {
|
|
26
|
+
createHTMLDocument(title?: string): unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The `jsdom.JSDOM` surface `@blocknote/server-util` consumes: a constructible
|
|
31
|
+
* class exposing `window` (with `document` on it). Nothing else from jsdom's
|
|
32
|
+
* API is provided — BlockNote's server path never touches it.
|
|
33
|
+
*/
|
|
34
|
+
export class JSDOM {
|
|
35
|
+
readonly window: ReturnType<typeof parseHTML>;
|
|
36
|
+
|
|
37
|
+
constructor(html: string = EMPTY_PAGE) {
|
|
38
|
+
const { window } = parseHTML(html);
|
|
39
|
+
const doc = window.document;
|
|
40
|
+
if (!doc.implementation) {
|
|
41
|
+
const implementation: DomImplementationLike = {
|
|
42
|
+
createHTMLDocument(title = '') {
|
|
43
|
+
return parseHTML(
|
|
44
|
+
`<!doctype html><html><head><title>${title}</title></head><body></body></html>`,
|
|
45
|
+
).document;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperty(doc, 'implementation', {
|
|
49
|
+
value: implementation,
|
|
50
|
+
configurable: true,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
this.window = window;
|
|
54
|
+
}
|
|
55
|
+
}
|