@echofiles/echo-pdf 0.4.1 → 0.4.3

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 (72) hide show
  1. package/README.md +302 -11
  2. package/bin/echo-pdf.js +176 -8
  3. package/bin/lib/http.js +26 -1
  4. package/dist/agent-defaults.d.ts +3 -0
  5. package/dist/agent-defaults.js +18 -0
  6. package/dist/auth.d.ts +18 -0
  7. package/dist/auth.js +36 -0
  8. package/dist/core/index.d.ts +50 -0
  9. package/dist/core/index.js +7 -0
  10. package/dist/file-ops.d.ts +11 -0
  11. package/dist/file-ops.js +36 -0
  12. package/dist/file-store-do.d.ts +36 -0
  13. package/dist/file-store-do.js +298 -0
  14. package/dist/file-utils.d.ts +6 -0
  15. package/dist/file-utils.js +36 -0
  16. package/dist/http-error.d.ts +9 -0
  17. package/dist/http-error.js +14 -0
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +1 -0
  20. package/dist/local/index.d.ts +135 -0
  21. package/dist/local/index.js +555 -0
  22. package/dist/mcp-server.d.ts +3 -0
  23. package/dist/mcp-server.js +124 -0
  24. package/dist/node/pdfium-local.d.ts +8 -0
  25. package/dist/node/pdfium-local.js +147 -0
  26. package/dist/node/semantic-local.d.ts +16 -0
  27. package/dist/node/semantic-local.js +113 -0
  28. package/dist/pdf-agent.d.ts +18 -0
  29. package/dist/pdf-agent.js +217 -0
  30. package/dist/pdf-config.d.ts +4 -0
  31. package/dist/pdf-config.js +140 -0
  32. package/dist/pdf-storage.d.ts +8 -0
  33. package/dist/pdf-storage.js +86 -0
  34. package/dist/pdf-types.d.ts +83 -0
  35. package/dist/pdf-types.js +1 -0
  36. package/dist/pdfium-engine.d.ts +9 -0
  37. package/dist/pdfium-engine.js +180 -0
  38. package/dist/provider-client.d.ts +20 -0
  39. package/dist/provider-client.js +173 -0
  40. package/dist/provider-keys.d.ts +10 -0
  41. package/dist/provider-keys.js +27 -0
  42. package/dist/r2-file-store.d.ts +20 -0
  43. package/dist/r2-file-store.js +176 -0
  44. package/dist/response-schema.d.ts +15 -0
  45. package/dist/response-schema.js +159 -0
  46. package/dist/tool-registry.d.ts +16 -0
  47. package/dist/tool-registry.js +175 -0
  48. package/dist/types.d.ts +91 -0
  49. package/dist/types.js +1 -0
  50. package/dist/worker.d.ts +7 -0
  51. package/dist/worker.js +386 -0
  52. package/package.json +34 -5
  53. package/wrangler.toml +1 -1
  54. package/src/agent-defaults.ts +0 -25
  55. package/src/file-ops.ts +0 -50
  56. package/src/file-store-do.ts +0 -349
  57. package/src/file-utils.ts +0 -43
  58. package/src/http-error.ts +0 -21
  59. package/src/index.ts +0 -415
  60. package/src/mcp-server.ts +0 -171
  61. package/src/pdf-agent.ts +0 -252
  62. package/src/pdf-config.ts +0 -143
  63. package/src/pdf-storage.ts +0 -109
  64. package/src/pdf-types.ts +0 -85
  65. package/src/pdfium-engine.ts +0 -207
  66. package/src/provider-client.ts +0 -176
  67. package/src/provider-keys.ts +0 -44
  68. package/src/r2-file-store.ts +0 -195
  69. package/src/response-schema.ts +0 -182
  70. package/src/tool-registry.ts +0 -203
  71. package/src/types.ts +0 -40
  72. package/src/wasm.d.ts +0 -4
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@echofiles/echo-pdf",
3
- "description": "MCP-first PDF agent on Cloudflare Workers with CLI and web demo.",
4
- "version": "0.4.1",
3
+ "description": "Local-first PDF document component core with CLI, workspace artifacts, and reusable page primitives.",
4
+ "version": "0.4.3",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -9,24 +9,53 @@
9
9
  "bin": {
10
10
  "echo-pdf": "./bin/echo-pdf.js"
11
11
  },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./core": {
20
+ "types": "./dist/core/index.d.ts",
21
+ "import": "./dist/core/index.js"
22
+ },
23
+ "./local": {
24
+ "types": "./dist/local/index.d.ts",
25
+ "import": "./dist/local/index.js"
26
+ },
27
+ "./worker": {
28
+ "types": "./dist/worker.d.ts",
29
+ "import": "./dist/worker.js"
30
+ }
31
+ },
12
32
  "files": [
13
33
  "bin",
14
- "src",
34
+ "dist",
15
35
  "scripts",
16
36
  "README.md",
17
37
  "wrangler.toml",
18
38
  "echo-pdf.config.json"
19
39
  ],
20
40
  "scripts": {
41
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
21
42
  "check:runtime": "bash ./scripts/check-runtime.sh",
22
43
  "dev": "wrangler dev",
23
44
  "deploy": "wrangler deploy",
45
+ "document:dev": "ECHO_PDF_SOURCE_DEV=1 bun ./bin/echo-pdf.js",
46
+ "eval": "node ./eval/run-local.mjs",
47
+ "eval:smoke": "node ./eval/run-local.mjs --suite smoke",
48
+ "eval:core": "node ./eval/run-local.mjs --suite core",
49
+ "eval:stress": "node ./eval/run-local.mjs --suite stress",
50
+ "eval:known-bad": "node ./eval/run-local.mjs --suite known-bad",
51
+ "eval:fetch-public-samples": "node ./eval/fetch-public-samples.mjs",
24
52
  "typecheck": "npm run check:runtime && tsc --noEmit",
25
53
  "test:unit": "npm run check:runtime && vitest run tests/unit",
26
- "test:integration": "npm run check:runtime && vitest run tests/integration",
54
+ "test:import-smoke": "npm run check:runtime && npm run build && vitest run tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
55
+ "test:integration": "npm run check:runtime && npm run build && vitest run tests/integration",
27
56
  "test": "npm run test:unit && npm run test:integration",
28
57
  "smoke": "bash ./scripts/smoke.sh",
29
- "prepublishOnly": "npm run typecheck && npm run test"
58
+ "prepublishOnly": "npm run build && npm run typecheck && npm run test"
30
59
  },
31
60
  "engines": {
32
61
  "node": ">=20.0.0"
package/wrangler.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  name = "echo-pdf"
2
- main = "src/index.ts"
2
+ main = "src/worker.ts"
3
3
  compatibility_date = "2026-03-06"
4
4
 
5
5
  [assets]
@@ -1,25 +0,0 @@
1
- import type { EchoPdfConfig } from "./pdf-types"
2
-
3
- const normalize = (value: string): string => value.trim()
4
-
5
- export const resolveProviderAlias = (
6
- config: EchoPdfConfig,
7
- requestedProvider?: string
8
- ): string => {
9
- const raw = normalize(requestedProvider ?? "")
10
- if (raw.length === 0) return config.agent.defaultProvider
11
- if (config.providers[raw]) return raw
12
- const fromType = Object.entries(config.providers).find(([, provider]) => provider.type === raw)?.[0]
13
- if (fromType) return fromType
14
- throw new Error(`Provider "${raw}" not configured`)
15
- }
16
-
17
- export const resolveModelForProvider = (
18
- config: EchoPdfConfig,
19
- _providerAlias: string,
20
- requestedModel?: string
21
- ): string => {
22
- const explicit = normalize(requestedModel ?? "")
23
- if (explicit.length > 0) return explicit
24
- return normalize(config.agent.defaultModel ?? "")
25
- }
package/src/file-ops.ts DELETED
@@ -1,50 +0,0 @@
1
- import { fromBase64, normalizeReturnMode, toInlineFilePayload } from "./file-utils"
2
- import type { FileStore, ReturnMode } from "./types"
3
-
4
- export const runFileOp = async (
5
- fileStore: FileStore,
6
- input: {
7
- readonly op: "list" | "read" | "delete" | "put"
8
- readonly fileId?: string
9
- readonly includeBase64?: boolean
10
- readonly text?: string
11
- readonly filename?: string
12
- readonly mimeType?: string
13
- readonly base64?: string
14
- readonly returnMode?: ReturnMode
15
- }
16
- ): Promise<unknown> => {
17
- if (input.op === "list") {
18
- return { files: await fileStore.list() }
19
- }
20
-
21
- if (input.op === "put") {
22
- const bytes = input.base64 ? fromBase64(input.base64) : new TextEncoder().encode(input.text ?? "")
23
- const meta = await fileStore.put({
24
- filename: input.filename ?? `file-${Date.now()}.txt`,
25
- mimeType: input.mimeType ?? "text/plain; charset=utf-8",
26
- bytes,
27
- })
28
- const returnMode = normalizeReturnMode(input.returnMode)
29
- if (returnMode === "file_id") return { returnMode, file: meta }
30
- if (returnMode === "url") return { returnMode, file: meta, url: `/api/files/get?fileId=${encodeURIComponent(meta.id)}` }
31
- const stored = await fileStore.get(meta.id)
32
- if (!stored) throw new Error(`File not found after put: ${meta.id}`)
33
- return {
34
- returnMode,
35
- ...toInlineFilePayload(stored, true),
36
- }
37
- }
38
-
39
- if (!input.fileId) {
40
- throw new Error("fileId is required")
41
- }
42
-
43
- if (input.op === "delete") {
44
- return { deleted: await fileStore.delete(input.fileId), fileId: input.fileId }
45
- }
46
-
47
- const file = await fileStore.get(input.fileId)
48
- if (!file) throw new Error(`File not found: ${input.fileId}`)
49
- return toInlineFilePayload(file, Boolean(input.includeBase64))
50
- }
@@ -1,349 +0,0 @@
1
- import { fromBase64, toBase64 } from "./file-utils"
2
- import type { StoragePolicy } from "./pdf-types"
3
- import type { StoredFileMeta, StoredFileRecord } from "./types"
4
-
5
- interface StoredValue {
6
- readonly id: string
7
- readonly filename: string
8
- readonly mimeType: string
9
- readonly sizeBytes: number
10
- readonly createdAt: string
11
- readonly bytesBase64: string
12
- }
13
-
14
- interface StoreStats {
15
- readonly fileCount: number
16
- readonly totalBytes: number
17
- }
18
-
19
- const json = (data: unknown, status = 200): Response =>
20
- new Response(JSON.stringify(data), {
21
- status,
22
- headers: { "Content-Type": "application/json; charset=utf-8" },
23
- })
24
-
25
- const readJson = async (request: Request): Promise<Record<string, unknown>> => {
26
- try {
27
- const body = await request.json()
28
- if (typeof body === "object" && body !== null && !Array.isArray(body)) {
29
- return body as Record<string, unknown>
30
- }
31
- return {}
32
- } catch {
33
- return {}
34
- }
35
- }
36
-
37
- const defaultPolicy = (): StoragePolicy => ({
38
- maxFileBytes: 1_200_000,
39
- maxTotalBytes: 52_428_800,
40
- ttlHours: 24,
41
- cleanupBatchSize: 50,
42
- })
43
-
44
- const parsePolicy = (input: unknown): StoragePolicy => {
45
- const raw = typeof input === "object" && input !== null && !Array.isArray(input)
46
- ? (input as Record<string, unknown>)
47
- : {}
48
- const fallback = defaultPolicy()
49
-
50
- const maxFileBytes = Number(raw.maxFileBytes ?? fallback.maxFileBytes)
51
- const maxTotalBytes = Number(raw.maxTotalBytes ?? fallback.maxTotalBytes)
52
- const ttlHours = Number(raw.ttlHours ?? fallback.ttlHours)
53
- const cleanupBatchSize = Number(raw.cleanupBatchSize ?? fallback.cleanupBatchSize)
54
-
55
- return {
56
- maxFileBytes: Number.isFinite(maxFileBytes) && maxFileBytes > 0 ? Math.floor(maxFileBytes) : fallback.maxFileBytes,
57
- maxTotalBytes: Number.isFinite(maxTotalBytes) && maxTotalBytes > 0 ? Math.floor(maxTotalBytes) : fallback.maxTotalBytes,
58
- ttlHours: Number.isFinite(ttlHours) && ttlHours > 0 ? ttlHours : fallback.ttlHours,
59
- cleanupBatchSize:
60
- Number.isFinite(cleanupBatchSize) && cleanupBatchSize > 0 ? Math.floor(cleanupBatchSize) : fallback.cleanupBatchSize,
61
- }
62
- }
63
-
64
- const toMeta = (value: StoredValue): StoredFileMeta => ({
65
- id: value.id,
66
- filename: value.filename,
67
- mimeType: value.mimeType,
68
- sizeBytes: value.sizeBytes,
69
- createdAt: value.createdAt,
70
- })
71
-
72
- const listStoredValues = async (state: DurableObjectState): Promise<StoredValue[]> => {
73
- const listed = await state.storage.list<StoredValue>({ prefix: "file:" })
74
- return [...listed.values()]
75
- }
76
-
77
- const computeStats = (files: ReadonlyArray<StoredValue>): StoreStats => ({
78
- fileCount: files.length,
79
- totalBytes: files.reduce((sum, file) => sum + file.sizeBytes, 0),
80
- })
81
-
82
- const isExpired = (createdAt: string, ttlHours: number): boolean => {
83
- const createdMs = Date.parse(createdAt)
84
- if (!Number.isFinite(createdMs)) return false
85
- return Date.now() - createdMs > ttlHours * 60 * 60 * 1000
86
- }
87
-
88
- const deleteFiles = async (state: DurableObjectState, files: ReadonlyArray<StoredValue>): Promise<number> => {
89
- let deleted = 0
90
- for (const file of files) {
91
- const ok = await state.storage.delete(`file:${file.id}`)
92
- if (ok) deleted += 1
93
- }
94
- return deleted
95
- }
96
-
97
- export class FileStoreDO {
98
- constructor(private readonly state: DurableObjectState) {}
99
-
100
- async fetch(request: Request): Promise<Response> {
101
- const url = new URL(request.url)
102
-
103
- if (request.method === "POST" && url.pathname === "/put") {
104
- const body = await readJson(request)
105
- const policy = parsePolicy(body.policy)
106
- const filename = typeof body.filename === "string" ? body.filename : `file-${Date.now()}`
107
- const mimeType = typeof body.mimeType === "string" ? body.mimeType : "application/octet-stream"
108
- const bytesBase64 = typeof body.bytesBase64 === "string" ? body.bytesBase64 : ""
109
-
110
- const bytes = fromBase64(bytesBase64)
111
- if (bytes.byteLength > policy.maxFileBytes) {
112
- return json(
113
- {
114
- error: `file too large: ${bytes.byteLength} bytes exceeds maxFileBytes ${policy.maxFileBytes}`,
115
- code: "FILE_TOO_LARGE",
116
- policy,
117
- },
118
- 413
119
- )
120
- }
121
-
122
- let files = await listStoredValues(this.state)
123
- const expired = files.filter((file) => isExpired(file.createdAt, policy.ttlHours))
124
- if (expired.length > 0) {
125
- await deleteFiles(this.state, expired)
126
- files = await listStoredValues(this.state)
127
- }
128
-
129
- let stats = computeStats(files)
130
- const projected = stats.totalBytes + bytes.byteLength
131
- if (projected > policy.maxTotalBytes) {
132
- const needFree = projected - policy.maxTotalBytes
133
- const candidates = [...files]
134
- .sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt))
135
- .slice(0, policy.cleanupBatchSize)
136
-
137
- let freed = 0
138
- const evictList: StoredValue[] = []
139
- for (const file of candidates) {
140
- evictList.push(file)
141
- freed += file.sizeBytes
142
- if (freed >= needFree) break
143
- }
144
- if (evictList.length > 0) {
145
- await deleteFiles(this.state, evictList)
146
- files = await listStoredValues(this.state)
147
- stats = computeStats(files)
148
- }
149
- }
150
-
151
- if (stats.totalBytes + bytes.byteLength > policy.maxTotalBytes) {
152
- return json(
153
- {
154
- error: `storage quota exceeded: total ${stats.totalBytes} + incoming ${bytes.byteLength} > maxTotalBytes ${policy.maxTotalBytes}`,
155
- code: "STORAGE_QUOTA_EXCEEDED",
156
- policy,
157
- stats,
158
- },
159
- 507
160
- )
161
- }
162
-
163
- const id = crypto.randomUUID()
164
- const value: StoredValue = {
165
- id,
166
- filename,
167
- mimeType,
168
- sizeBytes: bytes.byteLength,
169
- createdAt: new Date().toISOString(),
170
- bytesBase64,
171
- }
172
- await this.state.storage.put(`file:${id}`, value)
173
- return json({ file: toMeta(value), policy })
174
- }
175
-
176
- if (request.method === "GET" && url.pathname === "/get") {
177
- const fileId = url.searchParams.get("fileId")
178
- if (!fileId) return json({ error: "Missing fileId" }, 400)
179
- const value = await this.state.storage.get<StoredValue>(`file:${fileId}`)
180
- if (!value) return json({ file: null })
181
- return json({ file: value })
182
- }
183
-
184
- if (request.method === "GET" && url.pathname === "/list") {
185
- const listed = await this.state.storage.list<StoredValue>({ prefix: "file:" })
186
- const files = [...listed.values()].map(toMeta)
187
- return json({ files })
188
- }
189
-
190
- if (request.method === "POST" && url.pathname === "/delete") {
191
- const body = await readJson(request)
192
- const fileId = typeof body.fileId === "string" ? body.fileId : ""
193
- if (!fileId) return json({ error: "Missing fileId" }, 400)
194
- const key = `file:${fileId}`
195
- const existing = await this.state.storage.get(key)
196
- if (!existing) return json({ deleted: false })
197
- await this.state.storage.delete(key)
198
- return json({ deleted: true })
199
- }
200
-
201
- if (request.method === "GET" && url.pathname === "/stats") {
202
- let policyInput: unknown
203
- const encoded = url.searchParams.get("policy")
204
- if (encoded) {
205
- try {
206
- policyInput = JSON.parse(encoded)
207
- } catch {
208
- policyInput = undefined
209
- }
210
- }
211
- const policy = parsePolicy(policyInput)
212
- const files = await listStoredValues(this.state)
213
- const stats = computeStats(files)
214
- return json({ policy, stats })
215
- }
216
-
217
- if (request.method === "POST" && url.pathname === "/cleanup") {
218
- const body = await readJson(request)
219
- const policy = parsePolicy(body.policy)
220
- const files = await listStoredValues(this.state)
221
- const expired = files.filter((file) => isExpired(file.createdAt, policy.ttlHours))
222
- const deletedExpired = await deleteFiles(this.state, expired)
223
-
224
- const afterExpired = await listStoredValues(this.state)
225
- let stats = computeStats(afterExpired)
226
- let deletedEvicted = 0
227
- if (stats.totalBytes > policy.maxTotalBytes) {
228
- const sorted = [...afterExpired].sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt))
229
- const evictList: StoredValue[] = []
230
- for (const file of sorted) {
231
- evictList.push(file)
232
- const projected = stats.totalBytes - evictList.reduce((sum, item) => sum + item.sizeBytes, 0)
233
- if (projected <= policy.maxTotalBytes) break
234
- if (evictList.length >= policy.cleanupBatchSize) break
235
- }
236
- deletedEvicted = await deleteFiles(this.state, evictList)
237
- stats = computeStats(await listStoredValues(this.state))
238
- }
239
-
240
- return json({
241
- policy,
242
- deletedExpired,
243
- deletedEvicted,
244
- stats,
245
- })
246
- }
247
-
248
- return json({ error: "Not found" }, 404)
249
- }
250
- }
251
-
252
- export class DurableObjectFileStore {
253
- constructor(
254
- private readonly namespace: DurableObjectNamespace,
255
- private readonly policy: StoragePolicy
256
- ) {}
257
-
258
- private stub(): DurableObjectStub {
259
- return this.namespace.get(this.namespace.idFromName("echo-pdf-file-store"))
260
- }
261
-
262
- async put(input: {
263
- readonly filename: string
264
- readonly mimeType: string
265
- readonly bytes: Uint8Array
266
- }): Promise<StoredFileMeta> {
267
- const response = await this.stub().fetch("https://do/put", {
268
- method: "POST",
269
- headers: { "Content-Type": "application/json" },
270
- body: JSON.stringify({
271
- filename: input.filename,
272
- mimeType: input.mimeType,
273
- bytesBase64: toBase64(input.bytes),
274
- policy: this.policy,
275
- }),
276
- })
277
- const payload = (await response.json()) as { file?: StoredFileMeta; error?: string }
278
- if (!response.ok || !payload.file) {
279
- const details = payload as { error?: string; code?: string; policy?: unknown; stats?: unknown }
280
- const error = new Error(payload.error ?? "DO put failed") as Error & {
281
- status?: number
282
- code?: string
283
- details?: unknown
284
- }
285
- error.status = response.status
286
- error.code = typeof details.code === "string" ? details.code : undefined
287
- error.details = { policy: details.policy, stats: details.stats }
288
- throw error
289
- }
290
- return payload.file
291
- }
292
-
293
- async get(fileId: string): Promise<StoredFileRecord | null> {
294
- const response = await this.stub().fetch(`https://do/get?fileId=${encodeURIComponent(fileId)}`)
295
- const payload = (await response.json()) as { file?: StoredValue | null }
296
- if (!response.ok) throw new Error("DO get failed")
297
- if (!payload.file) return null
298
- return {
299
- id: payload.file.id,
300
- filename: payload.file.filename,
301
- mimeType: payload.file.mimeType,
302
- sizeBytes: payload.file.sizeBytes,
303
- createdAt: payload.file.createdAt,
304
- bytes: fromBase64(payload.file.bytesBase64),
305
- }
306
- }
307
-
308
- async list(): Promise<ReadonlyArray<StoredFileMeta>> {
309
- const response = await this.stub().fetch("https://do/list")
310
- const payload = (await response.json()) as { files?: StoredFileMeta[] }
311
- if (!response.ok) throw new Error("DO list failed")
312
- return payload.files ?? []
313
- }
314
-
315
- async delete(fileId: string): Promise<boolean> {
316
- const response = await this.stub().fetch("https://do/delete", {
317
- method: "POST",
318
- headers: { "Content-Type": "application/json" },
319
- body: JSON.stringify({ fileId }),
320
- })
321
- const payload = (await response.json()) as { deleted?: boolean }
322
- if (!response.ok) throw new Error("DO delete failed")
323
- return payload.deleted === true
324
- }
325
-
326
- async stats(): Promise<{ policy: StoragePolicy; stats: StoreStats }> {
327
- const policyEncoded = encodeURIComponent(JSON.stringify(this.policy))
328
- const response = await this.stub().fetch(`https://do/stats?policy=${policyEncoded}`)
329
- const payload = (await response.json()) as { policy: StoragePolicy; stats: StoreStats }
330
- if (!response.ok) throw new Error("DO stats failed")
331
- return payload
332
- }
333
-
334
- async cleanup(): Promise<{ policy: StoragePolicy; deletedExpired: number; deletedEvicted: number; stats: StoreStats }> {
335
- const response = await this.stub().fetch("https://do/cleanup", {
336
- method: "POST",
337
- headers: { "Content-Type": "application/json" },
338
- body: JSON.stringify({ policy: this.policy }),
339
- })
340
- const payload = (await response.json()) as {
341
- policy: StoragePolicy
342
- deletedExpired: number
343
- deletedEvicted: number
344
- stats: StoreStats
345
- }
346
- if (!response.ok) throw new Error("DO cleanup failed")
347
- return payload
348
- }
349
- }
package/src/file-utils.ts DELETED
@@ -1,43 +0,0 @@
1
- import type { ReturnMode, StoredFileRecord } from "./types"
2
-
3
- export const fromBase64 = (value: string): Uint8Array => {
4
- const raw = atob(value.replace(/^data:.*;base64,/, ""))
5
- const out = new Uint8Array(raw.length)
6
- for (let i = 0; i < raw.length; i++) {
7
- out[i] = raw.charCodeAt(i)
8
- }
9
- return out
10
- }
11
-
12
- export const toBase64 = (bytes: Uint8Array): string => {
13
- let binary = ""
14
- const chunkSize = 0x8000
15
- for (let i = 0; i < bytes.length; i += chunkSize) {
16
- const chunk = bytes.subarray(i, i + chunkSize)
17
- binary += String.fromCharCode(...chunk)
18
- }
19
- return btoa(binary)
20
- }
21
-
22
- export const toDataUrl = (bytes: Uint8Array, mimeType: string): string =>
23
- `data:${mimeType};base64,${toBase64(bytes)}`
24
-
25
- export const normalizeReturnMode = (value: unknown): ReturnMode => {
26
- if (value === "file_id" || value === "url" || value === "inline") {
27
- return value
28
- }
29
- return "inline"
30
- }
31
-
32
- export const toInlineFilePayload = (file: StoredFileRecord, includeBase64: boolean): Record<string, unknown> => ({
33
- file: {
34
- id: file.id,
35
- filename: file.filename,
36
- mimeType: file.mimeType,
37
- sizeBytes: file.sizeBytes,
38
- createdAt: file.createdAt,
39
- },
40
- dataUrl: file.mimeType.startsWith("image/") ? toDataUrl(file.bytes, file.mimeType) : undefined,
41
- base64: includeBase64 ? toBase64(file.bytes) : undefined,
42
- text: file.mimeType.startsWith("text/") ? new TextDecoder().decode(file.bytes) : undefined,
43
- })
package/src/http-error.ts DELETED
@@ -1,21 +0,0 @@
1
- export class HttpError extends Error {
2
- readonly status: number
3
- readonly code: string
4
- readonly details?: unknown
5
-
6
- constructor(status: number, code: string, message: string, details?: unknown) {
7
- super(message)
8
- this.status = status
9
- this.code = code
10
- this.details = details
11
- }
12
- }
13
-
14
- export const badRequest = (code: string, message: string, details?: unknown): HttpError =>
15
- new HttpError(400, code, message, details)
16
-
17
- export const notFound = (code: string, message: string, details?: unknown): HttpError =>
18
- new HttpError(404, code, message, details)
19
-
20
- export const unprocessable = (code: string, message: string, details?: unknown): HttpError =>
21
- new HttpError(422, code, message, details)