@ixo/oracle-runtime-workers 0.30.0 → 0.32.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 (53) hide show
  1. package/package.json +7 -5
  2. package/src/attachments/channel-links.ts +135 -0
  3. package/src/attachments/channel.ts +203 -0
  4. package/src/attachments/download.ts +23 -6
  5. package/src/attachments/index.ts +23 -0
  6. package/src/attachments/pipeline.ts +120 -42
  7. package/src/attachments/retention.ts +11 -3
  8. package/src/attachments/sanitize.ts +20 -8
  9. package/src/attachments/types.ts +7 -0
  10. package/src/attachments/view.ts +24 -7
  11. package/src/channels/contract.ts +69 -0
  12. package/src/channels/turns.ts +23 -1
  13. package/src/core/main-agent-types.ts +2 -0
  14. package/src/core/middlewares/tool-repetition-guard.ts +6 -3
  15. package/src/core/middlewares/turn-boundary.ts +6 -1
  16. package/src/core/runtime-context.ts +3 -0
  17. package/src/core/tool-not-run.ts +30 -0
  18. package/src/core/wrap-plugin-tool.ts +9 -1
  19. package/src/delivery/plan.ts +3 -1
  20. package/src/do/contracts.ts +3 -0
  21. package/src/do/turn-attachments.ts +103 -0
  22. package/src/do/user-oracle-do.ts +220 -57
  23. package/src/hub-actions/chain-tx-refusals.ts +217 -0
  24. package/src/hub-actions/client.ts +170 -26
  25. package/src/hub-actions/hub-text.ts +82 -0
  26. package/src/hub-actions/request.ts +39 -29
  27. package/src/hub-actions/store.ts +5 -1
  28. package/src/hub-actions/transfer.ts +99 -32
  29. package/src/index.ts +1 -0
  30. package/src/plugin-api/index.ts +1 -0
  31. package/src/plugin-api/types.ts +102 -20
  32. package/src/plugins/attachments/view-attachment-tool.ts +7 -3
  33. package/src/plugins/hub-actions/chain-tx-catalog.ts +1239 -0
  34. package/src/plugins/hub-actions/hub-actions.plugin.ts +490 -232
  35. package/src/plugins/ixo-chain/blocksync.ts +6 -2
  36. package/src/plugins/ixo-chain/http.ts +16 -8
  37. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +1 -1
  38. package/src/plugins/ixo-chain/kinds/chain/index.ts +2 -0
  39. package/src/plugins/ixo-chain/kinds/chain/validators.ts +151 -0
  40. package/src/plugins/ixo-chain/rest.ts +7 -1
  41. package/src/plugins/yoma/catalog.ts +780 -0
  42. package/src/plugins/yoma/clef.ts +98 -0
  43. package/src/plugins/yoma/drafts.ts +339 -0
  44. package/src/plugins/yoma/engine.ts +978 -0
  45. package/src/plugins/yoma/index.ts +13 -0
  46. package/src/plugins/yoma/judge.ts +358 -0
  47. package/src/plugins/yoma/permission.ts +62 -0
  48. package/src/plugins/yoma/reads.ts +195 -0
  49. package/src/plugins/yoma/submission.ts +531 -0
  50. package/src/plugins/yoma/text.ts +16 -0
  51. package/src/plugins/yoma/work.ts +190 -0
  52. package/src/plugins/yoma/yoma.plugin.ts +2171 -0
  53. package/src/sqlite/serialization.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ixo/oracle-runtime-workers",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "description": "QiForge oracle runtime for Cloudflare Workers — Durable Object per user (SQLite in WASM over DO storage), Matrix E2EE gateway Durable Object, Hono HTTP/SSE shell.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -21,7 +21,8 @@
21
21
  "./gateway": "./src/gateway-worker.ts",
22
22
  "./hub-actions/client": "./src/hub-actions/client.ts",
23
23
  "./hub-actions/request": "./src/hub-actions/request.ts",
24
- "./prompt": "./src/core/prompt-composer.ts"
24
+ "./prompt": "./src/core/prompt-composer.ts",
25
+ "./plugins/yoma": "./src/plugins/yoma/index.ts"
25
26
  },
26
27
  "files": [
27
28
  "src",
@@ -33,7 +34,7 @@
33
34
  "!src/plugins/pod-creator/test-fixtures.ts"
34
35
  ],
35
36
  "dependencies": {
36
- "@ixo/action-protocol": "0.6.0",
37
+ "@ixo/action-protocol": "0.8.0",
37
38
  "@blocknote/server-util": "0.29.1",
38
39
  "@composio/core": "0.10.0",
39
40
  "@composio/langchain": "0.9.2",
@@ -65,9 +66,10 @@
65
66
  "@ixo/impactxclient-sdk": "^3.0.0",
66
67
  "protobufjs": "^7.6.5",
67
68
  "@ixo/topic-protocol": "1.0.0-rc.8",
69
+ "survey-core": "2.3.11",
68
70
  "@ixo/common": "1.7.1",
69
- "@ixo/ixo-transaction": "0.2.0",
70
- "@ixo/ucan": "2.3.0"
71
+ "@ixo/ucan": "2.3.0",
72
+ "@ixo/ixo-transaction": "0.2.0"
71
73
  },
72
74
  "devDependencies": {
73
75
  "@cloudflare/vitest-pool-workers": "^0.22.0",
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The links behind a session's channel files (`channel.ts`), kept in the
3
+ * user's own SQLite file in their own table, apart from the messages: the
4
+ * messages, the model and the transcript carry only the `channel:`
5
+ * reference, and `view_attachment` resolves it here once the turn that
6
+ * brought the file has ended. A row lives until its link expires (the
7
+ * gateway signs links for 30 days; `exp` in the link) and goes with its
8
+ * session (`forgetSession`). Failures are logged and treated as a miss: a
9
+ * missing link makes the file unavailable, never the turn.
10
+ */
11
+ import type { DoSqliteDatabase } from '../sqlite/database';
12
+ import type { Logger } from '../plugin-api/types';
13
+ import type { ChannelMediaLink } from './channel';
14
+
15
+ /** The gateway's link lifetime, for a link that carries no readable `exp`. */
16
+ export const CHANNEL_LINK_DEFAULT_TTL_MS = 30 * 24 * 3600 * 1000;
17
+
18
+ /** When `url` stops working: its `exp` (unix seconds), else 30 days from `now`. */
19
+ export function channelLinkExpiry(url: string, now: number): number {
20
+ let exp: string | null = null;
21
+ try {
22
+ exp = new URL(url).searchParams.get('exp');
23
+ } catch {
24
+ exp = null;
25
+ }
26
+ return exp && /^\d{1,12}$/.test(exp)
27
+ ? Number(exp) * 1000
28
+ : now + CHANNEL_LINK_DEFAULT_TTL_MS;
29
+ }
30
+
31
+ export class ChannelAttachmentLinkStore {
32
+ private setupPromise: Promise<void> | undefined;
33
+
34
+ constructor(
35
+ private readonly db: Pick<DoSqliteDatabase, 'run' | 'get'>,
36
+ private readonly log: Pick<Logger, 'warn'>,
37
+ private readonly now: () => number = Date.now,
38
+ ) {}
39
+
40
+ private setup(): Promise<void> {
41
+ this.setupPromise ??= this.createTables().catch((error: unknown) => {
42
+ this.setupPromise = undefined;
43
+ throw error;
44
+ });
45
+ return this.setupPromise;
46
+ }
47
+
48
+ private async createTables(): Promise<void> {
49
+ await this.db.run(
50
+ `CREATE TABLE IF NOT EXISTS channel_attachment_links (
51
+ session_id TEXT NOT NULL,
52
+ ref TEXT NOT NULL,
53
+ binding_id TEXT NOT NULL,
54
+ url TEXT NOT NULL,
55
+ sha256 TEXT NOT NULL,
56
+ expires_at INTEGER NOT NULL,
57
+ UNIQUE (session_id, ref)
58
+ )`,
59
+ );
60
+ await this.db.run(
61
+ `CREATE INDEX IF NOT EXISTS idx_channel_attachment_links_expiry
62
+ ON channel_attachment_links(expires_at)`,
63
+ );
64
+ }
65
+
66
+ /** Keep the links of a turn's files for its session (a resend refreshes them). */
67
+ async put(
68
+ sessionId: string,
69
+ links: ReadonlyArray<ChannelMediaLink & { ref: string }>,
70
+ ): Promise<void> {
71
+ if (links.length === 0) return;
72
+ try {
73
+ await this.setup();
74
+ const now = this.now();
75
+ for (const link of links)
76
+ await this.db.run(
77
+ `INSERT OR REPLACE INTO channel_attachment_links
78
+ (session_id, ref, binding_id, url, sha256, expires_at)
79
+ VALUES (?, ?, ?, ?, ?, ?)`,
80
+ [
81
+ sessionId,
82
+ link.ref,
83
+ link.bindingId,
84
+ link.url,
85
+ link.sha256,
86
+ channelLinkExpiry(link.url, now),
87
+ ],
88
+ );
89
+ await this.db.run(
90
+ 'DELETE FROM channel_attachment_links WHERE expires_at <= ?',
91
+ [now],
92
+ );
93
+ } catch (error) {
94
+ this.log.warn(
95
+ `[attachments] could not keep the channel file links of ${sessionId}: ${error instanceof Error ? error.message : String(error)}`,
96
+ );
97
+ }
98
+ }
99
+
100
+ /** The unexpired link behind `ref` in `sessionId`, if one is kept. */
101
+ async get(
102
+ sessionId: string,
103
+ ref: string,
104
+ ): Promise<ChannelMediaLink | undefined> {
105
+ try {
106
+ await this.setup();
107
+ const row = await this.db.get<{
108
+ url: string;
109
+ sha256: string;
110
+ binding_id: string;
111
+ }>(
112
+ `SELECT url, sha256, binding_id FROM channel_attachment_links
113
+ WHERE session_id = ? AND ref = ? AND expires_at > ?`,
114
+ [sessionId, ref, this.now()],
115
+ );
116
+ return row
117
+ ? { url: row.url, sha256: row.sha256, bindingId: row.binding_id }
118
+ : undefined;
119
+ } catch (error) {
120
+ this.log.warn(
121
+ `[attachments] channel file link read failed: ${error instanceof Error ? error.message : String(error)}`,
122
+ );
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ /** Drop every link of a deleted session. */
128
+ async forgetSession(sessionId: string): Promise<void> {
129
+ await this.setup();
130
+ await this.db.run(
131
+ 'DELETE FROM channel_attachment_links WHERE session_id = ?',
132
+ [sessionId],
133
+ );
134
+ }
135
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Files the person sent on an IXO Channels turn. The channel gateway keeps
3
+ * each file and serves it from a signed link of its own,
4
+ * `https://<gateway host>/media/<binding id>/<object id>?exp=…&kid=…&sig=…`,
5
+ * valid for 30 days. That link is a bearer capability: it never enters
6
+ * model context, hidden notes, stored message metadata or traces. The model,
7
+ * the transcript and `view_attachment` name a channel file by an opaque
8
+ * reference, `channel:<the first 16 hex digits of its sha256>`, and the
9
+ * owner object resolves it to the link server-side (`ChannelMediaSource`):
10
+ * from the turn's own list, else from the links it keeps for the session
11
+ * (`ChannelAttachmentLinkStore`).
12
+ *
13
+ * A link is fetched only when it is the gateway's own (its origin is the
14
+ * host of a `did:web` in `CHANNEL_SERVICE_DID` and its path lies under
15
+ * `/media/<the binding the file came on>/`), without following redirects,
16
+ * and the bytes are kept only when their SHA-256 is the one the gateway
17
+ * sent. The file always goes to a model as these bytes, never by URL.
18
+ */
19
+ import {
20
+ AttachmentRejectedError,
21
+ AttachmentTooLargeError,
22
+ downloadFromUrl,
23
+ } from './download';
24
+
25
+ export const CHANNEL_REF_PREFIX = 'channel:';
26
+
27
+ /**
28
+ * The gateway's per-file bound for a video or a document
29
+ * (`maxLargeMediaBytes`, 50 MiB); photos are at most 16 MB there. A file
30
+ * above `MAX_FILE_SIZE` is archived to the sandbox but never handed to a
31
+ * model (see the pipeline).
32
+ */
33
+ export const CHANNEL_MAX_FILE_BYTES = 52_428_800;
34
+
35
+ /** The opaque reference of a channel file: `channel:` + 16 hex digits of its sha256. */
36
+ export function channelAttachmentRef(sha256: string): string {
37
+ return `${CHANNEL_REF_PREFIX}${sha256.slice(0, 16)}`;
38
+ }
39
+
40
+ export function isChannelAttachmentRef(ref: string): boolean {
41
+ return /^channel:[0-9a-f]{16}$/.test(ref);
42
+ }
43
+
44
+ /** What the owner object knows of a channel file: never shown to a model. */
45
+ export interface ChannelMediaLink {
46
+ url: string;
47
+ /** Lowercase hex SHA-256 of the bytes the gateway serves. */
48
+ sha256: string;
49
+ /** The binding the file arrived on: its link lies under `/media/<bindingId>/`. */
50
+ bindingId: string;
51
+ }
52
+
53
+ /**
54
+ * A channel file the gateway could not serve as sent (the link expired,
55
+ * was refused, or is not the gateway's own): noted, never retried, and
56
+ * described without its link.
57
+ */
58
+ export class ChannelFileUnavailableError extends AttachmentRejectedError {
59
+ override readonly name: string = 'ChannelFileUnavailableError';
60
+ }
61
+
62
+ /** The https origins of the gateways allowed to call this deployment. */
63
+ export function channelMediaOrigins(
64
+ channelServiceDids: readonly string[],
65
+ ): string[] {
66
+ const origins = new Set<string>();
67
+ for (const did of channelServiceDids) {
68
+ const host = /^did:web:([a-z0-9.-]+)$/.exec(did)?.[1];
69
+ if (host) origins.add(`https://${host}`);
70
+ }
71
+ return [...origins];
72
+ }
73
+
74
+ /** Whether `link.url` is the gateway's own link for a file of `link.bindingId`. */
75
+ export function isGatewayMediaUrl(
76
+ link: Pick<ChannelMediaLink, 'url' | 'bindingId'>,
77
+ origins: readonly string[],
78
+ ): boolean {
79
+ let parsed: URL;
80
+ try {
81
+ parsed = new URL(link.url);
82
+ } catch {
83
+ return false;
84
+ }
85
+ const prefix = `/media/${link.bindingId}/`;
86
+ return (
87
+ parsed.protocol === 'https:' &&
88
+ !parsed.username &&
89
+ !parsed.password &&
90
+ origins.includes(parsed.origin) &&
91
+ parsed.pathname.startsWith(prefix) &&
92
+ parsed.pathname.length > prefix.length &&
93
+ !parsed.pathname.slice(prefix.length).includes('/')
94
+ );
95
+ }
96
+
97
+ function hex(bytes: ArrayBuffer): string {
98
+ return Array.from(new Uint8Array(bytes), (byte) =>
99
+ byte.toString(16).padStart(2, '0'),
100
+ ).join('');
101
+ }
102
+
103
+ /**
104
+ * The bytes of a channel file. Every failure is described without the
105
+ * link: a refused or expired link, a redirect or a transport error is
106
+ * `ChannelFileUnavailableError`, and so are bytes whose SHA-256 is not the
107
+ * one the gateway sent; a file over `maxBytes` stays an
108
+ * `AttachmentTooLargeError`, and an abort keeps its reason.
109
+ */
110
+ export async function downloadChannelFile(
111
+ link: ChannelMediaLink,
112
+ opts: {
113
+ origins: readonly string[];
114
+ maxBytes: number;
115
+ fetchImpl?: typeof fetch;
116
+ signal?: AbortSignal;
117
+ timeoutMs?: number;
118
+ },
119
+ ): Promise<{ bytes: Uint8Array; contentType?: string }> {
120
+ if (!isGatewayMediaUrl(link, opts.origins))
121
+ throw new ChannelFileUnavailableError(
122
+ 'its link is not one this channel gateway serves',
123
+ );
124
+ let result: Awaited<ReturnType<typeof downloadFromUrl>>;
125
+ try {
126
+ result = await downloadFromUrl(link.url, {
127
+ ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
128
+ ...(opts.signal ? { signal: opts.signal } : {}),
129
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
130
+ maxBytes: opts.maxBytes,
131
+ maxRedirects: 0,
132
+ });
133
+ } catch (error) {
134
+ if (opts.signal?.aborted) throw error;
135
+ if (error instanceof AttachmentTooLargeError) throw error;
136
+ const status =
137
+ error instanceof Error ? /^HTTP (\d{3}) /.exec(error.message)?.[1] : '';
138
+ throw new ChannelFileUnavailableError(
139
+ status
140
+ ? `the channel gateway answered ${status}`
141
+ : 'the channel gateway could not be reached',
142
+ );
143
+ }
144
+ const digest = hex(await crypto.subtle.digest('SHA-256', result.data));
145
+ if (digest !== link.sha256)
146
+ throw new ChannelFileUnavailableError(
147
+ 'its bytes do not match the digest the channel sent',
148
+ );
149
+ return {
150
+ bytes: result.data,
151
+ ...(result.contentType ? { contentType: result.contentType } : {}),
152
+ };
153
+ }
154
+
155
+ /** How the attachment layer reads a `channel:` reference (built by the owner object). */
156
+ export interface ChannelMediaSource {
157
+ load(
158
+ ref: string,
159
+ opts: {
160
+ maxBytes: number;
161
+ fetchImpl?: typeof fetch;
162
+ signal?: AbortSignal;
163
+ timeoutMs?: number;
164
+ },
165
+ ): Promise<{ bytes: Uint8Array; contentType?: string }>;
166
+ }
167
+
168
+ /**
169
+ * A `ChannelMediaSource` over `resolve` (the turn's files, then the
170
+ * session's stored links) for the gateways in `origins`.
171
+ */
172
+ export function createChannelMediaSource(input: {
173
+ resolve: (ref: string) => Promise<ChannelMediaLink | undefined>;
174
+ origins: readonly string[];
175
+ }): ChannelMediaSource {
176
+ return {
177
+ load: async (ref, opts) => {
178
+ const link = await input.resolve(ref);
179
+ if (!link)
180
+ throw new ChannelFileUnavailableError(
181
+ 'it is no longer available here; ask the person to send it again',
182
+ );
183
+ return downloadChannelFile(link, { ...opts, origins: input.origins });
184
+ },
185
+ };
186
+ }
187
+
188
+ /**
189
+ * The sandbox archive name of a channel file: the cleaned name with the
190
+ * first 12 hex digits of its digest before the extension
191
+ * (`photo-0123456789ab.jpg`), so two photos the gateway names alike never
192
+ * overwrite each other.
193
+ */
194
+ export function channelArchiveName(safeName: string, ref: string): string {
195
+ const tag = ref.slice(
196
+ CHANNEL_REF_PREFIX.length,
197
+ CHANNEL_REF_PREFIX.length + 12,
198
+ );
199
+ const dot = safeName.lastIndexOf('.');
200
+ return dot > 0
201
+ ? `${safeName.slice(0, dot)}-${tag}${safeName.slice(dot)}`
202
+ : `${safeName}-${tag}`;
203
+ }
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Fetching attachment bytes — Matrix media (through the gateway, which holds
3
- * the authenticated client and the room keys) and plain `http(s)` URLs with
4
- * the Node runtime's SSRF guard, manual redirect validation and streaming
5
- * size cap.
3
+ * the authenticated client and the room keys), channel files (through the
4
+ * owner object's `ChannelMediaSource`, see `channel.ts`) and plain `http(s)`
5
+ * URLs with the Node runtime's SSRF guard, manual redirect validation and
6
+ * streaming size cap.
6
7
  */
8
+ import type { ChannelMediaSource } from './channel';
7
9
  import { containerMatchesClaim, detectMimeFromMagicBytes } from './magic';
8
10
  import type { AttachmentInput } from './types';
9
11
 
@@ -142,6 +144,10 @@ export interface DownloadOptions {
142
144
  signal?: AbortSignal;
143
145
  timeoutMs?: number;
144
146
  maxBytes?: number;
147
+ /** Redirects followed (each hop SSRF-checked); default `MAX_REDIRECT_COUNT`. */
148
+ maxRedirects?: number;
149
+ /** Where a `channelRef` attachment's bytes come from; absent = none can be read. */
150
+ channel?: ChannelMediaSource;
145
151
  }
146
152
 
147
153
  /**
@@ -248,7 +254,7 @@ export async function downloadFromUrl(
248
254
  try {
249
255
  let currentUrl = url;
250
256
  let response: Response | undefined;
251
- for (let i = 0; i <= MAX_REDIRECT_COUNT; i += 1) {
257
+ for (let i = 0; i <= (opts.maxRedirects ?? MAX_REDIRECT_COUNT); i += 1) {
252
258
  response = await fetchImpl(currentUrl, {
253
259
  signal: bound.signal,
254
260
  redirect: 'manual',
@@ -307,7 +313,7 @@ export async function loadAttachmentBytes(
307
313
  source: MatrixMediaSource,
308
314
  opts: DownloadOptions = {},
309
315
  ): Promise<{ bytes: Uint8Array; mimetype: string; sniffed: boolean }> {
310
- if (!attachment.eventId && !attachment.mxcUri) {
316
+ if (!attachment.eventId && !attachment.mxcUri && !attachment.channelRef) {
311
317
  throw new Error('Either mxcUri or eventId must be provided');
312
318
  }
313
319
  if (attachment.mxcUri && !ALLOWED_URI_SCHEMES.test(attachment.mxcUri)) {
@@ -320,7 +326,18 @@ export async function loadAttachmentBytes(
320
326
 
321
327
  let bytes: Uint8Array;
322
328
  let httpType: string | undefined;
323
- if (attachment.eventId || attachment.mxcUri!.startsWith('mxc://')) {
329
+ if (attachment.channelRef) {
330
+ if (!opts.channel)
331
+ throw new Error('Channel files cannot be read on this host');
332
+ const loaded = await opts.channel.load(attachment.channelRef, {
333
+ maxBytes,
334
+ ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
335
+ ...(opts.signal ? { signal: opts.signal } : {}),
336
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
337
+ });
338
+ bytes = loaded.bytes;
339
+ httpType = loaded.contentType;
340
+ } else if (attachment.eventId || attachment.mxcUri!.startsWith('mxc://')) {
324
341
  const bound = boundedSignal(
325
342
  opts.signal,
326
343
  opts.timeoutMs ?? DOWNLOAD_TIMEOUT_MS,
@@ -1,3 +1,22 @@
1
+ export {
2
+ CHANNEL_MAX_FILE_BYTES,
3
+ CHANNEL_REF_PREFIX,
4
+ channelArchiveName,
5
+ channelAttachmentRef,
6
+ ChannelFileUnavailableError,
7
+ channelMediaOrigins,
8
+ createChannelMediaSource,
9
+ downloadChannelFile,
10
+ isChannelAttachmentRef,
11
+ isGatewayMediaUrl,
12
+ type ChannelMediaLink,
13
+ type ChannelMediaSource,
14
+ } from './channel';
15
+ export {
16
+ CHANNEL_LINK_DEFAULT_TTL_MS,
17
+ ChannelAttachmentLinkStore,
18
+ channelLinkExpiry,
19
+ } from './channel-links';
1
20
  export { classifyAttachment, type AttachmentKind } from './classify';
2
21
  export {
3
22
  buildUserMessageContent,
@@ -40,6 +59,7 @@ export {
40
59
  type FileCategory,
41
60
  } from './magic';
42
61
  export {
62
+ attachmentMetaFor,
43
63
  nativeAttachmentNote,
44
64
  prepareAttachments,
45
65
  SANDBOX_TRUNCATE_LIMIT,
@@ -61,8 +81,11 @@ export {
61
81
  type SandboxUploadResult,
62
82
  } from './sandbox-archive';
63
83
  export {
84
+ NOTE_FILENAME_MAX,
85
+ NOTE_MIMETYPE_MAX,
64
86
  SANDBOX_OUTPUT_PREFIX,
65
87
  sanitizeAttachmentFilename,
88
+ sanitizeNoteField,
66
89
  sanitizeSandboxPath,
67
90
  } from './sanitize';
68
91
  export {