├── .gitignore ├── LICENSE ├── README.md ├── dist ├── index.d.ts └── index.js ├── package.json ├── pnpm-lock.yaml ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # vitepress build output 107 | **/.vitepress/dist 108 | 109 | # vitepress cache directory 110 | **/.vitepress/cache 111 | 112 | # Docusaurus cache and generated files 113 | .docusaurus 114 | 115 | # Serverless directories 116 | .serverless/ 117 | 118 | # FuseBox cache 119 | .fusebox/ 120 | 121 | # DynamoDB Local files 122 | .dynamodb/ 123 | 124 | # TernJS port file 125 | .tern-port 126 | 127 | # Stores VSCode versions used for testing VSCode extensions 128 | .vscode-test 129 | 130 | # yarn v2 131 | .yarn/cache 132 | .yarn/unplugged 133 | .yarn/build-state.yml 134 | .yarn/install-state.gz 135 | .pnp.* 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Outerbase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @outerbase/browsable-durable-object 2 | 3 | A browsable SQL query interface for Cloudflare Durable Objects. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install @outerbase/browsable-durable-object 9 | ``` 10 | 11 | ## Usage 12 | Various ways to implement the browsable experience. 13 | 14 | ### Class Decorator 15 | ```typescript 16 | import { DurableObject } from "cloudflare:workers"; 17 | import { Browsable } from "@outerbase/browsable-durable-object"; 18 | 19 | @Browsable() 20 | export class MyDurableObject extends DurableObject { 21 | public sql: SqlStorage 22 | 23 | constructor(ctx: DurableObjectState, env: Env) { 24 | super(ctx, env); 25 | this.sql = ctx.storage.sql; 26 | } 27 | 28 | async fetch(request: Request): Promise { 29 | return new Response('Hello from MyDurableObject'); 30 | } 31 | } 32 | 33 | export default { 34 | async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { 35 | const path = new URL(request.url).pathname 36 | let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName(path); 37 | let stub = env.MY_DURABLE_OBJECT.get(id); 38 | 39 | return stub.fetch(request); 40 | } 41 | } satisfies ExportedHandler; 42 | ``` 43 | 44 | ### Inheritance 45 | ```typescript 46 | export class MyDurableObject extends BrowsableDurableObject { 47 | public sql: SqlStorage 48 | 49 | constructor(ctx: DurableObjectState, env: Env) { 50 | super(ctx, env); 51 | this.sql = ctx.storage.sql 52 | } 53 | 54 | async fetch(request: Request): Promise { 55 | const baseResponse = await super.fetch(request); 56 | 57 | if (baseResponse.status === 404) { 58 | return new Response('Hello from MyDurableObject'); 59 | } 60 | 61 | return baseResponse; 62 | } 63 | } 64 | ``` 65 | 66 | ### Composition 67 | ```typescript 68 | export class MyDurableObject extends BrowsableDurableObject { 69 | public sql: SqlStorage 70 | private handler: BrowsableHandler; 71 | 72 | constructor(ctx: DurableObjectState, env: Env) { 73 | super(ctx, env); 74 | this.sql = ctx.storage.sql 75 | this.handler = new BrowsableHandler(this.sql); 76 | } 77 | 78 | async fetch(request: Request): Promise { 79 | const url = new URL(request.url); 80 | const path = url.pathname; 81 | if (path === '/query/raw') { 82 | return await this.handler.fetch(request); 83 | } 84 | 85 | return new Response('Hello from MyDurableObject'); 86 | } 87 | } 88 | ``` 89 | 90 | ### Studio UI Support 91 | ```typescript 92 | import { DurableObject } from 'cloudflare:workers'; 93 | import { Browsable, studio } from './browsable'; 94 | 95 | @Browsable() 96 | export class MyDurableObject extends DurableObject {} 97 | 98 | export default { 99 | async fetch(request, env, ctx): Promise { 100 | const url = new URL(request.url); 101 | 102 | if (url.pathname === '/studio') { 103 | return await studio(request, env.MY_DURABLE_OBJECT, { 104 | basicAuth: { 105 | username: 'admin', 106 | password: 'password', 107 | }, 108 | }); 109 | } 110 | 111 | // the rest of your code here 112 | // .... 113 | 114 | return new Response('Hello World', { status: 200 }); 115 | }, 116 | } satisfies ExportedHandler; 117 | ``` 118 | 119 | ## License 120 | 121 | MIT -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from "cloudflare:workers"; 2 | export declare const corsHeaders: { 3 | readonly 'Access-Control-Allow-Origin': "*"; 4 | readonly 'Access-Control-Allow-Methods': "GET, POST, PATCH, PUT, DELETE, OPTIONS"; 5 | readonly 'Access-Control-Allow-Headers': "Authorization, Content-Type, X-Starbase-Source, X-Data-Source"; 6 | readonly 'Access-Control-Max-Age': "86400"; 7 | }; 8 | export declare function corsPreflight(): Response; 9 | export type QueryTransactionRequest = { 10 | transaction?: QueryRequest[]; 11 | }; 12 | export type QueryRequest = { 13 | sql: string; 14 | params?: any[]; 15 | }; 16 | export declare function createResponse(result: unknown, error: string | undefined, status: number): Response; 17 | export declare class BrowsableHandler { 18 | sql: SqlStorage | undefined; 19 | private supportedRoutes; 20 | constructor(sql: SqlStorage | undefined); 21 | fetch(request: Request): Promise; 22 | executeTransaction(opts: { 23 | queries: { 24 | sql: string; 25 | params?: any[]; 26 | }[]; 27 | }): Promise; 28 | private executeRawQuery; 29 | executeQuery(opts: { 30 | sql: string; 31 | params?: unknown[]; 32 | isRaw?: boolean; 33 | }): Promise[] | { 34 | columns: string[]; 35 | rows: SqlStorageValue[][]; 36 | meta: { 37 | rows_read: number; 38 | rows_written: number; 39 | }; 40 | }>; 41 | } 42 | export declare function Browsable(): (constructor: T) => { 45 | new (...args: any[]): { 46 | [x: string]: any; 47 | _bdoHandler?: BrowsableHandler; 48 | fetch(request: Request): Promise; 49 | __studio(cmd: StudioRequest): Promise<{ 50 | headers: { 51 | name: string; 52 | displayName: string; 53 | originalType: string; 54 | type: undefined; 55 | }[]; 56 | rows: Record[]; 57 | stat: { 58 | queryDurationMs: number; 59 | rowsAffected: number; 60 | rowsRead: number; 61 | rowsWritten: number; 62 | }; 63 | } | { 64 | headers: { 65 | name: string; 66 | displayName: string; 67 | originalType: string; 68 | type: undefined; 69 | }[]; 70 | rows: Record[]; 71 | stat: { 72 | queryDurationMs: number; 73 | rowsAffected: number; 74 | rowsRead: number; 75 | rowsWritten: number; 76 | }; 77 | }[] | undefined>; 78 | }; 79 | } & T; 80 | export declare class BrowsableDurableObject extends DurableObject { 81 | sql: SqlStorage | undefined; 82 | protected _bdoHandler?: BrowsableHandler; 83 | constructor(state: DurableObjectState, env: TEnv); 84 | fetch(request: Request): Promise; 85 | } 86 | /** 87 | * Studio 88 | * ------ 89 | * 90 | * This is the built in Studio UI inside of the Browsable extension. It allows you to optionally 91 | * setup a route to enable it. The landing page has an input for you to decide which Durable Object 92 | * ID you want to view the data for. After you have entered the identifier the second page is the 93 | * Studio database browser experience. 94 | */ 95 | interface StudioQueryRequest { 96 | type: 'query'; 97 | id: string; 98 | statement: string; 99 | } 100 | interface StudioTransactionRequest { 101 | type: 'transaction'; 102 | id: string; 103 | statements: string[]; 104 | } 105 | type StudioRequest = StudioQueryRequest | StudioTransactionRequest; 106 | interface StudioOptions { 107 | basicAuth?: { 108 | username: string; 109 | password: string; 110 | }; 111 | } 112 | export declare function studio(request: Request, doNamespace: DurableObjectNamespace, options?: StudioOptions): Promise; 113 | export {}; 114 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import { DurableObject } from "cloudflare:workers"; 2 | export const corsHeaders = { 3 | 'Access-Control-Allow-Origin': '*', 4 | 'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS', 5 | 'Access-Control-Allow-Headers': 'Authorization, Content-Type, X-Starbase-Source, X-Data-Source', 6 | 'Access-Control-Max-Age': '86400', 7 | }; 8 | export function corsPreflight() { 9 | return new Response(null, { 10 | status: 204, 11 | headers: corsHeaders, 12 | }); 13 | } 14 | export function createResponse(result, error, status) { 15 | return new Response(JSON.stringify({ result, error }), { 16 | status, 17 | headers: { 18 | ...corsHeaders, 19 | 'Content-Type': 'application/json', 20 | }, 21 | }); 22 | } 23 | export class BrowsableHandler { 24 | constructor(sql) { 25 | this.supportedRoutes = ['/query/raw']; 26 | this.sql = sql; 27 | } 28 | async fetch(request) { 29 | const url = new URL(request.url); 30 | const path = url.pathname; 31 | // Check if this is a supported route that we should handle in our browsable 32 | // class. If no matches are found we call up to super and as a last resort 33 | // return a 404 to let the user know this inheritance class did not find a 34 | // request to match on. 35 | if (this.supportedRoutes.includes(path)) { 36 | // Handle CORS preflight, at the moment this acts as a very permissive 37 | // acceptance of requests. Expecting the users to add in their own 38 | // version of authentication to protect against users misusing these 39 | // endpoints. 40 | if (request.method === 'OPTIONS') { 41 | return corsPreflight(); 42 | } 43 | if (path === '/query/raw' && request.method === 'POST') { 44 | const { sql, params, transaction } = (await request.json()); 45 | let data = await this.executeTransaction({ 46 | queries: transaction ?? [{ sql, params }] 47 | }); 48 | return createResponse(data, undefined, 200); 49 | } 50 | } 51 | return new Response('Not found', { status: 404 }); 52 | } 53 | async executeTransaction(opts) { 54 | const { queries } = opts; 55 | const results = []; 56 | for (const query of queries) { 57 | let result = await this.executeQuery({ 58 | sql: query.sql, 59 | params: query.params ?? [], 60 | isRaw: true 61 | }); 62 | if (!result) { 63 | console.error('Returning empty array.'); 64 | return []; 65 | } 66 | results.push(result); 67 | } 68 | return results; 69 | } 70 | async executeRawQuery(opts) { 71 | const { sql, params } = opts; 72 | try { 73 | let cursor; 74 | if (params && params.length) { 75 | cursor = this.sql?.exec(sql, ...params); 76 | } 77 | else { 78 | cursor = this.sql?.exec(sql); 79 | } 80 | return cursor; 81 | } 82 | catch (error) { 83 | console.error('SQL Execution Error:', error); 84 | throw error; 85 | } 86 | } 87 | async executeQuery(opts) { 88 | const cursor = await this.executeRawQuery(opts); 89 | if (!cursor) 90 | return []; 91 | if (opts.isRaw) { 92 | return { 93 | columns: cursor.columnNames, 94 | rows: Array.from(cursor.raw()), 95 | meta: { 96 | rows_read: cursor.rowsRead, 97 | rows_written: cursor.rowsWritten, 98 | }, 99 | }; 100 | } 101 | return cursor.toArray(); 102 | } 103 | } 104 | export function Browsable() { 105 | return function (constructor) { 106 | return class extends constructor { 107 | async fetch(request) { 108 | // Initialize handler if not already done 109 | if (!this._bdoHandler) { 110 | this._bdoHandler = new BrowsableHandler(this.sql); 111 | } 112 | // Try browsable handler first 113 | const browsableResponse = await this._bdoHandler.fetch(request); 114 | // If browsable handler returns 404, try the parent class's fetch 115 | if (browsableResponse.status === 404) { 116 | return super.fetch(request); 117 | } 118 | return browsableResponse; 119 | } 120 | async __studio(cmd) { 121 | const storage = this.ctx.storage; 122 | const sql = storage.sql; 123 | if (cmd.type === 'query') { 124 | return executeQuery(sql, cmd.statement); 125 | } 126 | else if (cmd.type === 'transaction') { 127 | return storage.transactionSync(() => { 128 | const results = []; 129 | for (const statement of cmd.statements) { 130 | results.push(executeQuery(sql, statement)); 131 | } 132 | return results; 133 | }); 134 | } 135 | } 136 | }; 137 | }; 138 | } 139 | export class BrowsableDurableObject extends DurableObject { 140 | constructor(state, env) { 141 | super(state, env); 142 | this.sql = undefined; 143 | } 144 | async fetch(request) { 145 | this._bdoHandler = new BrowsableHandler(this.sql); 146 | return this._bdoHandler.fetch(request); 147 | } 148 | } 149 | function executeQuery(sql, statement) { 150 | const cursor = sql.exec(statement); 151 | const columnSet = new Set(); 152 | const columnNames = cursor.columnNames.map((colName) => { 153 | let renameColName = colName; 154 | for (let i = 0; i < 20; i++) { 155 | if (!columnSet.has(renameColName)) 156 | break; 157 | renameColName = '__' + colName + '_' + i; 158 | } 159 | return { 160 | name: renameColName, 161 | displayName: colName, 162 | originalType: 'text', 163 | type: undefined, 164 | }; 165 | }); 166 | return { 167 | headers: columnNames, 168 | rows: Array.from(cursor.raw()).map((r) => columnNames.reduce((a, b, idx) => { 169 | a[b.name] = r[idx]; 170 | return a; 171 | }, {})), 172 | stat: { 173 | queryDurationMs: 0, 174 | rowsAffected: 0, 175 | rowsRead: cursor.rowsRead, 176 | rowsWritten: cursor.rowsWritten, 177 | }, 178 | }; 179 | } 180 | function createHomepageInterface() { 181 | return ` 182 | 183 | Outerbase Studio 184 | 205 | 206 | 207 |

Outerbase Studio

208 | 209 |
210 |

env.MY_DURABLE_OBJECT.idFromName(

211 |
212 | 213 | 214 |
215 |

)

216 |
217 | 218 | `; 219 | } 220 | function createStudioInterface(stubId) { 221 | return ` 222 | 223 | 224 | 240 | Your Starbase - Outerbase Studio 241 | 246 | 247 | 248 | 297 | 298 | 303 | 304 | `; 305 | } 306 | export async function studio(request, doNamespace, options) { 307 | // Protecting 308 | if (options?.basicAuth) { 309 | const authHeader = request.headers.get('Authorization'); 310 | if (!authHeader || !authHeader.startsWith('Basic ')) { 311 | return new Response('Authentication required', { 312 | status: 401, 313 | headers: { 314 | 'WWW-Authenticate': 'Basic realm="Secure Area"', 315 | }, 316 | }); 317 | } 318 | const encoded = authHeader.split(' ')[1]; 319 | const decoded = atob(encoded); 320 | const [username, password] = decoded.split(':'); 321 | if (username !== options.basicAuth.username || password !== options.basicAuth.password) { 322 | return new Response('Invalid credentials', { 323 | status: 401, 324 | headers: { 325 | 'WWW-Authenticate': 'Basic realm="Secure Area"', 326 | }, 327 | }); 328 | } 329 | } 330 | // We run on a single endpoint, we will make use the METHOD to determine what to do 331 | if (request.method === 'GET') { 332 | // This is where we render the interface 333 | const url = new URL(request.url); 334 | const stubId = url.searchParams.get('id'); 335 | if (!stubId) { 336 | return new Response(createHomepageInterface(), { headers: { 'Content-Type': 'text/html' } }); 337 | } 338 | return new Response(createStudioInterface(stubId), { headers: { 'Content-Type': 'text/html' } }); 339 | } 340 | else if (request.method === 'POST') { 341 | const body = (await request.json()); 342 | if (body.type === 'query' || body.type === 'transaction') { 343 | const stubId = doNamespace.idFromName(body.id); 344 | const stub = doNamespace.get(stubId); 345 | try { 346 | // @ts-ignore - accessing __studio method that we know exists 347 | const result = await stub.__studio(body); 348 | return Response.json({ result }); 349 | } 350 | catch (e) { 351 | if (e instanceof Error) { 352 | return Response.json({ error: e.message }); 353 | } 354 | return Response.json({ error: 'Unknown error' }); 355 | } 356 | } 357 | return Response.json({ error: 'Invalid request' }); 358 | } 359 | return new Response('Method not allowed'); 360 | } 361 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@outerbase/browsable-durable-object", 3 | "version": "0.1.1", 4 | "type": "module", 5 | "module": "./dist/index.js", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "import": "./dist/index.js", 12 | "require": "./dist/index.js", 13 | "default": "./dist/index.js" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "description": "A browsable SQL query interface for Cloudflare Durable Objects", 20 | "scripts": { 21 | "build": "tsc", 22 | "prepare": "npm run build", 23 | "publish-npm-module": "npm publish --access public" 24 | }, 25 | "keywords": [ 26 | "cloudflare", 27 | "durable-objects", 28 | "browser" 29 | ], 30 | "author": "Outerbase", 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@cloudflare/workers-types": "^4.20250303.0", 34 | "typescript": "^5.5.2", 35 | "wrangler": "^3.114.0" 36 | } 37 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | devDependencies: 11 | '@cloudflare/workers-types': 12 | specifier: ^4.20250303.0 13 | version: 4.20250312.0 14 | typescript: 15 | specifier: ^5.5.2 16 | version: 5.8.2 17 | wrangler: 18 | specifier: ^3.114.0 19 | version: 3.114.1(@cloudflare/workers-types@4.20250312.0) 20 | 21 | packages: 22 | 23 | '@cloudflare/kv-asset-handler@0.3.4': 24 | resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} 25 | engines: {node: '>=16.13'} 26 | 27 | '@cloudflare/unenv-preset@2.0.2': 28 | resolution: {integrity: sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==} 29 | peerDependencies: 30 | unenv: 2.0.0-rc.14 31 | workerd: ^1.20250124.0 32 | peerDependenciesMeta: 33 | workerd: 34 | optional: true 35 | 36 | '@cloudflare/workerd-darwin-64@1.20250310.0': 37 | resolution: {integrity: sha512-LkLJO6F8lRNaCbK5sQCITi66SyCirDpffRuI5/5iILDJWQU4KVvAOKPvHrd4E5h/WDm9FGd22zMJwky7SxaNjg==} 38 | engines: {node: '>=16'} 39 | cpu: [x64] 40 | os: [darwin] 41 | 42 | '@cloudflare/workerd-darwin-arm64@1.20250310.0': 43 | resolution: {integrity: sha512-WythDJQbsU3Ii1hhA7pJZLBQlHezeYWAnaMnv3gS2Exj45oF8G4chFvrO7zCzjlcJXwSeBTtQRJqxw9AiUDhyA==} 44 | engines: {node: '>=16'} 45 | cpu: [arm64] 46 | os: [darwin] 47 | 48 | '@cloudflare/workerd-linux-64@1.20250310.0': 49 | resolution: {integrity: sha512-LbP769tT4/5QBHSj4lCt99QIKTi6cU+wYhLfF7rEtYHBnZS2+nIw9xttAzxeERx/aFrU+mxLcYPFV8fUeVxGng==} 50 | engines: {node: '>=16'} 51 | cpu: [x64] 52 | os: [linux] 53 | 54 | '@cloudflare/workerd-linux-arm64@1.20250310.0': 55 | resolution: {integrity: sha512-FzWeKM6id20EMZACaDg0Kkvg1C4lvXZgLBXVI6h6xaXTNFReoyEp4v4eMrRTuja5ec5k+m5iGKjP4/bMWJp9ew==} 56 | engines: {node: '>=16'} 57 | cpu: [arm64] 58 | os: [linux] 59 | 60 | '@cloudflare/workerd-windows-64@1.20250310.0': 61 | resolution: {integrity: sha512-04OgaDzm8/8nkjF3tovB+WywZLjSdAHCQT2omXKCwH3EDd1kpd8vvzE1pErtdIyKCOf9/sArY4BhPdxRj7ijlg==} 62 | engines: {node: '>=16'} 63 | cpu: [x64] 64 | os: [win32] 65 | 66 | '@cloudflare/workers-types@4.20250312.0': 67 | resolution: {integrity: sha512-LQBDkrXxm/L0FM4NoT8EXaKCA7+2roOAZAWg+31RGxLKcoAWWSQpbf0PFMBAyFIN/eNADu5RKKrt4qHWNsztHQ==} 68 | 69 | '@cspotcode/source-map-support@0.8.1': 70 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 71 | engines: {node: '>=12'} 72 | 73 | '@emnapi/runtime@1.3.1': 74 | resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} 75 | 76 | '@esbuild-plugins/node-globals-polyfill@0.2.3': 77 | resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} 78 | peerDependencies: 79 | esbuild: '*' 80 | 81 | '@esbuild-plugins/node-modules-polyfill@0.2.2': 82 | resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} 83 | peerDependencies: 84 | esbuild: '*' 85 | 86 | '@esbuild/android-arm64@0.17.19': 87 | resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} 88 | engines: {node: '>=12'} 89 | cpu: [arm64] 90 | os: [android] 91 | 92 | '@esbuild/android-arm@0.17.19': 93 | resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} 94 | engines: {node: '>=12'} 95 | cpu: [arm] 96 | os: [android] 97 | 98 | '@esbuild/android-x64@0.17.19': 99 | resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} 100 | engines: {node: '>=12'} 101 | cpu: [x64] 102 | os: [android] 103 | 104 | '@esbuild/darwin-arm64@0.17.19': 105 | resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} 106 | engines: {node: '>=12'} 107 | cpu: [arm64] 108 | os: [darwin] 109 | 110 | '@esbuild/darwin-x64@0.17.19': 111 | resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} 112 | engines: {node: '>=12'} 113 | cpu: [x64] 114 | os: [darwin] 115 | 116 | '@esbuild/freebsd-arm64@0.17.19': 117 | resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} 118 | engines: {node: '>=12'} 119 | cpu: [arm64] 120 | os: [freebsd] 121 | 122 | '@esbuild/freebsd-x64@0.17.19': 123 | resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} 124 | engines: {node: '>=12'} 125 | cpu: [x64] 126 | os: [freebsd] 127 | 128 | '@esbuild/linux-arm64@0.17.19': 129 | resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} 130 | engines: {node: '>=12'} 131 | cpu: [arm64] 132 | os: [linux] 133 | 134 | '@esbuild/linux-arm@0.17.19': 135 | resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} 136 | engines: {node: '>=12'} 137 | cpu: [arm] 138 | os: [linux] 139 | 140 | '@esbuild/linux-ia32@0.17.19': 141 | resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} 142 | engines: {node: '>=12'} 143 | cpu: [ia32] 144 | os: [linux] 145 | 146 | '@esbuild/linux-loong64@0.17.19': 147 | resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} 148 | engines: {node: '>=12'} 149 | cpu: [loong64] 150 | os: [linux] 151 | 152 | '@esbuild/linux-mips64el@0.17.19': 153 | resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} 154 | engines: {node: '>=12'} 155 | cpu: [mips64el] 156 | os: [linux] 157 | 158 | '@esbuild/linux-ppc64@0.17.19': 159 | resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} 160 | engines: {node: '>=12'} 161 | cpu: [ppc64] 162 | os: [linux] 163 | 164 | '@esbuild/linux-riscv64@0.17.19': 165 | resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} 166 | engines: {node: '>=12'} 167 | cpu: [riscv64] 168 | os: [linux] 169 | 170 | '@esbuild/linux-s390x@0.17.19': 171 | resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} 172 | engines: {node: '>=12'} 173 | cpu: [s390x] 174 | os: [linux] 175 | 176 | '@esbuild/linux-x64@0.17.19': 177 | resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} 178 | engines: {node: '>=12'} 179 | cpu: [x64] 180 | os: [linux] 181 | 182 | '@esbuild/netbsd-x64@0.17.19': 183 | resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} 184 | engines: {node: '>=12'} 185 | cpu: [x64] 186 | os: [netbsd] 187 | 188 | '@esbuild/openbsd-x64@0.17.19': 189 | resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} 190 | engines: {node: '>=12'} 191 | cpu: [x64] 192 | os: [openbsd] 193 | 194 | '@esbuild/sunos-x64@0.17.19': 195 | resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} 196 | engines: {node: '>=12'} 197 | cpu: [x64] 198 | os: [sunos] 199 | 200 | '@esbuild/win32-arm64@0.17.19': 201 | resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} 202 | engines: {node: '>=12'} 203 | cpu: [arm64] 204 | os: [win32] 205 | 206 | '@esbuild/win32-ia32@0.17.19': 207 | resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} 208 | engines: {node: '>=12'} 209 | cpu: [ia32] 210 | os: [win32] 211 | 212 | '@esbuild/win32-x64@0.17.19': 213 | resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} 214 | engines: {node: '>=12'} 215 | cpu: [x64] 216 | os: [win32] 217 | 218 | '@fastify/busboy@2.1.1': 219 | resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} 220 | engines: {node: '>=14'} 221 | 222 | '@img/sharp-darwin-arm64@0.33.5': 223 | resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} 224 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 225 | cpu: [arm64] 226 | os: [darwin] 227 | 228 | '@img/sharp-darwin-x64@0.33.5': 229 | resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} 230 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 231 | cpu: [x64] 232 | os: [darwin] 233 | 234 | '@img/sharp-libvips-darwin-arm64@1.0.4': 235 | resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} 236 | cpu: [arm64] 237 | os: [darwin] 238 | 239 | '@img/sharp-libvips-darwin-x64@1.0.4': 240 | resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} 241 | cpu: [x64] 242 | os: [darwin] 243 | 244 | '@img/sharp-libvips-linux-arm64@1.0.4': 245 | resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} 246 | cpu: [arm64] 247 | os: [linux] 248 | 249 | '@img/sharp-libvips-linux-arm@1.0.5': 250 | resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} 251 | cpu: [arm] 252 | os: [linux] 253 | 254 | '@img/sharp-libvips-linux-s390x@1.0.4': 255 | resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} 256 | cpu: [s390x] 257 | os: [linux] 258 | 259 | '@img/sharp-libvips-linux-x64@1.0.4': 260 | resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} 261 | cpu: [x64] 262 | os: [linux] 263 | 264 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 265 | resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} 266 | cpu: [arm64] 267 | os: [linux] 268 | 269 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 270 | resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} 271 | cpu: [x64] 272 | os: [linux] 273 | 274 | '@img/sharp-linux-arm64@0.33.5': 275 | resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} 276 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 277 | cpu: [arm64] 278 | os: [linux] 279 | 280 | '@img/sharp-linux-arm@0.33.5': 281 | resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} 282 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 283 | cpu: [arm] 284 | os: [linux] 285 | 286 | '@img/sharp-linux-s390x@0.33.5': 287 | resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} 288 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 289 | cpu: [s390x] 290 | os: [linux] 291 | 292 | '@img/sharp-linux-x64@0.33.5': 293 | resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} 294 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 295 | cpu: [x64] 296 | os: [linux] 297 | 298 | '@img/sharp-linuxmusl-arm64@0.33.5': 299 | resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} 300 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 301 | cpu: [arm64] 302 | os: [linux] 303 | 304 | '@img/sharp-linuxmusl-x64@0.33.5': 305 | resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} 306 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 307 | cpu: [x64] 308 | os: [linux] 309 | 310 | '@img/sharp-wasm32@0.33.5': 311 | resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} 312 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 313 | cpu: [wasm32] 314 | 315 | '@img/sharp-win32-ia32@0.33.5': 316 | resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} 317 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 318 | cpu: [ia32] 319 | os: [win32] 320 | 321 | '@img/sharp-win32-x64@0.33.5': 322 | resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} 323 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 324 | cpu: [x64] 325 | os: [win32] 326 | 327 | '@jridgewell/resolve-uri@3.1.2': 328 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 329 | engines: {node: '>=6.0.0'} 330 | 331 | '@jridgewell/sourcemap-codec@1.5.0': 332 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 333 | 334 | '@jridgewell/trace-mapping@0.3.9': 335 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 336 | 337 | acorn-walk@8.3.2: 338 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} 339 | engines: {node: '>=0.4.0'} 340 | 341 | acorn@8.14.0: 342 | resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} 343 | engines: {node: '>=0.4.0'} 344 | hasBin: true 345 | 346 | as-table@1.0.55: 347 | resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} 348 | 349 | blake3-wasm@2.1.5: 350 | resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} 351 | 352 | color-convert@2.0.1: 353 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 354 | engines: {node: '>=7.0.0'} 355 | 356 | color-name@1.1.4: 357 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 358 | 359 | color-string@1.9.1: 360 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 361 | 362 | color@4.2.3: 363 | resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} 364 | engines: {node: '>=12.5.0'} 365 | 366 | cookie@0.5.0: 367 | resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} 368 | engines: {node: '>= 0.6'} 369 | 370 | data-uri-to-buffer@2.0.2: 371 | resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} 372 | 373 | defu@6.1.4: 374 | resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} 375 | 376 | detect-libc@2.0.3: 377 | resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} 378 | engines: {node: '>=8'} 379 | 380 | esbuild@0.17.19: 381 | resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} 382 | engines: {node: '>=12'} 383 | hasBin: true 384 | 385 | escape-string-regexp@4.0.0: 386 | resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 387 | engines: {node: '>=10'} 388 | 389 | estree-walker@0.6.1: 390 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 391 | 392 | exit-hook@2.2.1: 393 | resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} 394 | engines: {node: '>=6'} 395 | 396 | exsolve@1.0.4: 397 | resolution: {integrity: sha512-xsZH6PXaER4XoV+NiT7JHp1bJodJVT+cxeSH1G0f0tlT0lJqYuHUP3bUx2HtfTDvOagMINYp8rsqusxud3RXhw==} 398 | 399 | fsevents@2.3.3: 400 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 401 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 402 | os: [darwin] 403 | 404 | get-source@2.0.12: 405 | resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} 406 | 407 | glob-to-regexp@0.4.1: 408 | resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} 409 | 410 | is-arrayish@0.3.2: 411 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 412 | 413 | magic-string@0.25.9: 414 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 415 | 416 | mime@3.0.0: 417 | resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} 418 | engines: {node: '>=10.0.0'} 419 | hasBin: true 420 | 421 | miniflare@3.20250310.0: 422 | resolution: {integrity: sha512-TQAxoo2ZiQYjiOJoK3bbcyjKD/u1E3akYOeSHc2Zcp1sLVydrgzSjmxtrn65/3BfDIrUgfYHyy9wspT6wzBy/A==} 423 | engines: {node: '>=16.13'} 424 | hasBin: true 425 | 426 | mustache@4.2.0: 427 | resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} 428 | hasBin: true 429 | 430 | ohash@2.0.11: 431 | resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} 432 | 433 | path-to-regexp@6.3.0: 434 | resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} 435 | 436 | pathe@2.0.3: 437 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 438 | 439 | printable-characters@1.0.42: 440 | resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} 441 | 442 | rollup-plugin-inject@3.0.2: 443 | resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} 444 | deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. 445 | 446 | rollup-plugin-node-polyfills@0.2.1: 447 | resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} 448 | 449 | rollup-pluginutils@2.8.2: 450 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 451 | 452 | semver@7.7.1: 453 | resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} 454 | engines: {node: '>=10'} 455 | hasBin: true 456 | 457 | sharp@0.33.5: 458 | resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} 459 | engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} 460 | 461 | simple-swizzle@0.2.2: 462 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 463 | 464 | source-map@0.6.1: 465 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 466 | engines: {node: '>=0.10.0'} 467 | 468 | sourcemap-codec@1.4.8: 469 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 470 | deprecated: Please use @jridgewell/sourcemap-codec instead 471 | 472 | stacktracey@2.1.8: 473 | resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} 474 | 475 | stoppable@1.1.0: 476 | resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} 477 | engines: {node: '>=4', npm: '>=6'} 478 | 479 | tslib@2.8.1: 480 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 481 | 482 | typescript@5.8.2: 483 | resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} 484 | engines: {node: '>=14.17'} 485 | hasBin: true 486 | 487 | ufo@1.5.4: 488 | resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} 489 | 490 | undici@5.28.5: 491 | resolution: {integrity: sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==} 492 | engines: {node: '>=14.0'} 493 | 494 | unenv@2.0.0-rc.14: 495 | resolution: {integrity: sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==} 496 | 497 | workerd@1.20250310.0: 498 | resolution: {integrity: sha512-bAaZ9Bmts3mArbIrXYAtr+ZRsAJAAUEsCtvwfBavIYXaZ5sgdEOJBEiBbvsHp6CsVObegOM85tIWpYLpbTxQrQ==} 499 | engines: {node: '>=16'} 500 | hasBin: true 501 | 502 | wrangler@3.114.1: 503 | resolution: {integrity: sha512-GuS6SrnAZZDiNb20Vf2Ww0KCfnctHUEzi5GyML1i2brfQPI6BikgI/W/u6XDtYtah0OkbIWIiNJ+SdhWT7KEcw==} 504 | engines: {node: '>=16.17.0'} 505 | hasBin: true 506 | peerDependencies: 507 | '@cloudflare/workers-types': ^4.20250310.0 508 | peerDependenciesMeta: 509 | '@cloudflare/workers-types': 510 | optional: true 511 | 512 | ws@8.18.0: 513 | resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} 514 | engines: {node: '>=10.0.0'} 515 | peerDependencies: 516 | bufferutil: ^4.0.1 517 | utf-8-validate: '>=5.0.2' 518 | peerDependenciesMeta: 519 | bufferutil: 520 | optional: true 521 | utf-8-validate: 522 | optional: true 523 | 524 | youch@3.2.3: 525 | resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} 526 | 527 | zod@3.22.3: 528 | resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} 529 | 530 | snapshots: 531 | 532 | '@cloudflare/kv-asset-handler@0.3.4': 533 | dependencies: 534 | mime: 3.0.0 535 | 536 | '@cloudflare/unenv-preset@2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250310.0)': 537 | dependencies: 538 | unenv: 2.0.0-rc.14 539 | optionalDependencies: 540 | workerd: 1.20250310.0 541 | 542 | '@cloudflare/workerd-darwin-64@1.20250310.0': 543 | optional: true 544 | 545 | '@cloudflare/workerd-darwin-arm64@1.20250310.0': 546 | optional: true 547 | 548 | '@cloudflare/workerd-linux-64@1.20250310.0': 549 | optional: true 550 | 551 | '@cloudflare/workerd-linux-arm64@1.20250310.0': 552 | optional: true 553 | 554 | '@cloudflare/workerd-windows-64@1.20250310.0': 555 | optional: true 556 | 557 | '@cloudflare/workers-types@4.20250312.0': {} 558 | 559 | '@cspotcode/source-map-support@0.8.1': 560 | dependencies: 561 | '@jridgewell/trace-mapping': 0.3.9 562 | 563 | '@emnapi/runtime@1.3.1': 564 | dependencies: 565 | tslib: 2.8.1 566 | optional: true 567 | 568 | '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': 569 | dependencies: 570 | esbuild: 0.17.19 571 | 572 | '@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19)': 573 | dependencies: 574 | esbuild: 0.17.19 575 | escape-string-regexp: 4.0.0 576 | rollup-plugin-node-polyfills: 0.2.1 577 | 578 | '@esbuild/android-arm64@0.17.19': 579 | optional: true 580 | 581 | '@esbuild/android-arm@0.17.19': 582 | optional: true 583 | 584 | '@esbuild/android-x64@0.17.19': 585 | optional: true 586 | 587 | '@esbuild/darwin-arm64@0.17.19': 588 | optional: true 589 | 590 | '@esbuild/darwin-x64@0.17.19': 591 | optional: true 592 | 593 | '@esbuild/freebsd-arm64@0.17.19': 594 | optional: true 595 | 596 | '@esbuild/freebsd-x64@0.17.19': 597 | optional: true 598 | 599 | '@esbuild/linux-arm64@0.17.19': 600 | optional: true 601 | 602 | '@esbuild/linux-arm@0.17.19': 603 | optional: true 604 | 605 | '@esbuild/linux-ia32@0.17.19': 606 | optional: true 607 | 608 | '@esbuild/linux-loong64@0.17.19': 609 | optional: true 610 | 611 | '@esbuild/linux-mips64el@0.17.19': 612 | optional: true 613 | 614 | '@esbuild/linux-ppc64@0.17.19': 615 | optional: true 616 | 617 | '@esbuild/linux-riscv64@0.17.19': 618 | optional: true 619 | 620 | '@esbuild/linux-s390x@0.17.19': 621 | optional: true 622 | 623 | '@esbuild/linux-x64@0.17.19': 624 | optional: true 625 | 626 | '@esbuild/netbsd-x64@0.17.19': 627 | optional: true 628 | 629 | '@esbuild/openbsd-x64@0.17.19': 630 | optional: true 631 | 632 | '@esbuild/sunos-x64@0.17.19': 633 | optional: true 634 | 635 | '@esbuild/win32-arm64@0.17.19': 636 | optional: true 637 | 638 | '@esbuild/win32-ia32@0.17.19': 639 | optional: true 640 | 641 | '@esbuild/win32-x64@0.17.19': 642 | optional: true 643 | 644 | '@fastify/busboy@2.1.1': {} 645 | 646 | '@img/sharp-darwin-arm64@0.33.5': 647 | optionalDependencies: 648 | '@img/sharp-libvips-darwin-arm64': 1.0.4 649 | optional: true 650 | 651 | '@img/sharp-darwin-x64@0.33.5': 652 | optionalDependencies: 653 | '@img/sharp-libvips-darwin-x64': 1.0.4 654 | optional: true 655 | 656 | '@img/sharp-libvips-darwin-arm64@1.0.4': 657 | optional: true 658 | 659 | '@img/sharp-libvips-darwin-x64@1.0.4': 660 | optional: true 661 | 662 | '@img/sharp-libvips-linux-arm64@1.0.4': 663 | optional: true 664 | 665 | '@img/sharp-libvips-linux-arm@1.0.5': 666 | optional: true 667 | 668 | '@img/sharp-libvips-linux-s390x@1.0.4': 669 | optional: true 670 | 671 | '@img/sharp-libvips-linux-x64@1.0.4': 672 | optional: true 673 | 674 | '@img/sharp-libvips-linuxmusl-arm64@1.0.4': 675 | optional: true 676 | 677 | '@img/sharp-libvips-linuxmusl-x64@1.0.4': 678 | optional: true 679 | 680 | '@img/sharp-linux-arm64@0.33.5': 681 | optionalDependencies: 682 | '@img/sharp-libvips-linux-arm64': 1.0.4 683 | optional: true 684 | 685 | '@img/sharp-linux-arm@0.33.5': 686 | optionalDependencies: 687 | '@img/sharp-libvips-linux-arm': 1.0.5 688 | optional: true 689 | 690 | '@img/sharp-linux-s390x@0.33.5': 691 | optionalDependencies: 692 | '@img/sharp-libvips-linux-s390x': 1.0.4 693 | optional: true 694 | 695 | '@img/sharp-linux-x64@0.33.5': 696 | optionalDependencies: 697 | '@img/sharp-libvips-linux-x64': 1.0.4 698 | optional: true 699 | 700 | '@img/sharp-linuxmusl-arm64@0.33.5': 701 | optionalDependencies: 702 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 703 | optional: true 704 | 705 | '@img/sharp-linuxmusl-x64@0.33.5': 706 | optionalDependencies: 707 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 708 | optional: true 709 | 710 | '@img/sharp-wasm32@0.33.5': 711 | dependencies: 712 | '@emnapi/runtime': 1.3.1 713 | optional: true 714 | 715 | '@img/sharp-win32-ia32@0.33.5': 716 | optional: true 717 | 718 | '@img/sharp-win32-x64@0.33.5': 719 | optional: true 720 | 721 | '@jridgewell/resolve-uri@3.1.2': {} 722 | 723 | '@jridgewell/sourcemap-codec@1.5.0': {} 724 | 725 | '@jridgewell/trace-mapping@0.3.9': 726 | dependencies: 727 | '@jridgewell/resolve-uri': 3.1.2 728 | '@jridgewell/sourcemap-codec': 1.5.0 729 | 730 | acorn-walk@8.3.2: {} 731 | 732 | acorn@8.14.0: {} 733 | 734 | as-table@1.0.55: 735 | dependencies: 736 | printable-characters: 1.0.42 737 | 738 | blake3-wasm@2.1.5: {} 739 | 740 | color-convert@2.0.1: 741 | dependencies: 742 | color-name: 1.1.4 743 | optional: true 744 | 745 | color-name@1.1.4: 746 | optional: true 747 | 748 | color-string@1.9.1: 749 | dependencies: 750 | color-name: 1.1.4 751 | simple-swizzle: 0.2.2 752 | optional: true 753 | 754 | color@4.2.3: 755 | dependencies: 756 | color-convert: 2.0.1 757 | color-string: 1.9.1 758 | optional: true 759 | 760 | cookie@0.5.0: {} 761 | 762 | data-uri-to-buffer@2.0.2: {} 763 | 764 | defu@6.1.4: {} 765 | 766 | detect-libc@2.0.3: 767 | optional: true 768 | 769 | esbuild@0.17.19: 770 | optionalDependencies: 771 | '@esbuild/android-arm': 0.17.19 772 | '@esbuild/android-arm64': 0.17.19 773 | '@esbuild/android-x64': 0.17.19 774 | '@esbuild/darwin-arm64': 0.17.19 775 | '@esbuild/darwin-x64': 0.17.19 776 | '@esbuild/freebsd-arm64': 0.17.19 777 | '@esbuild/freebsd-x64': 0.17.19 778 | '@esbuild/linux-arm': 0.17.19 779 | '@esbuild/linux-arm64': 0.17.19 780 | '@esbuild/linux-ia32': 0.17.19 781 | '@esbuild/linux-loong64': 0.17.19 782 | '@esbuild/linux-mips64el': 0.17.19 783 | '@esbuild/linux-ppc64': 0.17.19 784 | '@esbuild/linux-riscv64': 0.17.19 785 | '@esbuild/linux-s390x': 0.17.19 786 | '@esbuild/linux-x64': 0.17.19 787 | '@esbuild/netbsd-x64': 0.17.19 788 | '@esbuild/openbsd-x64': 0.17.19 789 | '@esbuild/sunos-x64': 0.17.19 790 | '@esbuild/win32-arm64': 0.17.19 791 | '@esbuild/win32-ia32': 0.17.19 792 | '@esbuild/win32-x64': 0.17.19 793 | 794 | escape-string-regexp@4.0.0: {} 795 | 796 | estree-walker@0.6.1: {} 797 | 798 | exit-hook@2.2.1: {} 799 | 800 | exsolve@1.0.4: {} 801 | 802 | fsevents@2.3.3: 803 | optional: true 804 | 805 | get-source@2.0.12: 806 | dependencies: 807 | data-uri-to-buffer: 2.0.2 808 | source-map: 0.6.1 809 | 810 | glob-to-regexp@0.4.1: {} 811 | 812 | is-arrayish@0.3.2: 813 | optional: true 814 | 815 | magic-string@0.25.9: 816 | dependencies: 817 | sourcemap-codec: 1.4.8 818 | 819 | mime@3.0.0: {} 820 | 821 | miniflare@3.20250310.0: 822 | dependencies: 823 | '@cspotcode/source-map-support': 0.8.1 824 | acorn: 8.14.0 825 | acorn-walk: 8.3.2 826 | exit-hook: 2.2.1 827 | glob-to-regexp: 0.4.1 828 | stoppable: 1.1.0 829 | undici: 5.28.5 830 | workerd: 1.20250310.0 831 | ws: 8.18.0 832 | youch: 3.2.3 833 | zod: 3.22.3 834 | transitivePeerDependencies: 835 | - bufferutil 836 | - utf-8-validate 837 | 838 | mustache@4.2.0: {} 839 | 840 | ohash@2.0.11: {} 841 | 842 | path-to-regexp@6.3.0: {} 843 | 844 | pathe@2.0.3: {} 845 | 846 | printable-characters@1.0.42: {} 847 | 848 | rollup-plugin-inject@3.0.2: 849 | dependencies: 850 | estree-walker: 0.6.1 851 | magic-string: 0.25.9 852 | rollup-pluginutils: 2.8.2 853 | 854 | rollup-plugin-node-polyfills@0.2.1: 855 | dependencies: 856 | rollup-plugin-inject: 3.0.2 857 | 858 | rollup-pluginutils@2.8.2: 859 | dependencies: 860 | estree-walker: 0.6.1 861 | 862 | semver@7.7.1: 863 | optional: true 864 | 865 | sharp@0.33.5: 866 | dependencies: 867 | color: 4.2.3 868 | detect-libc: 2.0.3 869 | semver: 7.7.1 870 | optionalDependencies: 871 | '@img/sharp-darwin-arm64': 0.33.5 872 | '@img/sharp-darwin-x64': 0.33.5 873 | '@img/sharp-libvips-darwin-arm64': 1.0.4 874 | '@img/sharp-libvips-darwin-x64': 1.0.4 875 | '@img/sharp-libvips-linux-arm': 1.0.5 876 | '@img/sharp-libvips-linux-arm64': 1.0.4 877 | '@img/sharp-libvips-linux-s390x': 1.0.4 878 | '@img/sharp-libvips-linux-x64': 1.0.4 879 | '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 880 | '@img/sharp-libvips-linuxmusl-x64': 1.0.4 881 | '@img/sharp-linux-arm': 0.33.5 882 | '@img/sharp-linux-arm64': 0.33.5 883 | '@img/sharp-linux-s390x': 0.33.5 884 | '@img/sharp-linux-x64': 0.33.5 885 | '@img/sharp-linuxmusl-arm64': 0.33.5 886 | '@img/sharp-linuxmusl-x64': 0.33.5 887 | '@img/sharp-wasm32': 0.33.5 888 | '@img/sharp-win32-ia32': 0.33.5 889 | '@img/sharp-win32-x64': 0.33.5 890 | optional: true 891 | 892 | simple-swizzle@0.2.2: 893 | dependencies: 894 | is-arrayish: 0.3.2 895 | optional: true 896 | 897 | source-map@0.6.1: {} 898 | 899 | sourcemap-codec@1.4.8: {} 900 | 901 | stacktracey@2.1.8: 902 | dependencies: 903 | as-table: 1.0.55 904 | get-source: 2.0.12 905 | 906 | stoppable@1.1.0: {} 907 | 908 | tslib@2.8.1: 909 | optional: true 910 | 911 | typescript@5.8.2: {} 912 | 913 | ufo@1.5.4: {} 914 | 915 | undici@5.28.5: 916 | dependencies: 917 | '@fastify/busboy': 2.1.1 918 | 919 | unenv@2.0.0-rc.14: 920 | dependencies: 921 | defu: 6.1.4 922 | exsolve: 1.0.4 923 | ohash: 2.0.11 924 | pathe: 2.0.3 925 | ufo: 1.5.4 926 | 927 | workerd@1.20250310.0: 928 | optionalDependencies: 929 | '@cloudflare/workerd-darwin-64': 1.20250310.0 930 | '@cloudflare/workerd-darwin-arm64': 1.20250310.0 931 | '@cloudflare/workerd-linux-64': 1.20250310.0 932 | '@cloudflare/workerd-linux-arm64': 1.20250310.0 933 | '@cloudflare/workerd-windows-64': 1.20250310.0 934 | 935 | wrangler@3.114.1(@cloudflare/workers-types@4.20250312.0): 936 | dependencies: 937 | '@cloudflare/kv-asset-handler': 0.3.4 938 | '@cloudflare/unenv-preset': 2.0.2(unenv@2.0.0-rc.14)(workerd@1.20250310.0) 939 | '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) 940 | '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) 941 | blake3-wasm: 2.1.5 942 | esbuild: 0.17.19 943 | miniflare: 3.20250310.0 944 | path-to-regexp: 6.3.0 945 | unenv: 2.0.0-rc.14 946 | workerd: 1.20250310.0 947 | optionalDependencies: 948 | '@cloudflare/workers-types': 4.20250312.0 949 | fsevents: 2.3.3 950 | sharp: 0.33.5 951 | transitivePeerDependencies: 952 | - bufferutil 953 | - utf-8-validate 954 | 955 | ws@8.18.0: {} 956 | 957 | youch@3.2.3: 958 | dependencies: 959 | cookie: 0.5.0 960 | mustache: 4.2.0 961 | stacktracey: 2.1.8 962 | 963 | zod@3.22.3: {} 964 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { DurableObject } from "cloudflare:workers"; 2 | 3 | export const corsHeaders = { 4 | 'Access-Control-Allow-Origin': '*', 5 | 'Access-Control-Allow-Methods': 'GET, POST, PATCH, PUT, DELETE, OPTIONS', 6 | 'Access-Control-Allow-Headers': 7 | 'Authorization, Content-Type, X-Starbase-Source, X-Data-Source', 8 | 'Access-Control-Max-Age': '86400', 9 | } as const 10 | 11 | export function corsPreflight(): Response { 12 | return new Response(null, { 13 | status: 204, 14 | headers: corsHeaders, 15 | }) 16 | } 17 | 18 | export type QueryTransactionRequest = { 19 | transaction?: QueryRequest[] 20 | } 21 | 22 | export type QueryRequest = { 23 | sql: string 24 | params?: any[] 25 | } 26 | 27 | export function createResponse( 28 | result: unknown, 29 | error: string | undefined, 30 | status: number 31 | ): Response { 32 | return new Response(JSON.stringify({ result, error }), { 33 | status, 34 | headers: { 35 | ...corsHeaders, 36 | 'Content-Type': 'application/json', 37 | }, 38 | }) 39 | } 40 | 41 | export class BrowsableHandler { 42 | public sql: SqlStorage | undefined; 43 | private supportedRoutes = ['/query/raw']; 44 | 45 | constructor(sql: SqlStorage | undefined) { 46 | this.sql = sql; 47 | } 48 | 49 | async fetch(request: Request) { 50 | const url = new URL(request.url); 51 | const path = url.pathname; 52 | 53 | // Check if this is a supported route that we should handle in our browsable 54 | // class. If no matches are found we call up to super and as a last resort 55 | // return a 404 to let the user know this inheritance class did not find a 56 | // request to match on. 57 | if (this.supportedRoutes.includes(path)) { 58 | // Handle CORS preflight, at the moment this acts as a very permissive 59 | // acceptance of requests. Expecting the users to add in their own 60 | // version of authentication to protect against users misusing these 61 | // endpoints. 62 | if (request.method === 'OPTIONS') { 63 | return corsPreflight(); 64 | } 65 | 66 | if (path === '/query/raw' && request.method === 'POST') { 67 | const { sql, params, transaction } = (await request.json()) as any; 68 | let data = await this.executeTransaction({ 69 | queries: transaction ?? [{ sql, params }] 70 | }); 71 | 72 | return createResponse(data, undefined, 200); 73 | } 74 | } 75 | 76 | return new Response('Not found', { status: 404 }); 77 | } 78 | 79 | async executeTransaction(opts: { 80 | queries: { sql: string; params?: any[] }[] 81 | }): Promise { 82 | const { queries } = opts 83 | const results = [] 84 | 85 | for (const query of queries) { 86 | let result = await this.executeQuery({ 87 | sql: query.sql, 88 | params: query.params ?? [], 89 | isRaw: true 90 | }) 91 | 92 | if (!result) { 93 | console.error('Returning empty array.') 94 | return [] 95 | } 96 | 97 | results.push(result) 98 | } 99 | 100 | return results 101 | } 102 | 103 | private async executeRawQuery< 104 | U extends Record = Record< 105 | string, 106 | SqlStorageValue 107 | >, 108 | >(opts: { sql: string; params?: unknown[] }) { 109 | const { sql, params } = opts 110 | 111 | try { 112 | let cursor 113 | 114 | if (params && params.length) { 115 | cursor = this.sql?.exec(sql, ...params) 116 | } else { 117 | cursor = this.sql?.exec(sql) 118 | } 119 | 120 | return cursor 121 | } catch (error) { 122 | console.error('SQL Execution Error:', error) 123 | throw error 124 | } 125 | } 126 | 127 | public async executeQuery(opts: { 128 | sql: string 129 | params?: unknown[] 130 | isRaw?: boolean 131 | }) { 132 | const cursor = await this.executeRawQuery(opts) 133 | if (!cursor) return [] 134 | 135 | if (opts.isRaw) { 136 | return { 137 | columns: cursor.columnNames, 138 | rows: Array.from(cursor.raw()), 139 | meta: { 140 | rows_read: cursor.rowsRead, 141 | rows_written: cursor.rowsWritten, 142 | }, 143 | } 144 | } 145 | 146 | return cursor.toArray() 147 | } 148 | } 149 | 150 | export function Browsable() { 151 | return function (constructor: T) { 152 | return class extends constructor { 153 | public _bdoHandler?: BrowsableHandler; 154 | 155 | async fetch(request: Request): Promise { 156 | // Initialize handler if not already done 157 | if (!this._bdoHandler) { 158 | this._bdoHandler = new BrowsableHandler(this.sql); 159 | } 160 | 161 | // Try browsable handler first 162 | const browsableResponse = await this._bdoHandler.fetch(request); 163 | 164 | // If browsable handler returns 404, try the parent class's fetch 165 | if (browsableResponse.status === 404) { 166 | return super.fetch(request); 167 | } 168 | 169 | return browsableResponse; 170 | } 171 | 172 | async __studio(cmd: StudioRequest) { 173 | const storage = this.ctx.storage as DurableObjectStorage; 174 | const sql = storage.sql as SqlStorage; 175 | 176 | if (cmd.type === 'query') { 177 | return executeQuery(sql, cmd.statement); 178 | } else if (cmd.type === 'transaction') { 179 | return storage.transactionSync(() => { 180 | const results = []; 181 | for (const statement of cmd.statements) { 182 | results.push(executeQuery(sql, statement)); 183 | } 184 | 185 | return results; 186 | }); 187 | } 188 | } 189 | }; 190 | }; 191 | } 192 | 193 | export class BrowsableDurableObject extends DurableObject { 194 | public sql: SqlStorage | undefined; 195 | protected _bdoHandler?: BrowsableHandler; 196 | 197 | constructor(state: DurableObjectState, env: TEnv) { 198 | super(state, env); 199 | this.sql = undefined; 200 | } 201 | 202 | async fetch(request: Request): Promise { 203 | this._bdoHandler = new BrowsableHandler(this.sql); 204 | return this._bdoHandler.fetch(request); 205 | } 206 | } 207 | 208 | 209 | 210 | /** 211 | * Studio 212 | * ------ 213 | * 214 | * This is the built in Studio UI inside of the Browsable extension. It allows you to optionally 215 | * setup a route to enable it. The landing page has an input for you to decide which Durable Object 216 | * ID you want to view the data for. After you have entered the identifier the second page is the 217 | * Studio database browser experience. 218 | */ 219 | interface StudioQueryRequest { 220 | type: 'query'; 221 | id: string; 222 | statement: string; 223 | } 224 | 225 | interface StudioTransactionRequest { 226 | type: 'transaction'; 227 | id: string; 228 | statements: string[]; 229 | } 230 | 231 | type StudioRequest = StudioQueryRequest | StudioTransactionRequest; 232 | 233 | function executeQuery(sql: SqlStorage, statement: string) { 234 | const cursor = sql.exec(statement); 235 | 236 | const columnSet = new Set(); 237 | const columnNames = cursor.columnNames.map((colName) => { 238 | let renameColName = colName; 239 | 240 | for (let i = 0; i < 20; i++) { 241 | if (!columnSet.has(renameColName)) break; 242 | renameColName = '__' + colName + '_' + i; 243 | } 244 | 245 | return { 246 | name: renameColName, 247 | displayName: colName, 248 | originalType: 'text', 249 | type: undefined, 250 | }; 251 | }); 252 | 253 | return { 254 | headers: columnNames, 255 | rows: Array.from(cursor.raw()).map((r) => 256 | columnNames.reduce((a, b, idx) => { 257 | a[b.name] = r[idx]; 258 | return a; 259 | }, {} as Record) 260 | ), 261 | stat: { 262 | queryDurationMs: 0, 263 | rowsAffected: 0, 264 | rowsRead: cursor.rowsRead, 265 | rowsWritten: cursor.rowsWritten, 266 | }, 267 | }; 268 | } 269 | 270 | function createHomepageInterface() { 271 | return ` 272 | 273 | Outerbase Studio 274 | 295 | 296 | 297 |

Outerbase Studio

298 | 299 |
300 |

env.MY_DURABLE_OBJECT.idFromName(

301 |
302 | 303 | 304 |
305 |

)

306 |
307 | 308 | `; 309 | } 310 | 311 | function createStudioInterface(stubId: string) { 312 | return ` 313 | 314 | 315 | 331 | Your Starbase - Outerbase Studio 332 | 337 | 338 | 339 | 388 | 389 | 394 | 395 | `; 396 | } 397 | 398 | interface StudioOptions { 399 | basicAuth?: { 400 | username: string; 401 | password: string; 402 | }; 403 | } 404 | 405 | export async function studio(request: Request, doNamespace: DurableObjectNamespace, options?: StudioOptions) { 406 | // Protecting 407 | if (options?.basicAuth) { 408 | const authHeader = request.headers.get('Authorization'); 409 | if (!authHeader || !authHeader.startsWith('Basic ')) { 410 | return new Response('Authentication required', { 411 | status: 401, 412 | headers: { 413 | 'WWW-Authenticate': 'Basic realm="Secure Area"', 414 | }, 415 | }); 416 | } 417 | 418 | const encoded = authHeader.split(' ')[1]; 419 | const decoded = atob(encoded); 420 | const [username, password] = decoded.split(':'); 421 | 422 | if (username !== options.basicAuth.username || password !== options.basicAuth.password) { 423 | return new Response('Invalid credentials', { 424 | status: 401, 425 | headers: { 426 | 'WWW-Authenticate': 'Basic realm="Secure Area"', 427 | }, 428 | }); 429 | } 430 | } 431 | 432 | // We run on a single endpoint, we will make use the METHOD to determine what to do 433 | if (request.method === 'GET') { 434 | // This is where we render the interface 435 | const url = new URL(request.url); 436 | const stubId = url.searchParams.get('id'); 437 | 438 | if (!stubId) { 439 | return new Response(createHomepageInterface(), { headers: { 'Content-Type': 'text/html' } }); 440 | } 441 | 442 | return new Response(createStudioInterface(stubId), { headers: { 'Content-Type': 'text/html' } }); 443 | } else if (request.method === 'POST') { 444 | const body = (await request.json()) as StudioRequest; 445 | 446 | if (body.type === 'query' || body.type === 'transaction') { 447 | const stubId = doNamespace.idFromName(body.id); 448 | const stub = doNamespace.get(stubId); 449 | 450 | try { 451 | // @ts-ignore - accessing __studio method that we know exists 452 | const result = await stub.__studio(body); 453 | return Response.json({ result }); 454 | } catch (e) { 455 | if (e instanceof Error) { 456 | return Response.json({ error: e.message }); 457 | } 458 | return Response.json({ error: 'Unknown error' }); 459 | } 460 | } 461 | 462 | return Response.json({ error: 'Invalid request' }); 463 | } 464 | 465 | return new Response('Method not allowed'); 466 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "moduleResolution": "bundler", 6 | "lib": ["ES2020"], 7 | "types": ["@cloudflare/workers-types"], 8 | "strict": true, 9 | "outDir": "dist", 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "include": ["src"], 16 | "exclude": ["node_modules", "dist"] 17 | } --------------------------------------------------------------------------------