@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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Per-room JWE secrets for the Workers runtime — the port of the Node
3
+ * runtime's `SecretsService` (`modules/secrets/secrets.service.ts`).
4
+ *
5
+ * Storage is Matrix room state + timeline, wire-identical to the portal and
6
+ * the Node runtime:
7
+ * - `ixo.room.secret.index` STATE events, one per secret name
8
+ * (`state_key` = name, content `{ eventId, publicKeyId }`; empty content
9
+ * marks a deleted entry);
10
+ * - `ixo.room.secret` TIMELINE events carrying `{ value: <compact JWE> }`.
11
+ *
12
+ * Values are encrypted to the oracle's published P-256 key
13
+ * (`ECDH-ES+A256KW` / `A256GCM`, see `jwe.ts`); the private JWK is loaded
14
+ * from the oracle's Matrix account room via the gateway and seated once at
15
+ * boot. All Matrix I/O goes through the gateway stub (the only object holding
16
+ * the bot session) using its JSON-string RPC surface.
17
+ */
18
+ import type { Logger } from '../plugin-api/types';
19
+ import { decryptJwe, encryptJwe, type JWK } from './jwe';
20
+
21
+ /**
22
+ * The key id recorded on secret-index entries written by this runtime.
23
+ * Matches the id the oracle's P-256 key is published under
24
+ * (`ixo.room.encryption_key.index` / `p256_encryption`), so portal-written
25
+ * and runtime-written secrets carry the same shape.
26
+ */
27
+ export const RUNTIME_PUBLIC_KEY_ID = 'p256_encryption';
28
+
29
+ const SECRET_INDEX_TYPE = 'ixo.room.secret.index';
30
+ const SECRET_VALUE_TYPE = 'ixo.room.secret';
31
+ const REDACTION_TYPE = 'm.room.redaction';
32
+
33
+ const TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1000;
34
+
35
+ export interface SecretIndexEntry {
36
+ name: string;
37
+ eventId: string;
38
+ publicKeyId: string;
39
+ }
40
+
41
+ interface CachedSecret {
42
+ value: string;
43
+ eventId: string;
44
+ expiresAt: number;
45
+ }
46
+
47
+ /**
48
+ * The slice of the gateway RPC surface this service needs — structurally
49
+ * satisfied by `DurableObjectStub<MatrixGatewayObject>` and trivially mocked
50
+ * in tests. All content crosses as JSON strings (`do/contracts.ts` rule).
51
+ */
52
+ export interface SecretsGateway {
53
+ /** Full room state as JSON `[{ type, state_key, content, ... }]`. */
54
+ getRoomState(roomId: string): Promise<string>;
55
+ /** Send a timeline event (`content` JSON-encoded); returns the event id. */
56
+ sendEvent(roomId: string, type: string, content: string): Promise<string>;
57
+ /** Send a state event (`content` JSON-encoded); returns the event id. */
58
+ sendStateEvent(
59
+ roomId: string,
60
+ type: string,
61
+ content: string,
62
+ stateKey?: string,
63
+ ): Promise<string>;
64
+ /** One timeline event as JSON `{ event_id, type, content, ... }`, or null. */
65
+ getEvent(roomId: string, eventId: string): Promise<string | null>;
66
+ }
67
+
68
+ export interface WorkersSecretsServiceOptions {
69
+ gateway: SecretsGateway;
70
+ /** The oracle's P-256 private JWK, when already loaded. Seat later via {@link WorkersSecretsService.setEncryptionKey}. */
71
+ encryptionKey?: JWK | null;
72
+ logger?: Logger;
73
+ }
74
+
75
+ const NOOP: Logger = {
76
+ log: () => undefined,
77
+ warn: () => undefined,
78
+ error: () => undefined,
79
+ };
80
+
81
+ interface StateEventRow {
82
+ type?: unknown;
83
+ state_key?: unknown;
84
+ content?: unknown;
85
+ }
86
+
87
+ export class WorkersSecretsService {
88
+ private readonly gateway: SecretsGateway;
89
+ private readonly logger: Logger;
90
+ // TODO: Key rotation — Map<publicKeyId, JWK> once multiple keys exist.
91
+ private encryptionKey: JWK | null;
92
+ /** 24h in-memory cache per (room, name), invalidated by eventId changes. */
93
+ private readonly cache = new Map<string, CachedSecret>();
94
+
95
+ constructor(opts: WorkersSecretsServiceOptions) {
96
+ this.gateway = opts.gateway;
97
+ this.encryptionKey = opts.encryptionKey ?? null;
98
+ this.logger = opts.logger ?? NOOP;
99
+ }
100
+
101
+ setEncryptionKey(key: JWK): void {
102
+ this.encryptionKey = key;
103
+ }
104
+
105
+ hasEncryptionKey(): boolean {
106
+ return this.encryptionKey !== null;
107
+ }
108
+
109
+ private cacheKey(roomId: string, name: string): string {
110
+ return JSON.stringify([roomId, name]);
111
+ }
112
+
113
+ /**
114
+ * The secret index from room state. One gateway round-trip; the Matrix CS
115
+ * API has no per-type state listing, so full state + client-side filter is
116
+ * the only option (same as the Node service). Errors degrade to `[]`.
117
+ */
118
+ async getIndex(roomId: string): Promise<SecretIndexEntry[]> {
119
+ let rows: unknown;
120
+ try {
121
+ rows = JSON.parse(await this.gateway.getRoomState(roomId));
122
+ } catch (error) {
123
+ this.logger.error(
124
+ `[secrets] failed to read secret index for room ${roomId}: ${error instanceof Error ? error.message : String(error)}`,
125
+ );
126
+ return [];
127
+ }
128
+ if (!Array.isArray(rows)) return [];
129
+
130
+ const index: SecretIndexEntry[] = [];
131
+ for (const row of rows as StateEventRow[]) {
132
+ if (row?.type !== SECRET_INDEX_TYPE) continue;
133
+ if (typeof row.state_key !== 'string') continue;
134
+ const content = row.content;
135
+ // Empty content is the "deleted" marker.
136
+ if (!content || typeof content !== 'object') continue;
137
+ const record = content as Record<string, unknown>;
138
+ if (Object.keys(record).length === 0) continue;
139
+ if (typeof record.eventId !== 'string' || record.eventId.length === 0)
140
+ continue;
141
+ index.push({
142
+ name: row.state_key,
143
+ eventId: record.eventId,
144
+ publicKeyId:
145
+ typeof record.publicKeyId === 'string'
146
+ ? record.publicKeyId
147
+ : RUNTIME_PUBLIC_KEY_ID,
148
+ });
149
+ }
150
+ return index;
151
+ }
152
+
153
+ /**
154
+ * Decrypted values for the named secrets. Uses the cache where the index's
155
+ * eventId still matches; otherwise fetches the timeline event and decrypts.
156
+ * Without a seated key this returns `{}` (each undecryptable entry is
157
+ * logged and skipped — the Node service degrades identically).
158
+ */
159
+ async getValues(
160
+ roomId: string,
161
+ names: string[],
162
+ ): Promise<Record<string, string>> {
163
+ if (names.length === 0) return {};
164
+ const requested = new Set(names);
165
+ const index = (await this.getIndex(roomId)).filter((entry) =>
166
+ requested.has(entry.name),
167
+ );
168
+ return this.loadValues(roomId, index);
169
+ }
170
+
171
+ private async loadValues(
172
+ roomId: string,
173
+ index: SecretIndexEntry[],
174
+ ): Promise<Record<string, string>> {
175
+ const result: Record<string, string> = {};
176
+ const now = Date.now();
177
+
178
+ for (const entry of index) {
179
+ const key = this.cacheKey(roomId, entry.name);
180
+ const cached = this.cache.get(key);
181
+ if (
182
+ cached &&
183
+ cached.eventId === entry.eventId &&
184
+ cached.expiresAt > now
185
+ ) {
186
+ result[entry.name] = cached.value;
187
+ continue;
188
+ }
189
+ if (!this.encryptionKey) {
190
+ this.logger.error(
191
+ `[secrets] secret "${entry.name}" is encrypted but no encryption key is seated — skipping`,
192
+ );
193
+ continue;
194
+ }
195
+ try {
196
+ const raw = await this.gateway.getEvent(roomId, entry.eventId);
197
+ if (!raw) {
198
+ this.logger.warn(
199
+ `[secrets] secret event ${entry.eventId} for "${entry.name}" not found in ${roomId}`,
200
+ );
201
+ continue;
202
+ }
203
+ const parsed: unknown = JSON.parse(raw);
204
+ const content =
205
+ parsed && typeof parsed === 'object'
206
+ ? (parsed as { content?: unknown }).content
207
+ : undefined;
208
+ const jwe =
209
+ content && typeof content === 'object'
210
+ ? (content as Record<string, unknown>).value
211
+ : undefined;
212
+ if (typeof jwe !== 'string' || jwe.length === 0) {
213
+ this.logger.warn(
214
+ `[secrets] secret event ${entry.eventId} for "${entry.name}" carries no value`,
215
+ );
216
+ continue;
217
+ }
218
+ // TODO: Key rotation — select the key via entry.publicKeyId.
219
+ const value = await decryptJwe(jwe, this.encryptionKey);
220
+ result[entry.name] = value;
221
+ this.cache.set(key, {
222
+ value,
223
+ eventId: entry.eventId,
224
+ expiresAt: Date.now() + TWENTY_FOUR_HOURS_MS,
225
+ });
226
+ } catch (error) {
227
+ this.logger.error(
228
+ `[secrets] failed to fetch/decrypt secret "${entry.name}" (event ${entry.eventId}): ${error instanceof Error ? error.message : String(error)}`,
229
+ );
230
+ }
231
+ }
232
+ return result;
233
+ }
234
+
235
+ /**
236
+ * Write (or overwrite) a room secret from the runtime side, using the same
237
+ * event shape the portal writes: the JWE value in an `ixo.room.secret`
238
+ * timeline event, referenced by an `ixo.room.secret.index` state event
239
+ * keyed by the secret name. Encrypts to the oracle's own published key so
240
+ * the read path decrypts it identically to a portal-written secret. The
241
+ * cache is primed with the plaintext; the superseded ciphertext event is
242
+ * redacted best-effort.
243
+ */
244
+ async putSecret(roomId: string, name: string, value: string): Promise<void> {
245
+ if (!this.encryptionKey) {
246
+ throw new Error(
247
+ `[secrets] cannot write secret "${name}" — no encryption key seated`,
248
+ );
249
+ }
250
+
251
+ const previous = (await this.getIndex(roomId)).find(
252
+ (entry) => entry.name === name,
253
+ );
254
+
255
+ const jwe = await encryptJwe(value, this.encryptionKey);
256
+ const eventId = await this.gateway.sendEvent(
257
+ roomId,
258
+ SECRET_VALUE_TYPE,
259
+ JSON.stringify({ value: jwe }),
260
+ );
261
+ await this.gateway.sendStateEvent(
262
+ roomId,
263
+ SECRET_INDEX_TYPE,
264
+ JSON.stringify({ eventId, publicKeyId: RUNTIME_PUBLIC_KEY_ID }),
265
+ name,
266
+ );
267
+
268
+ if (previous && previous.eventId !== eventId) {
269
+ try {
270
+ await this.gateway.sendEvent(
271
+ roomId,
272
+ REDACTION_TYPE,
273
+ JSON.stringify({ redacts: previous.eventId }),
274
+ );
275
+ } catch (error) {
276
+ this.logger.warn(
277
+ `[secrets] could not redact superseded secret event ${previous.eventId} in ${roomId}: ${error instanceof Error ? error.message : String(error)}`,
278
+ );
279
+ }
280
+ }
281
+
282
+ this.cache.set(this.cacheKey(roomId, name), {
283
+ value,
284
+ eventId,
285
+ expiresAt: Date.now() + TWENTY_FOUR_HOURS_MS,
286
+ });
287
+ this.logger.log(
288
+ `[secrets] wrote secret "${name}" to room ${roomId} (event ${eventId})`,
289
+ );
290
+ }
291
+
292
+ /**
293
+ * Delete a room secret: clears the index entry (empty state content is the
294
+ * "deleted" marker `getIndex` filters on), best-effort redacts the
295
+ * timeline event carrying the value, and drops the cache entry.
296
+ */
297
+ async deleteSecret(roomId: string, name: string): Promise<void> {
298
+ const entry = (await this.getIndex(roomId)).find((e) => e.name === name);
299
+
300
+ await this.gateway.sendStateEvent(roomId, SECRET_INDEX_TYPE, '{}', name);
301
+
302
+ if (entry) {
303
+ try {
304
+ await this.gateway.sendEvent(
305
+ roomId,
306
+ REDACTION_TYPE,
307
+ JSON.stringify({ redacts: entry.eventId }),
308
+ );
309
+ } catch (error) {
310
+ this.logger.warn(
311
+ `[secrets] could not redact secret event ${entry.eventId} in ${roomId}: ${error instanceof Error ? error.message : String(error)}`,
312
+ );
313
+ }
314
+ }
315
+
316
+ this.cache.delete(this.cacheKey(roomId, name));
317
+ this.logger.log(`[secrets] deleted secret "${name}" from room ${roomId}`);
318
+ }
319
+ }