From 3dcb5f08868b1574b5c2086683843de9a9dce26a Mon Sep 17 00:00:00 2001 From: Stephen Lacy Date: Fri, 11 Apr 2025 14:51:39 -0700 Subject: [PATCH 1/3] feat: add cli auth --- .gitignore | 1 + deno.jsonc | 28 +++++++++--- deno.lock | 38 +++++++--------- src/commands/auth.ts | 100 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 2 + src/lib/preferences.ts | 58 ++++++++++++++++++++++++ src/lib/schema.ts | 5 ++- 7 files changed, 201 insertions(+), 31 deletions(-) create mode 100644 src/commands/auth.ts create mode 100644 src/lib/preferences.ts diff --git a/.gitignore b/.gitignore index 1be6b26..5888e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ build scratch/ # This file is generated by the runreal CLI during the test .runreal/dist/script.esm.js +.DS_Store diff --git a/deno.jsonc b/deno.jsonc index 69f9732..f3d89d4 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -10,15 +10,29 @@ "generate-schema": "deno run -A src/generate-schema.ts" }, "lint": { - "include": ["src/", "tests/"], + "include": [ + "src/", + "tests/" + ], "rules": { - "tags": ["recommended"], - "include": ["ban-untagged-todo"], - "exclude": ["no-unused-vars", "no-explicit-any"] + "tags": [ + "recommended" + ], + "include": [ + "ban-untagged-todo" + ], + "exclude": [ + "no-unused-vars", + "no-explicit-any" + ] } }, "fmt": { - "include": ["src/", "tests/"], + "include": [ + "src/", + "tests/", + "deno.jsonc" + ], "useTabs": true, "lineWidth": 120, "indentWidth": 2, @@ -27,6 +41,7 @@ "semiColons": false }, "imports": { + "@cliffy/ansi": "jsr:@cliffy/ansi@1.0.0-rc.7", "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.7", "@cliffy/testing": "jsr:@cliffy/testing@1.0.0-rc.7", "@david/dax": "jsr:@david/dax@0.42.0", @@ -41,10 +56,11 @@ "@std/streams": "jsr:@std/streams@1.0.9", "@std/testing": "jsr:@std/testing@1.0.11", "esbuild": "npm:esbuild@0.25.2", - "ueblueprint":"npm:ueblueprint@2.0.0", + "ueblueprint": "npm:ueblueprint@2.0.0", "zod": "npm:zod@3.24.2", "zod-to-json-schema": "npm:zod-to-json-schema@3.24.5", "ndjson": "https://deno.land/x/ndjson@1.1.0/mod.ts", + "nanoid": "npm:nanoid@5.1", "ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts", "xml2js": "https://deno.land/x/xml2js@1.0.0/mod.ts", "globber": "https://deno.land/x/globber@0.1.0/mod.ts" diff --git a/deno.lock b/deno.lock index 3d5a1ac..1615f11 100644 --- a/deno.lock +++ b/deno.lock @@ -16,7 +16,6 @@ "jsr:@std/assert@1.0.12": "1.0.12", "jsr:@std/assert@^1.0.12": "1.0.12", "jsr:@std/assert@^1.0.2": "1.0.12", - "jsr:@std/assert@^1.0.8": "1.0.12", "jsr:@std/assert@~1.0.6": "1.0.12", "jsr:@std/async@^1.0.12": "1.0.12", "jsr:@std/bytes@0.221": "0.221.0", @@ -28,20 +27,16 @@ "jsr:@std/encoding@~1.0.5": "1.0.9", "jsr:@std/fmt@1": "1.0.6", "jsr:@std/fmt@1.0.6": "1.0.6", - "jsr:@std/fmt@^1.0.6": "1.0.6", "jsr:@std/fmt@~1.0.2": "1.0.6", - "jsr:@std/fs@*": "1.0.16", "jsr:@std/fs@1": "1.0.16", "jsr:@std/fs@^1.0.1": "1.0.16", "jsr:@std/fs@^1.0.16": "1.0.16", - "jsr:@std/fs@^1.0.5": "1.0.16", "jsr:@std/internal@^1.0.1": "1.0.6", - "jsr:@std/internal@^1.0.5": "1.0.6", "jsr:@std/internal@^1.0.6": "1.0.6", "jsr:@std/io@0.221": "0.221.0", + "jsr:@std/io@~0.224.9": "0.224.9", "jsr:@std/json@1": "1.0.1", "jsr:@std/jsonc@1.0.1": "1.0.1", - "jsr:@std/path@*": "1.0.8", "jsr:@std/path@1": "1.0.8", "jsr:@std/path@1.0.8": "1.0.8", "jsr:@std/path@^1.0.2": "1.0.8", @@ -52,8 +47,8 @@ "jsr:@std/testing@1.0.0": "1.0.0", "jsr:@std/testing@1.0.11": "1.0.11", "jsr:@std/text@~1.0.7": "1.0.12", - "npm:@types/node@*": "22.12.0", "npm:esbuild@0.25.2": "0.25.2", + "npm:nanoid@5.1": "5.1.5", "npm:ueblueprint@2.0.0": "2.0.0", "npm:zod-to-json-schema@3.24.5": "3.24.5_zod@3.24.2", "npm:zod@3.24.2": "3.24.2" @@ -63,7 +58,9 @@ "integrity": "f71c921cce224c13d322e5cedba4f38e8f7354c7d855c9cb22729362a53f25aa", "dependencies": [ "jsr:@cliffy/internal", - "jsr:@std/encoding@~1.0.5" + "jsr:@std/encoding@~1.0.5", + "jsr:@std/fmt@~1.0.2", + "jsr:@std/io@~0.224.9" ] }, "@cliffy/command@1.0.0-rc.7": { @@ -111,7 +108,7 @@ "jsr:@david/which", "jsr:@std/fmt@1", "jsr:@std/fs@1", - "jsr:@std/io", + "jsr:@std/io@0.221", "jsr:@std/path@1", "jsr:@std/streams@0.221" ] @@ -183,6 +180,9 @@ "jsr:@std/bytes@0.221" ] }, + "@std/io@0.224.9": { + "integrity": "4414664b6926f665102e73c969cfda06d2c4c59bd5d0c603fd4f1b1c840d6ee3" + }, "@std/json@1.0.1": { "integrity": "1f0f70737e8827f9acca086282e903677bc1bb0c8ffcd1f21bca60039563049f" }, @@ -198,7 +198,7 @@ "@std/streams@0.221.0": { "integrity": "47f2f74634b47449277c0ee79fe878da4424b66bd8975c032e3afdca88986e61", "dependencies": [ - "jsr:@std/io" + "jsr:@std/io@0.221" ] }, "@std/streams@1.0.9": { @@ -316,12 +316,6 @@ "@lit-labs/ssr-dom-shim" ] }, - "@types/node@22.12.0": { - "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", - "dependencies": [ - "undici-types" - ] - }, "@types/trusted-types@2.0.7": { "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, @@ -377,6 +371,9 @@ "lit-html" ] }, + "nanoid@5.1.5": { + "integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==" + }, "parsernostrum@1.2.6": { "integrity": "sha512-Ho+y3yoqVCHRtqsKVqltsv17MgjP8Np+VIC8nd2cyEAsko5hNiZZpA6mi0krvfv8XmrS+tOpra83d4UctJtmQg==" }, @@ -387,9 +384,6 @@ "parsernostrum" ] }, - "undici-types@6.20.0": { - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" - }, "zod-to-json-schema@3.24.5_zod@3.24.2": { "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", "dependencies": [ @@ -417,10 +411,6 @@ "https://deno.land/std@0.150.0/path/posix.ts": "c1f7afe274290ea0b51da07ee205653b2964bd74909a82deb07b69a6cc383aaa", "https://deno.land/std@0.150.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", "https://deno.land/std@0.150.0/path/win32.ts": "bd7549042e37879c68ff2f8576a25950abbfca1d696d41d82c7bca0b7e6f452c", - "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", - "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", - "https://deno.land/x/esbuild@v0.24.0/mod.js": "15b51f08198c373555700a695b6c6630a86f2c254938e81be7711eb6d4edc74e", "https://deno.land/x/globber@0.1.0/mod.ts": "971e58757909b2ef722e3dda1125aea8f5694601203ad835bdfc020f202bd5b8", "https://deno.land/x/globber@0.1.0/src/create_matcher.ts": "85be3a6d67376905521aed9da51db756d1ee747ebd0d52b88fc7b78a6831a393", "https://deno.land/x/globber@0.1.0/src/deps.ts": "179ba170213f7a35b7b794c409e7ca523da58644139c053721f93575dcbe616e", @@ -449,6 +439,7 @@ }, "workspace": { "dependencies": [ + "jsr:@cliffy/ansi@1.0.0-rc.7", "jsr:@cliffy/command@1.0.0-rc.7", "jsr:@cliffy/testing@1.0.0-rc.7", "jsr:@david/dax@0.42.0", @@ -463,6 +454,7 @@ "jsr:@std/streams@1.0.9", "jsr:@std/testing@1.0.11", "npm:esbuild@0.25.2", + "npm:nanoid@5.1", "npm:ueblueprint@2.0.0", "npm:zod-to-json-schema@3.24.5", "npm:zod@3.24.2" diff --git a/src/commands/auth.ts b/src/commands/auth.ts new file mode 100644 index 0000000..18a9f8d --- /dev/null +++ b/src/commands/auth.ts @@ -0,0 +1,100 @@ +import { Command } from '@cliffy/command' +import { customAlphabet } from 'nanoid' +import { colors } from '@cliffy/ansi/colors' +import type { GlobalOptions } from '../lib/types.ts' +import { preferences } from '../lib/preferences.ts' + +// base58 uppercase characters +const idgen = customAlphabet('123456789ABCDEFGHJKMNPQRSTUVWXYZ', 6) + +const RUNREAL_AUTH_ENDPOINT = Deno.env.get('RUNREAL_AUTH_ENDPOINT') || 'https://auth.runreal.dev' + +let _server: any = null +let _code: string | null = null +export const auth = new Command() + .description('auth') + .arguments(' [args...]') + .stopEarly() + .action(async (options, command, ...args) => { + if (command === 'login') { + _code = idgen(6) + const hostname = Deno.hostname().replace(/[^a-z0-9A-Z-_\.]/, '') + + let port = 56833 + for (let i = 0; i < 10; i++) { + try { + _server = Deno.serve({ port, onListen: () => {} }, handler) + break + } catch (e) { + port++ + if (i === 9) { + console.error(colors.red('Unable to start server. Please try again.')) + Deno.exit(1) + } + } + } + + const link = `${RUNREAL_AUTH_ENDPOINT}/auth/cli?port=${port}&hostname=${hostname}&code=${_code}` + + console.log(`Login code: ${colors.bold.green(_code)}`) + console.log(`Please open ${colors.bold.cyan(link)} to authorize.`) + return + } + + if (command === 'logout') { + const prefs = await preferences.get() + if (prefs.accessToken) { + delete prefs.accessToken + await preferences.set(prefs) + console.log(colors.bold.cyan('Logged out successfully!')) + } else { + console.log(colors.red('You are not logged in.')) + } + return + } + + throw new Error('Invalid command. Use "login" or "logout".') + }) + +async function handler(req: Request): Promise { + const url = new URL(req.url) + const headers = new Headers() + + headers.append('Access-Control-Allow-Origin', RUNREAL_AUTH_ENDPOINT) + headers.append('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + headers.append('Access-Control-Allow-Headers', 'Content-Type') + + if (url.pathname !== '/auth/callback') { + return new Response('Not found', { status: 404, headers }) + } + if (req.method === 'OPTIONS') { + return new Response('OK', { status: 200, headers }) + } + + try { + const body = await req.json() + if (!body.access_token || body.code !== _code) { + console.log(colors.red('Unable to authenticate. Please try again.')) + shutdown() + return new Response('Invalid request', { status: 400, headers }) + } + + await preferences.set({ + accessToken: body.access_token, + }) + } catch (e) { + console.log(colors.red('Unable to authenticate. Please try again.')) + shutdown() + return new Response('Invalid request', { status: 400, headers }) + } + + console.log(colors.bold.cyan('Authenticated successfully!')) + shutdown() + return new Response('OK', { status: 200, headers }) +} + +function shutdown() { + setTimeout(() => { + Deno.exit(0) + }, 500) +} diff --git a/src/index.ts b/src/index.ts index dccffd8..bf7b8fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { cmd } from './cmd.ts' import { script } from './commands/script.ts' import { runpython } from './commands/runpython.ts' import { uasset } from './commands/uasset/index.ts' +import { auth } from './commands/auth.ts' await cmd .name('runreal') @@ -33,6 +34,7 @@ await cmd .command('buildgraph', buildgraph) .command('workflow', workflow) .command('script', script) + .command('auth', auth) .command('runpython', runpython) .command('uasset', uasset) .parse(Deno.args) diff --git a/src/lib/preferences.ts b/src/lib/preferences.ts new file mode 100644 index 0000000..b5b06aa --- /dev/null +++ b/src/lib/preferences.ts @@ -0,0 +1,58 @@ +import { UserRunrealConfigSchema } from './schema.ts' +import { UserRunrealConfig } from './types.ts' + +const homeDir = Deno.env.get('HOME') +const preferencesPath = `${homeDir}/.runreal/` +const preferencesFile = 'preferences.json' +const preferencesFullPath = `${preferencesPath}${preferencesFile}` + +const get = async (): Promise => { + let prefs: UserRunrealConfig = {} + try { + const fileInfo = await Deno.stat(preferencesFullPath) + if (!fileInfo.isFile) { + return prefs + } + const file = await Deno.readTextFile(preferencesFullPath) + const parsed = JSON.parse(file) + try { + prefs = UserRunrealConfigSchema.parse(parsed) + } catch (e) { + console.error('Invalid preferences file:', e) + return prefs + } + } catch (e) { + console.error('Error reading preferences file:', e) + if (e instanceof Deno.errors.NotFound) { + return prefs + } + throw e + } + + return prefs +} + +const set = async (prefs: UserRunrealConfig): Promise => { + try { + const fileInfo = await Deno.stat(preferencesFullPath) + if (!fileInfo.isFile) { + await Deno.mkdir(preferencesPath, { recursive: true }) + } + } catch (e) { + if (e instanceof Deno.errors.NotFound) { + await Deno.mkdir(preferencesPath, { recursive: true }) + } else { + throw e + } + } + + const data = JSON.stringify(prefs, null, 2) + await Deno.writeTextFile(preferencesFullPath, data) + + return prefs +} + +export const preferences = { + get, + set, +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index e3e5f5b..bb2c333 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -61,5 +61,6 @@ export const ConfigSchema = z.object({ export const RunrealConfigSchema = ConfigSchema.merge(InternalSchema) -// Depraecated 😭 -export const UserRunrealConfigSchema = ConfigSchema.deepPartial() +export const UserRunrealConfigSchema = z.object({ + accessToken: z.string().optional().describe('RUNREAL access token'), +}) From 6bbe40c71e6a2b25315e7cc186188682656e4ab0 Mon Sep 17 00:00:00 2001 From: Stephen Lacy Date: Mon, 14 Apr 2025 13:01:54 -0700 Subject: [PATCH 2/3] chore: fix types --- src/lib/preferences.ts | 12 ++++++------ src/lib/schema.ts | 5 ++++- src/lib/types.ts | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/lib/preferences.ts b/src/lib/preferences.ts index b5b06aa..f8b5651 100644 --- a/src/lib/preferences.ts +++ b/src/lib/preferences.ts @@ -1,13 +1,13 @@ -import { UserRunrealConfigSchema } from './schema.ts' -import { UserRunrealConfig } from './types.ts' +import { UserRunrealPreferencesSchema } from './schema.ts' +import { UserRunrealPreferences } from './types.ts' const homeDir = Deno.env.get('HOME') const preferencesPath = `${homeDir}/.runreal/` const preferencesFile = 'preferences.json' const preferencesFullPath = `${preferencesPath}${preferencesFile}` -const get = async (): Promise => { - let prefs: UserRunrealConfig = {} +const get = async (): Promise => { + let prefs: UserRunrealPreferences = {} try { const fileInfo = await Deno.stat(preferencesFullPath) if (!fileInfo.isFile) { @@ -16,7 +16,7 @@ const get = async (): Promise => { const file = await Deno.readTextFile(preferencesFullPath) const parsed = JSON.parse(file) try { - prefs = UserRunrealConfigSchema.parse(parsed) + prefs = UserRunrealPreferencesSchema.parse(parsed) } catch (e) { console.error('Invalid preferences file:', e) return prefs @@ -32,7 +32,7 @@ const get = async (): Promise => { return prefs } -const set = async (prefs: UserRunrealConfig): Promise => { +const set = async (prefs: UserRunrealPreferences): Promise => { try { const fileInfo = await Deno.stat(preferencesFullPath) if (!fileInfo.isFile) { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index bb2c333..17d3cd9 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -61,6 +61,9 @@ export const ConfigSchema = z.object({ export const RunrealConfigSchema = ConfigSchema.merge(InternalSchema) -export const UserRunrealConfigSchema = z.object({ +// Deprecated +export const UserRunrealConfigSchema = ConfigSchema.deepPartial() + +export const UserRunrealPreferencesSchema = z.object({ accessToken: z.string().optional().describe('RUNREAL access token'), }) diff --git a/src/lib/types.ts b/src/lib/types.ts index dc9b7cb..c8c31c8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -8,7 +8,7 @@ import type { DebugConfigOptions } from '../commands/debug/debug-config.ts' import type { SetupOptions } from '../commands/engine/setup.ts' import type { InstallOptions } from '../commands/engine/install.ts' import type { UpdateOptions } from '../commands/engine/update.ts' -import type { ConfigSchema, InternalSchema, UserRunrealConfigSchema } from './schema.ts' +import type { ConfigSchema, InternalSchema, UserRunrealConfigSchema, UserRunrealPreferencesSchema } from './schema.ts' import type { Type } from '@cliffy/command' export type GlobalOptions = typeof cmd extends Command ? Options @@ -29,6 +29,8 @@ export type RunrealConfig = z.infer & InternalRunrealConfig export type UserRunrealConfig = z.infer +export type UserRunrealPreferences = z.infer + export interface UeDepsManifestData { Name: string Hash: string From 5dac115be352e6fbed7880199fce541bd2fbd39c Mon Sep 17 00:00:00 2001 From: Stephen Lacy Date: Tue, 15 Apr 2025 13:14:41 -0700 Subject: [PATCH 3/3] feat: cli auth with polling --- src/commands/auth.ts | 95 +++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 55 deletions(-) diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 18a9f8d..d99bb0b 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -7,37 +7,59 @@ import { preferences } from '../lib/preferences.ts' // base58 uppercase characters const idgen = customAlphabet('123456789ABCDEFGHJKMNPQRSTUVWXYZ', 6) +const RUNREAL_API_ENDPOINT = Deno.env.get('RUNREAL_API_ENDPOINT') || 'https://api.dashboard.runreal.dev/v1' const RUNREAL_AUTH_ENDPOINT = Deno.env.get('RUNREAL_AUTH_ENDPOINT') || 'https://auth.runreal.dev' -let _server: any = null -let _code: string | null = null export const auth = new Command() .description('auth') .arguments(' [args...]') .stopEarly() .action(async (options, command, ...args) => { if (command === 'login') { - _code = idgen(6) + const code = idgen(6) const hostname = Deno.hostname().replace(/[^a-z0-9A-Z-_\.]/, '') - let port = 56833 - for (let i = 0; i < 10; i++) { - try { - _server = Deno.serve({ port, onListen: () => {} }, handler) - break - } catch (e) { - port++ - if (i === 9) { - console.error(colors.red('Unable to start server. Please try again.')) - Deno.exit(1) - } + const link = `${RUNREAL_AUTH_ENDPOINT}/auth/cli?hostname=${hostname}&code=${code}` + + console.log(`Login code: ${colors.bold.green(code)}`) + console.log(`Please open ${colors.bold.cyan(link)} to authorize.`) + + let count = 0 + const interval = setInterval(async () => { + if (count > 20) { + clearInterval(interval) + console.log(colors.red('Authentication timed out.')) + shutdown() + return } - } - const link = `${RUNREAL_AUTH_ENDPOINT}/auth/cli?port=${port}&hostname=${hostname}&code=${_code}` + await fetch(`${RUNREAL_API_ENDPOINT}/cli`, { + method: 'POST', + body: JSON.stringify({ + hostname, + code, + }), + headers: { + 'Content-Type': 'application/json', + }, + }).then(async (res) => { + if (res.status === 200) { + clearInterval(interval) + const body = await res.json() + + await preferences.set({ + accessToken: body.token, + }) + console.log(colors.bold.cyan('Authenticated successfully!')) + shutdown() + } + }).catch((err) => { + console.log(err) + // ignore error + }) + count++ + }, 5000) - console.log(`Login code: ${colors.bold.green(_code)}`) - console.log(`Please open ${colors.bold.cyan(link)} to authorize.`) return } @@ -56,43 +78,6 @@ export const auth = new Command() throw new Error('Invalid command. Use "login" or "logout".') }) -async function handler(req: Request): Promise { - const url = new URL(req.url) - const headers = new Headers() - - headers.append('Access-Control-Allow-Origin', RUNREAL_AUTH_ENDPOINT) - headers.append('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') - headers.append('Access-Control-Allow-Headers', 'Content-Type') - - if (url.pathname !== '/auth/callback') { - return new Response('Not found', { status: 404, headers }) - } - if (req.method === 'OPTIONS') { - return new Response('OK', { status: 200, headers }) - } - - try { - const body = await req.json() - if (!body.access_token || body.code !== _code) { - console.log(colors.red('Unable to authenticate. Please try again.')) - shutdown() - return new Response('Invalid request', { status: 400, headers }) - } - - await preferences.set({ - accessToken: body.access_token, - }) - } catch (e) { - console.log(colors.red('Unable to authenticate. Please try again.')) - shutdown() - return new Response('Invalid request', { status: 400, headers }) - } - - console.log(colors.bold.cyan('Authenticated successfully!')) - shutdown() - return new Response('OK', { status: 200, headers }) -} - function shutdown() { setTimeout(() => { Deno.exit(0)