@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,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompts for the content-assistant editor surface: one sub-agent prompt and
|
|
3
|
+
* two main-agent overlays (a document is open / a workspace is in scope).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GRANT_ACCESS_TOOL } from './failures';
|
|
7
|
+
|
|
8
|
+
/** The Portal browser tool that lists the user's documents. */
|
|
9
|
+
const LIST_PAGES_TOOL = 'list_workspace_pages';
|
|
10
|
+
const CREATE_PAGE_TOOL = 'create_page_room';
|
|
11
|
+
|
|
12
|
+
const TOOL_REFERENCE = `## Your tools
|
|
13
|
+
|
|
14
|
+
**Reading**
|
|
15
|
+
- \`read_document\` — every block in order: id, type, props, text (markdown), plus the page title. Paginate with \`start\`/\`limit\`.
|
|
16
|
+
- \`read_block\` — one block in full, including nested children.
|
|
17
|
+
- \`search_document\` — find blocks by a phrase in their text or props.
|
|
18
|
+
|
|
19
|
+
**Writing**
|
|
20
|
+
- \`insert_content\` — add new content from markdown, at the end (default), at the start, or before/after a block.
|
|
21
|
+
- \`edit_block\` — change existing blocks. Takes a **batch** of edits applied atomically: if one is refused, none are applied.
|
|
22
|
+
- \`delete_block\` — remove a block and everything nested in it.
|
|
23
|
+
- \`move_block\` — reorder by moving a block before/after another.
|
|
24
|
+
- \`replace_text\` — swap a phrase wherever it appears, preserving surrounding formatting.
|
|
25
|
+
|
|
26
|
+
## Rules
|
|
27
|
+
|
|
28
|
+
1. **Read before you write.** Block ids come from \`read_document\` or \`search_document\` in this turn. Never guess an id, and never reuse one from an earlier turn — the user may have edited the document since.
|
|
29
|
+
2. **Small edits use \`replace_text\`.** Reserve \`edit_block\` text rewrites for when a block is genuinely being replaced; rewriting a whole block to fix one word loses the author's formatting.
|
|
30
|
+
3. **Batch related \`edit_block\` edits into one call.** One call is one atomic change to the document.
|
|
31
|
+
4. **You edit prose, not behaviour.** Paragraphs, headings, lists, quotes and code blocks are yours. Custom blocks (checkboxes, actions, forms, proposals, claims, …) accept only \`title\` and \`description\` — everything else configures how they behave and will be refused. \`secrets\` and \`skills\` blocks are never editable and their values are never shown to you.
|
|
32
|
+
5. **Confirm before destroying.** Ask the user before deleting or wholesale-replacing content they did not explicitly ask you to change.
|
|
33
|
+
6. **Report what you changed, precisely.** Name the blocks and the values. Never claim an edit that a tool did not confirm.
|
|
34
|
+
|
|
35
|
+
## When a tool refuses
|
|
36
|
+
|
|
37
|
+
Tool results carry a \`code\` you must act on:
|
|
38
|
+
|
|
39
|
+
- \`needs_access\` — the document is readable but not writable by you. You do NOT hold the tool that fixes this. Stop and return the \`needs_access\` code to whoever delegated the task; they hold \`${GRANT_ACCESS_TOOL}\` and will grant access and re-delegate. Do not retry the edit, and do not tell the user to grant access themselves.
|
|
40
|
+
- \`read_only_flow\` — the document is a live flow. Read it and describe it; say that edits have to be made in the flow builder. Do not retry.
|
|
41
|
+
- \`prop_not_editable\` — name the exact property in your reply and say why it cannot be changed. Do not retry with the same property.
|
|
42
|
+
- \`block_not_found\` — the id is stale. Re-read the document and use current ids.
|
|
43
|
+
- \`not_a_member\` — the document is not the user's. Say so plainly. Do not retry.
|
|
44
|
+
- \`no_document\` — no document is open and no room id was given. Return the code; do not invent a room id.
|
|
45
|
+
- \`flush_timeout\` — the edit could **not** be confirmed as saved. Treat it as not applied and say so.
|
|
46
|
+
|
|
47
|
+
Never present a refusal as a success, and never silently retry a refusal.`;
|
|
48
|
+
|
|
49
|
+
/** System prompt for the room-bound content sub-agent. */
|
|
50
|
+
export const editorAgentPrompt = `You are the Editor Agent: a content assistant for one document in the user's workspace.
|
|
51
|
+
|
|
52
|
+
You read and edit the document's content — its words, its structure, its ordering. You do not build flows, configure action blocks, run anything, or fill in forms.
|
|
53
|
+
|
|
54
|
+
You receive a single self-contained task and have no access to the wider conversation. Do exactly what the task asks, then report concretely what you read or changed.
|
|
55
|
+
|
|
56
|
+
${TOOL_REFERENCE}`;
|
|
57
|
+
|
|
58
|
+
/** Why the editor surface could not be attached to this request. */
|
|
59
|
+
export type EditorUnavailableReason = 'not-member' | 'bind-error';
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the assistant may write, and what it may see.
|
|
3
|
+
*
|
|
4
|
+
* The assistant is a *content* assistant: it owns prose. Custom IXO blocks are
|
|
5
|
+
* behaviour — their props configure actions, authorisation, conditions, and
|
|
6
|
+
* delegation — so only their two human-readable labels are writable, and two of
|
|
7
|
+
* them are off limits entirely.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { proseBlockTypes } from './blocknote-bridge';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The custom blocks the IXO editor registers on top of BlockNote's own set,
|
|
14
|
+
* from `packages/editor/src/mantine/blocks/index.ts` (`blockSpecs`). All of
|
|
15
|
+
* them declare `content: 'none'` — they carry no inline text, only props.
|
|
16
|
+
*/
|
|
17
|
+
export const IXO_BLOCK_TYPES: readonly string[] = [
|
|
18
|
+
'action',
|
|
19
|
+
'apiRequest',
|
|
20
|
+
'checkbox',
|
|
21
|
+
'claim',
|
|
22
|
+
'domainCardViewer',
|
|
23
|
+
'domainCreator',
|
|
24
|
+
'domainCreatorSign',
|
|
25
|
+
'dynamicList',
|
|
26
|
+
'email',
|
|
27
|
+
'embed',
|
|
28
|
+
'enumChecklist',
|
|
29
|
+
'evaluator',
|
|
30
|
+
'flowLink',
|
|
31
|
+
'form',
|
|
32
|
+
'governanceGroup',
|
|
33
|
+
'list',
|
|
34
|
+
'location',
|
|
35
|
+
'notify',
|
|
36
|
+
'overview',
|
|
37
|
+
'proposal',
|
|
38
|
+
'protocolSelector',
|
|
39
|
+
'secrets',
|
|
40
|
+
'skills',
|
|
41
|
+
'visualization',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** Blocks the assistant never writes to, whatever the prop. */
|
|
45
|
+
export const LOCKED_BLOCK_TYPES: readonly string[] = ['secrets', 'skills'];
|
|
46
|
+
|
|
47
|
+
/** Blocks whose prop values are withheld on read. */
|
|
48
|
+
export const REDACTED_BLOCK_TYPES: readonly string[] = ['secrets'];
|
|
49
|
+
|
|
50
|
+
/** The only props writable on a custom IXO block. */
|
|
51
|
+
export const IXO_WRITABLE_PROPS: readonly string[] = ['title', 'description'];
|
|
52
|
+
|
|
53
|
+
/** Placeholder shown instead of a redacted prop value. */
|
|
54
|
+
export const REDACTED_VALUE = '[redacted]';
|
|
55
|
+
|
|
56
|
+
export type BlockClass = 'prose' | 'ixo' | 'locked' | 'unknown';
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Classify a block type.
|
|
60
|
+
*
|
|
61
|
+
* `unknown` covers block types this runtime has never heard of — a newer editor
|
|
62
|
+
* version, or a block from another schema. They are treated like IXO blocks
|
|
63
|
+
* (labels only, no text) rather than like prose, because assuming a block is
|
|
64
|
+
* inert prose is the destructive assumption.
|
|
65
|
+
*/
|
|
66
|
+
export function classifyBlockType(blockType: string): BlockClass {
|
|
67
|
+
if (LOCKED_BLOCK_TYPES.includes(blockType)) return 'locked';
|
|
68
|
+
if (proseBlockTypes().has(blockType)) return 'prose';
|
|
69
|
+
if (IXO_BLOCK_TYPES.includes(blockType)) return 'ixo';
|
|
70
|
+
return 'unknown';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Prose blocks own their inline content; everything else has none. */
|
|
74
|
+
export function isTextEditable(blockType: string): boolean {
|
|
75
|
+
return classifyBlockType(blockType) === 'prose';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PropFilterResult {
|
|
79
|
+
/** Props that may be written, normalised to attribute strings. */
|
|
80
|
+
allowed: Record<string, string>;
|
|
81
|
+
/** Props refused, with the reason to report to the agent. */
|
|
82
|
+
rejected: Array<{ prop: string; reason: string }>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalise(value: unknown): string {
|
|
86
|
+
if (typeof value === 'string') return value;
|
|
87
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
88
|
+
return String(value);
|
|
89
|
+
}
|
|
90
|
+
return JSON.stringify(value) ?? '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Split a requested prop patch into what may be written and what may not.
|
|
95
|
+
*
|
|
96
|
+
* - prose blocks: every prop (they are presentation only — alignment, colour,
|
|
97
|
+
* heading level).
|
|
98
|
+
* - IXO blocks and unknown types: `title` and `description` only. Behavioural
|
|
99
|
+
* props (`conditions`, `authorisedActors`, action `type`, `inputs`,
|
|
100
|
+
* delegation config, …) are refused by falling outside that pair.
|
|
101
|
+
* - locked blocks: nothing.
|
|
102
|
+
*/
|
|
103
|
+
export function filterWritableProps(
|
|
104
|
+
blockType: string,
|
|
105
|
+
props: Record<string, unknown>,
|
|
106
|
+
): PropFilterResult {
|
|
107
|
+
const result: PropFilterResult = { allowed: {}, rejected: [] };
|
|
108
|
+
const blockClass = classifyBlockType(blockType);
|
|
109
|
+
|
|
110
|
+
for (const [prop, value] of Object.entries(props)) {
|
|
111
|
+
if (blockClass === 'locked') {
|
|
112
|
+
result.rejected.push({
|
|
113
|
+
prop,
|
|
114
|
+
reason: `'${blockType}' blocks are never editable by the assistant`,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (blockClass === 'prose' || IXO_WRITABLE_PROPS.includes(prop)) {
|
|
119
|
+
result.allowed[prop] = normalise(value);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
result.rejected.push({
|
|
123
|
+
prop,
|
|
124
|
+
reason:
|
|
125
|
+
`'${prop}' configures how the '${blockType}' block behaves — only ` +
|
|
126
|
+
`${IXO_WRITABLE_PROPS.join(' and ')} are editable on this block type`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Redact prop values the assistant must not relay. Keys stay visible so the
|
|
135
|
+
* agent can still describe the block's shape.
|
|
136
|
+
*/
|
|
137
|
+
export function redactProps(
|
|
138
|
+
blockType: string,
|
|
139
|
+
props: Record<string, unknown>,
|
|
140
|
+
): Record<string, unknown> {
|
|
141
|
+
if (!REDACTED_BLOCK_TYPES.includes(blockType)) return props;
|
|
142
|
+
const redacted: Record<string, unknown> = {};
|
|
143
|
+
for (const key of Object.keys(props)) {
|
|
144
|
+
redacted[key] = REDACTED_VALUE;
|
|
145
|
+
}
|
|
146
|
+
return redacted;
|
|
147
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { MatrixProvider } from '@ixo/matrix-crdt';
|
|
2
|
+
import type { MatrixClient } from 'matrix-js-sdk';
|
|
3
|
+
import * as Y from 'yjs';
|
|
4
|
+
|
|
5
|
+
import { createConsoleLogger } from '../../core/utils';
|
|
6
|
+
|
|
7
|
+
const logger = createConsoleLogger({ module: 'editor-provider' });
|
|
8
|
+
|
|
9
|
+
const wait = (ms: number) =>
|
|
10
|
+
new Promise<void>((resolve) => {
|
|
11
|
+
setTimeout(resolve, ms);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
type RoomDescriptor =
|
|
15
|
+
| { type: 'id'; id: string }
|
|
16
|
+
| { type: 'alias'; alias: string };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The oracle cannot see the room at all — it is neither a member nor able to
|
|
20
|
+
* join it. Distinct from a sync timeout so callers can tell "no access" from
|
|
21
|
+
* "service is slow".
|
|
22
|
+
*/
|
|
23
|
+
export class RoomNotAccessibleError extends Error {
|
|
24
|
+
constructor(
|
|
25
|
+
readonly roomId: string,
|
|
26
|
+
readonly detail: string,
|
|
27
|
+
) {
|
|
28
|
+
super(`Room ${roomId} not accessible: ${detail}`);
|
|
29
|
+
this.name = 'RoomNotAccessibleError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProviderInitResult {
|
|
34
|
+
doc: Y.Doc;
|
|
35
|
+
awareness?: unknown;
|
|
36
|
+
provider: MatrixProvider;
|
|
37
|
+
}
|
|
38
|
+
export interface MatrixRoomById {
|
|
39
|
+
type: 'id';
|
|
40
|
+
value: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MatrixRoomByAlias {
|
|
44
|
+
type: 'alias';
|
|
45
|
+
value: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ProviderConfig {
|
|
48
|
+
docName: string;
|
|
49
|
+
enableAwareness: boolean;
|
|
50
|
+
retryAttempts: number;
|
|
51
|
+
retryDelayMs: number;
|
|
52
|
+
/** How long matrix-crdt batches Y.Doc updates before sending them. */
|
|
53
|
+
flushInterval?: number;
|
|
54
|
+
/** How long to wait before retrying a write the homeserver rejected. */
|
|
55
|
+
retryIfForbiddenInterval?: number;
|
|
56
|
+
/** How many rejected writes to retry before giving up (0 = unlimited). */
|
|
57
|
+
maxForbiddenRetries?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type MatrixRoomConfig = MatrixRoomById | MatrixRoomByAlias;
|
|
61
|
+
|
|
62
|
+
export interface MatrixConfig {
|
|
63
|
+
baseUrl: string;
|
|
64
|
+
accessToken: string;
|
|
65
|
+
userId: string;
|
|
66
|
+
room: MatrixRoomConfig;
|
|
67
|
+
initialSyncTimeoutMs: number;
|
|
68
|
+
}
|
|
69
|
+
export interface BlockNoteConfig {
|
|
70
|
+
defaultBlockId?: string;
|
|
71
|
+
blockNamespace?: string;
|
|
72
|
+
mutableAttributeKeys: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AppConfig {
|
|
76
|
+
matrix: MatrixConfig;
|
|
77
|
+
provider: ProviderConfig;
|
|
78
|
+
blocknote: BlockNoteConfig;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* MatrixProviderManager
|
|
83
|
+
*
|
|
84
|
+
* Manages a Y.Doc CRDT for a specific Matrix room.
|
|
85
|
+
*
|
|
86
|
+
* IMPORTANT: This class assumes the Matrix client is ALREADY synced
|
|
87
|
+
* via the EditorMatrixClient singleton. It does NOT manage client lifecycle.
|
|
88
|
+
*
|
|
89
|
+
* Each instance creates:
|
|
90
|
+
* - A new Y.Doc for the room
|
|
91
|
+
* - A MatrixProvider to sync Y.Doc with Matrix
|
|
92
|
+
*
|
|
93
|
+
* The singleton EditorMatrixClient handles:
|
|
94
|
+
* - Matrix connection
|
|
95
|
+
* - Background sync
|
|
96
|
+
* - Client lifecycle
|
|
97
|
+
*/
|
|
98
|
+
export class MatrixProviderManager {
|
|
99
|
+
private readonly doc: Y.Doc;
|
|
100
|
+
private provider: MatrixProvider | undefined;
|
|
101
|
+
private readonly disposables: Array<{ dispose: () => void }> = [];
|
|
102
|
+
private documentAvailable = false;
|
|
103
|
+
private disposed = false;
|
|
104
|
+
private availabilityResolvers: Array<() => void> = [];
|
|
105
|
+
|
|
106
|
+
constructor(
|
|
107
|
+
private readonly matrixClient: MatrixClient,
|
|
108
|
+
private readonly cfg: AppConfig,
|
|
109
|
+
) {
|
|
110
|
+
this.doc = new Y.Doc();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public get ydoc(): Y.Doc {
|
|
114
|
+
return this.doc;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public get matrixProvider(): MatrixProvider | undefined {
|
|
118
|
+
return this.provider;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Initialize the provider for the configured room.
|
|
123
|
+
*
|
|
124
|
+
* Assumes the Matrix client is already synced (via EditorMatrixClient singleton).
|
|
125
|
+
* Creates a MatrixProvider to sync the Y.Doc with the Matrix room.
|
|
126
|
+
*/
|
|
127
|
+
public async init(): Promise<ProviderInitResult> {
|
|
128
|
+
if (this.disposed) {
|
|
129
|
+
throw new Error('MatrixProviderManager was already disposed');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const attempts = Math.max(1, this.cfg.provider.retryAttempts);
|
|
133
|
+
const delayMs = Math.max(0, this.cfg.provider.retryDelayMs);
|
|
134
|
+
|
|
135
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
136
|
+
try {
|
|
137
|
+
// Initialize the provider (creates MatrixProvider, fetches room history)
|
|
138
|
+
await this.initializeProvider();
|
|
139
|
+
|
|
140
|
+
// Wait for document to become available
|
|
141
|
+
await this.waitForAvailability(this.cfg.matrix.initialSyncTimeoutMs);
|
|
142
|
+
|
|
143
|
+
logger.log('Matrix provider initialized', { attempt });
|
|
144
|
+
logger.log(`📄 Y.Doc GUID: ${this.doc.guid}`);
|
|
145
|
+
logger.log(`📄 Room ID: ${JSON.stringify(this.cfg.matrix.room)}`);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
doc: this.doc,
|
|
149
|
+
awareness: this.provider?.awarenessInstance,
|
|
150
|
+
provider: this.ensureProvider(),
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.warn(`Matrix provider init attempt ${attempt} failed`, error);
|
|
154
|
+
await this.cleanupProvider();
|
|
155
|
+
|
|
156
|
+
if (attempt === attempts) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const backoff = delayMs * attempt;
|
|
161
|
+
logger.log(`Retrying provider initialization in ${backoff}ms`);
|
|
162
|
+
await wait(backoff);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw new Error('Matrix provider initialization failed');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Ensures the room is available in the Matrix client's store.
|
|
171
|
+
* This is critical for createMessagesRequest() to work correctly.
|
|
172
|
+
*/
|
|
173
|
+
private async ensureRoomAvailable(roomId: string): Promise<void> {
|
|
174
|
+
const room = this.matrixClient.getRoom(roomId);
|
|
175
|
+
|
|
176
|
+
if (!room) {
|
|
177
|
+
logger.warn(
|
|
178
|
+
`Room ${roomId} not in client store, attempting to join/peek...`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
// Try joining first — peek fails when room previews are disabled
|
|
183
|
+
await this.matrixClient.joinRoom(roomId);
|
|
184
|
+
logger.log(`Successfully joined room ${roomId}`);
|
|
185
|
+
} catch (joinError) {
|
|
186
|
+
logger.warn(joinError);
|
|
187
|
+
// Fall back to peek if join fails (e.g. already joined but not synced)
|
|
188
|
+
try {
|
|
189
|
+
await this.matrixClient.peekInRoom(roomId);
|
|
190
|
+
logger.log(`Successfully peeked into room ${roomId}`);
|
|
191
|
+
} catch (peekError) {
|
|
192
|
+
const errorMsg =
|
|
193
|
+
peekError instanceof Error ? peekError.message : String(peekError);
|
|
194
|
+
throw new RoomNotAccessibleError(roomId, errorMsg);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
logger.debug?.(`Room ${roomId} found in client store`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private async initializeProvider() {
|
|
203
|
+
this.cleanupProviderListeners();
|
|
204
|
+
|
|
205
|
+
const roomDescriptor = this.resolveRoomDescriptor();
|
|
206
|
+
|
|
207
|
+
// Resolve room ID (handle both 'id' and 'alias' types)
|
|
208
|
+
let roomId: string;
|
|
209
|
+
if (roomDescriptor.type === 'id') {
|
|
210
|
+
roomId = roomDescriptor.id;
|
|
211
|
+
} else {
|
|
212
|
+
// Resolve room alias to room ID
|
|
213
|
+
const ret = await this.matrixClient.getRoomIdForAlias(
|
|
214
|
+
roomDescriptor.alias,
|
|
215
|
+
);
|
|
216
|
+
roomId = ret.room_id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ensure the room is in the client's store before proceeding
|
|
220
|
+
// This is critical for MatrixProvider to fetch room history correctly
|
|
221
|
+
await this.ensureRoomAvailable(roomId);
|
|
222
|
+
|
|
223
|
+
logger.log('Creating MatrixProvider', roomDescriptor);
|
|
224
|
+
|
|
225
|
+
this.provider = new MatrixProvider(
|
|
226
|
+
this.doc,
|
|
227
|
+
this.matrixClient,
|
|
228
|
+
roomDescriptor,
|
|
229
|
+
{
|
|
230
|
+
enableAwareness: this.cfg.provider.enableAwareness,
|
|
231
|
+
reader: {
|
|
232
|
+
snapshotInterval: 10,
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// A rejected write is final within one request — room power levels do
|
|
236
|
+
// not change mid-call — so retry once, fast, instead of matrix-crdt's
|
|
237
|
+
// default 3 attempts 30s apart. Without this a caller awaiting
|
|
238
|
+
// `waitForFlush()` before reporting success would block for ~90s.
|
|
239
|
+
writer: {
|
|
240
|
+
flushInterval: this.cfg.provider.flushInterval ?? 10,
|
|
241
|
+
retryIfForbiddenInterval:
|
|
242
|
+
this.cfg.provider.retryIfForbiddenInterval ?? 1_000,
|
|
243
|
+
maxForbiddenRetries: this.cfg.provider.maxForbiddenRetries ?? 1,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
this.registerProviderListeners(this.provider);
|
|
249
|
+
|
|
250
|
+
await this.provider.initialize();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private waitForAvailability(timeoutMs: number): Promise<void> {
|
|
254
|
+
if (this.documentAvailable) {
|
|
255
|
+
return Promise.resolve();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const timeoutHandle = setTimeout(() => {
|
|
260
|
+
this.availabilityResolvers = this.availabilityResolvers.filter(
|
|
261
|
+
(candidate) => candidate !== resolver,
|
|
262
|
+
);
|
|
263
|
+
reject(
|
|
264
|
+
new Error(
|
|
265
|
+
`Document did not become available within ${timeoutMs}ms. Check room permissions or connectivity.`,
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
}, timeoutMs);
|
|
269
|
+
|
|
270
|
+
const resolver = () => {
|
|
271
|
+
clearTimeout(timeoutHandle);
|
|
272
|
+
resolve();
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
this.availabilityResolvers.push(resolver);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private registerProviderListeners(provider: MatrixProvider) {
|
|
280
|
+
this.disposables.push(
|
|
281
|
+
provider.onDocumentAvailable(() => {
|
|
282
|
+
logger.log('Matrix document available');
|
|
283
|
+
|
|
284
|
+
// Wait briefly for Y.Doc to apply initial sync
|
|
285
|
+
// Can't rely on onReceivedEvents - it only fires for NEW events,
|
|
286
|
+
// not when connecting to an already-synced client
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
logger.log('Marking document as available after sync delay');
|
|
289
|
+
this.documentAvailable = true;
|
|
290
|
+
const resolvers = [...this.availabilityResolvers];
|
|
291
|
+
this.availabilityResolvers = [];
|
|
292
|
+
resolvers.forEach((resolver) => resolver());
|
|
293
|
+
}, 500); // Short delay for Y.Doc sync
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
this.disposables.push(
|
|
298
|
+
provider.onDocumentUnavailable(() => {
|
|
299
|
+
logger.warn('Matrix document unavailable');
|
|
300
|
+
this.documentAvailable = false;
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
this.disposables.push(
|
|
305
|
+
provider.onReceivedEvents(() => {
|
|
306
|
+
logger.debug?.('Received Matrix events - Y.Doc syncing content');
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private cleanupProviderListeners() {
|
|
312
|
+
while (this.disposables.length > 0) {
|
|
313
|
+
const disposable = this.disposables.pop();
|
|
314
|
+
try {
|
|
315
|
+
disposable?.dispose();
|
|
316
|
+
} catch (error) {
|
|
317
|
+
logger.warn('Failed to dispose provider listener', error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private resolveRoomDescriptor(): RoomDescriptor {
|
|
323
|
+
if (this.cfg.matrix.room.type === 'id') {
|
|
324
|
+
return { type: 'id', id: this.cfg.matrix.room.value };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { type: 'alias', alias: this.cfg.matrix.room.value };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private ensureProvider(): MatrixProvider {
|
|
331
|
+
if (!this.provider) {
|
|
332
|
+
throw new Error('Matrix provider not initialized');
|
|
333
|
+
}
|
|
334
|
+
return this.provider;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Dispose of this provider manager.
|
|
339
|
+
*
|
|
340
|
+
* Cleans up:
|
|
341
|
+
* - MatrixProvider (stops polling, removes listeners)
|
|
342
|
+
* - Y.Doc (destroys local document state)
|
|
343
|
+
*
|
|
344
|
+
* Does NOT touch the Matrix client - it's managed by EditorMatrixClient singleton
|
|
345
|
+
* and shared across all providers/rooms.
|
|
346
|
+
*/
|
|
347
|
+
public async dispose() {
|
|
348
|
+
if (this.disposed) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
logger.log('Disposing Matrix provider manager');
|
|
353
|
+
|
|
354
|
+
// Clean up provider and Y.Doc
|
|
355
|
+
await this.cleanupProvider();
|
|
356
|
+
this.doc.destroy();
|
|
357
|
+
|
|
358
|
+
// NOTE: We NEVER call matrixClient.stopClient() here!
|
|
359
|
+
// The Matrix client is managed by EditorMatrixClient singleton
|
|
360
|
+
// and is shared across all rooms. Stopping it would break
|
|
361
|
+
// all other active providers.
|
|
362
|
+
|
|
363
|
+
this.disposed = true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private async cleanupProvider() {
|
|
367
|
+
this.cleanupProviderListeners();
|
|
368
|
+
|
|
369
|
+
if (this.provider) {
|
|
370
|
+
try {
|
|
371
|
+
await this.provider.waitForFlush();
|
|
372
|
+
this.provider.dispose();
|
|
373
|
+
} catch (error) {
|
|
374
|
+
logger.warn('Error while disposing MatrixProvider', error);
|
|
375
|
+
} finally {
|
|
376
|
+
this.provider = undefined;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!this.documentAvailable) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
this.documentAvailable = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cached Matrix room-membership guard, Workers edition.
|
|
3
|
+
*
|
|
4
|
+
* The editor and flows plugins act on a room with the oracle's privileged
|
|
5
|
+
* admin identity, where the *room id comes from the request*. Left unchecked,
|
|
6
|
+
* a user could pass another user's room id and have the oracle read or edit a
|
|
7
|
+
* page they don't own. Before any such read/write we assert the requesting
|
|
8
|
+
* user is actually a joined member of the room.
|
|
9
|
+
*
|
|
10
|
+
* On Workers, membership is resolved through the gateway Durable Object via
|
|
11
|
+
* `ctx.matrix.getRoomState(roomId)` — the full state array carries every
|
|
12
|
+
* `m.room.member` event — and cached per room for a short TTL so repeated
|
|
13
|
+
* tool calls in one turn don't pay the gateway round-trip each time.
|
|
14
|
+
*/
|
|
15
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
16
|
+
|
|
17
|
+
interface MembershipEntry {
|
|
18
|
+
members: Set<string>;
|
|
19
|
+
expiresAt: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
23
|
+
const cache = new Map<string, MembershipEntry>();
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === 'object' && value !== null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Joined member ids from a room-state snapshot's event array. */
|
|
30
|
+
function joinedMembers(state: ReadonlyArray<unknown>): Set<string> {
|
|
31
|
+
const members = new Set<string>();
|
|
32
|
+
for (const event of state) {
|
|
33
|
+
if (!isRecord(event)) continue;
|
|
34
|
+
if (event.type !== 'm.room.member') continue;
|
|
35
|
+
const stateKey = event.state_key;
|
|
36
|
+
if (typeof stateKey !== 'string' || stateKey.length === 0) continue;
|
|
37
|
+
const content = event.content;
|
|
38
|
+
if (isRecord(content) && content.membership === 'join') {
|
|
39
|
+
members.add(stateKey);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return members;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sweepExpiredEntries(): void {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
for (const [key, entry] of cache) {
|
|
48
|
+
if (entry.expiresAt <= now) cache.delete(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function getRoomMembers(
|
|
53
|
+
ctx: Pick<RuntimeContext, 'matrix'>,
|
|
54
|
+
roomId: string,
|
|
55
|
+
ttlMs: number,
|
|
56
|
+
): Promise<Set<string>> {
|
|
57
|
+
const cached = cache.get(roomId);
|
|
58
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
59
|
+
return cached.members;
|
|
60
|
+
}
|
|
61
|
+
const snapshot = await ctx.matrix.getRoomState(roomId);
|
|
62
|
+
const members = joinedMembers(snapshot.state);
|
|
63
|
+
sweepExpiredEntries();
|
|
64
|
+
cache.set(roomId, { members, expiresAt: Date.now() + ttlMs });
|
|
65
|
+
return members;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* True when `matrixUserId` is a joined member of `roomId`. Fails closed:
|
|
70
|
+
* returns `false` on a missing id or any lookup error, so a Matrix outage
|
|
71
|
+
* denies access rather than silently granting it.
|
|
72
|
+
*/
|
|
73
|
+
export async function isUserInRoom(
|
|
74
|
+
ctx: Pick<RuntimeContext, 'matrix'>,
|
|
75
|
+
roomId: string,
|
|
76
|
+
matrixUserId: string | undefined,
|
|
77
|
+
opts: { ttlMs?: number } = {},
|
|
78
|
+
): Promise<boolean> {
|
|
79
|
+
if (!roomId || !matrixUserId) return false;
|
|
80
|
+
try {
|
|
81
|
+
const members = await getRoomMembers(
|
|
82
|
+
ctx,
|
|
83
|
+
roomId,
|
|
84
|
+
opts.ttlMs ?? DEFAULT_TTL_MS,
|
|
85
|
+
);
|
|
86
|
+
return members.has(matrixUserId);
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Drop a room's cached membership (e.g. after a known membership change). */
|
|
93
|
+
export function invalidateRoomMembership(roomId: string): void {
|
|
94
|
+
cache.delete(roomId);
|
|
95
|
+
}
|