├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── cli-webworker └── worker.ts ├── cli ├── .vscode │ └── settings.json ├── api_kv_namespace.ts ├── api_r2_bucket.ts ├── bundle.ts ├── cli.ts ├── cli_ae_proxy.ts ├── cli_analytics.ts ├── cli_analytics_durable_objects.ts ├── cli_analytics_r2.ts ├── cli_cfapi.ts ├── cli_command.ts ├── cli_common.ts ├── cli_d1.ts ├── cli_pubsub.ts ├── cli_pubsub_publish.ts ├── cli_pubsub_subscribe.ts ├── cli_push.ts ├── cli_push_deploy.ts ├── cli_push_lambda.ts ├── cli_push_supabase.ts ├── cli_r2.ts ├── cli_r2_abort_multipart_upload.ts ├── cli_r2_complete_multipart_upload.ts ├── cli_r2_copy_object.ts ├── cli_r2_create_bucket.ts ├── cli_r2_create_multipart_upload.ts ├── cli_r2_delete_bucket.ts ├── cli_r2_delete_bucket_encryption.ts ├── cli_r2_delete_object.ts ├── cli_r2_delete_objects.ts ├── cli_r2_generate_credentials.ts ├── cli_r2_get_bucket_encryption.ts ├── cli_r2_get_bucket_location.ts ├── cli_r2_get_head_object.ts ├── cli_r2_head_bucket.ts ├── cli_r2_list_buckets.ts ├── cli_r2_list_multipart_uploads.ts ├── cli_r2_list_objects.ts ├── cli_r2_list_objects_v1.ts ├── cli_r2_presign.ts ├── cli_r2_put_bucket_encryption.ts ├── cli_r2_put_large_object.ts ├── cli_r2_put_object.ts ├── cli_r2_upload_part.ts ├── cli_r2_upload_part_copy.ts ├── cli_serve.ts ├── cli_site.ts ├── cli_site_generate.ts ├── cli_site_serve.ts ├── cli_tail.ts ├── cli_version.ts ├── config_loader.ts ├── config_loader_test.ts ├── deno_bundle.ts ├── deno_check.ts ├── deno_graph.ts ├── deno_info.ts ├── deps_cli.ts ├── docker_cli.ts ├── docker_registry_api.ts ├── flag_parser.ts ├── flag_parser_test.ts ├── fs_util.ts ├── in_memory_r2_bucket.ts ├── in_memory_rpc_channels.ts ├── jsonc.ts ├── module_watcher.ts ├── repo_dir.ts ├── rpc_host_d1_database.ts ├── rpc_host_d1_database_test.ts ├── rpc_host_durable_object_storage.ts ├── rpc_host_durable_object_storage_test.ts ├── rpc_host_sockets.ts ├── rpc_host_web_sockets.ts ├── site │ ├── design.html │ ├── design.ts │ ├── page.ts │ ├── sidebar.ts │ ├── site_config.schema.json │ ├── site_config.ts │ ├── site_config_validation.ts │ ├── site_model.ts │ └── toc.ts ├── spawn.ts ├── sqlite_d1_database.ts ├── sqlite_d1_database_test.ts ├── sqlite_dbpath_for_instance.ts ├── versions.ts ├── versions_test.ts ├── wasm_crypto.ts └── worker_manager.ts ├── common ├── analytics │ ├── cfgql_client.ts │ ├── durable_objects_costs.ts │ ├── graphql.ts │ ├── r2_costs.ts │ └── r2_costs_test.ts ├── aws │ ├── aws_lambda.ts │ ├── emailer.ts │ ├── lambda_runtime.d.ts │ ├── lambda_runtime.ts │ └── lambda_runtime_zip.ts ├── bytes.ts ├── check.ts ├── cloudflare_api.ts ├── cloudflare_email.ts ├── cloudflare_pipeline_transform.ts ├── cloudflare_sockets.ts ├── cloudflare_sockets_deno_conn_factory.ts ├── cloudflare_workers_runtime.ts ├── cloudflare_workers_types.d.ts ├── config.schema.json ├── config.ts ├── config_validation.ts ├── console.ts ├── constants.ts ├── crypto_keys.ts ├── deno_workarounds.ts ├── denoflare_response.ts ├── deploy │ ├── deno_deploy_app.ts │ ├── deno_deploy_common_api.ts │ ├── deno_deploy_dash_api.ts │ └── deno_deploy_rest_api.ts ├── deps_xml.ts ├── fake_web_socket.ts ├── fetch_util.ts ├── import_binary.ts ├── import_text.ts ├── import_wasm.ts ├── incoming_request_cf_properties.ts ├── jwt.ts ├── local_durable_objects.ts ├── local_durable_objects_test.ts ├── local_web_sockets.ts ├── module_worker_execution.ts ├── mqtt │ ├── README.md │ ├── deno_tcp_connection.ts │ ├── mod_deno.ts │ ├── mod_iso.ts │ ├── mqtt.ts │ ├── mqtt_client.ts │ ├── mqtt_connection.ts │ ├── mqtt_messages.ts │ └── web_socket_connection.ts ├── mutex.ts ├── noop_analytics_engine.ts ├── noop_cf_global_caches.ts ├── noop_d1_database.ts ├── noop_email_sender.ts ├── noop_queue.ts ├── oauth.ts ├── r2 │ ├── abort_multipart_upload.ts │ ├── complete_multipart_upload.ts │ ├── copy_object.ts │ ├── create_bucket.ts │ ├── create_multipart_upload.ts │ ├── delete_bucket.ts │ ├── delete_bucket_encryption.ts │ ├── delete_object.ts │ ├── delete_objects.ts │ ├── get_bucket_encryption.ts │ ├── get_bucket_location.ts │ ├── get_head_object.ts │ ├── head_bucket.ts │ ├── known_element.ts │ ├── list_buckets.ts │ ├── list_multipart_uploads.ts │ ├── list_objects.ts │ ├── list_objects_v2.ts │ ├── put_bucket_encryption.ts │ ├── put_object.ts │ ├── r2.ts │ ├── upload_part.ts │ └── upload_part_copy.ts ├── rpc_channel.ts ├── rpc_cloudflare_sockets.ts ├── rpc_fetch.ts ├── rpc_kv_namespace.ts ├── rpc_r2_bucket.ts ├── rpc_r2_model.ts ├── rpc_script.ts ├── rpc_stub_d1_database.ts ├── rpc_stub_durable_object_storage.ts ├── rpc_stub_web_sockets.ts ├── script_worker_execution.ts ├── sets.ts ├── sha1.ts ├── signal.ts ├── sleep.ts ├── storage │ ├── durable_object_storage_test.ts │ ├── in_memory_alarms.ts │ ├── in_memory_durable_object_storage.ts │ ├── sqlite_durable_object_storage.ts │ ├── sqlite_durable_object_storage_test.ts │ └── web_storage_durable_object_storage.ts ├── supabase │ ├── eszip.ts │ ├── supabase_api.ts │ ├── supabase_app.ts │ └── supabase_import_template.js ├── tail.ts ├── tail_connection.ts ├── tail_pretty.ts ├── uint8array_.ts ├── unimplemented_cloudflare_stubs.ts ├── uuid_v4.ts ├── worker_execution.ts ├── xml_parser.ts ├── xml_parser_test.ts ├── xml_util.ts └── xml_util_test.ts ├── denoflare.code-workspace ├── devcli ├── deps.ts ├── devcli.ts ├── devcli_auth.ts ├── devcli_generate_npm.ts ├── devcli_pubsub.ts ├── devcli_r2.ts ├── devcli_regenerate_docs.ts └── tsc.ts ├── examples ├── hello-wasm-worker │ ├── deps.ts │ ├── hello.ts │ ├── hello.wasm │ └── sub │ │ ├── hello2.wasm │ │ └── sub.ts ├── hello-worker │ ├── deps.ts │ └── hello.ts ├── image-demo-worker │ ├── .vscode │ │ └── settings.json │ ├── 404.ts │ ├── README.md │ ├── app_manifest.d.ts │ ├── cli.ts │ ├── cli_transform_generator.ts │ ├── deps_cli.ts │ ├── deps_worker.ts │ ├── ext │ │ ├── photon_rs_bg.js │ │ ├── photon_rs_bg.wasm │ │ ├── pngs.ts │ │ ├── pngs_bg.js │ │ └── pngs_bg.wasm │ ├── favicon.ico │ ├── favicons.ts │ ├── homepage.js │ ├── homepage.ts │ ├── html.ts │ ├── img.ts │ ├── theme.ts │ ├── transforms.json │ ├── transforms.ts │ ├── twitter.ts │ ├── twitter_image.png │ ├── worker.ts │ └── worker_env.d.ts ├── keyspace-worker │ ├── deps.ts │ ├── env.ts │ ├── html.ts │ └── worker.ts ├── mqtt-demo-worker │ ├── deps.ts │ ├── static │ │ ├── mqtt-demo.html │ │ └── mqtt-demo.js │ └── worker.ts ├── multiplat-worker │ ├── README.md │ ├── deps.ts │ ├── multiplat.ts │ └── static │ │ ├── deno.png │ │ ├── example.txt │ │ └── hello.wasm ├── r2-presigned-url-worker │ ├── deps.ts │ ├── worker.ts │ └── worker_env.d.ts ├── r2-public-read-worker │ ├── deps.ts │ ├── listing.ts │ ├── worker.ts │ └── worker_env.d.ts ├── raw-worker │ └── worker.ts ├── tiered-websockets-worker │ ├── client.ts │ ├── colo_from_trace.ts │ ├── colo_tier_do.ts │ ├── deps.ts │ ├── globals.d.ts │ ├── isolate_id.ts │ ├── server.ts │ ├── tiered_worker.ts │ └── tiered_worker_env.d.ts ├── webtail-app │ ├── .vscode │ │ ├── settings.json │ │ └── tasks.json │ ├── app_constants.ts │ ├── demo_mode.ts │ ├── deno.jsonc │ ├── deps_app.ts │ ├── material.ts │ ├── qps_controller.ts │ ├── static_data.ts │ ├── tail_controller.ts │ ├── views │ │ ├── analytics_view.ts │ │ ├── circular_progress_view.ts │ │ ├── console_view.ts │ │ ├── filter_editor_view.ts │ │ ├── header_view.ts │ │ ├── icons.ts │ │ ├── modal_view.ts │ │ ├── profile_editor_view.ts │ │ ├── sidebar_view.ts │ │ └── welcome_panel.ts │ ├── webtail_app.ts │ └── webtail_app_vm.ts └── webtail-worker │ ├── app_manifest.d.ts │ ├── cli.ts │ ├── deps_cli.ts │ ├── deps_worker.ts │ ├── favicons.ts │ ├── material.ts │ ├── static │ └── webtail_app.js │ ├── twitter.ts │ └── webtail_worker.ts └── npm └── denoflare-mqtt ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── cjs ├── main.js └── package.json ├── esm ├── main.js └── package.json ├── main.d.ts └── package.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: [ 'https://denoflare.dev/#support-this-project' ] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Skymethod LLC 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 | # denoflare 2 | Develop, test, and deploy Cloudflare Workers with Deno. 3 | 4 | ## Project documentation 5 | Docs content site: [https://denoflare.dev](https://denoflare.dev) (generated by `denoflare` itself, hosted on Cloudflare Pages) 6 | 7 | Docs content source repo: https://github.com/skymethod/denoflare-docs/ 8 | -------------------------------------------------------------------------------- /cli-webworker/worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { consoleLog } from '../common/console.ts'; 4 | import { RpcChannel } from '../common/rpc_channel.ts'; 5 | import { addRequestHandlerForRunScript } from '../common/rpc_script.ts'; 6 | 7 | (function() { 8 | consoleLog('worker: start'); 9 | 10 | const rpcChannel = new RpcChannel('worker', self.postMessage.bind(self)); 11 | self.onmessage = async function(event) { 12 | if (await rpcChannel.receiveMessage(event.data)) return; 13 | consoleLog('worker: onmessage', event.data); 14 | }; 15 | self.onmessageerror = function(event) { 16 | consoleLog('worker: onmessageerror', event); 17 | }; 18 | 19 | addRequestHandlerForRunScript(rpcChannel); 20 | 21 | })(); 22 | -------------------------------------------------------------------------------- /cli/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true, 5 | "deno.suggest.imports.hosts": { 6 | "https://github.com": false, 7 | "https://deno.land": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cli/cli.ts: -------------------------------------------------------------------------------- 1 | import { serve, SERVE_COMMAND } from './cli_serve.ts'; 2 | import { tail, TAIL_COMMAND } from './cli_tail.ts'; 3 | import { push, PUSH_COMMAND } from './cli_push.ts'; 4 | import { pushLambda, PUSH_LAMBDA_COMMAND } from './cli_push_lambda.ts'; 5 | import { site, SITE_COMMAND } from './cli_site.ts'; 6 | import { parseFlags } from './flag_parser.ts'; 7 | import { CLI_VERSION } from './cli_version.ts'; 8 | import { analytics, ANALYTICS_COMMAND } from './cli_analytics.ts'; 9 | import { cfapi, CFAPI_COMMAND } from './cli_cfapi.ts'; 10 | import { r2, R2_COMMAND } from './cli_r2.ts'; 11 | import { pubsub, PUBSUB_COMMAND } from './cli_pubsub.ts'; 12 | import { CliCommand } from './cli_command.ts'; 13 | import { denoflareCliCommand } from './cli_common.ts'; 14 | import { d1, D1_COMMAND } from './cli_d1.ts'; 15 | import { AE_PROXY_COMMAND, aeProxy } from './cli_ae_proxy.ts'; 16 | import { PUSH_DEPLOY_COMMAND, pushDeploy } from './cli_push_deploy.ts'; 17 | import { PUSH_SUPABASE_COMMAND, pushSupabase } from './cli_push_supabase.ts'; 18 | 19 | const { args, options } = parseFlags(Deno.args); 20 | 21 | const VERSION_COMMAND = denoflareCliCommand('version', 'Dump cli version'); 22 | 23 | export const DENOFLARE_COMMAND = CliCommand.of(['denoflare'], undefined, { version: CLI_VERSION }) 24 | .subcommand(SERVE_COMMAND, serve) 25 | .subcommand(PUSH_COMMAND, push) 26 | .subcommand(PUSH_LAMBDA_COMMAND, pushLambda) 27 | .subcommand(PUSH_DEPLOY_COMMAND, pushDeploy) 28 | .subcommand(PUSH_SUPABASE_COMMAND, pushSupabase) 29 | .subcommand(TAIL_COMMAND, tail) 30 | .subcommand(SITE_COMMAND, site) 31 | .subcommand(ANALYTICS_COMMAND, analytics) 32 | .subcommand(CFAPI_COMMAND, cfapi) 33 | .subcommand(R2_COMMAND, r2) 34 | .subcommand(PUBSUB_COMMAND, pubsub) 35 | .subcommand(D1_COMMAND, d1) 36 | .subcommand(AE_PROXY_COMMAND, aeProxy) 37 | .subcommand(VERSION_COMMAND, () => console.log(CLI_VERSION)) 38 | ; 39 | 40 | if (import.meta.main) { 41 | await DENOFLARE_COMMAND.routeSubcommand(args, options); 42 | } 43 | -------------------------------------------------------------------------------- /cli/cli_analytics.ts: -------------------------------------------------------------------------------- 1 | import { denoflareCliCommand } from './cli_common.ts'; 2 | import { analyticsDurableObjects, ANALYTICS_DURABLE_OBJECTS_COMMAND } from './cli_analytics_durable_objects.ts'; 3 | import { analyticsR2, ANALYTICS_R2_COMMAND } from './cli_analytics_r2.ts'; 4 | 5 | export const ANALYTICS_COMMAND = denoflareCliCommand('analytics', 'Dump stats via the Cloudflare GraphQL Analytics API') 6 | .subcommand(ANALYTICS_DURABLE_OBJECTS_COMMAND, analyticsDurableObjects) 7 | .subcommand(ANALYTICS_R2_COMMAND, analyticsR2) 8 | .docsLink('/cli/analytics') 9 | ; 10 | 11 | export async function analytics(args: (string | number)[], options: Record): Promise { 12 | await ANALYTICS_COMMAND.routeSubcommand(args, options); 13 | } 14 | 15 | export function dumpTable(rows: (string | number)[][], opts: { leftAlignColumns?: number[] } = {}) { 16 | const { leftAlignColumns = [] } = opts; 17 | const sizes: number[] = []; 18 | for (const row of rows) { 19 | for (let i = 0; i < row.length; i++) { 20 | const size = `${row[i]}`.length; 21 | sizes[i] = typeof sizes[i] === 'number' ? Math.max(sizes[i], size) : size; 22 | } 23 | } 24 | for (const row of rows) { 25 | const pieces = []; 26 | for (let i = 0; i < row.length; i++) { 27 | const size = sizes[i]; 28 | const val = `${row[i]}`; 29 | const piece = leftAlignColumns.includes(i) ? val.padEnd(size, ' ') : val.padStart(size, ' '); 30 | pieces.push(piece); 31 | } 32 | console.log(pieces.join(' ')); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cli/cli_pubsub_publish.ts: -------------------------------------------------------------------------------- 1 | import { Mqtt } from '../common/mqtt/mqtt.ts'; 2 | import { MqttClient } from '../common/mqtt/mod_deno.ts'; 3 | import { DISCONNECT } from '../common/mqtt/mqtt_messages.ts'; 4 | import { denoflareCliCommand } from './cli_common.ts'; 5 | import { commandOptionsForPubsub, parseCloudflareEndpoint, parsePubsubOptions } from './cli_pubsub.ts'; 6 | 7 | export const PUBLISH_COMMAND = denoflareCliCommand(['pubsub', 'publish'], 'Publish a message to a Pub/Sub broker') 8 | .option('text', 'string', 'Plaintext message payload') 9 | .option('file', 'string', 'Path to file with the message payload') 10 | .option('topic', 'required-string', 'Topic on which to publish the message') 11 | .option('n', 'integer', 'Times to repeat the message') 12 | .option('maxMessagesPerSecond', 'integer', 'Maximum rate of message to send per second') 13 | .include(commandOptionsForPubsub()) 14 | .docsLink('/cli/pubsub#publish') 15 | ; 16 | 17 | export async function publish(args: (string | number)[], options: Record) { 18 | if (PUBLISH_COMMAND.dumpHelp(args, options)) return; 19 | 20 | const { verbose, text, file, topic, n, maxMessagesPerSecond = 10 } = PUBLISH_COMMAND.parse(args, options); // 10 = current beta limit: https://developers.cloudflare.com/pub-sub/platform/limits/ 21 | 22 | if ((text ?? file) === undefined) throw new Error(`Specify a payload with --text or --file`); 23 | const basePayload = text ? text : await Deno.readFile(file!); 24 | 25 | if (verbose) { 26 | Mqtt.DEBUG = true; 27 | } 28 | 29 | const { endpoint, clientId, password, keepAlive, debugMessages } = await parsePubsubOptions(options); 30 | 31 | const { hostname, port, protocol } = parseCloudflareEndpoint(endpoint); 32 | 33 | const client = new MqttClient({ hostname, port, protocol, maxMessagesPerSecond }); 34 | client.onMqttMessage = message => { 35 | if (debugMessages) console.log(JSON.stringify(message, undefined, 2)); 36 | if (message.type === DISCONNECT) { 37 | console.log('disconnect', message.reason); 38 | } 39 | }; 40 | 41 | console.log('connecting'); 42 | await client.connect({ clientId, password, keepAlive }); 43 | { 44 | const { clientId, keepAlive } = client; 45 | console.log('connected', { clientId, keepAlive }); 46 | } 47 | 48 | for (let i = 0; i < (n ?? 1); i++) { 49 | console.log(`publishing${n ? ` ${i + 1} of ${n}` : ''}`); 50 | let payload = basePayload; 51 | if (n !== undefined && typeof payload === 'string' && payload.includes('$n')) { 52 | payload = payload.replace('$n', String(i + 1)); 53 | } 54 | await client.publish({ topic, payload }); 55 | } 56 | 57 | console.log('disconnecting'); 58 | await client.disconnect(); 59 | 60 | console.log('disconnected'); 61 | } 62 | -------------------------------------------------------------------------------- /cli/cli_pubsub_subscribe.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../common/bytes.ts'; 2 | import { Mqtt } from '../common/mqtt/mqtt.ts'; 3 | import { MqttClient } from '../common/mqtt/mod_deno.ts'; 4 | import { DISCONNECT } from '../common/mqtt/mqtt_messages.ts'; 5 | import { denoflareCliCommand } from './cli_common.ts'; 6 | import { commandOptionsForPubsub, parseCloudflareEndpoint, parsePubsubOptions } from './cli_pubsub.ts'; 7 | 8 | export const SUBSCRIBE_COMMAND = denoflareCliCommand(['pubsub', 'subscribe'], 'Subscribe to a Pub/Sub broker') 9 | .option('topic', 'required-string', 'Topic on which to subscribe') 10 | .include(commandOptionsForPubsub()) 11 | .docsLink('/cli/pubsub#subscribe') 12 | ; 13 | 14 | export async function subscribe(args: (string | number)[], options: Record) { 15 | if (SUBSCRIBE_COMMAND.dumpHelp(args, options)) return; 16 | 17 | const { verbose, topic } = SUBSCRIBE_COMMAND.parse(args, options); 18 | 19 | if (verbose) { 20 | Mqtt.DEBUG = true; 21 | } 22 | 23 | const { endpoint, clientId, password, keepAlive, debugMessages } = await parsePubsubOptions(options); 24 | 25 | const { hostname, port, protocol } = parseCloudflareEndpoint(endpoint); 26 | 27 | const client = new MqttClient({ hostname, port, protocol }); 28 | client.onMqttMessage = message => { 29 | if (debugMessages) console.log(JSON.stringify(message, undefined, 2)); 30 | if (message.type === DISCONNECT) { 31 | console.log('disconnect', message.reason); 32 | } 33 | }; 34 | 35 | client.onReceive = opts => { 36 | const { topic, payload, contentType } = opts; 37 | const display = typeof payload === 'string' ? payload : `(${payload.length} bytes)${payload.length < 1000 ? ` ${new Bytes(payload).utf8()}` : ''}`; 38 | console.log(`[topic: ${topic}]${contentType ? ` [content-type: ${contentType}]` : ''} ${display}`); 39 | }; 40 | 41 | console.log('connecting'); 42 | await client.connect({ clientId, password, keepAlive }); 43 | { 44 | const { clientId, keepAlive } = client; 45 | console.log('connected', { clientId, keepAlive }); 46 | } 47 | 48 | console.log('subscribing'); 49 | await client.subscribe({ topicFilter: topic }); 50 | 51 | console.log('listening'); 52 | await client.completion(); 53 | console.log('completed'); 54 | } 55 | -------------------------------------------------------------------------------- /cli/cli_r2_abort_multipart_upload.ts: -------------------------------------------------------------------------------- 1 | import { abortMultipartUpload as abortMultipartUploadR2, R2 } from '../common/r2/r2.ts'; 2 | import { denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const ABORT_MULTIPART_UPLOAD_COMMAND = denoflareCliCommand(['r2', 'abort-multipart-upload'], 'Abort an existing multipart upload') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'string', 'Key of the object') 8 | .arg('uploadId', 'string', 'Id of the existing multipart upload to abort') 9 | .include(commandOptionsForR2()) 10 | .docsLink('/cli/r2#abort-multipart-upload') 11 | ; 12 | 13 | export async function abortMultipartUpload(args: (string | number)[], options: Record) { 14 | if (ABORT_MULTIPART_UPLOAD_COMMAND.dumpHelp(args, options)) return; 15 | 16 | const { bucket, key, uploadId, verbose } = ABORT_MULTIPART_UPLOAD_COMMAND.parse(args, options); 17 | 18 | if (verbose) { 19 | R2.DEBUG = true; 20 | } 21 | 22 | const { origin, region, context, urlStyle } = await loadR2Options(options); 23 | 24 | await abortMultipartUploadR2({ bucket, key, uploadId, origin, region, urlStyle }, context); 25 | } 26 | -------------------------------------------------------------------------------- /cli/cli_r2_complete_multipart_upload.ts: -------------------------------------------------------------------------------- 1 | import { CompletedPart, completeMultipartUpload as completeMultipartUploadR2, R2 } from '../common/r2/r2.ts'; 2 | import { CliStats, denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options, surroundWithDoubleQuotesIfNecessary } from './cli_r2.ts'; 4 | 5 | export const COMPLETE_MULTIPART_UPLOAD_COMMAND = denoflareCliCommand(['r2', 'complete-multipart-upload'], 'Complete an existing multipart upload') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'string', 'Key of the object') 8 | .arg('uploadId', 'string', 'Id of the existing multipart upload to complete') 9 | .arg('part', 'strings', 'partNumber:etag') 10 | .include(commandOptionsForR2()) 11 | .docsLink('/cli/r2#complete-multipart-upload') 12 | ; 13 | 14 | export async function completeMultipartUpload(args: (string | number)[], options: Record) { 15 | if (COMPLETE_MULTIPART_UPLOAD_COMMAND.dumpHelp(args, options)) return; 16 | 17 | const { bucket, key, uploadId, part: partSpecs, verbose } = COMPLETE_MULTIPART_UPLOAD_COMMAND.parse(args, options); 18 | 19 | if (verbose) { 20 | R2.DEBUG = true; 21 | } 22 | 23 | const parts = partSpecs.map(parsePartSpec); 24 | 25 | const { origin, region, context, urlStyle } = await loadR2Options(options); 26 | 27 | const result = await completeMultipartUploadR2({ bucket, key, uploadId, parts, origin, region, urlStyle }, context); 28 | console.log(JSON.stringify(result, undefined, 2)); 29 | 30 | const millis = Date.now() - CliStats.launchTime; 31 | console.log(`completed in ${millis}ms`); 32 | } 33 | 34 | // 35 | 36 | function parsePartSpec(partSpec: unknown): CompletedPart { 37 | if (typeof partSpec !== 'string') throw new Error(`Invalid part: ${partSpec}`); 38 | const [ partNumberStr, etagStr ] = partSpec.split(':'); 39 | if (typeof partNumberStr !== 'string' || typeof etagStr !== 'string') throw new Error(`Invalid part: ${partSpec}`); 40 | const partNumber = parseInt(partNumberStr); 41 | const etag = surroundWithDoubleQuotesIfNecessary(etagStr)!; 42 | return { partNumber, etag }; 43 | } 44 | -------------------------------------------------------------------------------- /cli/cli_r2_create_bucket.ts: -------------------------------------------------------------------------------- 1 | import { createBucket as createBucketR2, R2 } from '../common/r2/r2.ts'; 2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const CREATE_BUCKET_COMMAND = denoflareCliCommand(['r2', 'create-bucket'], 'Create a new R2 bucket') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#create-bucket') 9 | ; 10 | 11 | export async function createBucket(args: (string | number)[], options: Record) { 12 | if (CREATE_BUCKET_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = CREATE_BUCKET_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const { origin, region, context, urlStyle } = await loadR2Options(options); 21 | 22 | const { location } = await createBucketR2({ bucket, origin, region, urlStyle }, context); 23 | console.log(location); 24 | } 25 | -------------------------------------------------------------------------------- /cli/cli_r2_create_multipart_upload.ts: -------------------------------------------------------------------------------- 1 | import { createMultipartUpload as createMultipartUploadR2, R2 } from '../common/r2/r2.ts'; 2 | import { CliStats, denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const CREATE_MULTIPART_UPLOAD_COMMAND = denoflareCliCommand(['r2', 'create-multipart-upload'], 'Start a multipart upload and return an upload ID') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'string', 'Key of the object to upload') 8 | .option('cacheControl', 'string', 'Specify caching behavior along the request/reply chain') 9 | .option('contentDisposition', 'string', 'Specify presentational information for the object') 10 | .option('contentEncoding', 'string', 'Specify what content encodings have been applied to the object') 11 | .option('contentLanguage', 'string', 'Specify the language the object is in') 12 | .option('contentType', 'string', 'A standard MIME type describing the format of the contents') 13 | .option('expires', 'string', 'The date and time at which the object is no longer cacheable') 14 | .option('custom', 'name-value-pairs', 'Custom metadata for the object') 15 | .option('ifMatch', 'string', 'Put the object only if its entity tag (ETag) is the same as the one specified') 16 | .option('ifNoneMatch', 'string', 'Put the object only if its entity tag (ETag) is different from the one specified') 17 | .option('ifModifiedSince', 'string', 'Put the object only if it has been modified since the specified time') 18 | .option('ifUnmodifiedSince', 'string', 'Put the object only if it has not been modified since the specified time') 19 | .include(commandOptionsForR2()) 20 | .docsLink('/cli/r2#create-multipart-upload') 21 | ; 22 | 23 | export async function createMultipartUpload(args: (string | number)[], options: Record) { 24 | if (CREATE_MULTIPART_UPLOAD_COMMAND.dumpHelp(args, options)) return; 25 | 26 | const { bucket, key, verbose, cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType, expires, custom: customMetadata, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince } = CREATE_MULTIPART_UPLOAD_COMMAND.parse(args, options); 27 | 28 | if (verbose) { 29 | R2.DEBUG = true; 30 | } 31 | 32 | const { origin, region, context, urlStyle } = await loadR2Options(options); 33 | 34 | const result = await createMultipartUploadR2({ 35 | bucket, key, origin, region, urlStyle, cacheControl, contentDisposition, contentEncoding, contentLanguage, expires, contentType, customMetadata, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince 36 | }, context); 37 | console.log(JSON.stringify(result, undefined, 2)); 38 | 39 | const millis = Date.now() - CliStats.launchTime; 40 | console.log(`copied in ${millis}ms`); 41 | } 42 | -------------------------------------------------------------------------------- /cli/cli_r2_delete_bucket.ts: -------------------------------------------------------------------------------- 1 | import { deleteBucket as deleteBucketR2, R2 } from '../common/r2/r2.ts'; 2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const DELETE_BUCKET_COMMAND = denoflareCliCommand(['r2', 'delete-bucket'], 'Delete an R2 bucket') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#delete-bucket') 9 | ; 10 | 11 | export async function deleteBucket(args: (string | number)[], options: Record) { 12 | if (DELETE_BUCKET_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = DELETE_BUCKET_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const { origin, region, context, urlStyle } = await loadR2Options(options); 21 | 22 | await deleteBucketR2({ bucket, origin, region, urlStyle }, context); 23 | } 24 | -------------------------------------------------------------------------------- /cli/cli_r2_delete_bucket_encryption.ts: -------------------------------------------------------------------------------- 1 | import { deleteBucketEncryption as deleteBucketEncryptionR2, R2 } from '../common/r2/r2.ts'; 2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const DELETE_BUCKET_ENCRYPTION_COMMAND = denoflareCliCommand(['r2', 'delete-bucket-encryption'], 'Reset encryption config for a bucket') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#delete-bucket-encryption') 9 | ; 10 | 11 | export async function deleteBucketEncryption(args: (string | number)[], options: Record) { 12 | if (DELETE_BUCKET_ENCRYPTION_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = DELETE_BUCKET_ENCRYPTION_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const { origin, region, context, urlStyle } = await loadR2Options(options); 21 | 22 | const result = await deleteBucketEncryptionR2({ bucket, origin, region, urlStyle }, context); 23 | console.log(JSON.stringify(result, undefined, 2)); 24 | } 25 | -------------------------------------------------------------------------------- /cli/cli_r2_delete_object.ts: -------------------------------------------------------------------------------- 1 | import { deleteObject as deleteObjectR2, R2 } from '../common/r2/r2.ts'; 2 | import { denoflareCliCommand, parseOptionalStringOption } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const DELETE_OBJECT_COMMAND = denoflareCliCommand(['r2', 'delete-object'], 'Delete R2 object for a given key') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'string', 'Key of the object to delete') 8 | .option('versionId', 'string', 'Returns the version ID of the delete marker created as a result of the DELETE operation') 9 | .include(commandOptionsForR2()) 10 | .docsLink('/cli/r2#delete-object') 11 | ; 12 | 13 | export async function deleteObject(args: (string | number)[], options: Record) { 14 | if (DELETE_OBJECT_COMMAND.dumpHelp(args, options)) return; 15 | 16 | const { bucket, key, verbose } = DELETE_OBJECT_COMMAND.parse(args, options); 17 | 18 | if (verbose) { 19 | R2.DEBUG = true; 20 | } 21 | 22 | const versionId = parseOptionalStringOption('version-id', options); 23 | 24 | const { origin, region, context, urlStyle } = await loadR2Options(options); 25 | 26 | await deleteObjectR2({ bucket, key, origin, region, versionId, urlStyle }, context); 27 | } 28 | -------------------------------------------------------------------------------- /cli/cli_r2_delete_objects.ts: -------------------------------------------------------------------------------- 1 | import { deleteObjects as deleteObjectsR2, R2 } from '../common/r2/r2.ts'; 2 | import { denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const DELETE_OBJECTS_COMMAND = denoflareCliCommand(['r2', 'delete-objects'], 'Delete R2 objects for the given keys') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'strings', 'Keys of the objects to delete') 8 | .option('quiet', 'boolean', 'Enable quiet mode, response will only include keys where the delete action encountered an error') 9 | .include(commandOptionsForR2()) 10 | .docsLink('/cli/r2#delete-objects') 11 | ; 12 | 13 | export async function deleteObjects(args: (string | number)[], options: Record) { 14 | if (DELETE_OBJECTS_COMMAND.dumpHelp(args, options)) return; 15 | 16 | const { bucket, key: items, verbose, quiet } = DELETE_OBJECTS_COMMAND.parse(args, options); 17 | 18 | if (verbose) { 19 | R2.DEBUG = true; 20 | } 21 | 22 | const { origin, region, context, urlStyle } = await loadR2Options(options); 23 | 24 | const result = await deleteObjectsR2({ bucket, items, origin, region, quiet, urlStyle }, context); 25 | console.log(JSON.stringify(result, undefined, 2)); 26 | } 27 | -------------------------------------------------------------------------------- /cli/cli_r2_generate_credentials.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../common/bytes.ts'; 2 | import { denoflareCliCommand } from './cli_common.ts'; 3 | 4 | export const GENERATE_CREDENTIALS_COMMAND = denoflareCliCommand(['r2', 'generate-credentials'], 'Generate private R2-looking credentials for any use') 5 | .docsLink('/cli/r2#generate-credentials') 6 | ; 7 | 8 | export function generateCredentials(args: (string | number)[], options: Record) { 9 | if (GENERATE_CREDENTIALS_COMMAND.dumpHelp(args, options)) return; 10 | 11 | GENERATE_CREDENTIALS_COMMAND.parse(args, options); 12 | 13 | const accessKeyId = new Bytes(crypto.getRandomValues(new Uint8Array(16))).hex(); 14 | const secretAccessKey = new Bytes(crypto.getRandomValues(new Uint8Array(32))).hex(); 15 | console.log(JSON.stringify({ accessKeyId, secretAccessKey }, undefined, 2)); 16 | } 17 | -------------------------------------------------------------------------------- /cli/cli_r2_get_bucket_encryption.ts: -------------------------------------------------------------------------------- 1 | import { getBucketEncryption as getBucketEncryptionR2, R2 } from '../common/r2/r2.ts'; 2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const GET_BUCKET_ENCRYPTION_COMMAND = denoflareCliCommand(['r2', 'get-bucket-encryption'], 'Gets encryption config for a bucket') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#get-bucket-encryption') 9 | ; 10 | 11 | export async function getBucketEncryption(args: (string | number)[], options: Record) { 12 | if (GET_BUCKET_ENCRYPTION_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = GET_BUCKET_ENCRYPTION_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const { origin, region, context, urlStyle } = await loadR2Options(options); 21 | 22 | const result = await getBucketEncryptionR2({ bucket, origin, region, urlStyle }, context); 23 | console.log(JSON.stringify(result, undefined, 2)); 24 | } 25 | -------------------------------------------------------------------------------- /cli/cli_r2_get_bucket_location.ts: -------------------------------------------------------------------------------- 1 | import { getBucketLocation as getBucketLocationR2, R2 } from '../common/r2/r2.ts'; 2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const GET_BUCKET_LOCATION_COMMAND = denoflareCliCommand(['r2', 'get-bucket-location'], 'Returns the region the bucket resides in') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#get-bucket-location') 9 | ; 10 | 11 | export async function getBucketLocation(args: (string | number)[], options: Record) { 12 | if (GET_BUCKET_LOCATION_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = GET_BUCKET_LOCATION_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const { origin, region, context, urlStyle } = await loadR2Options(options); 21 | 22 | const result = await getBucketLocationR2({ bucket, origin, region, urlStyle }, context); 23 | console.log(JSON.stringify(result, undefined, 2)); 24 | } 25 | -------------------------------------------------------------------------------- /cli/cli_r2_head_bucket.ts: -------------------------------------------------------------------------------- 1 | import { computeHeadersString, headBucket as headBucketR2, R2 } from '../common/r2/r2.ts'; 2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const HEAD_BUCKET_COMMAND = denoflareCliCommand(['r2', 'head-bucket'], 'Determine if an R2 bucket exists') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#head-bucket') 9 | ; 10 | 11 | export async function headBucket(args: (string | number)[], options: Record) { 12 | if (HEAD_BUCKET_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = HEAD_BUCKET_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const { origin, region, context, urlStyle } = await loadR2Options(options); 21 | 22 | const response = await headBucketR2({ bucket, origin, region, urlStyle }, context); 23 | console.log(`${response.status} ${computeHeadersString(response.headers)}`); 24 | } 25 | -------------------------------------------------------------------------------- /cli/cli_r2_list_buckets.ts: -------------------------------------------------------------------------------- 1 | import { listBuckets as listBucketsR2, R2 } from '../common/r2/r2.ts'; 2 | import { denoflareCliCommand } from './cli_common.ts'; 3 | import { loadR2Options, commandOptionsForR2 } from './cli_r2.ts'; 4 | 5 | export const LIST_BUCKETS_COMMAND = denoflareCliCommand(['r2', 'list-buckets'], 'List all R2 buckets') 6 | .include(commandOptionsForR2({ hideUrlStyle: true })) 7 | .docsLink('/cli/r2#list-buckets') 8 | ; 9 | 10 | export async function listBuckets(args: (string | number)[], options: Record) { 11 | if (LIST_BUCKETS_COMMAND.dumpHelp(args, options)) return; 12 | 13 | const { verbose } = LIST_BUCKETS_COMMAND.parse(args, options); 14 | 15 | if (verbose) { 16 | R2.DEBUG = true; 17 | } 18 | 19 | const { origin, region, context } = await loadR2Options(options); 20 | 21 | const result = await listBucketsR2({ origin, region }, context); 22 | console.log(JSON.stringify(result, undefined, 2)); 23 | } 24 | -------------------------------------------------------------------------------- /cli/cli_r2_list_multipart_uploads.ts: -------------------------------------------------------------------------------- 1 | import { listMultipartUploads as listMultipartUploadsR2, R2 } from '../common/r2/r2.ts'; 2 | import { loadR2Options, commandOptionsForR2 } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const LIST_MULTIPART_UPLOADS_COMMAND = denoflareCliCommand(['r2', 'list-multipart-uploads'], 'List in-progress multipart uploads within an R2 bucket') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .option('delimiter', 'string', 'The character used to group keys', { hint: 'char' }) 8 | .option('encodingType', 'enum', 'Encoding used to encode keys in the response', { value: 'url', description: 'Url encoding' }) 9 | .option('keyMarker', 'string', 'Together with upload-id-marker, specifies the multipart upload after which listing should begin') 10 | .option('maxUploads', 'integer', 'Limit the number of multipart uploads to return', { min: 0, max: 1000 }) 11 | .option('prefix', 'string', 'Limit to uploads for keys that begin with the specified prefix') 12 | .option('uploadIdMarker', 'string', 'Together with key-marker, specifies the upload after which listing should begin') 13 | .include(commandOptionsForR2()) 14 | .docsLink('/cli/r2#list-multipart-uploads') 15 | ; 16 | 17 | export async function listMultipartUploads(args: (string | number)[], options: Record) { 18 | if (LIST_MULTIPART_UPLOADS_COMMAND.dumpHelp(args, options)) return; 19 | 20 | const { bucket, verbose, delimiter, encodingType, keyMarker, maxUploads, prefix, uploadIdMarker } = LIST_MULTIPART_UPLOADS_COMMAND.parse(args, options); 21 | 22 | if (verbose) { 23 | R2.DEBUG = true; 24 | } 25 | 26 | const { origin, region, context, urlStyle } = await loadR2Options(options); 27 | 28 | const result = await listMultipartUploadsR2({ bucket, origin, region, delimiter, encodingType, keyMarker, maxUploads, prefix, uploadIdMarker, urlStyle }, context); 29 | console.log(JSON.stringify(result, undefined, 2)); 30 | } 31 | -------------------------------------------------------------------------------- /cli/cli_r2_list_objects.ts: -------------------------------------------------------------------------------- 1 | import { listObjectsV2, R2 } from '../common/r2/r2.ts'; 2 | import { loadR2Options, commandOptionsForR2 } from './cli_r2.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const LIST_OBJECTS_COMMAND = denoflareCliCommand(['r2', 'list-objects'], 'List objects within an R2 bucket (list-objects-v2)') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .option('maxKeys', 'integer', 'Limit the number of keys to return', { min: 0, max: 1000 }) 8 | .option('continuationToken', 'string', 'Continue the listing on this bucket with a previously returned token (token is obfuscated and is not a real key)') 9 | .option('startAfter', 'string', 'Start listing after this specified key, can be any key in the bucket', { hint: 'key' }) 10 | .option('prefix', 'string', 'Limit to keys that begin with the specified prefix') 11 | .option('delimiter', 'string', 'The character used to group keys', { hint: 'char' }) 12 | .option('encodingType', 'enum', 'Encoding used to encode keys in the response', { value: 'url', description: 'Url encoding' }) 13 | .option('fetchOwner', 'boolean', 'If set, return the owner info for each item') 14 | .include(commandOptionsForR2()) 15 | .docsLink('/cli/r2#list-objects') 16 | ; 17 | 18 | export async function listObjects(args: (string | number)[], options: Record) { 19 | if (LIST_OBJECTS_COMMAND.dumpHelp(args, options)) return; 20 | 21 | const { bucket, verbose, maxKeys, continuationToken, startAfter, prefix, delimiter, encodingType, fetchOwner } = LIST_OBJECTS_COMMAND.parse(args, options); 22 | 23 | if (verbose) { 24 | R2.DEBUG = true; 25 | } 26 | 27 | const { origin, region, context, urlStyle } = await loadR2Options(options); 28 | 29 | const result = await listObjectsV2({ bucket, origin, region, maxKeys, continuationToken, delimiter, prefix, startAfter, encodingType, fetchOwner, urlStyle }, context); 30 | console.log(JSON.stringify(result, undefined, 2)); 31 | } 32 | -------------------------------------------------------------------------------- /cli/cli_r2_list_objects_v1.ts: -------------------------------------------------------------------------------- 1 | import { listObjects, R2 } from '../common/r2/r2.ts'; 2 | import { denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const LIST_OBJECTS_V1_COMMAND = denoflareCliCommand(['r2', 'list-objects-v1'], 'List objects within a bucket (deprecated v1 version)') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .option('maxKeys', 'integer', 'Limit the number of keys to return', { min: 0, max: 1000 }) 8 | .option('marker', 'string', 'Start listing after this specified key, can be any key in the bucket') 9 | .option('prefix', 'string', 'Limit to keys that begin with the specified prefix') 10 | .option('delimiter', 'string', 'The character used to group keys', { hint: 'char' }) 11 | .option('encodingType', 'enum', 'Encoding used to encode keys in the response', { value: 'url', description: 'Url encoding' }) 12 | .include(commandOptionsForR2()) 13 | .docsLink('/cli/r2#list-objects-v1') 14 | ; 15 | 16 | export async function listObjectsV1(args: (string | number)[], options: Record) { 17 | if (LIST_OBJECTS_V1_COMMAND.dumpHelp(args, options)) return; 18 | 19 | const { bucket, verbose, maxKeys, marker, prefix, delimiter, encodingType } = LIST_OBJECTS_V1_COMMAND.parse(args, options); 20 | 21 | if (verbose) { 22 | R2.DEBUG = true; 23 | } 24 | 25 | const { origin, region, context, urlStyle } = await loadR2Options(options); 26 | 27 | const result = await listObjects({ bucket, origin, region, urlStyle, maxKeys, marker, delimiter, prefix, encodingType }, context); 28 | console.log(JSON.stringify(result, undefined, 2)); 29 | } 30 | -------------------------------------------------------------------------------- /cli/cli_r2_put_bucket_encryption.ts: -------------------------------------------------------------------------------- 1 | import { putBucketEncryption as putBucketEncryptionR2, R2 } from '../common/r2/r2.ts'; 2 | import { denoflareCliCommand, parseOptionalBooleanOption, parseOptionalStringOption } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const PUT_BUCKET_ENCRYPTION_COMMAND = denoflareCliCommand(['r2', 'put-bucket-encryption'], 'Sets encryption config for a bucket') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .include(commandOptionsForR2()) 8 | .docsLink('/cli/r2#put-bucket-encryption') 9 | ; 10 | 11 | export async function putBucketEncryption(args: (string | number)[], options: Record) { 12 | if (PUT_BUCKET_ENCRYPTION_COMMAND.dumpHelp(args, options)) return; 13 | 14 | const { verbose, bucket } = PUT_BUCKET_ENCRYPTION_COMMAND.parse(args, options); 15 | 16 | if (verbose) { 17 | R2.DEBUG = true; 18 | } 19 | 20 | const sseAlgorithm = parseOptionalStringOption('sse-algorithm', options); if (sseAlgorithm === undefined) throw new Error(`--sse-algorithm is required`); 21 | const bucketKeyEnabled = parseOptionalBooleanOption('bucket-key-enabled', options); if (bucketKeyEnabled === undefined) throw new Error(`--bucket-key-enabled is required`); 22 | 23 | const { origin, region, context, urlStyle } = await loadR2Options(options); 24 | 25 | await putBucketEncryptionR2({ bucket, sseAlgorithm, bucketKeyEnabled, origin, region, urlStyle }, context); 26 | } 27 | -------------------------------------------------------------------------------- /cli/cli_r2_put_large_object.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../common/bytes.ts'; 2 | import { createMultipartUpload, uploadPart, abortMultipartUpload, completeMultipartUpload, R2, CompletedPart } from '../common/r2/r2.ts'; 3 | import { CliStats, denoflareCliCommand } from './cli_common.ts'; 4 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 5 | 6 | const PUT_LARGE_OBJECT_COMMAND = denoflareCliCommand(['cli', 'r2', 'put-large-object'], 'Upload a large file in multiple chunks') 7 | .arg('bucket', 'string', 'Name of the R2 bucket') 8 | .arg('key', 'string', 'Name of the R2 object key') 9 | .option('file', 'required-string', 'Local path to the file', { hint: 'path' }) 10 | .include(commandOptionsForR2()) 11 | ; 12 | 13 | 14 | export async function putLargeObject(args: (string | number)[], options: Record) { 15 | if (PUT_LARGE_OBJECT_COMMAND.dumpHelp(args, options)) return; 16 | 17 | const { verbose, bucket, key, file } = PUT_LARGE_OBJECT_COMMAND.parse(args, options); 18 | 19 | if (verbose) { 20 | R2.DEBUG = true; 21 | } 22 | 23 | const { origin, region, context, urlStyle } = await loadR2Options(options); 24 | 25 | const start = Date.now(); 26 | const bytes = await Deno.readFile(file); 27 | console.log(`Read ${bytes.length} bytes in ${Date.now() - start}ms`); 28 | 29 | const { uploadId } = await createMultipartUpload({ bucket, key, origin, region, urlStyle }, context); 30 | let completed = false; 31 | try { 32 | const partSize = 1024 * 1024 * 200; 33 | const partsNum = Math.ceil(bytes.length / partSize); 34 | const parts: CompletedPart[] = []; 35 | for (let i = 0; i < partsNum; i++) { 36 | const start = i * partSize; 37 | const end = Math.min((i + 1) * partSize, bytes.length); 38 | const part = new Bytes(bytes.slice(start, end)); 39 | const partNumber = i + 1; 40 | console.log(`Uploading part ${partNumber} of ${partsNum}`); 41 | const start2 = Date.now(); 42 | const { etag } = await uploadPart({ bucket, key, uploadId, partNumber, body: part, origin, region, urlStyle }, context); 43 | console.log(`Put ${part.length} bytes in ${Date.now() - start2}ms`); 44 | parts.push({ partNumber, etag }); 45 | } 46 | 47 | console.log(`Completing upload`); 48 | await completeMultipartUpload({ bucket, key, uploadId, parts, origin, region, urlStyle }, context); 49 | completed = true; 50 | } finally { 51 | if (!completed) { 52 | console.log(`Aborting upload`); 53 | await abortMultipartUpload({ bucket, key, uploadId, origin, region, urlStyle }, context); 54 | } 55 | } 56 | 57 | const millis = Date.now() - CliStats.launchTime; 58 | console.log(`put ${bytes.length} total bytes in ${millis}ms`); 59 | } 60 | -------------------------------------------------------------------------------- /cli/cli_r2_upload_part.ts: -------------------------------------------------------------------------------- 1 | import { computeAwsCallBodyLength, uploadPart as uploadPartR2, R2 } from '../common/r2/r2.ts'; 2 | import { CliStats, denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForLoadBodyFromOptions, commandOptionsForR2, loadBodyFromOptions, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const UPLOAD_PART_COMMAND = denoflareCliCommand(['r2', 'upload-part'], 'Upload part of a multipart upload') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'string', 'Key of the object') 8 | .option('uploadId', 'required-string', 'Id of the existing multipart upload to complete') 9 | .option('partNumber', 'required-integer', 'Number of the part', { min: 1, max: 10000 }) 10 | .include(commandOptionsForLoadBodyFromOptions) 11 | .include(commandOptionsForR2()) 12 | .docsLink('/cli/r2#upload-part') 13 | ; 14 | 15 | export async function uploadPart(args: (string | number)[], options: Record) { 16 | if (UPLOAD_PART_COMMAND.dumpHelp(args, options)) return; 17 | 18 | const { bucket, key, uploadId, partNumber, verbose } = UPLOAD_PART_COMMAND.parse(args, options); 19 | 20 | if (verbose) { 21 | R2.DEBUG = true; 22 | } 23 | 24 | const { origin, region, context, urlStyle } = await loadR2Options(options); 25 | 26 | const { body, contentMd5 } = await loadBodyFromOptions(options, context.unsignedPayload); 27 | 28 | const result = await uploadPartR2({ bucket, key, uploadId, partNumber, body, origin, region, contentMd5, urlStyle }, context); 29 | const millis = Date.now() - CliStats.launchTime; 30 | console.log(JSON.stringify(result)); 31 | console.log(`put ${computeAwsCallBodyLength(body)} bytes in ${millis}ms`); 32 | } 33 | -------------------------------------------------------------------------------- /cli/cli_r2_upload_part_copy.ts: -------------------------------------------------------------------------------- 1 | import { uploadPartCopy as uploadPartCopyR2, R2 } from '../common/r2/r2.ts'; 2 | import { CliStats, denoflareCliCommand } from './cli_common.ts'; 3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts'; 4 | 5 | export const UPLOAD_PART_COPY_COMMAND = denoflareCliCommand(['r2', 'upload-part-copy'], 'Copy R2 part from a given source bucket and key') 6 | .arg('bucket', 'string', 'Name of the R2 bucket') 7 | .arg('key', 'string', 'Key of the object') 8 | .option('uploadId', 'required-string', 'Id of the existing multipart upload to complete') 9 | .option('partNumber', 'required-integer', 'Number of the part', { min: 1, max: 10000 }) 10 | .option('sourceBucket', 'string', 'R2 Bucket of the source object (default: destination bucket)') 11 | .option('sourceKey', 'required-string', 'Key of the source object') 12 | .option('sourceRange', 'string', 'The range of bytes to copy from the source object') 13 | .optionGroup() 14 | .option('ifMatch', 'string', 'Copies the object part if its entity tag (ETag) matches the specified tag') 15 | .option('ifNoneMatch', 'string', 'Copies the object part if its entity tag (ETag) is different than the specified ETag') 16 | .option('ifModifiedSince', 'string', 'Copies the object part if it has been modified since the specified time') 17 | .option('ifUnmodifiedSince', 'string', `Copies the object part if it hasn't been modified since the specified time`) 18 | .include(commandOptionsForR2()) 19 | .docsLink('/cli/r2#upload-part-copy') 20 | ; 21 | 22 | export async function uploadPartCopy(args: (string | number)[], options: Record) { 23 | if (UPLOAD_PART_COPY_COMMAND.dumpHelp(args, options)) return; 24 | 25 | const { bucket, key, uploadId, partNumber, verbose, sourceKey, sourceBucket, sourceRange, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince } = UPLOAD_PART_COPY_COMMAND.parse(args, options); 26 | 27 | if (verbose) { 28 | R2.DEBUG = true; 29 | } 30 | 31 | const { origin, region, context, urlStyle } = await loadR2Options(options); 32 | 33 | const result = await uploadPartCopyR2({ 34 | bucket, key, uploadId, partNumber, origin, region, urlStyle, 35 | sourceBucket: sourceBucket ?? bucket, sourceKey, sourceRange, ifMatch, ifModifiedSince, ifNoneMatch, ifUnmodifiedSince, 36 | }, context); 37 | console.log(JSON.stringify(result, undefined, 2)); 38 | 39 | const millis = Date.now() - CliStats.launchTime; 40 | console.log(`copied in ${millis}ms`); 41 | } 42 | -------------------------------------------------------------------------------- /cli/cli_site.ts: -------------------------------------------------------------------------------- 1 | import { generate, SITE_GENERATE_COMMAND } from './cli_site_generate.ts'; 2 | import { serve, SITE_SERVE_COMMAND } from './cli_site_serve.ts'; 3 | import { denoflareCliCommand } from './cli_common.ts'; 4 | 5 | export const SITE_COMMAND = denoflareCliCommand('site', 'Develop and deploy a static docs site to Cloudflare Pages') 6 | .subcommand(SITE_GENERATE_COMMAND, generate) 7 | .subcommand(SITE_SERVE_COMMAND, serve) 8 | .docsLink('/cli/site') 9 | ; 10 | 11 | export async function site(args: (string | number)[], options: Record): Promise { 12 | await SITE_COMMAND.routeSubcommand(args, options); 13 | } 14 | -------------------------------------------------------------------------------- /cli/cli_site_generate.ts: -------------------------------------------------------------------------------- 1 | import { CliStats, denoflareCliCommand } from './cli_common.ts'; 2 | import { ensureDir, resolve } from './deps_cli.ts'; 3 | import { fileExists } from './fs_util.ts'; 4 | import { RepoDir } from './repo_dir.ts'; 5 | import { InputFileInfo, SiteModel } from './site/site_model.ts'; 6 | 7 | export const SITE_GENERATE_COMMAND = denoflareCliCommand(['site', 'generate'], 'Develop and deploy a static docs site to Cloudflare Pages') 8 | .arg('repoDir', 'string', 'Local path to the git repo to use as the source input for generation') 9 | .arg('outputDir', 'string', 'Local path to the directory to use for generated output') 10 | .docsLink('/cli/site/generate') 11 | ; 12 | 13 | export async function generate(args: (string | number)[], options: Record) { 14 | if (SITE_GENERATE_COMMAND.dumpHelp(args, options)) return; 15 | 16 | const { verbose, repoDir: repoDirOpt, outputDir: outputDirOpt } = SITE_GENERATE_COMMAND.parse(args, options); 17 | 18 | const repoDir = await RepoDir.of(resolve(Deno.cwd(), repoDirOpt)); 19 | 20 | if (await fileExists(outputDirOpt)) throw new Error(`Bad output-dir, exists as file: ${outputDirOpt}`); 21 | const outputDir = resolve(Deno.cwd(), outputDirOpt); 22 | 23 | const siteModel = new SiteModel(repoDir.path); 24 | 25 | // 3-7ms to here 26 | let start = Date.now(); 27 | console.log('Building site...'); 28 | const inputFiles: InputFileInfo[] = (await repoDir.listFiles()).map(v => ({ path: v.path, version: '0' })); 29 | await siteModel.setInputFiles(inputFiles); 30 | console.log(`Built site, took ${Date.now() - start}ms`); 31 | 32 | start = Date.now(); 33 | if (verbose) console.log(`Ensuring dir exists: ${outputDir}`); 34 | await ensureDir(outputDir); 35 | console.log(`Writing output`); 36 | await siteModel.writeOutput(outputDir); 37 | console.log(`Wrote output to ${outputDir}, took ${Date.now() - start}ms`); 38 | console.log(`Done in ${Date.now() - CliStats.launchTime}ms`); 39 | } 40 | -------------------------------------------------------------------------------- /cli/cli_site_serve.ts: -------------------------------------------------------------------------------- 1 | import { denoflareCliCommand } from './cli_common.ts'; 2 | import { resolve } from './deps_cli.ts'; 3 | import { computeFileInfoVersion } from './fs_util.ts'; 4 | import { RepoDir } from './repo_dir.ts'; 5 | import { InputFileInfo, SiteModel } from './site/site_model.ts'; 6 | 7 | const DEFAULT_PORT = 8099; 8 | 9 | export const SITE_SERVE_COMMAND = denoflareCliCommand(['site', 'serve'], 'Host a static Cloudflare Pages site in a local Deno web server') 10 | .arg('repoDir', 'string', 'Local path to the git repo to use as the source input') 11 | .option('port', 'integer', `Local port to use for the http server (default: ${DEFAULT_PORT})`) 12 | .option('watch', 'boolean', `If set, rebuild the site when file system changes are detected in `) 13 | .docsLink('/cli/site/serve') 14 | ; 15 | 16 | export async function serve(args: (string | number)[], options: Record) { 17 | if (SITE_SERVE_COMMAND.dumpHelp(args, options)) return; 18 | 19 | const { verbose, repoDir: repoDirOpt, port: portOpt, watch } = SITE_SERVE_COMMAND.parse(args, options); 20 | 21 | if (verbose) RepoDir.VERBOSE = true; 22 | 23 | const port = portOpt ?? DEFAULT_PORT; 24 | const localOrigin = `http://localhost:${port}`; 25 | 26 | const repoDir = await RepoDir.of(resolve(Deno.cwd(), repoDirOpt)); 27 | const siteModel = new SiteModel(repoDir.path, { localOrigin }); 28 | 29 | const buildSite = async () => { 30 | const start = Date.now(); 31 | console.log('Building site...'); 32 | const inputFiles: InputFileInfo[] = (await repoDir.listFiles({ stat: true })).map(v => ({ path: v.path, version: computeFileInfoVersion(v.info!) })); 33 | await siteModel.setInputFiles(inputFiles); 34 | console.log(`Built site, took ${Date.now() - start}ms`); 35 | }; 36 | if (watch) { 37 | let timeoutId: number | undefined; 38 | repoDir.startWatching(_events => { 39 | clearTimeout(timeoutId); 40 | timeoutId = setTimeout(async () => { 41 | await buildSite(); 42 | }, 50); 43 | }); 44 | } 45 | 46 | await buildSite(); 47 | 48 | const server = Deno.serve({ port }, async request => await siteModel.handle(request)); 49 | console.log(`Local server running on ${localOrigin}`); 50 | await server.finished; 51 | } 52 | -------------------------------------------------------------------------------- /cli/cli_version.ts: -------------------------------------------------------------------------------- 1 | export const CLI_VERSION = '0.7.0+'; 2 | -------------------------------------------------------------------------------- /cli/config_loader_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts'; 2 | import { assertRejects } from 'https://deno.land/std@0.224.0/assert/assert_rejects.ts'; 3 | 4 | import { resolveBindings, resolveProfile } from './config_loader.ts'; 5 | import { Binding, Config } from '../common/config.ts'; 6 | 7 | Deno.test({ 8 | name: 'resolveBindings', 9 | fn: async () => { 10 | const opts = makeOpts(); 11 | const input: Record = { 12 | text1: { value: '${pushId}' }, 13 | text2: { value: '${localPort}' }, 14 | text3: { value: '${aws:foo}' }, 15 | text4: { value: 'asdf' }, 16 | text5: { value: 'pre${env:foo}post' }, 17 | kv1: { kvNamespace: '${env:foo}' }, 18 | }; 19 | const output = await resolveBindings(input, opts); 20 | assertEquals(input, input); // ensure input not modified 21 | assertEquals(output, { 22 | text1: { value: 'the-push-id' }, 23 | text2: { value: '123' }, 24 | text3: { value: 'ak:sak' }, 25 | text4: { value: 'asdf' }, 26 | text5: { value: 'prebarpost' }, 27 | kv1: { kvNamespace: 'bar' }, 28 | }); 29 | assertRejects(async () => { 30 | await resolveBindings({ bad: { value: '${asdf}' } }, opts); 31 | }) 32 | } 33 | }); 34 | 35 | Deno.test({ 36 | name: 'resolveProfile', 37 | fn: async () => { 38 | const opts = makeOpts(); 39 | const config: Config = { profiles: { first: { accountId: 'account-id', apiToken: '${env:API_TOKEN}' } } }; 40 | const profile = await resolveProfile(config, {}, undefined, opts); 41 | assertEquals(profile as unknown, { accountId: 'account-id', apiToken: 'api-token' }); 42 | } 43 | }); 44 | 45 | // 46 | 47 | function makeOpts() { 48 | const awsCredentialsLoader = async (profile: string) => { 49 | await Promise.resolve(); 50 | if (profile === 'foo') return { accessKeyId: 'ak', secretAccessKey: 'sak' }; 51 | throw new Error(`Profile ${profile} not found`); 52 | }; 53 | const envLoader = (name: string) => new Map([['foo', 'bar'], ['API_TOKEN', 'api-token']]).get(name); 54 | return { localPort: 123, pushId: 'the-push-id', awsCredentialsLoader, envLoader }; 55 | } 56 | -------------------------------------------------------------------------------- /cli/deno_check.ts: -------------------------------------------------------------------------------- 1 | import { DenoDiagnostic, DenoDiagnosticCategory, parseDiagnostics } from './deno_bundle.ts'; 2 | import { spawn } from './spawn.ts'; 3 | 4 | export interface DenoCheckResult { 5 | readonly success: boolean; 6 | readonly diagnostics: DenoDiagnostic[]; 7 | } 8 | 9 | export async function denoCheck(rootSpecifier: string, opts: { all?: boolean, compilerOptions?: { lib?: string[] } } = {}): Promise { 10 | const { all, compilerOptions } = opts; 11 | 12 | let config: string | undefined; 13 | try { 14 | if (compilerOptions?.lib) { 15 | config = await Deno.makeTempFile({ prefix: 'denoflare-deno-check', suffix: '.json'}); 16 | await Deno.writeTextFile(config, JSON.stringify({ compilerOptions })); 17 | } 18 | const { out, err, success } = await runDenoCheck(rootSpecifier, { all, config }); 19 | console.log({ out, err, success }); 20 | 21 | let diagnostics: DenoDiagnostic[] = []; 22 | if (err.length > 0 || !success) { 23 | diagnostics = parseDiagnostics(err); 24 | if (!success && err && diagnostics.length === 0) { 25 | // unparsed error (e.g. deno bundle not supporting npm specifiers) 26 | diagnostics.push({ 27 | category: DenoDiagnosticCategory.Error, 28 | code: 0, 29 | messageText: err, 30 | }); 31 | } 32 | } 33 | 34 | return { success, diagnostics }; 35 | } finally { 36 | if (config) { 37 | await Deno.remove(config); 38 | } 39 | } 40 | } 41 | 42 | // 43 | 44 | type RunDenoCheckResult = { code: number, success: boolean, out: string, err: string }; 45 | 46 | async function runDenoCheck(rootSpecifier: string, opts: { all?: boolean, config?: string } = {}): Promise { 47 | const { all, config } = opts; 48 | const args = [ 49 | 'check', 50 | '--allow-import', 51 | '--no-lock', 52 | ...(all ? [ '--all' ] : []), 53 | ...(config ? ['--config', config] : []), 54 | rootSpecifier, 55 | ]; 56 | const { code, success, stdout, stderr } = await spawn(Deno.execPath(), { 57 | args, 58 | env: { 59 | NO_COLOR: '1', // to make parsing the output easier 60 | } 61 | }); 62 | const out = new TextDecoder().decode(stdout); 63 | const err = new TextDecoder().decode(stderr); 64 | return { code, success, out, err }; 65 | } 66 | -------------------------------------------------------------------------------- /cli/deno_graph.ts: -------------------------------------------------------------------------------- 1 | import { createGraph, fromFileUrl, ModuleGraphJson, toFileUrl, isAbsolute, join, normalize } from './deps_cli.ts'; 2 | 3 | export async function computeDenoGraphLocalPaths(path: string): Promise { 4 | const graph = await computeDenoGraph(path); 5 | return findLocalPaths(graph); 6 | } 7 | 8 | export async function computeDenoGraph(path: string): Promise { 9 | return await createGraph(computeRootSpecifier(path)); 10 | } 11 | 12 | // 13 | 14 | function computeRootSpecifier(path: string): string { 15 | if (path.startsWith('file://')) return path; 16 | const absolutePath = toAbsolutePath(path); 17 | return toFileUrl(absolutePath).toString(); 18 | } 19 | 20 | function toAbsolutePath(path: string): string { 21 | return isAbsolute(path) ? path : normalize(join(Deno.cwd(), path)); 22 | } 23 | 24 | function findLocalPaths(graph: ModuleGraphJson): string[] { 25 | const rt = new Set(); 26 | for (const root of graph.roots) { 27 | const rootPath = fromFileUrl(root); 28 | rt.add(rootPath); 29 | } 30 | for (const { specifier } of graph.modules) { 31 | if (specifier.startsWith('file://')) { 32 | rt.add(fromFileUrl(specifier)); 33 | } 34 | } 35 | return [...rt].sort(); 36 | } 37 | -------------------------------------------------------------------------------- /cli/deno_info.ts: -------------------------------------------------------------------------------- 1 | import { fromFileUrl } from './deps_cli.ts'; 2 | 3 | export async function computeDenoInfoLocalPaths(path: string): Promise { 4 | const info = await computeDenoInfo(path); 5 | return findLocalPaths(info); 6 | } 7 | 8 | export async function computeDenoInfo(path: string): Promise { 9 | const p = Deno.run({ 10 | cmd: [Deno.execPath(), 'info', '--json', path], 11 | stdout: 'piped', 12 | stderr: 'piped', 13 | }); 14 | 15 | // await its completion 16 | const status = await p.status(); 17 | const stdout = new TextDecoder().decode(await p.output()); 18 | const stderr = new TextDecoder().decode(await p.stderrOutput()); 19 | if (!status.success) { 20 | throw new Error(`deno info failed: ${JSON.stringify({ status, stdout, stderr})}`); 21 | } 22 | const obj = JSON.parse(stdout); 23 | // console.log(JSON.stringify(obj, undefined, 2)); 24 | return obj as DenoInfo; 25 | } 26 | 27 | // 28 | 29 | function findLocalPaths(info: DenoInfo): string[] { 30 | const rt = new Set(); 31 | const rootPath = fromFileUrl(info.root); 32 | rt.add(rootPath); 33 | for (const moduleInfo of info.modules) { 34 | rt.add(moduleInfo.local); 35 | } 36 | return [...rt].sort(); 37 | } 38 | 39 | // 40 | 41 | export interface DenoInfo { 42 | readonly root: string; // e.g. file:///... 43 | readonly modules: readonly ModuleInfo[]; 44 | readonly size: number; 45 | } 46 | 47 | export interface ModuleInfo { 48 | readonly specifier: string; // e.g. file:/// or https://... 49 | readonly dependencies: readonly DependencyInfo[]; 50 | readonly size: number; 51 | readonly mediaType: string; // e.g. Dts, TypeScript 52 | readonly local: string; // e.g. /path/to/source 53 | readonly checksum: string; // e.g. f182ca01c68d2d914db5016db3d74c123651286ba6b95e26f0231e84b867f896 54 | readonly emit?: string; // e.g. /path/to/deno/cache 55 | readonly error?: string; // e.g. Cannot resolve module... 56 | } 57 | 58 | export interface DependencyInfo { 59 | readonly specifier: string; // e.g. ../asdf.ts or https:// 60 | readonly code: string; // e.g. file:///asdf or https:// 61 | } 62 | -------------------------------------------------------------------------------- /cli/deps_cli.ts: -------------------------------------------------------------------------------- 1 | export { basename, dirname, join, fromFileUrl, resolve, toFileUrl, extname, relative, isAbsolute, normalize, parse as parsePath, globToRegExp } from 'https://deno.land/std@0.224.0/path/mod.ts'; 2 | export { ensureDir, walk, emptyDir } from 'https://deno.land/std@0.224.0/fs/mod.ts'; 3 | export { SEPARATOR as systemSeparator } from 'https://deno.land/std@0.224.0/path/constants.ts'; 4 | export { sortBy } from 'https://deno.land/std@0.224.0/collections/sort_by.ts'; 5 | export { createGraph } from 'https://deno.land/x/deno_graph@0.43.3/mod.ts'; 6 | export type { ModuleGraphJson } from 'https://deno.land/x/deno_graph@0.43.3/lib/types.d.ts'; 7 | export { gzip } from 'https://deno.land/x/compress@v0.4.5/zlib/mod.ts'; 8 | export { parse as _parseJsonc } from 'https://cdn.skypack.dev/jsonc-parser@3.2.0'; 9 | export { default as marked } from 'https://cdn.skypack.dev/marked@3.0.8?dts'; 10 | export { html } from 'https://deno.land/x/html_escape@v1.1.5/html.ts'; 11 | export { default as hljs } from 'https://cdn.skypack.dev/highlight.js@11.7.0'; 12 | export { serve } from 'https://deno.land/std@0.224.0/http/server.ts'; 13 | export { mapValues } from 'https://deno.land/std@0.224.0/collections/map_values.ts'; 14 | export { TextLineStream } from 'https://deno.land/std@0.224.0/streams/text_line_stream.ts'; 15 | -------------------------------------------------------------------------------- /cli/flag_parser.ts: -------------------------------------------------------------------------------- 1 | export function parseFlags(denoArgs: string[]): { args: (string | number)[], options: Record } { 2 | const args: (string | number)[] = []; 3 | const options: Record = {}; 4 | 5 | const addOption = (name: string, value: string | number | boolean) => { 6 | const existing = options[name]; 7 | if (existing === undefined) { 8 | options[name] = value; 9 | } else if (Array.isArray(existing)) { 10 | existing.push(value); 11 | } else { 12 | options[name] = [ existing, value ]; 13 | } 14 | }; 15 | 16 | let currentOptionName: string | undefined; 17 | for (const denoArg of denoArgs) { 18 | const optionName = tryParseOptionName(denoArg); 19 | if (optionName) { 20 | if (currentOptionName) { 21 | addOption(currentOptionName, true); 22 | } 23 | currentOptionName = optionName; 24 | continue; 25 | } 26 | const value = /^true$/i.test(denoArg) ? true 27 | : /^false$/i.test(denoArg) ? false 28 | : /^-?\d+$/.test(denoArg) ? parseInt(denoArg) 29 | : /^-?\d+\.\d+$/.test(denoArg) ? parseFloat(denoArg) 30 | : denoArg; 31 | if (currentOptionName) { 32 | addOption(currentOptionName, value); 33 | currentOptionName = undefined; 34 | } else { 35 | if (Object.keys(options).length > 0) throw new Error(`Named options must come after positional arguments`); 36 | args.push(typeof value === 'boolean' ? String(value) : value); 37 | } 38 | } 39 | if (currentOptionName) { 40 | addOption(currentOptionName, true); 41 | } 42 | 43 | return { args, options }; 44 | } 45 | 46 | // 47 | 48 | function tryParseOptionName(denoArg: string): string | undefined { 49 | const m = /^--([^\s]+)$/.exec(denoArg); 50 | return m ? m[1] : undefined; 51 | } 52 | -------------------------------------------------------------------------------- /cli/flag_parser_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts'; 2 | import { assertThrows } from 'https://deno.land/std@0.224.0/assert/assert_throws.ts'; 3 | 4 | import { parseFlags } from './flag_parser.ts'; 5 | 6 | Deno.test({ 7 | name: 'parseFlags', 8 | fn: () => { 9 | const good: [ string[], (string | number)[], Record ][] = [ 10 | [ [], [], {} ], 11 | [ [ 'one' ], [ 'one' ], {} ], 12 | [ [ '--bool' ], [], { bool: true} ], 13 | [ [ 'arg', '--foo', 'bar' ], [ 'arg' ], { foo: 'bar'} ], 14 | [ [ '--foo', 'bar', '--foo' ], [], { foo: ['bar', true]} ], 15 | [ [ '--foo', 'bar', '--foo', 'baz' ], [], { foo: ['bar', 'baz']} ], 16 | [ [ '--bool', 'true' ], [], { bool: true } ], 17 | [ [ '--bool', 'True' ], [], { bool: true } ], 18 | [ [ '--bool', 'false' ], [], { bool: false } ], 19 | [ [ '--bool', 'FALSE' ], [], { bool: false } ], 20 | [ [ '--int', '0' ], [], { int: 0 } ], 21 | [ [ '--int', '-0' ], [], { int: 0 } ], 22 | [ [ '--int', '123456' ], [], { int: 123456 } ], 23 | [ [ '--float', '0.123456' ], [], { float: 0.123456 } ], 24 | [ [ '--float', '-123.123456' ], [], { float: -123.123456 } ], 25 | [ [ '--string', '-123.123456.0' ], [], { string: '-123.123456.0' } ], 26 | [ [ '--string', '-123.123456.0' ], [], { string: '-123.123456.0' } ], 27 | [ [ '', 'a b' ], ['', 'a b'], { } ], 28 | [ [ 'cfapi', 'list-scripts', '--per-page', '1', '--verbose' ], [ 'cfapi', 'list-scripts' ], { 'per-page': 1, verbose: true } ], 29 | ]; 30 | for (const [ input, args, options] of good) { 31 | assertEquals(parseFlags(input), { args, options }); 32 | } 33 | const bad: (string[])[] = [ 34 | ['--foo', 'bar', 'baz' ], 35 | ]; 36 | for (const input of bad) { 37 | assertThrows(() => parseFlags(input)); 38 | } 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /cli/fs_util.ts: -------------------------------------------------------------------------------- 1 | export async function fileExists(path: string): Promise { 2 | return await exists(path, info => info.isFile); 3 | } 4 | 5 | export async function directoryExists(path: string): Promise { 6 | return await exists(path, info => info.isDirectory); 7 | } 8 | 9 | export function computeFileInfoVersion(info: Deno.FileInfo): string { 10 | return `${info.size}|${info.mtime instanceof Date ? (info.mtime.getTime()) : ''}`; 11 | } 12 | 13 | // 14 | 15 | async function exists(path: string, test: (info: Deno.FileInfo) => boolean) { 16 | try { 17 | const info = await Deno.stat(path); 18 | return test(info); 19 | } catch (e) { 20 | if (e instanceof Deno.errors.NotFound) { 21 | return false; 22 | } else { 23 | throw e; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cli/in_memory_rpc_channels.ts: -------------------------------------------------------------------------------- 1 | import { RpcChannel } from '../common/rpc_channel.ts'; 2 | 3 | export class InMemoryRpcChannels { 4 | readonly host: RpcChannel; 5 | readonly stub: RpcChannel; 6 | 7 | constructor(tagBase: string) { 8 | this.host = new RpcChannel(`${tagBase}-host`, async (message, _transfer) => { 9 | await this.stub.receiveMessage(message); 10 | }); 11 | this.stub = new RpcChannel(`${tagBase}-stub`, async (message, _transfer) => { 12 | await this.host.receiveMessage(message); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cli/jsonc.ts: -------------------------------------------------------------------------------- 1 | import { _parseJsonc } from './deps_cli.ts'; 2 | 3 | // deno-lint-ignore no-explicit-any 4 | export function parseJsonc(input: string, errors?: ParseError[], options?: ParseOptions): any { 5 | return _parseJsonc(input, errors, options); 6 | } 7 | 8 | export function formatParseError(error: ParseError, input: string): string { 9 | const { error: code, offset, length } = error; 10 | let problem = escapeForPrinting(input.substring(offset, offset + length)); 11 | if (problem.length < 10) { 12 | const contextSize = 10; 13 | problem = escapeForPrinting(input.substring(Math.max(offset - contextSize, 0), Math.min(offset + length + contextSize, input.length))); 14 | } 15 | return `${parseErrorCodeToString(code)} at ${offset}: ${problem}`; 16 | } 17 | 18 | export function parseErrorCodeToString(code: ParseErrorCode): string { 19 | switch (code) { 20 | case ParseErrorCode.InvalidSymbol: return 'InvalidSymbol'; 21 | case ParseErrorCode.InvalidNumberFormat: return 'InvalidNumberFormat'; 22 | case ParseErrorCode.PropertyNameExpected: return 'PropertyNameExpected'; 23 | case ParseErrorCode.ValueExpected: return 'ValueExpected'; 24 | case ParseErrorCode.ColonExpected: return 'ColonExpected'; 25 | case ParseErrorCode.CommaExpected: return 'CommaExpected'; 26 | case ParseErrorCode.CloseBraceExpected: return 'CloseBraceExpected'; 27 | case ParseErrorCode.CloseBracketExpected: return 'CloseBracketExpected'; 28 | case ParseErrorCode.EndOfFileExpected: return 'EndOfFileExpected'; 29 | case ParseErrorCode.InvalidCommentToken: return 'InvalidCommentToken'; 30 | case ParseErrorCode.UnexpectedEndOfComment: return 'UnexpectedEndOfComment'; 31 | case ParseErrorCode.UnexpectedEndOfString: return 'UnexpectedEndOfString'; 32 | case ParseErrorCode.UnexpectedEndOfNumber: return 'UnexpectedEndOfNumber'; 33 | case ParseErrorCode.InvalidUnicode: return 'InvalidUnicode'; 34 | case ParseErrorCode.InvalidEscapeCharacter: return 'InvalidEscapeCharacter'; 35 | case ParseErrorCode.InvalidCharacter: return 'InvalidCharacter'; 36 | } 37 | return ``; 38 | } 39 | 40 | // 41 | 42 | function escapeForPrinting(input: string): string { 43 | return input.replaceAll('\r', '\\r').replaceAll('\n', '\\n'); 44 | } 45 | 46 | // 47 | 48 | export interface ParseOptions { 49 | disallowComments?: boolean; 50 | allowTrailingComma?: boolean; 51 | allowEmptyContent?: boolean; 52 | } 53 | 54 | export interface ParseError { 55 | error: ParseErrorCode; 56 | offset: number; 57 | length: number; 58 | } 59 | 60 | export const enum ParseErrorCode { 61 | InvalidSymbol = 1, 62 | InvalidNumberFormat = 2, 63 | PropertyNameExpected = 3, 64 | ValueExpected = 4, 65 | ColonExpected = 5, 66 | CommaExpected = 6, 67 | CloseBraceExpected = 7, 68 | CloseBracketExpected = 8, 69 | EndOfFileExpected = 9, 70 | InvalidCommentToken = 10, 71 | UnexpectedEndOfComment = 11, 72 | UnexpectedEndOfString = 12, 73 | UnexpectedEndOfNumber = 13, 74 | InvalidUnicode = 14, 75 | InvalidEscapeCharacter = 15, 76 | InvalidCharacter = 16 77 | } 78 | -------------------------------------------------------------------------------- /cli/module_watcher.ts: -------------------------------------------------------------------------------- 1 | import { computeDenoGraphLocalPaths } from './deno_graph.ts'; 2 | 3 | export class ModuleWatcher { 4 | static VERBOSE = false; 5 | 6 | private readonly entryPointPath: string; 7 | private readonly includes: string[]; 8 | private readonly modificationCallback: () => void; 9 | 10 | private watcher: Deno.FsWatcher | undefined; 11 | 12 | constructor(entryPointPath: string, modificationCallback: () => void, includes?: string[]) { 13 | this.entryPointPath = entryPointPath; 14 | this.includes = includes || []; 15 | this.modificationCallback = modificationCallback; 16 | this.initWatcher().catch(e => console.error('Error in initWatcher', e.stack || e)); 17 | } 18 | 19 | close() { 20 | if (this.watcher) { 21 | this.watcher.close(); 22 | this.watcher = undefined; 23 | } 24 | } 25 | 26 | // 27 | 28 | private dispatchModification() { 29 | this.modificationCallback(); 30 | } 31 | 32 | private async initWatcher() { 33 | const paths = await computeDenoGraphLocalPaths(this.entryPointPath); 34 | paths.push(...this.includes); 35 | if (ModuleWatcher.VERBOSE) console.log('watching', paths); 36 | const watcher = Deno.watchFs(paths); 37 | this.watcher = watcher; 38 | let timeoutId: number | undefined; 39 | for await (const event of watcher) { 40 | if (event.kind === 'modify') { 41 | // a single file modification sends two modify events, so coalesce them 42 | clearTimeout(timeoutId); 43 | timeoutId = setTimeout(() => this.dispatchModification(), 500); 44 | } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /cli/rpc_host_d1_database_test.ts: -------------------------------------------------------------------------------- 1 | import { makeRpcStubD1DatabaseProvider } from '../common/rpc_stub_d1_database.ts'; 2 | import { InMemoryRpcChannels } from './in_memory_rpc_channels.ts'; 3 | import { makeRpcHostD1Database } from './rpc_host_d1_database.ts'; 4 | import { SqliteD1Database } from './sqlite_d1_database.ts'; 5 | import { runSimpleD1DatabaseScenario } from './sqlite_d1_database_test.ts'; 6 | 7 | Deno.test({ 8 | name: 'makeRpcHostD1Database', 9 | ignore: (await Deno.permissions.query({ name: 'net' })).state !== 'granted', 10 | async fn() { 11 | const channels = new InMemoryRpcChannels('test'); 12 | makeRpcHostD1Database(channels.host, v => SqliteD1Database.provider(() => ':memory:')(v)); 13 | const provider = makeRpcStubD1DatabaseProvider(channels.stub); 14 | const db = provider(crypto.randomUUID()) 15 | await runSimpleD1DatabaseScenario(db); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /cli/rpc_host_durable_object_storage_test.ts: -------------------------------------------------------------------------------- 1 | import { makeRpcStubDurableObjectStorageProvider } from '../common/rpc_stub_durable_object_storage.ts'; 2 | import { runSimpleStorageTestScenario } from '../common/storage/durable_object_storage_test.ts'; 3 | import { InMemoryRpcChannels } from './in_memory_rpc_channels.ts'; 4 | import { makeRpcHostDurableObjectStorage } from './rpc_host_durable_object_storage.ts'; 5 | 6 | Deno.test('makeRpcHostDurableObjectStorage', async () => { 7 | const channels = new InMemoryRpcChannels('test'); 8 | makeRpcHostDurableObjectStorage(channels.host); 9 | const provider = makeRpcStubDurableObjectStorageProvider(channels.stub); 10 | const storage = await provider('class1', 'id1', { storage: 'memory', container: 'test.rpc' }, () => {}); 11 | await runSimpleStorageTestScenario(storage); 12 | }); 13 | -------------------------------------------------------------------------------- /cli/site/site_config.ts: -------------------------------------------------------------------------------- 1 | export interface SiteConfig { 2 | 3 | // leave these out if no org 4 | readonly organization?: string; // short first (bold) part of org name 5 | readonly organizationSuffix?: string; // short second part of org name 6 | readonly organizationSvg?: string; // content repo /path/to/organization.svg must use fill="currentColor" 7 | readonly organizationUrl?: string; // abs url to org 8 | 9 | readonly product: string; // (required) product name for sidebar, etc 10 | readonly productRepo?: string; // e.g. "ghuser/project-repo", used for gh link in header 11 | readonly productSvg?: string; // content repo /path/to/product.svg must use fill="currentColor" 12 | readonly contentRepo?: string; // e.g. "ghuser/docs-repo", used for edit this page in footer 13 | 14 | readonly themeColor?: string; // #rrggbb 15 | readonly themeColorDark?: string; // #rrggbb 16 | 17 | readonly search?: SiteSearchConfig; // Algolia DocSearch options, if applicable 18 | 19 | readonly siteMetadata: SiteMetadata; // (required for title, description) 20 | } 21 | 22 | export interface SiteMetadata { 23 | readonly title: string; // (required) (html title, og:title, twitter:title) = · 24 | readonly description: string; // (required) (html meta description, og:description, twitter:description) = 25 | readonly twitterUsername?: string; // @asdf for twitter:site 26 | readonly image?: string; // abs or relative url to twitter:image 27 | readonly imageAlt?: string; // alt text for twitter:image 28 | readonly origin?: string; // abs url to site (origin) 29 | readonly faviconIco?: string; // relative url favicon ico 30 | readonly faviconSvg?: string; // relative url favicon svg 31 | readonly faviconMaskSvg?: string; // relative url favicon mask svg 32 | readonly faviconMaskColor?: string; // #rrggbb favicon mask color (required if faviconMaskSvg provided) 33 | readonly manifest?: Record; // override default web app manifest members 34 | } 35 | 36 | export interface SiteSearchConfig { 37 | readonly indexName: string; // (required) Name of the Algolia DocSearch index name for this site 38 | readonly apiKey: string; // (required) Api key provided by Algolia for this site 39 | readonly appId: string; // (required) Application ID provided by Algolia for this site 40 | } 41 | -------------------------------------------------------------------------------- /cli/site/toc.ts: -------------------------------------------------------------------------------- 1 | export interface TocNode { 2 | readonly title: string; 3 | readonly anchorId: string; 4 | readonly children?: readonly TocNode[]; 5 | } 6 | 7 | export interface Heading { 8 | readonly level: 2 | 3 | 4 | 5 | 6 | 7; 9 | readonly id: string; 10 | readonly text: string; 11 | } 12 | 13 | export function computeToc(headings: Heading[]): TocNode[] { 14 | return headings.map(v => ({ title: v.text, anchorId: v.id })); 15 | } 16 | -------------------------------------------------------------------------------- /cli/spawn.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | 3 | // 1.24.0 version 4 | export interface SpawnOutput { 5 | get stdout(): Uint8Array; 6 | get stderr(): Uint8Array; 7 | success: boolean; 8 | code: number; 9 | signal: Deno.Signal | null; 10 | } 11 | 12 | export async function spawn(command: string | URL, options?: { args?: string[], env?: Record }): Promise { 13 | // use Deno.Command if 1.28+, spawn is deprecated 14 | if ('Command' in Deno) { 15 | const c = new (Deno as any).Command(command, options); 16 | return await c.output(); 17 | } 18 | 19 | // use Deno.spawn if < 1.28, not deprecated 20 | if ('spawn' in Deno) { 21 | const rt = await (Deno as any).spawn(command, options) as any; 22 | 23 | // pre-1.24.0, success, code, and signal were under a .status property 24 | // https://deno.com/blog/v1.24#updates-to-new-subprocess-api 25 | if (typeof rt.status === 'object') { 26 | const { success, code, signal } = rt.status; 27 | if (typeof success === 'boolean' && typeof code === 'number' && signal !== undefined) { 28 | rt.success = success; 29 | rt.code = code; 30 | rt.signal = signal; 31 | delete rt.status; 32 | } else { 33 | throw new Error(`Unexpected pre-1.24.0 spawn output status object`); 34 | } 35 | } 36 | return rt; 37 | } 38 | 39 | throw new Error(`Unable to spawn subprocess: did not find 'Deno.Command' or 'Deno.spawn'`); 40 | } 41 | -------------------------------------------------------------------------------- /cli/sqlite_d1_database_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts'; 2 | import { SqliteD1Database } from './sqlite_d1_database.ts'; 3 | import { assert } from 'https://deno.land/std@0.224.0/assert/assert.ts'; 4 | import { D1Database } from '../common/cloudflare_workers_types.d.ts'; 5 | 6 | Deno.test({ 7 | name: 'SqliteD1Database', 8 | ignore: (await Deno.permissions.query({ name: 'net' })).state !== 'granted', 9 | async fn() { 10 | const db = SqliteD1Database.provider(() => ':memory:')(crypto.randomUUID()) as SqliteD1Database; 11 | await runSimpleD1DatabaseScenario(db); 12 | db.close(); 13 | } 14 | }); 15 | 16 | export async function runSimpleD1DatabaseScenario(db: D1Database) { 17 | await db.prepare('create table t1(id int primary key, name text)').all(); 18 | { 19 | const result = await db.prepare(`insert into t1(id, name) values(1, 'one')`).all(); 20 | assert(result.success); 21 | assert(result.meta.changed_db); 22 | assertEquals(result.meta.changes, 1); 23 | assertEquals(result.results, []); 24 | } 25 | { 26 | const result = await db.prepare(`select * from t1`).all(); 27 | assert(result.success); 28 | assert(!result.meta.changed_db); 29 | assertEquals(result.meta.changes, 0); 30 | assertEquals(result.results, [ { id: 1, name: 'one' } ]); 31 | } 32 | { 33 | const result = await db.prepare(`select * from t1`).raw(); 34 | assertEquals(result, [ [ 1, 'one' ] ]); 35 | } 36 | { 37 | const result = await db.prepare(`select * from t1`).raw({ columnNames: true }); 38 | assertEquals(result, [ [ 'id', 'name' ], [ 1, 'one' ] ]); 39 | } 40 | { 41 | const result = await db.prepare(`select * from t1`).first(); 42 | assertEquals(result, { id: 1, name: 'one' }); 43 | } 44 | { 45 | const result = await db.prepare(`select * from t1`).first('id'); 46 | assertEquals(result, 1); 47 | } 48 | { 49 | const stmt = db.prepare(`insert into t1(id, name) values(?, ?)`); 50 | const result = await db.batch([ stmt.bind(2, 'two'), stmt.bind(3, 'three') ]); 51 | assertEquals(result.length, 2); 52 | const [ first, second ] = result; 53 | assertEquals(first.meta.last_row_id, 2); 54 | assertEquals(second.meta.last_row_id, 3); 55 | } 56 | { 57 | const result = await db.prepare(`select count(*) c from t1`).first('c'); 58 | assertEquals(result, 3); 59 | } 60 | { 61 | const result = await db.exec(`delete from t1\nselect * from sqlite_schema;\nselect * from sqlite_schema ;\r\n`); 62 | assertEquals(result.count, 3); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /cli/sqlite_dbpath_for_instance.ts: -------------------------------------------------------------------------------- 1 | // only import if used 2 | // import { DenoDir } from 'https://esm.sh/jsr/@deno/cache-dir@0.11.1'; 3 | import { ComputeDbPathForInstance } from '../common/storage/sqlite_durable_object_storage.ts'; 4 | import { join } from './deps_cli.ts'; 5 | 6 | export const sqliteDbPathForInstance: ComputeDbPathForInstance = async ({ container, className, id }: { container: string, className: string, id: string }) => { 7 | return join(await computeSqliteDosqlDirectory(), `${container}-${className}-${id}.db`); 8 | } 9 | 10 | export async function computeSqliteDosqlDirectory(): Promise { 11 | const { DenoDir } = await import('https://esm.sh/jsr/@deno/cache-dir@0.11.1' + ''); 12 | const root = DenoDir.tryResolveRootPath(undefined); 13 | if (root === undefined) throw new Error(`Unable to resolve deno cache dir`); 14 | const denoflareDosqlDir = join(root, 'denoflare', 'dosql'); 15 | await Deno.mkdir(denoflareDosqlDir, { recursive: true }); 16 | return denoflareDosqlDir; 17 | } 18 | -------------------------------------------------------------------------------- /cli/versions.ts: -------------------------------------------------------------------------------- 1 | export function versionCompare(lhs: string, rhs: string): number { 2 | if (lhs === rhs) return 0; 3 | const lhsTokens = lhs.split('.'); 4 | const rhsTokens = rhs.split('.'); 5 | for (let i = 0; i < Math.max(lhsTokens.length, rhsTokens.length); i++) { 6 | const lhsNum = parseInt(lhsTokens[i] ?? '0'); 7 | const rhsNum = parseInt(rhsTokens[i] ?? '0'); 8 | if (lhsNum < rhsNum) return -1; 9 | if (lhsNum > rhsNum) return 1; 10 | } 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /cli/versions_test.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'https://deno.land/std@0.224.0/assert/assert.ts'; 2 | 3 | import { versionCompare } from './versions.ts'; 4 | 5 | Deno.test({ 6 | name: 'versions', 7 | fn: () => { 8 | assert(versionCompare('1.0.0', '1.0.0') === 0); 9 | assert(versionCompare('1.21.3', '1.22.0') < 0); 10 | assert(versionCompare('1.21.3', '1.22') < 0); 11 | assert(versionCompare('1.21.4', '1.21') > 0); 12 | assert(versionCompare('1.21.4', '1.21.3') > 0); 13 | assert(versionCompare('1.21.3', '2.0') < 0); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /cli/wasm_crypto.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../common/bytes.ts'; 2 | import { versionCompare } from './versions.ts'; 3 | 4 | const { crypto: WasmCrypto } = await import(versionCompare(Deno.version.deno, '2.0.0') >= 0 ? 'jsr:@std/crypto@1.0.3': 'https://deno.land/std@0.224.0/crypto/mod.ts'); 5 | 6 | export async function computeStreamingSha256(stream: ReadableStream): Promise { 7 | const arr = await WasmCrypto.subtle.digest('SHA-256', stream); 8 | return new Bytes(new Uint8Array(arr)); 9 | } 10 | 11 | export async function computeStreamingMd5(stream: ReadableStream): Promise { 12 | const arr = await WasmCrypto.subtle.digest('MD5', stream); 13 | return new Bytes(new Uint8Array(arr)); 14 | } 15 | 16 | export async function computeMd5(input: Bytes | string): Promise { 17 | const bytes = typeof input === 'string' ? Bytes.ofUtf8(input) : input; 18 | const arr = await WasmCrypto.subtle.digest('MD5', bytes.array()); 19 | return new Bytes(new Uint8Array(arr)); 20 | } 21 | -------------------------------------------------------------------------------- /common/analytics/r2_costs_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts'; 2 | import { computeCosts } from './r2_costs.ts'; 3 | 4 | const DEBUG = false; 5 | 6 | Deno.test({ 7 | name: 'r2.computeCosts', 8 | fn: () => { 9 | { 10 | const { classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost } = computeCosts({ classAOperations: 499751, classBOperations: 430084, storageGb: 500, excludeFreeUsage: false }); 11 | if (DEBUG) console.log({ classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost }); 12 | assertEquals(Math.round(totalCost * 100) / 100, 2.65) 13 | } 14 | { 15 | const { classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost } = computeCosts({ classAOperations: 499751, classBOperations: 430084, storageGb: 500, excludeFreeUsage: true }); 16 | if (DEBUG) console.log({ classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost }); 17 | assertEquals(Math.round(totalCost * 100) / 100, 0.1); 18 | } 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /common/aws/emailer.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../bytes.ts'; 2 | import { AwsCall, AwsCallContext, signAwsCallV4 } from '../r2/r2.ts'; 3 | 4 | export type EmailerOpts = { readonly awsCallContext: AwsCallContext, readonly emailFrom: string, readonly emailTo: string, readonly region?: string, readonly endpoint?: string }; 5 | 6 | export type Email = { readonly subject: string, readonly text: string }; 7 | 8 | export type SendEmailResponse = { readonly status: number }; 9 | 10 | export class Emailer { 11 | 12 | private readonly opts: EmailerOpts; 13 | 14 | constructor(opts: EmailerOpts) { 15 | this.opts = opts; 16 | } 17 | 18 | async send(email: Email) { 19 | const { subject, text } = email; 20 | const { emailFrom: source, emailTo: to, awsCallContext: context, region, endpoint } = this.opts; 21 | const response = await sendEmail({ source, to, subject, text, context, region, endpoint }); 22 | if (response.status !== 200) throw new Error(`Error status ${response.status} sending email, expected 200`); 23 | } 24 | 25 | } 26 | 27 | export async function sendEmail({ source, to, subject, text, context, region = 'us-east-1', endpoint = 'https://email.us-east-1.amazonaws.com' }: { source: string, to: string, subject: string, text: string, context: AwsCallContext, region?: string, endpoint?: string }): Promise { 28 | const params = new URLSearchParams(); 29 | params.set('Action', 'SendEmail'); 30 | params.set('Destination.ToAddresses.member.1', to); 31 | params.set('Message.Body.Text.Charset', 'UTF-8'); 32 | params.set('Message.Body.Text.Data', text); 33 | params.set('Message.Subject.Charset', 'UTF-8'); 34 | params.set('Message.Subject.Data', subject); 35 | params.set('Source', source); 36 | 37 | const body = Bytes.ofUtf8(params.toString()); 38 | 39 | const call: AwsCall = { 40 | method: 'POST', 41 | service: 'email', 42 | region, 43 | url: new URL(endpoint), 44 | headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' }), 45 | body, 46 | }; 47 | 48 | const { signedHeaders, bodyInfo } = await signAwsCallV4(call, context); 49 | 50 | const request = new Request(call.url.toString(), { method: call.method, headers: signedHeaders, body: bodyInfo.body }); 51 | const { status } = await fetch(request); 52 | return { status }; 53 | } 54 | -------------------------------------------------------------------------------- /common/aws/lambda_runtime.d.ts: -------------------------------------------------------------------------------- 1 | export interface LambdaWorkerContext { 2 | readonly lambda: LambdaWorkerInfo; 3 | waitUntil(promise: Promise): void; 4 | passThroughOnException(): void; 5 | } 6 | 7 | export interface LambdaWorkerInfo { 8 | readonly request: LambdaHttpRequest; 9 | readonly times: LambdaWorkerTimes; 10 | readonly env: Record; 11 | readonly awsRequestId: string; 12 | readonly invokedFunctionArn: string; 13 | readonly traceId: string; 14 | } 15 | 16 | export interface LambdaWorkerTimes { 17 | readonly bootstrap: number; 18 | readonly start: number; 19 | readonly init: number; 20 | readonly request: number; 21 | readonly dispatch: number; 22 | readonly deadline: number; 23 | } 24 | 25 | // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format 26 | 27 | export interface LambdaHttpRequest { 28 | readonly version: string; // 2.0 29 | readonly routeKey: string; // $default 30 | readonly rawPath: string; // /path 31 | readonly rawQueryString: string // foo=bar or "" 32 | readonly cookies?: string[]; 33 | readonly headers: Record, 34 | readonly requestContext: { 35 | readonly accountId: string; // anonymous 36 | readonly apiId: string; // <32-loweralphanum> 37 | readonly domainName: string; // <32-loweralphanum>.lambda-url..on.aws 38 | readonly domainPrefix: string; // <32-loweralphanum> 39 | readonly http: { 40 | readonly method: string; // GET 41 | readonly path: string; // /path 42 | readonly protocol: string; // HTTP/1.1 43 | readonly sourceIp: string; 44 | readonly userAgent: string; 45 | }, 46 | readonly requestId: string; // v4 guid 47 | readonly routeKey: string; // $default 48 | readonly stage: string; // $default 49 | readonly time: string; // 08/Jul/2023:01:33:58 +0000 50 | readonly timeEpoch: number; // 1688780038228 51 | }, 52 | readonly body?: string; 53 | readonly isBase64Encoded: boolean; 54 | } 55 | -------------------------------------------------------------------------------- /common/cloudflare_email.ts: -------------------------------------------------------------------------------- 1 | import { EmailMessageConstructable, EmailMessage } from './cloudflare_workers_types.d.ts'; 2 | 3 | export function cloudflareEmail(): CloudflareEmail { 4 | // deno-lint-ignore no-explicit-any 5 | const provider = (globalThis as any).__cloudflareEmailProvider; 6 | if (typeof provider === 'function') return provider(); 7 | return { EmailMessage: StubEmailMessage }; 8 | } 9 | 10 | export interface CloudflareEmail { 11 | EmailMessage: EmailMessageConstructable, 12 | } 13 | 14 | class StubEmailMessage implements EmailMessage { 15 | readonly from: string; 16 | readonly to: string; 17 | get headers(): Headers { throw new Error(); } 18 | get raw(): ReadableStream { throw new Error(); } 19 | get rawSize(): number { throw new Error(); } 20 | 21 | constructor(from: string, to: string, _raw: ReadableStream | string) { 22 | this.from = from; 23 | this.to = to; 24 | } 25 | 26 | setReject(reason: string): void { 27 | throw new Error(`setReject(${JSON.stringify({ reason })})`); 28 | } 29 | 30 | forward(rcptTo: string, headers?: Headers | undefined): Promise { 31 | throw new Error(`forward(${JSON.stringify({ rcptTo, headers })})`); 32 | } 33 | 34 | reply(message: EmailMessage): Promise { 35 | throw new Error(`forward(${JSON.stringify({ message })})`); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /common/cloudflare_pipeline_transform.ts: -------------------------------------------------------------------------------- 1 | import type { PipelineTransform } from './cloudflare_workers_types.d.ts'; 2 | 3 | export function cloudflarePipelineTransform() { 4 | return { PipelineTransform: StubPipelineTransform } 5 | } 6 | 7 | class StubPipelineTransform implements PipelineTransform { 8 | constructor(ctx: unknown, env: unknown) { 9 | } 10 | 11 | transformJson(_data: object[]): Promise { 12 | throw new Error('Method not implemented.'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/console.ts: -------------------------------------------------------------------------------- 1 | // grab the native ones up front, they might be overridden by untrusted code later on 2 | export const consoleLog = console.log; 3 | export const consoleWarn = console.warn; 4 | export const consoleError = console.error; 5 | -------------------------------------------------------------------------------- /common/constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | static readonly MAX_CONTENT_LENGTH_TO_PACK_OVER_RPC = 1024 * 1024 * 5; // bypass read-body-chunk for fetch responses with defined content-length under this limit 3 | } -------------------------------------------------------------------------------- /common/crypto_keys.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../common/bytes.ts'; 2 | import { isStringRecord } from '../common/check.ts'; 3 | 4 | export interface CryptoKeyDef { 5 | readonly format: string; 6 | readonly algorithm: AesKeyGenParams | HmacKeyGenParams; 7 | readonly usages: KeyUsage[]; 8 | readonly base64: string; 9 | } 10 | 11 | export function parseCryptoKeyDef(json: string): CryptoKeyDef { 12 | const obj = JSON.parse(json); 13 | const { format, algorithm, usages, base64 } = obj; 14 | if (typeof format !== 'string') throw new Error(`Bad format: ${JSON.stringify(format)} in ${JSON.stringify(obj)}`); 15 | if (!isStringRecord(algorithm)) throw new Error(`Bad algorithm: ${JSON.stringify(algorithm)} in ${JSON.stringify(obj)}`); 16 | const { name } = algorithm; 17 | if (typeof name !== 'string') throw new Error(`Bad algorithm.name: ${JSON.stringify(name)} in ${JSON.stringify(obj)}`); 18 | if (!Array.isArray(usages) || !usages.every(isKeyUsage)) throw new Error(`Bad usages: ${JSON.stringify(usages)} in ${JSON.stringify(obj)}`); 19 | if (typeof base64 !== 'string') throw new Error(`Bad base64: ${JSON.stringify(base64)} in ${JSON.stringify(obj)}`); 20 | 21 | // deno-lint-ignore no-explicit-any 22 | return { format, algorithm: algorithm as any, usages, base64 }; 23 | } 24 | 25 | export async function toCryptoKey(def: CryptoKeyDef): Promise { 26 | const { format, base64, algorithm, usages } = def; 27 | if (format !== 'pkcs8' && format !== 'raw' && format !== 'spki') throw new Error(`Format ${format} not supported`); 28 | const keyData: BufferSource = Bytes.ofBase64(base64).array(); 29 | return await crypto.subtle.importKey(format, keyData, algorithm, true, usages); 30 | } 31 | 32 | export async function cryptoKeyProvider(json: string): Promise { 33 | const def = parseCryptoKeyDef(json); 34 | return await toCryptoKey(def); 35 | } 36 | 37 | export function isKeyUsage(obj: unknown): obj is KeyUsage { 38 | return typeof obj === 'string' && /^decrypt|deriveBits|deriveKey|encrypt|sign|unwrapKey|verify|wrapKey$/.test(obj); 39 | } 40 | -------------------------------------------------------------------------------- /common/deno_workarounds.ts: -------------------------------------------------------------------------------- 1 | const _fetch = fetch; 2 | 3 | export function redefineGlobalFetchToWorkaroundBareIpAddresses() { 4 | // https://github.com/denoland/deno/issues/7660 5 | // fixed in v1.33.4 ! 6 | 7 | // deno-lint-ignore no-explicit-any 8 | const fetchFromDeno = function(arg1: any, arg2: any) { 9 | if (typeof arg1 === 'string') { 10 | const url = tryModifyUrl(arg1); 11 | if (url !== undefined) { 12 | arg1 = url; 13 | } 14 | } else if (arg1 instanceof Request) { 15 | const url = tryModifyUrl(arg1.url); 16 | if (url !== undefined) { 17 | arg1 = new Request(url, arg1); 18 | } 19 | } 20 | return _fetch(arg1, arg2); 21 | }; 22 | globalThis.fetch = fetchFromDeno; 23 | } 24 | 25 | // 26 | 27 | function tryModifyUrl(url: string): string | undefined { 28 | if (url.startsWith('https://1.1.1.1/')) { 29 | return 'https://one.one.one.one/' + url.substring('https://1.1.1.1/'.length); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /common/deploy/deno_deploy_common_api.ts: -------------------------------------------------------------------------------- 1 | import { TextLineStream } from 'https://deno.land/std@0.224.0/streams/text_line_stream.ts'; 2 | 3 | export type ApiCall = { queryParams?: Record, requestBody?: unknown, method?: 'PATCH' | 'POST', accept?: string, apiToken: string, endpoint?: string }; 4 | 5 | export async function executeJsonForEndpoint(pathname: string, opts: ApiCall, defaultEndpoint: string): Promise { 6 | const res = await executeForEndpoint(pathname, opts, defaultEndpoint); 7 | const contentType = res.headers.get('content-type'); 8 | if (res.status !== 200 || contentType !== 'application/json') throw new Error(`Unexpected response: ${res.status} ${contentType} ${await res.text()}`); 9 | return await res.json(); 10 | } 11 | 12 | export async function* executeStreamForEndpoint(pathname: string, opts: ApiCall, defaultEndpoint: string): AsyncIterable { 13 | const res = await executeForEndpoint(pathname, opts, defaultEndpoint); 14 | if (res.status !== 200 || !res.body) throw new Error(`Unexpected response: ${res.status} ${!res.body ? '(no body)' : await res.text()}`); 15 | const lines = res.body.pipeThrough(new TextDecoderStream()).pipeThrough(new TextLineStream()); 16 | for await (const line of lines) { 17 | if (line === '') return; 18 | yield JSON.parse(line); 19 | } 20 | } 21 | 22 | export async function executeForEndpoint(pathname: string, call: ApiCall, defaultEndpoint: string): Promise { 23 | const { queryParams = {}, requestBody, method = requestBody ? 'POST' : 'GET', accept, apiToken, endpoint = defaultEndpoint } = call; 24 | const url = new URL(`${endpoint}${pathname}`); 25 | Object.entries(queryParams).forEach(([ n, v ]) => { 26 | if (v !== undefined) url.searchParams.set(n, String(v)); 27 | }); 28 | const body = requestBody instanceof FormData ? requestBody : requestBody ? JSON.stringify(requestBody) : undefined; 29 | const response = await fetch(url.toString(), { method, body, headers: { 30 | accept: 'application/json', 31 | authorization: `Bearer ${apiToken}`, 32 | ...(requestBody && !(requestBody instanceof FormData) ? { 'content-type': 'application/json' } : {}), 33 | ...(accept ? { accept } : {}), 34 | } }); 35 | return response; 36 | } 37 | -------------------------------------------------------------------------------- /common/deps_xml.ts: -------------------------------------------------------------------------------- 1 | export { getTraversalObj, validate } from 'https://cdn.skypack.dev/fast-xml-parser@3.21.1?dts'; 2 | -------------------------------------------------------------------------------- /common/fetch_util.ts: -------------------------------------------------------------------------------- 1 | export class FetchUtil { 2 | static VERBOSE = false; 3 | } 4 | 5 | export function cloneRequestWithHostname(request: Request, hostname: string): Request { 6 | const url = new URL(request.url); 7 | if (url.hostname === hostname) return request; 8 | const newUrl = url.origin.replace(url.host, hostname) + request.url.substring(url.origin.length); 9 | if (FetchUtil.VERBOSE) console.log(`cloneRequestWithHostname: ${url} + ${hostname} = ${newUrl}`); 10 | const { method, headers } = request; 11 | const body = (method === 'GET' || method === 'HEAD') ? undefined : request.body; 12 | return new Request(newUrl, { method, headers, body }); 13 | } 14 | -------------------------------------------------------------------------------- /common/import_binary.ts: -------------------------------------------------------------------------------- 1 | import { resolve, fromFileUrl, toFileUrl } from 'https://deno.land/std@0.224.0/path/mod.ts'; // intended to be self-contained, don't use shared deps 2 | 3 | /** 4 | * Meant to work properly in a standard Deno environment. 5 | * Call in the global scope like a standard esm import. 6 | * 7 | * However, when using `denoflare push`, calls to: 8 | * `const buffer = await importBinary(import.meta.url, './whatever.png');` 9 | * are rewritten to 10 | * `import buffer from "./relative/path/to/whatever.png";` 11 | * prior to Cloudflare upload, so that it works properly in Cloudflare as well. 12 | */ 13 | export async function importBinary(importMetaUrl: string, moduleSpecifier: string, fetcher: (url: string) => Promise = fetch): Promise { 14 | if (moduleSpecifier.startsWith('https://')) { 15 | return await importBinaryFromHttps(moduleSpecifier, fetcher); 16 | } 17 | 18 | if (importMetaUrl.startsWith('file://')) { 19 | return await (await fetcher(appendQueryHint(toFileUrl(resolve(resolve(fromFileUrl(importMetaUrl), '..'), moduleSpecifier))).toString())).arrayBuffer(); 20 | } else if (importMetaUrl.startsWith('https://')) { 21 | const { pathname, origin } = new URL(importMetaUrl); 22 | const binaryUrl = origin + resolve(resolve(pathname, '..'), moduleSpecifier); 23 | return await importBinaryFromHttps(binaryUrl, fetcher); 24 | } else { 25 | throw new Error(`importBinary: Unsupported importMetaUrl: ${importMetaUrl}`); 26 | } 27 | } 28 | 29 | // 30 | 31 | function appendQueryHint(fileUrl: URL): URL { 32 | fileUrl.searchParams.set('import', 'binary'); 33 | return fileUrl; 34 | } 35 | 36 | async function importBinaryFromHttps(url: string, fetcher: (url: string) => Promise): Promise { 37 | const res = await fetcher(url); 38 | if (res.status !== 200) throw new Error(`importBinary: Bad status ${res.status}, expected 200 for ${url}`); 39 | // no content-type check for binary files 40 | return await res.arrayBuffer(); 41 | } 42 | -------------------------------------------------------------------------------- /common/import_text.ts: -------------------------------------------------------------------------------- 1 | import { resolve, fromFileUrl, toFileUrl } from 'https://deno.land/std@0.224.0/path/mod.ts'; // intended to be self-contained, don't use shared deps 2 | 3 | /** 4 | * Meant to work properly in a standard Deno environment. 5 | * Call in the global scope like a standard esm import. 6 | * 7 | * However, when using `denoflare push`, calls to: 8 | * `const text = await importText(import.meta.url, './whatever.txt');` 9 | * are rewritten to 10 | * `import text from "./relative/path/to/whatever.txt";` 11 | * prior to Cloudflare upload, so that it works properly in Cloudflare as well. 12 | */ 13 | export async function importText(importMetaUrl: string, moduleSpecifier: string, fetcher: (url: string) => Promise = fetch): Promise { 14 | if (moduleSpecifier.startsWith('https://')) { 15 | return await importTextFromHttps(moduleSpecifier, fetcher); 16 | } 17 | 18 | if (importMetaUrl.startsWith('file://')) { 19 | return await (await fetcher(appendQueryHint(toFileUrl(resolve(resolve(fromFileUrl(importMetaUrl), '..'), moduleSpecifier))).toString())).text(); 20 | } else if (importMetaUrl.startsWith('https://')) { 21 | const { pathname, origin } = new URL(importMetaUrl); 22 | const textUrl = origin + resolve(resolve(pathname, '..'), moduleSpecifier); 23 | return await importTextFromHttps(textUrl, fetcher); 24 | } else { 25 | throw new Error(`importText: Unsupported importMetaUrl: ${importMetaUrl}`); 26 | } 27 | } 28 | 29 | // 30 | 31 | function appendQueryHint(fileUrl: URL): URL { 32 | fileUrl.searchParams.set('import', 'text'); 33 | return fileUrl; 34 | } 35 | 36 | async function importTextFromHttps(url: string, fetcher: (url: string) => Promise): Promise { 37 | const res = await fetcher(url); 38 | if (res.status !== 200) throw new Error(`importText: Bad status ${res.status}, expected 200 for ${url}`); 39 | const contentType = (res.headers.get('content-type') || '').toLowerCase(); 40 | if (contentType.startsWith('text/')) { 41 | return await res.text(); 42 | } else { 43 | throw new Error(`importText: Bad contentType ${contentType}, expected text/* for ${url}`); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /common/import_wasm.ts: -------------------------------------------------------------------------------- 1 | import { resolve, fromFileUrl, toFileUrl } from 'https://deno.land/std@0.224.0/path/mod.ts'; // intended to be self-contained, don't use shared deps 2 | 3 | /** 4 | * Meant to work properly in a standard Deno environment. 5 | * Call in the global scope like a standard esm import. 6 | * 7 | * However, when using `denoflare push`, calls to: 8 | * `const module = await importWasm(import.meta.url, './whatever.wasm');` 9 | * are rewritten to 10 | * `import module from "./relative/path/to/whatever.wasm";` 11 | * prior to Cloudflare upload, so that wasm works properly in Cloudflare as well. 12 | */ 13 | export async function importWasm(importMetaUrl: string, moduleSpecifier: string, fetcher: (url: string) => Promise = fetch): Promise { 14 | if (moduleSpecifier.startsWith('https://')) { 15 | return await instantiateModuleFromHttps(moduleSpecifier, fetcher); 16 | } 17 | 18 | if (importMetaUrl.startsWith('file://')) { 19 | return await WebAssembly.compileStreaming(await fetcher(appendQueryHint(toFileUrl(resolve(resolve(fromFileUrl(importMetaUrl), '..'), moduleSpecifier))).toString())); 20 | } else if (importMetaUrl.startsWith('https://')) { 21 | const { pathname, origin } = new URL(importMetaUrl); 22 | const wasmUrl = origin + resolve(resolve(pathname, '..'), moduleSpecifier); 23 | return await instantiateModuleFromHttps(wasmUrl, fetcher); 24 | } else { 25 | throw new Error(`importWasm: Unsupported importMetaUrl: ${importMetaUrl}`); 26 | } 27 | } 28 | 29 | // 30 | 31 | function appendQueryHint(fileUrl: URL): URL { 32 | fileUrl.searchParams.set('import', 'wasm'); 33 | return fileUrl; 34 | } 35 | 36 | async function instantiateModuleFromHttps(url: string, fetcher: (url: string) => Promise): Promise { 37 | const res = await fetcher(url); 38 | if (res.status !== 200) throw new Error(`importWasm: Bad status ${res.status}, expected 200 for ${url}`); 39 | const contentType = (res.headers.get('content-type') || '').toLowerCase(); 40 | if (contentType === 'application/wasm') { 41 | return await WebAssembly.compileStreaming(res); 42 | } else if (contentType === 'application/octet-stream') { 43 | // currently served by https://raw.githubusercontent.com 44 | // allow it for now 45 | return new WebAssembly.Module(await res.arrayBuffer()); 46 | } else { 47 | throw new Error(`importWasm: Bad contentType ${contentType}, expected application/wasm or application/octet-stream for ${url}`); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/incoming_request_cf_properties.ts: -------------------------------------------------------------------------------- 1 | import { IncomingRequestCfProperties } from './cloudflare_workers_types.d.ts'; 2 | 3 | export function makeIncomingRequestCfProperties(): IncomingRequestCfProperties { 4 | // deno-lint-ignore no-explicit-any 5 | return { colo: 'DNO', asn: 13335, city: 'Cleveland' } as any; 6 | } 7 | -------------------------------------------------------------------------------- /common/jwt.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from './bytes.ts'; 2 | 3 | export interface Jwt { 4 | readonly header: Record; 5 | readonly claims: Record; 6 | readonly signature: Uint8Array; 7 | } 8 | 9 | export function decodeJwt(token: string): Jwt { 10 | const m = /^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/.exec(token); 11 | if (!m) throw new Error(`Bad jwt token string: ${token}`); 12 | const header = JSON.parse(Bytes.ofBase64(m[1], { urlSafe: true }).utf8()); 13 | const claims = JSON.parse(Bytes.ofBase64(m[2], { urlSafe: true }).utf8()); 14 | const signature = Bytes.ofBase64(m[3], { urlSafe: true }).array(); 15 | return { header, claims, signature }; 16 | } 17 | -------------------------------------------------------------------------------- /common/local_durable_objects_test.ts: -------------------------------------------------------------------------------- 1 | import { DurableObjectState } from './cloudflare_workers_types.d.ts'; 2 | import { LocalDurableObjects } from './local_durable_objects.ts'; 3 | import { checkEqual, checkMatches } from './check.ts'; 4 | 5 | Deno.test({ 6 | name: 'blockConcurrencyWhile', 7 | ignore: true, // TODO re-enable once a solution to local DO fetch locking is found 8 | fn: async () => { 9 | const objects = new LocalDurableObjects({ moduleWorkerExportedFunctions: { 'DurableObject1': DurableObject1 } }); 10 | const ns = objects.resolveDoNamespace('local:DurableObject1'); 11 | await ns.get(ns.idFromName('name')).fetch('https://foo'); 12 | } 13 | }); 14 | 15 | Deno.test('newUniqueId', async () => { 16 | const objects = new LocalDurableObjects({ moduleWorkerExportedFunctions: { 'DurableObject2': DurableObject2 } }); 17 | const ns = objects.resolveDoNamespace('local:DurableObject2'); 18 | const id = ns.newUniqueId(); 19 | checkMatches('id', id.toString(), /^[0-9a-f]{64}$/); 20 | const res = await ns.get(id).fetch('https://foo'); 21 | const txt = await res.text(); 22 | checkEqual('txt', txt, id.toString()); 23 | }); 24 | 25 | Deno.test('idFromName', async () => { 26 | const objects = new LocalDurableObjects({ moduleWorkerExportedFunctions: { 'DurableObject2': DurableObject2 } }); 27 | const ns = objects.resolveDoNamespace('local:DurableObject2'); 28 | const id = ns.idFromName('foo'); 29 | checkMatches('id', id.toString(), /^[0-9a-f]{64}$/); 30 | const res = await ns.get(id).fetch('https://foo'); 31 | const txt = await res.text(); 32 | checkEqual('txt', txt, id.toString()); 33 | }); 34 | 35 | // 36 | 37 | function sleep(ms: number) { 38 | return new Promise(resolve => setTimeout(resolve, ms)); 39 | } 40 | 41 | // 42 | 43 | class DurableObject1 { 44 | 45 | constructor(state: DurableObjectState, _env: Record) { 46 | console.log('ctor before'); 47 | state.blockConcurrencyWhile(async () => { 48 | console.log('blockConcurrencyWhile before') 49 | await sleep(500); 50 | console.log('blockConcurrencyWhile after') 51 | }) 52 | console.log('ctor after'); 53 | } 54 | 55 | fetch(_request: Request): Promise { 56 | console.log('fetch'); 57 | return Promise.resolve(new Response('ok')); 58 | } 59 | 60 | } 61 | 62 | class DurableObject2 { 63 | readonly state: DurableObjectState; 64 | 65 | constructor(state: DurableObjectState, _env: Record) { 66 | this.state = state; 67 | } 68 | 69 | fetch(_request: Request): Promise { 70 | return Promise.resolve(new Response(this.state.id.toString())); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /common/mqtt/deno_tcp_connection.ts: -------------------------------------------------------------------------------- 1 | import { Mqtt } from './mqtt.ts'; 2 | import { MqttClient } from './mqtt_client.ts'; 3 | import { MqttConnection } from './mqtt_connection.ts'; 4 | 5 | export class DenoTcpConnection implements MqttConnection { 6 | 7 | readonly completionPromise: Promise; 8 | 9 | onRead: (bytes: Uint8Array) => void = () => {}; 10 | 11 | private readonly conn: Deno.TlsConn; 12 | 13 | private closed = false; 14 | 15 | private constructor(conn: Deno.TlsConn) { 16 | this.conn = conn; 17 | this.completionPromise = this.initCompletionPromise(); 18 | } 19 | 20 | private initCompletionPromise(): Promise { 21 | const { DEBUG } = Mqtt; 22 | return (async () => { 23 | while (true) { 24 | const buffer = new Uint8Array(8 * 1024); 25 | if (DEBUG) console.log('before read'); 26 | const result = await this.read(buffer); 27 | if (result === null) { 28 | if (DEBUG) console.log('EOF'); 29 | return; 30 | } 31 | if (DEBUG) console.log(`Received ${result} bytes`); 32 | this.onRead(buffer.slice(0, result)); 33 | } 34 | })(); 35 | } 36 | 37 | private async read(buffer: Uint8Array): Promise { 38 | try { 39 | return await this.conn.read(buffer); 40 | } catch (e) { 41 | if (this.closed) return null; // BadResource: Bad resource ID 42 | throw e; 43 | } 44 | } 45 | 46 | // 47 | 48 | static async create(opts: { hostname: string, port: number }): Promise { 49 | const { hostname, port } = opts; 50 | const connection = await Deno.connectTls({ hostname, port }); 51 | return new DenoTcpConnection(connection); 52 | } 53 | 54 | async write(bytes: Uint8Array): Promise { 55 | return await this.conn.write(bytes); 56 | } 57 | 58 | close() { 59 | this.closed = true; 60 | this.conn.close(); 61 | } 62 | 63 | static register() { 64 | MqttClient.protocolHandlers['mqtts'] = DenoTcpConnection.create; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /common/mqtt/mod_deno.ts: -------------------------------------------------------------------------------- 1 | import { DenoTcpConnection } from './deno_tcp_connection.ts'; 2 | 3 | export * from './mod_iso.ts'; 4 | 5 | // auto-register Deno tcp-based implementation with MqttClient for mqtts endpoints 6 | DenoTcpConnection.register(); 7 | -------------------------------------------------------------------------------- /common/mqtt/mod_iso.ts: -------------------------------------------------------------------------------- 1 | export { MqttClient } from './mqtt_client.ts'; 2 | export type { Protocol } from './mqtt_client.ts'; 3 | export type { MqttConnection } from './mqtt_connection.ts'; 4 | export { Mqtt } from './mqtt.ts'; 5 | 6 | export { 7 | computeControlPacketTypeName, 8 | CONNECT, CONNACK, PUBLISH, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, 9 | } from './mqtt_messages.ts'; 10 | 11 | export type { 12 | MqttMessage, 13 | ConnectMessage, ConnackMessage, PublishMessage, SubscribeMessage, SubackMessage, PingreqMessage, PingrespMessage, DisconnectMessage, 14 | ControlPacketType, 15 | Subscription, 16 | Reason, 17 | } from './mqtt_messages.ts'; 18 | -------------------------------------------------------------------------------- /common/mqtt/mqtt.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../bytes.ts'; 2 | 3 | /** Static constants for debugging MqttClient. */ 4 | export class Mqtt { 5 | 6 | /** Enable debug-level logging throughout MqttClient and its dependencies. */ 7 | static DEBUG = false; 8 | } 9 | 10 | /** @internal */ 11 | export function encodeVariableByteInteger(value: number): number[] { 12 | const rt = []; 13 | do { 14 | let encodedByte = value % 128; 15 | value = Math.floor(value / 128); 16 | if (value > 0) { 17 | encodedByte = encodedByte | 128; 18 | } 19 | rt.push(encodedByte); 20 | } while (value > 0); 21 | return rt; 22 | } 23 | 24 | /** @internal */ 25 | export function decodeVariableByteInteger(buffer: Uint8Array, startIndex: number): { value: number, bytesUsed: number } { 26 | let i = startIndex; 27 | let encodedByte = 0; 28 | let value = 0; 29 | let multiplier = 1; 30 | do { 31 | encodedByte = buffer[i++]; 32 | value += (encodedByte & 127) * multiplier; 33 | if (multiplier > 128 * 128 * 128) throw Error('malformed length'); 34 | multiplier *= 128; 35 | } while ((encodedByte & 128) != 0); 36 | return { value, bytesUsed: i - startIndex }; 37 | } 38 | 39 | /** @internal */ 40 | export function encodeUtf8(value: string): number[] { 41 | const arr = encoder.encode(value); 42 | // https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_UTF-8_Encoded_String 43 | if (arr.length > 65535) throw new Error('the maximum size of a UTF-8 Encoded String is 65,535 bytes.'); 44 | const lengthBytes = [ arr.length >> 8, arr.length & 0xff ]; // always exactly 2 bytes 45 | return [ ...lengthBytes, ...arr ]; 46 | } 47 | 48 | /** @internal */ 49 | export function decodeUtf8(buffer: Uint8Array, startIndex: number): { text: string, bytesUsed: number } { 50 | const length = (buffer[startIndex] << 8) + buffer[startIndex + 1]; 51 | const bytes = buffer.slice(startIndex + 2, startIndex + 2 + length); 52 | const text = decoder.decode(bytes); 53 | 54 | return { text, bytesUsed: length + 2 }; 55 | } 56 | 57 | /** @internal */ 58 | export function hex(bytes: number[] | Uint8Array): string { 59 | return new Bytes(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)).hex(); 60 | } 61 | 62 | // 63 | 64 | const encoder = new TextEncoder(); 65 | 66 | const decoder = new TextDecoder(); 67 | -------------------------------------------------------------------------------- /common/mqtt/mqtt_connection.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Low-level abstraction for a single bi-directional MQTT connection to a server. 3 | * 4 | * Can be used to provide custom protocol handler implementations for the higher-level MqttClient. 5 | */ 6 | export interface MqttConnection { 7 | 8 | /** Writes bytes to the outgoing connection to the server. */ 9 | write(bytes: Uint8Array): Promise; 10 | 11 | /** Called when incoming bytes are received from the server. */ 12 | onRead: (bytes: Uint8Array) => void; 13 | 14 | /** Resolves when the connection is closed. */ 15 | readonly completionPromise: Promise; 16 | 17 | /** Closes the connection. */ 18 | close(): void; 19 | } 20 | -------------------------------------------------------------------------------- /common/mutex.ts: -------------------------------------------------------------------------------- 1 | // adapted from https://spin.atomicobject.com/2018/09/10/javascript-concurrency/ 2 | 3 | export class Mutex { 4 | private mutex = Promise.resolve(); 5 | 6 | lock(): PromiseLike<() => void> { 7 | let begin: (unlock: () => void) => void = () => { }; 8 | 9 | this.mutex = this.mutex.then(() => { 10 | return new Promise(begin); 11 | }); 12 | 13 | return new Promise(res => { 14 | begin = res; 15 | }); 16 | } 17 | 18 | async dispatch(fn: () => Promise): Promise { 19 | const unlock = await this.lock(); 20 | try { 21 | return await Promise.resolve(fn()); 22 | } finally { 23 | unlock(); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /common/noop_analytics_engine.ts: -------------------------------------------------------------------------------- 1 | import { AnalyticsEngineProvider } from './cloudflare_workers_runtime.ts'; 2 | import { AnalyticsEngine, AnalyticsEngineEvent } from './cloudflare_workers_types.d.ts'; 3 | 4 | export class NoopAnalyticsEngine implements AnalyticsEngine { 5 | private readonly dataset: string; 6 | 7 | constructor(dataset: string) { 8 | this.dataset = dataset; 9 | } 10 | 11 | writeDataPoint(event: AnalyticsEngineEvent): void { 12 | console.log(`${this.dataset}.writeDataPoint (no-op)`, event); 13 | } 14 | 15 | static provider: AnalyticsEngineProvider = dataset => new NoopAnalyticsEngine(dataset); 16 | } 17 | -------------------------------------------------------------------------------- /common/noop_cf_global_caches.ts: -------------------------------------------------------------------------------- 1 | import { CfCache, CfCacheOptions, CfGlobalCaches } from './cloudflare_workers_types.d.ts'; 2 | 3 | export class NoopCfGlobalCaches implements CfGlobalCaches { 4 | readonly default = new NoopCfCache(); 5 | 6 | private namedCaches = new Map(); 7 | 8 | open(cacheName: string): Promise { 9 | const existing = this.namedCaches.get(cacheName); 10 | if (existing) return Promise.resolve(existing); 11 | const cache = new NoopCfCache(); 12 | this.namedCaches.set(cacheName, cache); 13 | return Promise.resolve(cache); 14 | } 15 | 16 | } 17 | 18 | // 19 | 20 | class NoopCfCache implements CfCache { 21 | 22 | put(_request: string | Request, _response: Response): Promise { 23 | return Promise.resolve(undefined); 24 | } 25 | 26 | match(_request: string | Request, _options?: CfCacheOptions): Promise { 27 | return Promise.resolve(undefined); 28 | } 29 | 30 | delete(_request: string | Request, _options?: CfCacheOptions): Promise { 31 | return Promise.resolve(false); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /common/noop_d1_database.ts: -------------------------------------------------------------------------------- 1 | import { D1DatabaseProvider } from './cloudflare_workers_runtime.ts'; 2 | import { D1Database, D1ExecResult, D1PreparedStatement, D1Result } from './cloudflare_workers_types.d.ts'; 3 | 4 | export class NoopD1Database implements D1Database { 5 | private readonly d1DatabaseUuid: string; 6 | 7 | private constructor(d1DatabaseUuid: string) { 8 | this.d1DatabaseUuid = d1DatabaseUuid; 9 | } 10 | 11 | prepare(_query: string): D1PreparedStatement { 12 | return new NoopD1PreparedStatement(); 13 | } 14 | 15 | dump(): Promise { 16 | return Promise.resolve(new ArrayBuffer(0)); 17 | } 18 | 19 | batch(statements: D1PreparedStatement[]): Promise[]> { 20 | return Promise.resolve(statements.map(() => computeNoopD1Result())); 21 | } 22 | 23 | exec(_query: string): Promise { 24 | return Promise.resolve({ count: 0, duration: 0 }); 25 | } 26 | 27 | static provider: D1DatabaseProvider = d1DatabaseUuid => new NoopD1Database(d1DatabaseUuid); 28 | } 29 | 30 | // 31 | 32 | function computeNoopD1Result(): D1Result { 33 | return { 34 | success: true, 35 | results: [], 36 | meta: { 37 | changed_db: false, 38 | changes: 0, 39 | duration: 0, 40 | last_row_id: 0, 41 | rows_read: 0, 42 | rows_written: 0, 43 | size_after: 0 44 | } 45 | }; 46 | } 47 | 48 | // 49 | 50 | class NoopD1PreparedStatement implements D1PreparedStatement { 51 | 52 | bind(..._values: unknown[]): D1PreparedStatement { 53 | return this; 54 | } 55 | 56 | first(column: string): Promise; 57 | first>(): Promise; 58 | first(_column?: string): Promise { 59 | return Promise.resolve(null); 60 | } 61 | 62 | all>(): Promise> { 63 | return Promise.resolve(computeNoopD1Result()); 64 | } 65 | 66 | raw(options: { columnNames: true }): Promise<[ string[], ...T[] ]>; 67 | raw(options?: { columnNames?: false }): Promise; 68 | raw({ columnNames }: { columnNames?: boolean } = {}): Promise<[ string[], ...T[] ] | T[]> { 69 | if (columnNames) { 70 | return Promise.resolve([ [] ] as [ string[], ...T[] ]); 71 | } else { 72 | return Promise.resolve([] as T[]); 73 | } 74 | } 75 | 76 | run>(): Promise> { 77 | return Promise.resolve(computeNoopD1Result()); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /common/noop_email_sender.ts: -------------------------------------------------------------------------------- 1 | import { EmailSenderProvider } from './cloudflare_workers_runtime.ts'; 2 | import { EmailMessage, EmailSender } from './cloudflare_workers_types.d.ts'; 3 | 4 | export class NoopEmailSender implements EmailSender { 5 | private readonly destinationAddresses: string; 6 | 7 | constructor(destinationAddresses: string) { 8 | this.destinationAddresses = destinationAddresses; 9 | } 10 | 11 | send(message: EmailMessage): Promise { 12 | console.log(`NoopEmailSender.send: ${JSON.stringify(message)}`); 13 | return Promise.resolve(); 14 | } 15 | 16 | static provider: EmailSenderProvider = destinationAddresses => new NoopEmailSender(destinationAddresses); 17 | } 18 | -------------------------------------------------------------------------------- /common/noop_queue.ts: -------------------------------------------------------------------------------- 1 | import { QueueProvider } from './cloudflare_workers_runtime.ts'; 2 | import { Queue, QueuesContentType } from './cloudflare_workers_types.d.ts'; 3 | 4 | export class NoopQueue implements Queue { 5 | private readonly queueName: string; 6 | 7 | constructor(queueName: string) { 8 | this.queueName = queueName; 9 | } 10 | 11 | send(message: unknown, opts?: { contentType?: QueuesContentType }): Promise { 12 | console.log(`NoopQueue.send(${JSON.stringify(message)}, ${JSON.stringify(opts)})`); 13 | return Promise.resolve(); 14 | } 15 | 16 | sendBatch(messages: Iterable<{ body: unknown, contentType?: QueuesContentType }>): Promise { 17 | console.log(`NoopQueue.sendBatch(${JSON.stringify(messages)})`); 18 | return Promise.resolve(); 19 | } 20 | 21 | static provider: QueueProvider = queueName => new NoopQueue(queueName); 22 | } 23 | -------------------------------------------------------------------------------- /common/r2/abort_multipart_upload.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type AbortMultipartUploadOpts = { bucket: string, key: string, uploadId: string, origin: string, region: string, urlStyle?: UrlStyle }; 4 | 5 | export async function abortMultipartUpload(opts: AbortMultipartUploadOpts, context: AwsCallContext): Promise { 6 | const { bucket, key, uploadId, origin, region, urlStyle } = opts; 7 | const method = 'DELETE'; 8 | const url = computeBucketUrl({ origin, bucket, key, urlStyle }); 9 | url.searchParams.set('uploadId', uploadId); 10 | 11 | const res = await s3Fetch({ method, url, region, context }); 12 | await throwIfUnexpectedStatus(res, 204); 13 | } 14 | -------------------------------------------------------------------------------- /common/r2/create_bucket.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type CreateBucketOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle }; 4 | 5 | export async function createBucket(opts: CreateBucketOpts, context: AwsCallContext): Promise<{ location: string }> { 6 | const { bucket, origin, region, urlStyle } = opts; 7 | const method = 'PUT'; 8 | const url = computeBucketUrl({ origin, bucket, urlStyle }); 9 | 10 | const body = payload(region); 11 | const res = await s3Fetch({ method, url, region, context, body }); 12 | await throwIfUnexpectedStatus(res, 200); 13 | 14 | const location = res.headers.get('location') || undefined; 15 | if (location === undefined) throw new Error(`Missing expected 'location' header in response`); 16 | return { location }; 17 | } 18 | 19 | // 20 | 21 | const payload = (locationConstraint: string) => ` 22 | 23 | ${locationConstraint} 24 | `; 25 | -------------------------------------------------------------------------------- /common/r2/delete_bucket.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type DeleteBucketOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle }; 4 | 5 | export async function deleteBucket(opts: DeleteBucketOpts, context: AwsCallContext): Promise { 6 | const { bucket, origin, region, urlStyle } = opts; 7 | const method = 'DELETE'; 8 | const url = computeBucketUrl({ origin, bucket, urlStyle }); 9 | 10 | const res = await s3Fetch({ method, url, region, context }); 11 | await throwIfUnexpectedStatus(res, 204); 12 | } 13 | -------------------------------------------------------------------------------- /common/r2/delete_bucket_encryption.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type DeleteBucketEncryptionOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle }; 4 | 5 | export async function deleteBucketEncryption(opts: DeleteBucketEncryptionOpts, context: AwsCallContext): Promise { 6 | const { bucket, origin, region, urlStyle } = opts; 7 | const method = 'DELETE'; 8 | const url = computeBucketUrl({ origin, bucket, urlStyle, subresource: 'encryption' }); 9 | 10 | const res = await s3Fetch({ method, url, region, context }); 11 | await throwIfUnexpectedStatus(res, 200); // r2 bug: should be 204 12 | } 13 | -------------------------------------------------------------------------------- /common/r2/delete_object.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type DeleteObjectOpts = { bucket: string, key: string, origin: string, region: string, urlStyle?: UrlStyle, versionId?: string }; 4 | 5 | export async function deleteObject(opts: DeleteObjectOpts, context: AwsCallContext): Promise { 6 | const { bucket, key, origin, region, versionId, urlStyle } = opts; 7 | const method = 'DELETE'; 8 | const url = computeBucketUrl({ origin, bucket, key, urlStyle }); 9 | if (typeof versionId === 'string') url.searchParams.set('versionId', versionId); 10 | 11 | const res = await s3Fetch({ method, url, region, context }); 12 | await throwIfUnexpectedStatus(res, 204); // r2 returns 204 whether or not the key existed 13 | } 14 | -------------------------------------------------------------------------------- /common/r2/get_bucket_encryption.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedXmlNode, parseXml } from '../xml_parser.ts'; 2 | import { AwsCallContext, computeBucketUrl, R2, s3Fetch, S3_XMLNS, throwIfUnexpectedContentType, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 3 | import { KnownElement } from './known_element.ts'; 4 | 5 | export type GetBucketEncryptionOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle }; 6 | 7 | export async function getBucketEncryption(opts: GetBucketEncryptionOpts, context: AwsCallContext): Promise { 8 | const { bucket, origin, region, urlStyle } = opts; 9 | const method = 'GET'; 10 | const url = computeBucketUrl({ origin, bucket, subresource: 'encryption', urlStyle }); 11 | 12 | const res = await s3Fetch({ method, url, region, context }); 13 | await throwIfUnexpectedStatus(res, 200); 14 | 15 | const txt = await res.text(); 16 | if (R2.DEBUG) console.log(txt); 17 | 18 | throwIfUnexpectedContentType(res, 'application/xml', txt); 19 | 20 | const xml = parseXml(txt); 21 | return parseServerSideEncryptionConfigurationXml(xml); 22 | } 23 | 24 | // 25 | 26 | export interface ServerSideEncryptionConfiguration { 27 | readonly rules: readonly ServerSideEncryptionRule[]; 28 | } 29 | 30 | export interface ServerSideEncryptionRule { 31 | readonly applyServerSideEncryptionByDefault: ServerSideEncryptionByDefault; 32 | readonly bucketKeyEnabled: boolean; 33 | } 34 | 35 | export interface ServerSideEncryptionByDefault { 36 | readonly sseAlgorithm: string; // e.g. AES256 37 | } 38 | 39 | // 40 | 41 | function parseServerSideEncryptionConfigurationXml(xml: ExtendedXmlNode): ServerSideEncryptionConfiguration { 42 | const doc = new KnownElement(xml).checkTagName('!xml'); 43 | const rt = parseServerSideEncryptionConfiguration(doc.getKnownElement('ServerSideEncryptionConfiguration', { xmlns: S3_XMLNS })); 44 | doc.check(); 45 | return rt; 46 | } 47 | 48 | function parseServerSideEncryptionConfiguration(element: KnownElement): ServerSideEncryptionConfiguration { 49 | const rules = element.getKnownElements('Rule').map(parseServerSideEncryptionRule); 50 | element.check(); 51 | return { rules }; 52 | } 53 | 54 | function parseServerSideEncryptionRule(element: KnownElement): ServerSideEncryptionRule { 55 | const bucketKeyEnabled = element.getElementText('BucketKeyEnabled') === 'true'; 56 | const applyServerSideEncryptionByDefault = parseServerSideEncryptionByDefault(element.getKnownElement('ApplyServerSideEncryptionByDefault')); 57 | return { applyServerSideEncryptionByDefault, bucketKeyEnabled }; 58 | } 59 | 60 | function parseServerSideEncryptionByDefault(element: KnownElement): ServerSideEncryptionByDefault { 61 | const sseAlgorithm = element.getElementText('SSEAlgorithm'); 62 | return { sseAlgorithm }; 63 | } 64 | -------------------------------------------------------------------------------- /common/r2/get_bucket_location.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedXmlNode, parseXml } from '../xml_parser.ts'; 2 | import { AwsCallContext, computeBucketUrl, R2, s3Fetch, S3_XMLNS, throwIfUnexpectedContentType, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 3 | import { KnownElement } from './known_element.ts'; 4 | 5 | export type GetBucketLocationOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle }; 6 | 7 | export async function getBucketLocation(opts: GetBucketLocationOpts, context: AwsCallContext): Promise { 8 | const { bucket, origin, region, urlStyle } = opts; 9 | const method = 'GET'; 10 | const url = computeBucketUrl({ origin, bucket, subresource: 'location', urlStyle }); 11 | 12 | const res = await s3Fetch({ method, url, region, context }); 13 | await throwIfUnexpectedStatus(res, 200); 14 | 15 | const txt = await res.text(); 16 | if (R2.DEBUG) console.log(txt); 17 | 18 | throwIfUnexpectedContentType(res, 'application/xml', txt); 19 | 20 | const xml = parseXml(txt); 21 | return parseLocationConstraintXml(xml); 22 | } 23 | 24 | // 25 | 26 | export interface LocationConstraint { 27 | readonly locationConstraint?: string; 28 | } 29 | 30 | // 31 | 32 | function parseLocationConstraintXml(xml: ExtendedXmlNode): LocationConstraint { 33 | const doc = new KnownElement(xml).checkTagName('!xml'); 34 | const locationConstraint = doc.getOptionalElementText('LocationConstraint', { xmlns: S3_XMLNS }); 35 | doc.check(); 36 | return { locationConstraint }; 37 | } 38 | -------------------------------------------------------------------------------- /common/r2/get_head_object.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type GetObjectOpts = { bucket: string, key: string, origin: string, region: string, urlStyle?: UrlStyle, ifMatch?: string, ifNoneMatch?: string, ifModifiedSince?: string, ifUnmodifiedSince?: string, partNumber?: number, range?: string, acceptEncoding?: string, ssecAlgorithm?: string, ssecKey?: string, ssecKeyMd5?: string }; 4 | 5 | export async function getObject(opts: GetObjectOpts, context: AwsCallContext): Promise { 6 | return await getOrHeadObject('GET', opts, context); 7 | } 8 | 9 | export type HeadObjectOpts = GetObjectOpts; 10 | 11 | export async function headObject(opts: HeadObjectOpts, context: AwsCallContext): Promise { 12 | return await getOrHeadObject('HEAD', opts, context); 13 | } 14 | 15 | export function computeGetOrHeadObjectRequest(opts: GetObjectOpts | HeadObjectOpts): { url: URL, headers: Headers, region: string } { 16 | const { bucket, key, origin, region, urlStyle, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince, partNumber, range, acceptEncoding, ssecAlgorithm, ssecKey, ssecKeyMd5 } = opts; 17 | const url = computeBucketUrl({ origin, bucket, key, urlStyle }); 18 | const headers = new Headers(); 19 | if (typeof acceptEncoding === 'string') headers.set('accept-encoding', acceptEncoding); 20 | if (typeof ifMatch === 'string') headers.set('if-match', ifMatch); 21 | if (typeof ifNoneMatch === 'string') headers.set('if-none-match', ifNoneMatch); 22 | if (typeof ifModifiedSince === 'string') headers.set('if-modified-since', ifModifiedSince); 23 | if (typeof ifUnmodifiedSince === 'string') headers.set('if-unmodified-since', ifUnmodifiedSince); 24 | if (typeof range === 'string') headers.set('range', range); 25 | if (typeof partNumber === 'number') url.searchParams.set('partNumber', String(partNumber)); 26 | if (typeof ssecAlgorithm === 'string') headers.set('x-amz-server-side-encryption-customer-algorithm', ssecAlgorithm); 27 | if (typeof ssecKey === 'string') headers.set('x-amz-server-side-encryption-customer-key', ssecKey); 28 | if (typeof ssecKeyMd5 === 'string') headers.set('x-amz-server-side-encryption-customer-key-md5', ssecKeyMd5); 29 | return { url, headers, region }; 30 | } 31 | 32 | // 33 | 34 | async function getOrHeadObject(method: 'GET' | 'HEAD', opts: GetObjectOpts | HeadObjectOpts, context: AwsCallContext): Promise { 35 | const { url, headers, region } = computeGetOrHeadObjectRequest(opts); 36 | 37 | const res = await s3Fetch({ method, url, headers, region, context }); 38 | if (res.status === 404) return undefined; 39 | await throwIfUnexpectedStatus(res, 200, 304, 206); 40 | return res; 41 | } 42 | -------------------------------------------------------------------------------- /common/r2/head_bucket.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, s3Fetch, UrlStyle } from './r2.ts'; 2 | 3 | export type HeadBucketOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle }; 4 | 5 | export async function headBucket(opts: HeadBucketOpts, context: AwsCallContext): Promise { 6 | const { bucket, origin, region, urlStyle } = opts; 7 | const method = 'HEAD'; 8 | const url = computeBucketUrl({ origin, bucket, urlStyle }); 9 | 10 | const res = await s3Fetch({ method, url, region, context }); 11 | return res; 12 | } 13 | -------------------------------------------------------------------------------- /common/r2/list_buckets.ts: -------------------------------------------------------------------------------- 1 | import { ExtendedXmlNode, parseXml } from '../xml_parser.ts'; 2 | import { AwsCallContext, BucketResultOwner, parseBucketResultOwner, R2, s3Fetch, S3_XMLNS, throwIfUnexpectedContentType, throwIfUnexpectedStatus } from './r2.ts'; 3 | import { KnownElement } from './known_element.ts'; 4 | 5 | export type ListBucketsOpts = { origin: string, region: string }; 6 | 7 | export async function listBuckets(opts: ListBucketsOpts, context: AwsCallContext): Promise { 8 | const { origin, region } = opts; 9 | const method = 'GET'; 10 | const url = new URL(`${origin}/`); 11 | 12 | const res = await s3Fetch({ method, url, region, context }); 13 | await throwIfUnexpectedStatus(res, 200); 14 | 15 | const txt = await res.text(); 16 | if (R2.DEBUG) console.log(txt); 17 | 18 | throwIfUnexpectedContentType(res, 'application/xml', txt); 19 | 20 | const xml = parseXml(txt); 21 | return parseListBucketsResultXml(xml); 22 | } 23 | 24 | // 25 | 26 | export interface ListBucketsResult { 27 | readonly buckets: readonly ListBucketsBucketItem[]; 28 | readonly owner: BucketResultOwner; 29 | } 30 | 31 | export interface ListBucketsBucketItem { 32 | readonly name: string; 33 | readonly creationDate: string; 34 | } 35 | 36 | // 37 | 38 | function parseListBucketsResultXml(xml: ExtendedXmlNode): ListBucketsResult { 39 | const doc = new KnownElement(xml).checkTagName('!xml'); 40 | const rt = parseListBucketsResult(doc.getKnownElement('ListAllMyBucketsResult', { xmlns: S3_XMLNS })); 41 | doc.check(); 42 | return rt; 43 | } 44 | 45 | function parseListBucketsResult(element: KnownElement): ListBucketsResult { 46 | const owner = parseBucketResultOwner(element.getKnownElement('Owner')); 47 | const buckets = parseBuckets(element.getKnownElement('Buckets')); 48 | 49 | element.check(); 50 | return { owner, buckets }; 51 | } 52 | 53 | function parseBuckets(element: KnownElement): ListBucketsBucketItem[] { 54 | const rt = element.getKnownElements('Bucket').map(parseListBucketsBucketItem); 55 | element.check(); 56 | return rt; 57 | } 58 | 59 | function parseListBucketsBucketItem(element: KnownElement): ListBucketsBucketItem { 60 | const creationDate = element.getElementText('CreationDate'); 61 | const name = element.getElementText('Name'); 62 | 63 | element.check(); 64 | return { creationDate, name }; 65 | } 66 | -------------------------------------------------------------------------------- /common/r2/put_bucket_encryption.ts: -------------------------------------------------------------------------------- 1 | import { AwsCallContext, computeBucketUrl, R2, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 2 | 3 | export type PutBucketEncryptionOpts = { bucket: string, origin: string, region: string, urlStyle?: UrlStyle, sseAlgorithm: string, bucketKeyEnabled: boolean }; 4 | 5 | export async function putBucketEncryption(opts: PutBucketEncryptionOpts, context: AwsCallContext): Promise { 6 | const { bucket, sseAlgorithm, bucketKeyEnabled, origin, region, urlStyle } = opts; 7 | const method = 'PUT'; 8 | const url = computeBucketUrl({ origin, bucket, subresource: 'encryption', urlStyle }); 9 | 10 | const body = computePayload(sseAlgorithm, bucketKeyEnabled); 11 | if (R2.DEBUG) console.log(body); 12 | const res = await s3Fetch({ method, url, body, region, context }); 13 | await throwIfUnexpectedStatus(res, 200); 14 | 15 | const txt = await res.text(); 16 | if (R2.DEBUG) console.log(txt); 17 | } 18 | 19 | // 20 | 21 | const computePayload = (sseAlgorithm: string, bucketKeyEnabled: boolean) => // ` // R2 bug: this endpoint does not support an xml declaration! 22 | ` 23 | 24 | 25 | ${sseAlgorithm} 26 | 27 | ${bucketKeyEnabled ? 'true' : 'false'} 28 | 29 | `; 30 | -------------------------------------------------------------------------------- /common/r2/upload_part.ts: -------------------------------------------------------------------------------- 1 | import { Bytes } from '../bytes.ts'; 2 | import { AwsCallBody, AwsCallContext, computeBucketUrl, s3Fetch, throwIfUnexpectedStatus, UrlStyle } from './r2.ts'; 3 | 4 | export type UploadPartOpts = { bucket: string, key: string, uploadId: string, partNumber: number, body: AwsCallBody, origin: string, region: string, urlStyle?: UrlStyle, contentMd5?: string }; 5 | 6 | export async function uploadPart(opts: UploadPartOpts, context: AwsCallContext): Promise<{ etag: string }> { 7 | const { bucket, key, uploadId, partNumber, body, origin, region, contentMd5, urlStyle } = opts; 8 | const method = 'PUT'; 9 | const url = computeBucketUrl({ origin, bucket, key, urlStyle }); 10 | url.searchParams.set('uploadId', uploadId) 11 | url.searchParams.set('partNumber', String(partNumber)); 12 | 13 | const headers = new Headers(); 14 | if (typeof contentMd5 === 'string') headers.set('content-md5', contentMd5); 15 | 16 | if (typeof body !== 'string' && !(body instanceof Bytes)) { 17 | // required only for stream bodies 18 | headers.set('content-length', String(body.length)) 19 | } 20 | 21 | const res = await s3Fetch({ method, url, headers, body, region, context }); 22 | await throwIfUnexpectedStatus(res, 200); // r2 returns 200 with content-length: 0 23 | const contentLength = res.headers.get('content-length') || '0'; 24 | if (contentLength !== '0') throw new Error(`Expected empty response body to upload-part, found: ${await res.text()}`); 25 | const etag = res.headers.get('etag') || undefined; 26 | if (etag === undefined) throw new Error(`Expected ETag in the response headers to upload-part`); 27 | return { etag }; 28 | } 29 | -------------------------------------------------------------------------------- /common/script_worker_execution.ts: -------------------------------------------------------------------------------- 1 | import { defineScriptGlobals } from './cloudflare_workers_runtime.ts'; 2 | import { Binding } from './config.ts'; 3 | import { consoleLog, consoleWarn } from './console.ts'; 4 | import { IncomingRequestCf } from './cloudflare_workers_types.d.ts'; 5 | import { WorkerExecutionCallbacks } from './worker_execution.ts'; 6 | 7 | export class ScriptWorkerExecution { 8 | private readonly worker: ScriptWorker; 9 | 10 | private constructor(worker: ScriptWorker) { 11 | this.worker = worker; 12 | } 13 | 14 | static async create(scriptPath: string, bindings: Record, callbacks: WorkerExecutionCallbacks): Promise { 15 | const { globalCachesProvider, kvNamespaceProvider, doNamespaceProvider, r2BucketProvider, analyticsEngineProvider, d1DatabaseProvider, secretKeyProvider, emailSenderProvider, queueProvider } = callbacks; 16 | 17 | await defineScriptGlobals(bindings, globalCachesProvider, kvNamespaceProvider, doNamespaceProvider, r2BucketProvider, analyticsEngineProvider, d1DatabaseProvider, secretKeyProvider, emailSenderProvider, queueProvider); 18 | 19 | let fetchListener: EventListener | undefined; 20 | 21 | const addEventListener = (type: string, listener: EventListener) => { 22 | consoleLog(`script: addEventListener type=${type}`); 23 | if (type === 'fetch') { 24 | fetchListener = listener; 25 | } 26 | }; 27 | // deno-lint-ignore no-explicit-any 28 | (self as any).addEventListener = addEventListener; 29 | 30 | await import(scriptPath); 31 | 32 | if (fetchListener === undefined) throw new Error(`Script did not add a fetch listener`); 33 | return new ScriptWorkerExecution({ fetchListener }); 34 | } 35 | 36 | async fetch(request: IncomingRequestCf): Promise { 37 | const e = new FetchEvent(request); 38 | await this.worker.fetchListener(e); 39 | if (e.responseFn === undefined) throw new Error(`Event handler did not set a response using respondWith`); 40 | const response = await e.responseFn; 41 | return response; 42 | } 43 | } 44 | 45 | // 46 | 47 | interface ScriptWorker { 48 | readonly fetchListener: EventListener; 49 | } 50 | 51 | class FetchEvent extends Event { 52 | readonly request: Request; 53 | responseFn: Promise | undefined; 54 | 55 | constructor(request: Request) { 56 | super('fetch'); 57 | this.request = request; 58 | } 59 | 60 | waitUntil(promise: Promise) { 61 | // consoleLog('waitUntil', promise); 62 | promise.then(() => { 63 | // consoleLog(`waitUntil complete`); 64 | }, e => consoleWarn(e)); 65 | } 66 | 67 | respondWith(responseFn: Promise) { 68 | // consoleLog('respondWith', responseFn); 69 | if (this.responseFn) throw new Error(`respondWith: already called`); 70 | this.responseFn = responseFn; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /common/sets.ts: -------------------------------------------------------------------------------- 1 | export function setSubtract(lhs: ReadonlySet, rhs: ReadonlySet): Set { 2 | const rt = new Set(lhs); 3 | for (const item of rhs) { 4 | rt.delete(item); 5 | } 6 | return rt; 7 | } 8 | 9 | export function setUnion(lhs: ReadonlySet, rhs: ReadonlySet): Set { 10 | const rt = new Set(lhs); 11 | for (const item of rhs) { 12 | rt.add(item); 13 | } 14 | return rt; 15 | } 16 | 17 | export function setIntersect(lhs: ReadonlySet, rhs: ReadonlySet): Set { 18 | const rt = new Set(); 19 | for (const item of lhs) { 20 | if (rhs.has(item)) rt.add(item); 21 | } 22 | for (const item of rhs) { 23 | if (lhs.has(item)) rt.add(item); 24 | } 25 | return rt; 26 | } 27 | 28 | export function setEqual(lhs: ReadonlySet, rhs: ReadonlySet): boolean { 29 | return lhs.size === rhs.size && [...lhs].every(v => rhs.has(v)); 30 | } 31 | -------------------------------------------------------------------------------- /common/signal.ts: -------------------------------------------------------------------------------- 1 | export class Signal { 2 | readonly promise: Promise; 3 | 4 | private resolveFn?: (result: T) => void; 5 | private rejectFn?: (reason: unknown) => void; 6 | 7 | constructor() { 8 | this.promise = new Promise((resolve, reject) => { 9 | this.resolveFn = resolve; 10 | this.rejectFn = reject; 11 | }); 12 | } 13 | 14 | resolve(result: T) { 15 | this.resolveFn!(result); 16 | } 17 | 18 | reject(reason: unknown) { 19 | this.rejectFn!(reason); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /common/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number) { 2 | return new Promise(resolve => setTimeout(resolve, ms)); 3 | } 4 | 5 | export async function executeWithRetries(fn: () => Promise, opts: { tag: string, maxRetries: number, isRetryable: (e: Error) => boolean }): Promise { 6 | const { maxRetries, isRetryable, tag } = opts; 7 | 8 | let retries = 0; 9 | while (true) { 10 | try { 11 | if (retries > 0) { 12 | const waitMillis = retries * 1000; 13 | await sleep(waitMillis); 14 | } 15 | return await fn(); 16 | } catch (e) { 17 | if (isRetryable(e as Error)) { 18 | if (retries >= maxRetries) { 19 | throw new Error(`${tag}: Out of retries (max=${maxRetries}): ${(e as Error).stack || e}`); 20 | } 21 | retries++; 22 | } else { 23 | throw e; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /common/storage/in_memory_alarms.ts: -------------------------------------------------------------------------------- 1 | import { DurableObjectGetAlarmOptions, DurableObjectSetAlarmOptions } from '../cloudflare_workers_types.d.ts'; 2 | 3 | export class InMemoryAlarms { 4 | private readonly dispatchAlarm: () => void; 5 | 6 | // alarms not durable, kept in memory only 7 | private alarm: number | null = null; 8 | private alarmTimeoutId = 0; 9 | 10 | constructor(dispatchAlarm: () => void) { 11 | this.dispatchAlarm = dispatchAlarm; 12 | } 13 | 14 | getAlarm(options: DurableObjectGetAlarmOptions = {}): Promise { 15 | const { allowConcurrency } = options; 16 | if (allowConcurrency !== undefined) throw new Error(`InMemoryAlarms.getAlarm(allowConcurrency) not implemented: options=${JSON.stringify(options)}`); 17 | return Promise.resolve(this.alarm); 18 | } 19 | 20 | setAlarm(scheduledTime: number | Date, options: DurableObjectSetAlarmOptions = {}): Promise { 21 | const { allowUnconfirmed } = options; 22 | if (allowUnconfirmed !== undefined) throw new Error(`InMemoryAlarms.setAlarm(allowUnconfirmed) not implemented: options=${JSON.stringify(options)}`); 23 | this.alarm = Math.max(Date.now(), typeof scheduledTime === 'number' ? scheduledTime : scheduledTime.getTime()); 24 | this.rescheduleAlarm(); 25 | return Promise.resolve(); 26 | } 27 | 28 | deleteAlarm(options: DurableObjectSetAlarmOptions = {}): Promise { 29 | const { allowUnconfirmed } = options; 30 | if (allowUnconfirmed !== undefined) throw new Error(`InMemoryAlarms.deleteAlarm(allowUnconfirmed) not implemented: options=${JSON.stringify(options)}`); 31 | this.alarm = null; 32 | this.rescheduleAlarm(); 33 | return Promise.resolve(); 34 | } 35 | 36 | // 37 | 38 | private rescheduleAlarm() { 39 | clearTimeout(this.alarmTimeoutId); 40 | if (typeof this.alarm === 'number') { 41 | this.alarmTimeoutId = setTimeout(() => { 42 | this.alarm = null; 43 | this.dispatchAlarm(); 44 | }, Math.max(0, this.alarm - Date.now())); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /common/storage/sqlite_durable_object_storage_test.ts: -------------------------------------------------------------------------------- 1 | import { runSimpleStorageTestScenario } from './durable_object_storage_test.ts'; 2 | import { SqliteDurableObjectStorage } from './sqlite_durable_object_storage.ts'; 3 | 4 | Deno.test({ 5 | name: 'SqliteDurableObjectStorage', 6 | ignore: (await Deno.permissions.query({ name: 'net' })).state !== 'granted', 7 | async fn() { 8 | const storage = await SqliteDurableObjectStorage.provider('class1', 'id1', {}, () => {}, () => ':memory:'); 9 | await runSimpleStorageTestScenario(storage); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /common/supabase/eszip.ts: -------------------------------------------------------------------------------- 1 | import { build, Parser } from 'https://deno.land/x/eszip@v0.57.0/mod.ts'; 2 | import { compress } from 'https://deno.land/x/brotli@0.1.7/mod.ts'; 3 | import { concat } from 'https://deno.land/std@0.224.0/bytes/concat.ts'; 4 | 5 | export async function buildEszip(roots: string[], contentFn: (spec: string) => Promise | string | undefined): Promise { 6 | return await build(roots, async specifier => { 7 | const content = await contentFn(specifier); 8 | if (content) { 9 | return { 10 | kind: 'module', 11 | content, 12 | specifier, 13 | }; 14 | } 15 | return undefined; 16 | }); 17 | } 18 | 19 | export type EszipEntry = { specifier: string, source: string, sourceMap?: string }; 20 | 21 | export async function parseEszip(bytes: Uint8Array): Promise { 22 | const parser = await Parser.createInstance(); 23 | const rt: EszipEntry[] = []; 24 | try { 25 | const specifiers = await parser.parseBytes(bytes) as string[]; 26 | await parser.load(); 27 | for (const specifier of specifiers) { 28 | const source = await parser.getModuleSource(specifier) as string; 29 | const sourceMap = await parser.getModuleSourceMap(specifier) as string | undefined; 30 | rt.push({ specifier, source, sourceMap }); 31 | } 32 | } finally { 33 | parser.free(); 34 | } 35 | return rt; 36 | } 37 | 38 | export function brotliCompressEszip(uncompressedEszip: Uint8Array): Uint8Array { 39 | const compressed = compress(uncompressedEszip); 40 | return concat([ new TextEncoder().encode('EZBR'), compressed ]); 41 | } 42 | -------------------------------------------------------------------------------- /common/supabase/supabase_app.ts: -------------------------------------------------------------------------------- 1 | const isolateId = crypto.randomUUID().split('-').pop()!; 2 | console.log(`${isolateId}: new isolate`); 3 | 4 | const { mod = {}, err, millis } = await (async () => { 5 | const start = Date.now(); 6 | try { 7 | const { default: mod } = await import('./worker.ts'); 8 | return { mod, millis: Date.now() - start }; 9 | } catch (e) { 10 | const err = `${e.stack || e}`; 11 | return { err, millis: Date.now() - start }; 12 | } 13 | })(); 14 | console.log(`${isolateId}: ${err ? `Failed to import` : 'Successfully imported'} worker module in ${millis}ms`); 15 | 16 | const SCRIPT_NAME = '${scriptName}'; 17 | const scriptNamePrefix = `${SCRIPT_NAME}-`; 18 | const env = Object.fromEntries(Object.entries({ ...Deno.env.toObject(), SCRIPT_NAME }).map(([ name, value ]) => [ name.startsWith(scriptNamePrefix) ? name.substring(scriptNamePrefix.length) : name, value ])); 19 | 20 | Deno.serve(async (req) => { 21 | try { 22 | if (err) throw new Error(`Failed to import worker module: ${err}`); 23 | 24 | const { fetch } = mod; 25 | if (typeof fetch !== 'function') throw new Error(`Worker module 'fetch' function not found: module keys: ${JSON.stringify(Object.keys(mod))}`); 26 | 27 | const context = { 28 | waitUntil: (_promise: Promise): void => { 29 | // assumes Supabase waits for all background promises 30 | } 31 | }; 32 | const { origin } = new URL(req.url); 33 | let url = req.url; 34 | if (url.startsWith(origin)) { 35 | url = `${env.SUPABASE_URL}/functions/v1${url.substring(origin.length)}`; 36 | } 37 | const workerReq = new Request(url, req); 38 | // deno-lint-ignore no-explicit-any 39 | (workerReq as any).cf = { colo: env.SB_REGION }; 40 | return await fetch(workerReq, env, context); 41 | } catch (e) { 42 | return new Response(`${e.stack || e}`, { status: 500 }); 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /common/supabase/supabase_import_template.js: -------------------------------------------------------------------------------- 1 | function base64Decode(str) { 2 | str = atob(str); 3 | const 4 | length = str.length, 5 | buf = new ArrayBuffer(length), 6 | bufView = new Uint8Array(buf); 7 | for (let i = 0; i < length; i++) { bufView[i] = str.charCodeAt(i) } 8 | return bufView; 9 | } 10 | 11 | export const BYTES = base64Decode('EXPORT_B64');; 12 | -------------------------------------------------------------------------------- /common/uint8array_.ts: -------------------------------------------------------------------------------- 1 | // typescript 5.7, es2024 (and deno 2.2) introduced a redefinition of Uint8Array that is extremely annoying 2 | // this is a clever utility type used by std that works in both 5.7+ and below 3 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-7.html#support-for---target-es2024-and---lib-es2024 4 | // https://github.com/denoland/deno/pull/27857 5 | // https://github.com/denoland/std/pull/6372/files 6 | 7 | export type Uint8Array_ = ReturnType; 8 | -------------------------------------------------------------------------------- /common/unimplemented_cloudflare_stubs.ts: -------------------------------------------------------------------------------- 1 | import { DurableObjectNamespace, DurableObjectId, DurableObjectStub } from './cloudflare_workers_types.d.ts'; 2 | 3 | export class UnimplementedDurableObjectNamespace implements DurableObjectNamespace { 4 | readonly doNamespace: string; 5 | 6 | constructor(doNamespace: string) { 7 | this.doNamespace = doNamespace; 8 | } 9 | 10 | newUniqueId(_opts?: { jurisdiction: 'eu' }): DurableObjectId { 11 | throw new Error(`UnimplementedDurableObjectNamespace.newUniqueId not implemented.`); 12 | } 13 | 14 | idFromName(_name: string): DurableObjectId { 15 | throw new Error(`UnimplementedDurableObjectNamespace.idFromName not implemented.`); 16 | } 17 | 18 | idFromString(_hexStr: string): DurableObjectId { 19 | throw new Error(`UnimplementedDurableObjectNamespace.idFromString not implemented.`); 20 | } 21 | 22 | get(_id: DurableObjectId): DurableObjectStub { 23 | throw new Error(`UnimplementedDurableObjectNamespace.get not implemented.`); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /common/uuid_v4.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. 2 | 3 | // inline just the v4 implementation to cut down on bundle size 4 | 5 | const UUID_RE = 6 | /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 7 | 8 | /** 9 | * Validates the UUID v4. 10 | * @param id UUID value. 11 | */ 12 | export function validateUuid(id: string): boolean { 13 | return UUID_RE.test(id); 14 | } 15 | 16 | /** Generates a RFC4122 v4 UUID (pseudo-randomly-based) */ 17 | export function generateUuid(): string { 18 | // https://wicg.github.io/uuid/ 19 | // deno-lint-ignore no-explicit-any 20 | const cryptoAsAny = crypto as any; 21 | if (typeof cryptoAsAny.randomUUID === 'function') { 22 | return cryptoAsAny.randomUUID(); 23 | } 24 | 25 | const rnds = crypto.getRandomValues(new Uint8Array(16)); 26 | 27 | rnds[6] = (rnds[6] & 0x0f) | 0x40; // Version 4 28 | rnds[8] = (rnds[8] & 0x3f) | 0x80; // Variant 10 29 | 30 | return bytesToUuid(rnds); 31 | } 32 | 33 | /** 34 | * Converts the byte array to a UUID string 35 | * @param bytes Used to convert Byte to Hex 36 | */ 37 | function bytesToUuid(bytes: number[] | Uint8Array): string { 38 | const bits = [...bytes].map((bit) => { 39 | const s = bit.toString(16); 40 | return bit < 0x10 ? "0" + s : s; 41 | }); 42 | return [ 43 | ...bits.slice(0, 4), 44 | "-", 45 | ...bits.slice(4, 6), 46 | "-", 47 | ...bits.slice(6, 8), 48 | "-", 49 | ...bits.slice(8, 10), 50 | "-", 51 | ...bits.slice(10, 16), 52 | ].join(""); 53 | } 54 | -------------------------------------------------------------------------------- /common/xml_parser_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts'; 2 | import { parseXml } from './xml_parser.ts'; 3 | 4 | Deno.test({ 5 | name: 'parseXml', 6 | fn: () => { 7 | // workaround for valid xml that fast-xml does not handle 8 | const n = parseXml(`\n`); 9 | assertEquals(Object.keys(n.child).length, 1); 10 | assertEquals(Object.values(n.child)[0][0].tagname, 'root'); 11 | assertEquals(Object.values(n.child)[0][0].attrsMap, { '@_xmlns:name2': 'https://example.com/namespace/1.0' }); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /common/xml_util.ts: -------------------------------------------------------------------------------- 1 | export function encodeXml(unencoded: string): string { 2 | return unencoded.replaceAll(/[&<>'"]/g, (char) => { 3 | return UNENCODED_CHARS_TO_ENTITIES[char]; 4 | }); 5 | } 6 | 7 | export function decodeXml(encoded: string, additionalEntities: { [char: string]: string } = {}): string { 8 | return encoded.replaceAll(/&(#(\d+)|[a-z]+);/g, (str, entity, decimal) => { 9 | if (typeof decimal === 'string') return String.fromCharCode(parseInt(decimal)); 10 | if (typeof entity === 'string') { 11 | const additional = additionalEntities[entity]; 12 | if (additional) return additional; 13 | const rt = ENTITIES_TO_UNENCODED_CHARS[entity]; 14 | if (rt) return rt; 15 | } 16 | throw new Error(`Unsupported entity: ${str}`); 17 | }); 18 | } 19 | 20 | // 21 | 22 | const UNENCODED_CHARS_TO_ENTITIES: { [char: string]: string } = { 23 | '<': '<', 24 | '>': '>', 25 | '&': '&', 26 | '\'': ''', // ''' is shorter than ''' 27 | '"': '"', // '"' is shorter than '"' 28 | }; 29 | 30 | const ENTITIES_TO_UNENCODED_CHARS: { [char: string]: string } = { 31 | 'lt': '<', 32 | 'gt': '>', 33 | 'amp': '&', 34 | 'apos': `'`, 35 | 'quot': '"', 36 | }; 37 | -------------------------------------------------------------------------------- /common/xml_util_test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts'; 2 | 3 | import { decodeXml, encodeXml } from './xml_util.ts'; 4 | 5 | Deno.test({ 6 | name: 'decodeXml', 7 | fn: () => { 8 | assertEquals(decodeXml('a'b'), `a'b`); 9 | assertEquals(decodeXml(encodeXml(`a<>'"&b`)), `a<>'"&b`); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /denoflare.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "common" 5 | }, 6 | { 7 | "path": "cli" 8 | }, 9 | { 10 | "path": "cli-webworker" 11 | }, 12 | { 13 | "path": "devcli" 14 | }, 15 | { 16 | "path": "examples/webtail-worker", 17 | "name": "examples/webtail-worker" 18 | }, 19 | { 20 | "path": "examples/webtail-app", 21 | "name": "examples/webtail-app" 22 | }, 23 | { 24 | "path": "examples/hello-worker", 25 | "name": "examples/hello-worker" 26 | }, 27 | { 28 | "path": "examples/hello-wasm-worker", 29 | "name": "examples/hello-wasm-worker" 30 | }, 31 | { 32 | "path": "examples/tiered-websockets-worker", 33 | "name": "examples/tiered-websockets-worker" 34 | }, 35 | { 36 | "path": "examples/r2-public-read-worker", 37 | "name": "examples/r2-public-read-worker" 38 | }, 39 | { 40 | "path": "examples/r2-presigned-url-worker", 41 | "name": "examples/r2-presigned-url-worker" 42 | }, 43 | { 44 | "path": "examples/mqtt-demo-worker", 45 | "name": "examples/mqtt-demo-worker" 46 | }, 47 | { 48 | "path": "examples/multiplat-worker", 49 | "name": "examples/multiplat-worker" 50 | }, 51 | { 52 | "path": "examples/keyspace-worker", 53 | "name": "examples/keyspace-worker" 54 | }, 55 | { 56 | "path": "examples/raw-worker", 57 | "name": "examples/raw-worker" 58 | }, 59 | { 60 | "path": "npm/denoflare-mqtt", 61 | "name": "npm/denoflare-mqtt" 62 | } 63 | ], 64 | "settings": { 65 | "deno.lint": true, 66 | "deno.unstable": true, 67 | "deno.suggest.imports.hosts": { 68 | "https://github.com": false, 69 | "https://deno.land": false, 70 | "https://cdn.skypack.dev": false, 71 | "https://raw.githubusercontent.com": false 72 | }, 73 | "deno.enable": true 74 | } 75 | } -------------------------------------------------------------------------------- /devcli/deps.ts: -------------------------------------------------------------------------------- 1 | export * from '../cli/deps_cli.ts'; 2 | -------------------------------------------------------------------------------- /devcli/devcli.ts: -------------------------------------------------------------------------------- 1 | import { CliCommand } from '../cli/cli_command.ts'; 2 | import { parseFlags } from '../cli/flag_parser.ts'; 3 | import { auth, AUTH_COMMAND } from './devcli_auth.ts'; 4 | import { generateNpm } from './devcli_generate_npm.ts'; 5 | import { generateReasonCodes } from './devcli_pubsub.ts'; 6 | import { tmp as r2Tmp } from './devcli_r2.ts'; 7 | import { regenerateDocs, REGENERATE_DOCS_COMMAND } from './devcli_regenerate_docs.ts'; 8 | 9 | const { args, options } = parseFlags(Deno.args); 10 | 11 | export const DENOFLAREDEV_COMMAND = CliCommand.of(['denoflaredev']) 12 | .subcommand(AUTH_COMMAND, auth) 13 | .subcommand(REGENERATE_DOCS_COMMAND, regenerateDocs) 14 | ; 15 | 16 | if (import.meta.main) { 17 | await DENOFLAREDEV_COMMAND.routeSubcommand(args, options, { generateNpm, generateReasonCodes, r2Tmp }); 18 | } 19 | -------------------------------------------------------------------------------- /devcli/devcli_pubsub.ts: -------------------------------------------------------------------------------- 1 | export async function generateReasonCodes(_args: (string | number)[], _options: Record): Promise { 2 | // await generateReasonCodesForType('Disconnect Reason Code values'); 3 | // await generateReasonCodesForType('Connect Reason Code values'); 4 | await generateReasonCodesForType('Subscribe Reason Codes'); 5 | } 6 | 7 | // 8 | 9 | async function generateReasonCodesForType(type: string) { 10 | const despan = (v: string) => { 11 | const m = />(.*?) v.replaceAll(/\s+/gs, ' ').trim(); 15 | const res = await fetch('https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html'); 16 | const text = await res.text(); 17 | let m = new RegExp(type + '

\\s+()', 'si').exec(text); 18 | if (m) { 19 | const table = m[1]; 20 | const pattern = /(.*?)<\/p>.*?

(.*?)<\/p>.*?

(.*?)<\/p>.*?

(.*?)<\/p>.*?<\/tr>/gs; 21 | while (null != (m = pattern.exec(table))) { 22 | const codeStr = despan(m[1]); 23 | const name = fixWhitespace(despan(m[3].trim())); 24 | const description = fixWhitespace(m[4]); 25 | const code = parseInt(codeStr); 26 | console.log(` ${code}: [ '${name}', '${description}' ],`); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /devcli/devcli_r2.ts: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import { ApiR2Bucket } from '../cli/api_r2_bucket.ts'; 4 | import { CLI_USER_AGENT } from '../cli/cli_common.ts'; 5 | import { loadConfig, resolveProfile } from '../cli/config_loader.ts'; 6 | import { R2 } from '../common/r2/r2.ts'; 7 | 8 | export async function tmp(args: (string | number)[], options: Record) { 9 | const [ bucketName, key ] = args; 10 | if (typeof bucketName !== 'string') throw new Error(); 11 | if (typeof key !== 'string') throw new Error(); 12 | 13 | const verbose = !!options.verbose; 14 | if (verbose) { 15 | R2.DEBUG = true; 16 | } 17 | 18 | const config = await loadConfig(options); 19 | const profile = await resolveProfile(config, options); 20 | const bucket = await ApiR2Bucket.ofProfile(profile, bucketName, CLI_USER_AGENT); 21 | const { body } = await fetch('https://yahoo.com'); 22 | const res = await bucket.put(key, body); 23 | console.log(res); 24 | // if (res) console.log(await res.text()); 25 | } 26 | -------------------------------------------------------------------------------- /devcli/tsc.ts: -------------------------------------------------------------------------------- 1 | import { walk } from 'https://deno.land/std@0.224.0/fs/mod.ts'; // isolated for sharing 2 | import { spawn } from '../cli/spawn.ts'; 3 | 4 | export type TscResult = { code: number, success: boolean, out: string, err: string, output: Record }; 5 | 6 | export async function runTsc(opts: { files: string[], compilerOptions: Record, tscPath?: string }): Promise { 7 | const { files, compilerOptions, tscPath = '/usr/local/bin/tsc' } = opts; 8 | const tsconfigFile = await Deno.makeTempFile({ prefix: 'run-tsc-tsconfig', suffix: '.json'}); 9 | const outDir = await Deno.makeTempDir({ prefix: 'run-tsc-outdir', suffix: '.json'}); 10 | compilerOptions.outDir = outDir; 11 | try { 12 | await Deno.writeTextFile(tsconfigFile, JSON.stringify({ files, compilerOptions }, undefined, 2)); 13 | const { code, success, stdout, stderr } = await spawn(tscPath, { 14 | args: [ 15 | '--project', tsconfigFile, 16 | ], 17 | env: { 18 | NO_COLOR: '1', // to make parsing the output easier 19 | } 20 | }); 21 | const out = new TextDecoder().decode(stdout); 22 | const err = new TextDecoder().decode(stderr); 23 | const output: Record = {}; 24 | for await (const entry of walk(outDir, { maxDepth: 1 })) { 25 | if (!entry.isFile) continue; 26 | const { name, path } = entry; 27 | output[name] = await Deno.readTextFile(path); 28 | } 29 | return { code, success, out, err, output }; 30 | } finally { 31 | await Deno.remove(tsconfigFile); 32 | await Deno.remove(outDir, { recursive: true }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/hello-wasm-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export { importWasm } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_wasm.ts'; 2 | -------------------------------------------------------------------------------- /examples/hello-wasm-worker/hello.ts: -------------------------------------------------------------------------------- 1 | import { importWasm } from './deps.ts'; 2 | import { callSub } from './sub/sub.ts'; // also works with relative module specifiers within relative imports 3 | const module = await importWasm(import.meta.url, './hello.wasm'); // rewritten to: import module from './hello.wasm'; 4 | 5 | export default { 6 | 7 | fetch(): Response { 8 | try { 9 | // call hello.wasm 10 | const instance = new WebAssembly.Instance(module); 11 | const result = (instance.exports.main as CallableFunction)(); 12 | 13 | // call hello2.wasm 14 | const subresult = callSub(); 15 | 16 | const html = `

Result from hello.wasm call: ${result}

Result from hello2.wasm call: ${subresult}

`; 17 | return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); 18 | } catch (e) { 19 | const html = `${e}`; 20 | return new Response(html, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); 21 | } 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /examples/hello-wasm-worker/hello.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/hello-wasm-worker/hello.wasm -------------------------------------------------------------------------------- /examples/hello-wasm-worker/sub/hello2.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/hello-wasm-worker/sub/hello2.wasm -------------------------------------------------------------------------------- /examples/hello-wasm-worker/sub/sub.ts: -------------------------------------------------------------------------------- 1 | import { importWasm } from '../deps.ts'; 2 | const module = await importWasm(import.meta.url, './hello2.wasm'); // rewritten to: import module from './sub/hello2.wasm'; 3 | 4 | export function callSub(): number { 5 | // call hello2.wasm 6 | const instance = new WebAssembly.Instance(module); 7 | return (instance.exports.main as CallableFunction)(); 8 | } 9 | -------------------------------------------------------------------------------- /examples/hello-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 2 | -------------------------------------------------------------------------------- /examples/hello-worker/hello.ts: -------------------------------------------------------------------------------- 1 | import { IncomingRequestCf } from './deps.ts'; 2 | 3 | // simplest possible module worker, echo back the client's city, as determined by Cloudflare 4 | export default { 5 | 6 | fetch(request: IncomingRequestCf): Response { 7 | const html = `

Hello ${'city' in request.cf ? request.cf.city : 'nowhere'}!

`; 8 | return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); 9 | } 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /examples/image-demo-worker/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": true 5 | } -------------------------------------------------------------------------------- /examples/image-demo-worker/404.ts: -------------------------------------------------------------------------------- 1 | export function compute404() { 2 | const title = 'Not found'; 3 | const body = ` 4 | 5 |
6 |

Not found

7 |
8 | `; 9 | return { title, body }; 10 | } 11 | -------------------------------------------------------------------------------- /examples/image-demo-worker/README.md: -------------------------------------------------------------------------------- 1 | # Transform Images in a Cloudflare Worker 2 | 3 | This example Cloudflare Worker fetches an image, applies transforms from the [Photon](https://silvia-odwyer.github.io/photon/) wasm library, then encodes to PNG using wasm from the [pngs](https://github.com/denosaurs/pngs) deno module. 4 | 5 | You can deploy the [module worker](worker.ts) to your own Cloudflare account by adding the script def to your [`.denoflare`](https://denoflare.dev/cli/configuration) file... 6 | 7 | ```jsonc 8 | "image-demo": { 9 | "path": "/path/to/this/image-demo-worker/worker.ts", 10 | "bindings": { 11 | "unsplashAppName": { "value": "image_demo_worker" }, 12 | "unsplashIxid": { "value": "MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8" }, 13 | }, 14 | "usageModel": "unbound", 15 | }, 16 | ``` 17 | ... then pushing to Cloudflare 18 | ```sh 19 | denoflare push image-demo 20 | ``` 21 | 22 | It runs as an `unbundled` worker, but you will eventually hit the Workers CPU limits with some of the transforms, so it's recommended to use `unbound` as the usage model. 23 | 24 | The size of the uploaded script + favicons/metadata images + all wasm modules is 734kb compressed. 25 | 26 | --- 27 | ## WebAssembly notes 28 | This worker uses Denoflare's new [WebAssembly import support](https://denoflare.dev/reference/wasm), the unmodified .wasm files from their respective third party libraries, and slightly tweaked js binding files (see the `/ext` subdir). 29 | 30 | The WebAssembly libraries are loaded and used in the [`/img`](img.ts) endpoint. 31 | -------------------------------------------------------------------------------- /examples/image-demo-worker/app_manifest.d.ts: -------------------------------------------------------------------------------- 1 | export interface AppManifest { 2 | /** 3 | * https://w3c.github.io/manifest/#short_name-member 4 | * maximum of 12 characters recommended per https://developer.chrome.com/extensions/manifest/name 5 | * used: android launcher icon title 6 | */ 7 | 'short_name': string, 8 | 9 | /** 10 | * https://w3c.github.io/manifest/#name-member 11 | * maximum of 45 characters per https://developer.chrome.com/extensions/manifest/name 12 | * used: app install banner android offline splash screen 13 | */ 14 | name: string, 15 | 16 | /** 17 | * https://w3c.github.io/manifest/#description-member 18 | * used ?? 19 | */ 20 | description: string, 21 | 22 | /** 23 | * https://w3c.github.io/manifest/#icons-member 24 | */ 25 | icons: readonly AppManifestIcon[], 26 | 27 | /** 28 | * https://w3c.github.io/manifest/#theme_color-member 29 | */ 30 | 'theme_color': string, 31 | 32 | /** 33 | * https://w3c.github.io/manifest/#background_color-member 34 | * should match page, also used for splash screen 35 | */ 36 | 'background_color': string, 37 | 38 | /** 39 | * https://w3c.github.io/manifest/#display-member 40 | */ 41 | display: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser', 42 | 43 | /** 44 | * https://w3c.github.io/manifest/#start_url-member 45 | */ 46 | 'start_url': string, 47 | 48 | /** 49 | * https://w3c.github.io/manifest/#lang-member 50 | */ 51 | lang: 'en-US', 52 | 53 | /** 54 | * https://w3c.github.io/manifest/#dir-member 55 | */ 56 | dir: 'ltr', 57 | 58 | /** 59 | * https://w3c.github.io/manifest/#scope-member 60 | */ 61 | scope?: string, 62 | 63 | } 64 | 65 | export interface AppManifestIcon { 66 | 67 | /** 68 | * url to image resource 69 | */ 70 | src: string; 71 | 72 | /** 73 | * e.g. "512x512", or "16x16 32x32 48x48" 74 | * 75 | * https://w3c.github.io/manifest/#declaring-multiple-icons 76 | */ 77 | sizes?: string; 78 | 79 | /** 80 | * MIME type 81 | * 82 | * https://w3c.github.io/manifest/#declaring-multiple-icons 83 | */ 84 | type?: string; 85 | 86 | /** 87 | * https://w3c.github.io/manifest/#purpose-member 88 | */ 89 | purpose?: 'monochrome' | 'maskable' | 'any'; 90 | } 91 | -------------------------------------------------------------------------------- /examples/image-demo-worker/cli.ts: -------------------------------------------------------------------------------- 1 | import { parseFlags } from './deps_cli.ts'; 2 | import { computeTransforms } from './cli_transform_generator.ts'; 3 | 4 | const args = parseFlags(Deno.args); 5 | 6 | if (args._.length > 0) { 7 | await imageDemo(args._, args); 8 | Deno.exit(0); 9 | } 10 | 11 | dumpHelp(); 12 | 13 | Deno.exit(1); 14 | 15 | // 16 | 17 | async function imageDemo(args: (string | number)[], options: Record) { 18 | const command = args[0]; 19 | const fn = { dumpTransforms, unsplash }[command]; 20 | if (options.help || !fn) { 21 | dumpHelp(); 22 | return; 23 | } 24 | await fn(args.slice(1), options); 25 | } 26 | 27 | async function dumpTransforms(_args: (string | number)[]) { 28 | const transforms = await computeTransforms(); 29 | console.log(JSON.stringify(transforms, undefined, 2)); 30 | } 31 | 32 | async function unsplash(args: (string | number)[], options: Record) { 33 | const photoId = args[0]; 34 | if (typeof photoId !== 'string') throw new Error(`Must provide photo id arg`); 35 | 36 | const accessKeyId = options['access-key-id']; 37 | if (typeof accessKeyId !== 'string') throw new Error(`Must provide --access-key-id`); 38 | const res = await fetch(`https://api.unsplash.com/photos/${photoId}`, { headers: { authorization: `Client-ID ${accessKeyId}`} }); 39 | if (res.status !== 200 || res.headers.get('content-type') !== 'application/json') { 40 | console.log(res); 41 | console.log(await res.text()); 42 | return; 43 | } 44 | const obj = await res.json(); 45 | console.log(JSON.stringify(obj, undefined, 2)); 46 | } 47 | 48 | function dumpHelp() { 49 | const lines = [ 50 | `image-demo-cli`, 51 | 'Tools for developing image-demo', 52 | '', 53 | 'USAGE:', 54 | '', 55 | 'FLAGS:', 56 | ' -h, --help Prints help information', 57 | ' --verbose Toggle verbose output (when applicable)', 58 | '', 59 | 'ARGS:', 60 | ]; 61 | for (const line of lines) { 62 | console.log(line); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/image-demo-worker/deps_cli.ts: -------------------------------------------------------------------------------- 1 | export { parse as parseFlags } from 'https://deno.land/std@0.179.0/flags/mod.ts'; 2 | -------------------------------------------------------------------------------- /examples/image-demo-worker/deps_worker.ts: -------------------------------------------------------------------------------- 1 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts'; 2 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 3 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts'; 4 | export { importBinary } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_binary.ts'; 5 | export { importWasm } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_wasm.ts'; 6 | -------------------------------------------------------------------------------- /examples/image-demo-worker/ext/photon_rs_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/ext/photon_rs_bg.wasm -------------------------------------------------------------------------------- /examples/image-demo-worker/ext/pngs.ts: -------------------------------------------------------------------------------- 1 | // adapted from https://deno.land/x/pngs@0.1.1/mod.ts 2 | 3 | /* OLD 4 | import init, { 5 | decode as wasmDecode, 6 | encode as wasmEncode, 7 | source, 8 | } from "./wasm.js"; 9 | 10 | await init(source); 11 | */ 12 | 13 | // NEW 14 | 15 | import { importWasm } from '../deps_worker.ts'; 16 | const pngsModule = await importWasm(import.meta.url, './pngs_bg.wasm'); 17 | import pngsInit, { decode as wasmDecode, encode as wasmEncode } from './pngs_bg.js'; 18 | await pngsInit(pngsModule); 19 | 20 | // 21 | 22 | type ValueOf = T[keyof T]; 23 | 24 | export const ColorType = { 25 | Grayscale: 0, 26 | RGB: 2, 27 | Indexed: 3, 28 | GrayscaleAlpha: 4, 29 | RGBA: 6, 30 | }; 31 | 32 | export const BitDepth = { 33 | One: 1, 34 | Two: 2, 35 | Four: 4, 36 | Eight: 8, 37 | Sixteen: 16, 38 | }; 39 | 40 | export const Compression = { 41 | Default: 0, 42 | Fast: 1, 43 | Best: 2, 44 | Huffman: 3, 45 | Rle: 4, 46 | }; 47 | 48 | export const FilterType = { 49 | NoFilter: 0, 50 | Sub: 1, 51 | Up: 2, 52 | Avg: 3, 53 | Paeth: 4, 54 | }; 55 | 56 | export interface DecodeResult { 57 | image: Uint8Array; 58 | width: number; 59 | height: number; 60 | colorType: ValueOf; 61 | bitDepth: ValueOf; 62 | lineSize: number; 63 | } 64 | 65 | export function encode( 66 | image: Uint8Array, 67 | width: number, 68 | height: number, 69 | options?: { 70 | palette?: Uint8Array; 71 | trns?: Uint8Array; 72 | color?: ValueOf; 73 | depth?: ValueOf; 74 | compression?: ValueOf; 75 | filter?: ValueOf; 76 | stripAlpha?: boolean; 77 | }, 78 | ): Uint8Array { 79 | if (options?.stripAlpha) { 80 | image = image.filter((_, i) => (i + 1) % 4); 81 | } 82 | 83 | return wasmEncode( 84 | image, 85 | width, 86 | height, 87 | options?.palette, 88 | options?.trns, 89 | options?.color ?? ColorType.RGBA, 90 | options?.depth ?? BitDepth.Eight, 91 | options?.compression, 92 | options?.filter, 93 | ); 94 | } 95 | 96 | export function decode(image: Uint8Array): DecodeResult { 97 | const res = wasmDecode(image); 98 | 99 | return { 100 | image: new Uint8Array(res.image), 101 | width: res.width, 102 | height: res.height, 103 | colorType: res.colorType, 104 | bitDepth: res.bitDepth, 105 | lineSize: res.lineSize, 106 | }; 107 | } -------------------------------------------------------------------------------- /examples/image-demo-worker/ext/pngs_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/ext/pngs_bg.wasm -------------------------------------------------------------------------------- /examples/image-demo-worker/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/favicon.ico -------------------------------------------------------------------------------- /examples/image-demo-worker/favicons.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from './theme.ts'; 2 | import { importBinary } from './deps_worker.ts'; 3 | 4 | export const FAVICON_SVG = ` 5 | 6 | 7 | 8 | 9 | `; 10 | 11 | export const FAVICON_VERSION = '2'; 12 | 13 | export const FAVICON_ICO = await importBinary(import.meta.url, './favicon.ico'); 14 | -------------------------------------------------------------------------------- /examples/image-demo-worker/html.ts: -------------------------------------------------------------------------------- 1 | export function encodeHtml(value: string): string { 2 | return value.replace(/&/g, '&') 3 | .replace(/\"/g, '"') 4 | .replace(/'/g, ''') 5 | .replace(//g, '>'); 7 | } 8 | 9 | export interface HtmlContribution { 10 | readonly title: string; 11 | readonly headContribution?: string; 12 | readonly body: string; 13 | } 14 | -------------------------------------------------------------------------------- /examples/image-demo-worker/theme.ts: -------------------------------------------------------------------------------- 1 | export class Theme { 2 | // tailwind sky 3 | static readonly primaryColor200Hex = '#0284c7'; 4 | static readonly primaryColor300Hex = '#0369a1'; // primary 5 | static readonly primaryColor900Hex = '#0c4a6e'; // darkest 6 | 7 | static readonly backgroundColorHex = '#101010'; 8 | } 9 | -------------------------------------------------------------------------------- /examples/image-demo-worker/transforms.ts: -------------------------------------------------------------------------------- 1 | export interface Transform { 2 | readonly name: string; 3 | readonly parameters: readonly TransformParameter[]; 4 | } 5 | 6 | export interface TransformParameter { 7 | readonly name: string; 8 | readonly type: TransformParameterType; 9 | } 10 | 11 | export type TransformParameterType = FloatParameterType | IntParameterType | EnumParameterType | RgbParameterType | ChannelParameterType; 12 | 13 | export interface FloatParameterType { 14 | kind: 'float'; 15 | min: number; 16 | max: number; 17 | default?: number; 18 | } 19 | 20 | export interface IntParameterType { 21 | kind: 'int'; 22 | min: number; 23 | max: number; 24 | default?: number; 25 | } 26 | 27 | export interface EnumParameterType { 28 | kind: 'enum'; 29 | values: readonly string[]; 30 | } 31 | 32 | export interface RgbParameterType { 33 | kind: 'rgb'; 34 | } 35 | 36 | export interface ChannelParameterType { 37 | kind: 'channel'; 38 | // 0 = red, 1 = green, 2 = blue 39 | } 40 | -------------------------------------------------------------------------------- /examples/image-demo-worker/twitter.ts: -------------------------------------------------------------------------------- 1 | import { importBinary } from './deps_worker.ts'; 2 | 3 | // https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/summary-card-with-large-image 4 | 5 | // Images for this Card support an aspect ratio of 2:1 with minimum dimensions of 300x157 or maximum of 4096x4096 pixels. 6 | // Images must be less than 5MB in size. JPG, PNG, WEBP and GIF formats are supported. 7 | // Only the first frame of an animated GIF will be used. SVG is not supported. 8 | export const TWITTER_IMAGE_PNG = await importBinary(import.meta.url, './twitter_image.png'); 9 | export const TWITTER_IMAGE_VERSION = '2'; 10 | -------------------------------------------------------------------------------- /examples/image-demo-worker/twitter_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/twitter_image.png -------------------------------------------------------------------------------- /examples/image-demo-worker/worker_env.d.ts: -------------------------------------------------------------------------------- 1 | export interface WorkerEnv { 2 | readonly twitter?: string; 3 | readonly unsplashAppName?: string; 4 | readonly unsplashIxid?: string; 5 | readonly imgOrigin?: string; 6 | readonly authorName?: string; 7 | readonly authorHref?: string; 8 | } 9 | -------------------------------------------------------------------------------- /examples/keyspace-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 2 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts'; 3 | export type { Kv, KvCommitError, KvCommitResult, KvKey, KvService, KvU64 } from 'https://raw.githubusercontent.com/skymethod/kv-connect-kit/f895fd50d2f9f37309aab0f0bb3080e3a9a2a5cf/kv_types.ts'; 4 | export { makeRemoteService } from 'https://raw.githubusercontent.com/skymethod/kv-connect-kit/f895fd50d2f9f37309aab0f0bb3080e3a9a2a5cf/client.ts'; 5 | -------------------------------------------------------------------------------- /examples/keyspace-worker/env.ts: -------------------------------------------------------------------------------- 1 | export type Env = { 2 | adminIp?: string, 3 | remoteKvUrl?: string, 4 | remoteKvAccessToken?: string, 5 | denoOrigin?: string, 6 | cloudflareOrigin?: string, 7 | denoCfAnalyticsToken?: string, 8 | cloudflareCfAnalyticsToken?: string, 9 | }; 10 | -------------------------------------------------------------------------------- /examples/mqtt-demo-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts'; 2 | -------------------------------------------------------------------------------- /examples/mqtt-demo-worker/worker.ts: -------------------------------------------------------------------------------- 1 | import { importText } from './deps.ts'; 2 | const mqttDemoHtml = await importText(import.meta.url, './static/mqtt-demo.html'); 3 | const mqttDemoJs = await importText(import.meta.url, './static/mqtt-demo.js'); 4 | 5 | export default { 6 | 7 | fetch(request: Request): Response { 8 | const { method } = request; 9 | if (method !== 'GET') return new Response(`method not allowed: ${method}`, { status: 405 }); 10 | 11 | const { pathname } = new URL(request.url); 12 | if (pathname === '/') return new Response(mqttDemoHtml, { headers: { 'content-type': 'text/html; charset=utf-8' } }); 13 | if (pathname === '/mqtt-demo.js') return new Response(mqttDemoJs, { headers: { 'content-type': 'text/javascript; charset=utf-8' } }); 14 | 15 | return new Response('not found', { status: 404 }); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /examples/multiplat-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export { importWasm } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_wasm.ts'; 2 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts'; 3 | export { importBinary } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_binary.ts'; 4 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 5 | -------------------------------------------------------------------------------- /examples/multiplat-worker/static/deno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/multiplat-worker/static/deno.png -------------------------------------------------------------------------------- /examples/multiplat-worker/static/example.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet -------------------------------------------------------------------------------- /examples/multiplat-worker/static/hello.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/multiplat-worker/static/hello.wasm -------------------------------------------------------------------------------- /examples/r2-presigned-url-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export type { IncomingRequestCf, R2Bucket, R2Object, R2ObjectBody, R2Range, R2GetOptions, R2Conditional, R2Objects, R2ListOptions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 2 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts'; 3 | export { computeExpectedAwsSignature, tryParseAmazonDate, R2 } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/r2/r2.ts'; 4 | export type { AwsCall } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/r2/r2.ts'; 5 | -------------------------------------------------------------------------------- /examples/r2-presigned-url-worker/worker_env.d.ts: -------------------------------------------------------------------------------- 1 | import { R2Bucket } from './deps.ts'; 2 | 3 | export interface WorkerEnv { 4 | readonly bucket: R2Bucket; 5 | readonly flags?: string; 6 | readonly allowIps?: string; 7 | readonly denyIps?: string; 8 | readonly credentials?: string; 9 | readonly maxSkewMinutes?: string; // default: 15 10 | readonly maxExpiresMinutes?: string; // default: 7 days 11 | readonly virtualHostname?: string; // e.g. custombucketname.example.com, useful if using vhost-style presigning 12 | } 13 | -------------------------------------------------------------------------------- /examples/r2-public-read-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export type { IncomingRequestCf, R2Bucket, R2Object, R2ObjectBody, R2Range, R2GetOptions, R2Conditional, R2Objects, R2ListOptions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 2 | export { encodeXml } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/xml_util.ts'; 3 | -------------------------------------------------------------------------------- /examples/r2-public-read-worker/worker_env.d.ts: -------------------------------------------------------------------------------- 1 | import { R2Bucket } from './deps.ts'; 2 | 3 | export interface WorkerEnv { 4 | readonly bucket: R2Bucket; 5 | readonly flags?: string; 6 | readonly allowIps?: string; 7 | readonly denyIps?: string; 8 | readonly directoryListingLimit?: string; // default: 1000 (max) to workaround r2 bug 9 | readonly allowCorsOrigins?: string; // e.g. * or https://origin1.com, https://origin2.com 10 | readonly allowCorsTypes?: string; // if allowed cors origin, further restricts by file extension (.mp4, .m3u8, .ts) or content-type (video/mp4, application/x-mpegurl, video/mp2t) 11 | } 12 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/client.ts: -------------------------------------------------------------------------------- 1 | export class Client { 2 | private readonly socket: WebSocket; 3 | 4 | constructor(socket: WebSocket, clientId: string, republish: (data: Record) => void) { 5 | socket.addEventListener('message', event => { 6 | const msg = JSON.parse(event.data); 7 | republish({ t: 'message', msg, clientId }); 8 | }); 9 | socket.addEventListener('close', event => { 10 | const { code, reason, wasClean } = event; 11 | republish({ t: 'close', clientId, code, reason, wasClean }); 12 | }); 13 | socket.addEventListener('error', event => { 14 | republish({ t: 'error', clientId, event }); 15 | }); 16 | this.socket = socket; 17 | 18 | // cf sockets are assumed open after accept! 19 | this.send({ t: 'open', clientId }); 20 | } 21 | 22 | send(data: Record) { 23 | this.socket.send(JSON.stringify(data)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/colo_from_trace.ts: -------------------------------------------------------------------------------- 1 | export class ColoFromTrace { 2 | 3 | private _colo: string | undefined; 4 | 5 | async get(): Promise { 6 | if (!this._colo) { 7 | this._colo = await this.computeColo(); 8 | } 9 | return this._colo; 10 | } 11 | 12 | // 13 | 14 | private async computeColo(): Promise { 15 | const res = await fetch('https://1.1.1.1/cdn-cgi/trace'); 16 | if (res.status !== 200) return res.status.toString(); 17 | const text = await res.text(); 18 | const lines = text.split('\n'); 19 | for (const line of lines) { 20 | if (line.startsWith('colo=')) return line.substring('colo='.length); 21 | } 22 | return 'nocolo'; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/colo_tier_do.ts: -------------------------------------------------------------------------------- 1 | import { ColoFromTrace } from './colo_from_trace.ts'; 2 | import { DurableObjectState } from './deps.ts'; 3 | import { getIsolateId } from './isolate_id.ts'; 4 | import { Server } from './server.ts'; 5 | 6 | export class ColoTierDO { 7 | private readonly state: DurableObjectState; 8 | 9 | constructor(state: DurableObjectState) { 10 | this.state = state; 11 | } 12 | 13 | async fetch(request: Request): Promise { 14 | console.log(request.url); 15 | const colo = await COLO_FROM_TRACE.get(); 16 | const durableObjectName = request.headers.get('do-name'); 17 | const isolateId = 'colo-tier-' + durableObjectName + '-' + getIsolateId(colo); 18 | const server = this.ensureServer(isolateId); 19 | const url = new URL(request.url); 20 | const { pathname } = url; 21 | this.ensureTicking(); 22 | console.log('logprops:', { colo, durableObjectClass: 'ColoTierDO', durableObjectId: this.state.id.toString(), durableObjectName }); 23 | 24 | const wsResponse = server.tryHandle(pathname, request, isolateId); 25 | if (wsResponse) return wsResponse; 26 | 27 | return new Response('not found', { status: 404 }); 28 | } 29 | 30 | // 31 | 32 | _server: Server | undefined; 33 | 34 | private ensureServer(serverId: string): Server { 35 | if (this._server) return this._server; 36 | this._server = new Server(serverId, true); 37 | return this._server; 38 | } 39 | 40 | _ticking = false; 41 | 42 | private ensureTicking() { 43 | if (this._ticking) return; 44 | this._ticking = true; 45 | setInterval(() => { 46 | const now = new Date().toISOString(); 47 | this._server?.broadcast({ t: 'tick', now }); 48 | }, 5000); 49 | } 50 | 51 | } 52 | 53 | const COLO_FROM_TRACE = new ColoFromTrace(); 54 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/deps.ts: -------------------------------------------------------------------------------- 1 | export type { IncomingRequestCf, DurableObjectState, DurableObjectNamespace, ModuleWorkerContext, CloudflareWebSocketExtensions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 2 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { CloudflareWebSocketExtensions } from './deps.ts'; 2 | 3 | export {}; 4 | 5 | declare global { 6 | 7 | // https://developers.cloudflare.com/workers/runtime-apis/websockets 8 | 9 | interface Response { 10 | // non-standard member 11 | webSocket?: WebSocket & CloudflareWebSocketExtensions; 12 | } 13 | 14 | // non-standard class, only on CF 15 | class WebSocketPair { 16 | 0: WebSocket & CloudflareWebSocketExtensions; 17 | 1: WebSocket & CloudflareWebSocketExtensions; 18 | } 19 | 20 | interface ResponseInit { 21 | // non-standard member 22 | webSocket?: WebSocket; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/isolate_id.ts: -------------------------------------------------------------------------------- 1 | export function getIsolateId(colo: string): string { 2 | if (_isolateId) return _isolateId; 3 | const tokens = crypto.randomUUID().split('-'); 4 | _isolateId = colo + '-' + tokens[3] + tokens[4]; 5 | return _isolateId; 6 | } 7 | 8 | // 9 | 10 | let _isolateId: string | undefined; 11 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/server.ts: -------------------------------------------------------------------------------- 1 | export class Server { 2 | private readonly sockets = new Map(); 3 | private readonly debug: boolean; 4 | 5 | readonly id: string; 6 | 7 | constructor(id: string, debug: boolean = false) { 8 | this.id = id; 9 | this.debug = debug; 10 | } 11 | 12 | broadcast(obj: Record) { 13 | obj.serverId = this.id; 14 | obj.clients = this.sockets.size; 15 | const json = JSON.stringify(obj); 16 | for (const socket of this.sockets.values()) { 17 | socket.send(json); 18 | } 19 | } 20 | 21 | tryHandle(pathname: string, request: Request, debug: string): Response | undefined { 22 | const m = /^\/ws\/(.+)$/.exec(pathname); 23 | if (!m) return undefined; 24 | const upgrade = request.headers.get('upgrade') || undefined; 25 | if (upgrade !== 'websocket') return new Response('expected upgrade: websocket', { status: 400 }); 26 | const clientId = m[1]; 27 | const pair = new WebSocketPair(); 28 | const socket = pair[1]; 29 | this.sockets.set(clientId, socket); 30 | socket.accept(); 31 | socket.addEventListener('message', event => { 32 | try { 33 | const obj = JSON.parse(event.data); 34 | if (obj.t === 'open') { 35 | if (typeof obj.clientId !== 'string') throw new Error(`Bad clientId: ${obj.clientId}`); 36 | this.broadcast({ t: 'opened', clientId, clients: this.sockets.size, debug }); 37 | } 38 | } catch (e) { 39 | this.broadcast({ t: 'messageError', clientId, msg: `Error handling event.data: ${event.data}, ${(e as Error).stack || e}` }); 40 | } 41 | }) 42 | socket.addEventListener('close', event => { 43 | const { code, reason, wasClean } = event; 44 | this.sockets.delete(clientId); 45 | this.broadcast({ t: 'closed', clientId, clients: this.sockets.size, code, reason, wasClean }); 46 | }); 47 | socket.addEventListener('error', event => { 48 | this.sockets.delete(clientId); 49 | this.broadcast({ t: 'errored', clientId, clients: this.sockets.size, event }); 50 | }); 51 | return new Response(null, { status: 101, webSocket: pair[0], headers: { 'debug': debug } }); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /examples/tiered-websockets-worker/tiered_worker_env.d.ts: -------------------------------------------------------------------------------- 1 | import { DurableObjectNamespace } from './deps.ts'; 2 | 3 | export interface TieredWorkerEnv { 4 | readonly ColoTierDO: DurableObjectNamespace; 5 | } 6 | -------------------------------------------------------------------------------- /examples/webtail-app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true 3 | } -------------------------------------------------------------------------------- /examples/webtail-app/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "deno", 6 | "command": "bundle", 7 | "problemMatcher": [ 8 | "$deno" 9 | ], 10 | "args": [ 11 | "--config", 12 | "deno.jsonc", 13 | "--watch", 14 | "webtail_app.ts", 15 | "../webtail-worker/static/webtail_app.js" 16 | ], 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | }, 21 | "label": "deno: bundle webtail_app.ts" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /examples/webtail-app/app_constants.ts: -------------------------------------------------------------------------------- 1 | export class AppConstants { 2 | static readonly WEBSOCKET_PING_INTERVAL_SECONDS = 10; // send an empty message down the ws to detect bad connections faster 3 | static readonly INACTIVE_TAIL_SECONDS = 5; // reclaim inactive tails older than this 4 | } 5 | -------------------------------------------------------------------------------- /examples/webtail-app/deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", "dom.iterable", "esnext" 5 | ] 6 | }, 7 | "lock": false, 8 | "fmt": { "singleQuote": true } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /examples/webtail-app/deps_app.ts: -------------------------------------------------------------------------------- 1 | export { css, html, LitElement, svg, type SVGTemplateResult, CSSResult, type TemplateResult, render } from 'https://cdn.skypack.dev/lit@2.6.1?dts'; 2 | export type { Tail } from '../../common/cloudflare_api.ts'; 3 | export { createTail, CloudflareApiError, listScripts, listTails, CloudflareApi } from '../../common/cloudflare_api.ts'; 4 | export { setSubtract, setEqual, setIntersect, setUnion } from '../../common/sets.ts'; 5 | export { TailConnection } from '../../common/tail_connection.ts'; 6 | export { formatLocalYyyyMmDdHhMmSs, dumpMessagePretty, parseLogProps } from '../../common/tail_pretty.ts'; 7 | export { generateUuid } from '../../common/uuid_v4.ts'; 8 | export type { AdditionalLog } from '../../common/tail_pretty.ts'; 9 | export type { ErrorInfo, TailConnectionCallbacks, UnparsedMessage } from '../../common/tail_connection.ts'; 10 | export type { TailMessage, TailOptions, TailFilter, HeaderFilter } from '../../common/tail.ts'; 11 | export { isTailMessageCronEvent, parseHeaderFilter } from '../../common/tail.ts'; 12 | export { CfGqlClient } from '../../common/analytics/cfgql_client.ts'; 13 | export { computeDurableObjectsCostsTable } from '../../common/analytics/durable_objects_costs.ts'; 14 | export type { DurableObjectsCostsTable, DurableObjectsDailyCostsTable } from '../../common/analytics/durable_objects_costs.ts'; 15 | -------------------------------------------------------------------------------- /examples/webtail-app/qps_controller.ts: -------------------------------------------------------------------------------- 1 | export interface QpsControllerCallbacks { 2 | onQpsChanged(qps: number): void; 3 | } 4 | 5 | export class QpsController { 6 | private readonly sortedEventTimes: number[] = []; 7 | private readonly callbacks: QpsControllerCallbacks; 8 | private readonly n: number; 9 | 10 | private _qps = 0; 11 | get qps(): number { return this._qps; } 12 | 13 | constructor(n: number, callbacks: QpsControllerCallbacks) { 14 | this.callbacks = callbacks; 15 | this.n = n; 16 | } 17 | 18 | addEvent(eventTime: number) { 19 | const qps = computeQps(this.n, this.sortedEventTimes, eventTime); 20 | if (qps === this._qps) return; 21 | this._qps = qps; 22 | this.callbacks.onQpsChanged(qps); 23 | } 24 | 25 | } 26 | 27 | // 28 | 29 | function computeQps(n: number, sortedEventTimes: number[], eventTime: number) { 30 | add(eventTime, sortedEventTimes); 31 | while (sortedEventTimes.length > n) { 32 | sortedEventTimes.shift(); 33 | } 34 | // currentAvgRate = (N - 1) / (time difference between last N events) 35 | const num = sortedEventTimes.length; 36 | if (num < 2) return 0; 37 | const timeDiffSeconds = (sortedEventTimes[sortedEventTimes.length - 1] - sortedEventTimes[0]) / 1000; 38 | return (num - 1) / timeDiffSeconds; 39 | } 40 | 41 | function add(el: number, arr: number[]) { 42 | arr.splice(findLoc(el, arr) + 1, 0, el); 43 | return arr; 44 | } 45 | 46 | function findLoc(el: number, arr: number[], st?: number, en?: number) { 47 | st = st || 0; 48 | en = en || arr.length; 49 | for (let i = 0; i < arr.length; i++) { 50 | if (arr[i] > el) { 51 | return i - 1; 52 | } 53 | } 54 | return en; 55 | } 56 | -------------------------------------------------------------------------------- /examples/webtail-app/static_data.ts: -------------------------------------------------------------------------------- 1 | export interface StaticData { 2 | readonly version?: string; 3 | readonly flags?: string; 4 | } 5 | -------------------------------------------------------------------------------- /examples/webtail-app/views/icons.ts: -------------------------------------------------------------------------------- 1 | import { html, svg, SVGTemplateResult } from '../deps_app.ts'; 2 | import { Material } from '../material.ts'; 3 | 4 | export function actionIcon(icon: SVGTemplateResult, opts: { text?: string, onclick?: () => void } = {}) { 5 | const { text, onclick } = opts; 6 | return html`
{ e.preventDefault(); onclick && onclick(); }}>${icon}${text || ''}
`; 7 | } 8 | 9 | export const CLEAR_ICON = svg``; 10 | export const EDIT_ICON = svg``; 11 | export const ADD_ICON = svg`>`; 12 | export const CHECK_BOX_UNCHECKED_ICON = svg``; 13 | export const CHECK_BOX_CHECKED_ICON = svg``; 14 | -------------------------------------------------------------------------------- /examples/webtail-app/views/modal_view.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { css, html } from '../deps_app.ts'; 4 | import { WebtailAppVM } from '../webtail_app_vm.ts'; 5 | import { FILTER_EDITOR_HTML, initFilterEditor } from './filter_editor_view.ts'; 6 | import { initProfileEditor, PROFILE_EDITOR_HTML } from './profile_editor_view.ts'; 7 | import { initWelcomePanel, WELCOME_PANEL_HTML } from './welcome_panel.ts'; 8 | 9 | export const MODAL_HTML = html` 10 | 17 | `; 18 | 19 | export const MODAL_CSS = css` 20 | .modal { 21 | display: none; 22 | position: fixed; 23 | z-index: 1; 24 | left: 0; 25 | top: 0; 26 | width: 100%; 27 | height: 100%; 28 | background-color: rgba(0, 0, 0, 0.4); 29 | } 30 | 31 | .modal-content { 32 | margin: 10% auto; 33 | width: 80%; 34 | max-width: 40rem; 35 | background-color: var(--background-color); 36 | } 37 | 38 | @media screen and (max-width: 600px) { 39 | .modal-content { 40 | margin: 10% 0; 41 | width: 100%; 42 | } 43 | } 44 | 45 | `; 46 | 47 | export function initModal(document: HTMLDocument, vm: WebtailAppVM): () => void { 48 | 49 | const modal = document.getElementById('modal') as HTMLDivElement; 50 | const updateProfileEditor = initProfileEditor(document, vm); 51 | const updateFilterEditor = initFilterEditor(document, vm); 52 | const updateWelcomePanel = initWelcomePanel(document, vm); 53 | 54 | const closeModal = () => { 55 | if (!vm.profileForm.showing && !vm.filterForm.showing && !vm.welcomeShowing && !vm.aboutShowing) return; 56 | if (vm.profileForm.progressVisible) return; // don't allow close if busy 57 | vm.profileForm.showing = false; 58 | vm.filterForm.showing = false; 59 | vm.aboutShowing = false; 60 | vm.onChange(); 61 | }; 62 | 63 | // Click outside modal content -> close modal 64 | window.addEventListener('click', event => { 65 | if (event.target == modal) { 66 | closeModal(); 67 | } 68 | }); 69 | 70 | // esc -> close modal 71 | document.addEventListener('keydown', event => { 72 | event = event || window.event; 73 | if (event.key === 'Escape') { 74 | closeModal(); 75 | } 76 | }); 77 | 78 | return () => { 79 | updateProfileEditor(); 80 | updateFilterEditor(); 81 | updateWelcomePanel(); 82 | modal.style.display = (vm.profileForm.showing || vm.filterForm.showing || vm.welcomeShowing || vm.aboutShowing) ? 'block' : 'none'; 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /examples/webtail-worker/app_manifest.d.ts: -------------------------------------------------------------------------------- 1 | export interface AppManifest { 2 | /** 3 | * https://w3c.github.io/manifest/#short_name-member 4 | * maximum of 12 characters recommended per https://developer.chrome.com/extensions/manifest/name 5 | * used: android launcher icon title 6 | */ 7 | 'short_name': string, 8 | 9 | /** 10 | * https://w3c.github.io/manifest/#name-member 11 | * maximum of 45 characters per https://developer.chrome.com/extensions/manifest/name 12 | * used: app install banner android offline splash screen 13 | */ 14 | name: string, 15 | 16 | /** 17 | * https://w3c.github.io/manifest/#description-member 18 | * used ?? 19 | */ 20 | description: string, 21 | 22 | /** 23 | * https://w3c.github.io/manifest/#icons-member 24 | */ 25 | icons: readonly AppManifestIcon[], 26 | 27 | /** 28 | * https://w3c.github.io/manifest/#theme_color-member 29 | */ 30 | 'theme_color': string, 31 | 32 | /** 33 | * https://w3c.github.io/manifest/#background_color-member 34 | * should match page, also used for splash screen 35 | */ 36 | 'background_color': string, 37 | 38 | /** 39 | * https://w3c.github.io/manifest/#display-member 40 | */ 41 | display: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser', 42 | 43 | /** 44 | * https://w3c.github.io/manifest/#start_url-member 45 | */ 46 | 'start_url': string, 47 | 48 | /** 49 | * https://w3c.github.io/manifest/#lang-member 50 | */ 51 | lang: 'en-US', 52 | 53 | /** 54 | * https://w3c.github.io/manifest/#dir-member 55 | */ 56 | dir: 'ltr', 57 | 58 | /** 59 | * https://w3c.github.io/manifest/#scope-member 60 | */ 61 | scope?: string, 62 | 63 | } 64 | 65 | export interface AppManifestIcon { 66 | 67 | /** 68 | * url to image resource 69 | */ 70 | src: string; 71 | 72 | /** 73 | * e.g. "512x512", or "16x16 32x32 48x48" 74 | * 75 | * https://w3c.github.io/manifest/#declaring-multiple-icons 76 | */ 77 | sizes?: string; 78 | 79 | /** 80 | * MIME type 81 | * 82 | * https://w3c.github.io/manifest/#declaring-multiple-icons 83 | */ 84 | type?: string; 85 | 86 | /** 87 | * https://w3c.github.io/manifest/#purpose-member 88 | */ 89 | purpose?: 'monochrome' | 'maskable' | 'any'; 90 | } 91 | -------------------------------------------------------------------------------- /examples/webtail-worker/cli.ts: -------------------------------------------------------------------------------- 1 | import { Bytes, parseFlags } from './deps_cli.ts'; 2 | 3 | const { args, options } = parseFlags(Deno.args); 4 | 5 | if (args.length > 0) { 6 | await webtail(args, options); 7 | Deno.exit(0); 8 | } 9 | 10 | dumpHelp(); 11 | 12 | Deno.exit(1); 13 | 14 | // 15 | 16 | async function webtail(args: (string | number)[], options: Record) { 17 | const command = args[0]; 18 | const fn = { b64 }[command]; 19 | if (options.help || !fn) { 20 | dumpHelp(); 21 | return; 22 | } 23 | await fn(args.slice(1)); 24 | } 25 | 26 | async function b64(args: (string | number)[]) { 27 | const path = args[0]; 28 | if (typeof path !== 'string') throw new Error('Must provide path to file'); 29 | const contents = await Deno.readFile(path); 30 | const b64 = new Bytes(contents).base64(); 31 | console.log(b64); 32 | } 33 | 34 | 35 | function dumpHelp() { 36 | const lines = [ 37 | `webtail-cli`, 38 | 'Tools for developing webtail', 39 | '', 40 | 'USAGE:', 41 | ' deno run --allow-net examples/webtail-worker/cli.ts [FLAGS] [OPTIONS] [--] build', 42 | ' deno run --allow-net --allow-read examples/webtail-worker/cli.ts [FLAGS] [OPTIONS] [--] b64 ', 43 | '', 44 | 'FLAGS:', 45 | ' -h, --help Prints help information', 46 | ' --verbose Toggle verbose output (when applicable)', 47 | '', 48 | 'ARGS:', 49 | ' b64 Dump out the b64 of a given file', 50 | ]; 51 | for (const line of lines) { 52 | console.log(line); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/webtail-worker/deps_cli.ts: -------------------------------------------------------------------------------- 1 | export { basename, dirname, join, fromFileUrl, resolve, toFileUrl, extname, relative, isAbsolute, normalize, parse as parsePath, globToRegExp } from 'https://deno.land/std@0.224.0/path/mod.ts'; 2 | export { ModuleWatcher } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/cli/module_watcher.ts'; 3 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts'; 4 | export { bundle } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/cli/bundle.ts'; 5 | export { parseFlags } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/cli/flag_parser.ts'; 6 | -------------------------------------------------------------------------------- /examples/webtail-worker/deps_worker.ts: -------------------------------------------------------------------------------- 1 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts'; 2 | export type { IncomingRequestCf, ModuleWorkerContext } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts'; 3 | export { encodeXml } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/xml_util.ts'; 4 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts'; 5 | -------------------------------------------------------------------------------- /examples/webtail-worker/material.ts: -------------------------------------------------------------------------------- 1 | export class Material { 2 | static readonly primaryColor200Hex = '#cfabfd'; 3 | static readonly primaryColor300Hex = '#bb86fc'; // primary 4 | static readonly primaryColor900Hex = '#5f27ca'; // darkest 5 | 6 | static readonly backgroundColorHex = '#121212'; 7 | static sansSerifFontFamily = '-apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif'; 8 | } 9 | -------------------------------------------------------------------------------- /npm/denoflare-mqtt/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /npm/denoflare-mqtt/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.DS_Store": true, 4 | "**/node_modules": true, 5 | "cjs": true, 6 | "esm": true, 7 | "main.d.ts": true, 8 | }, 9 | } -------------------------------------------------------------------------------- /npm/denoflare-mqtt/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /npm/denoflare-mqtt/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /npm/denoflare-mqtt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "denoflare-mqtt", 3 | "version": "0.0.2", 4 | "description": "Lightweight MQTT v5 client for Deno, Node, and the browser.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/skymethod/denoflare.git" 8 | }, 9 | "main": "./cjs/main.js", 10 | "module": "./esm/main.js", 11 | "exports": { 12 | ".": { 13 | "require": "./cjs/main.js", 14 | "default": "./esm/main.js" 15 | } 16 | }, 17 | "types": "main.d.ts", 18 | "keywords": [ "mqtt" ], 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/skymethod/denoflare/issues" 22 | }, 23 | "homepage": "https://github.com/skymethod/denoflare/tree/master/npm/denoflare-mqtt" 24 | } 25 | --------------------------------------------------------------------------------