@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,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
|
+
}
|