From b4c68b25eb0763151e27873bbd830563f064b41e Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 6 Aug 2026 10:58:29 +0200 Subject: [PATCH 1/4] chore: prepare token 1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b92fac8..818cdc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typetype-token", - "version": "1.3.1", + "version": "1.4.0", "private": true, "license": "MIT", "scripts": { From 263bfb1c22894fdb371707641553dd7b7864907e Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 7 Aug 2026 22:28:37 +0200 Subject: [PATCH 2/4] fix: expose remote login readiness --- src/remote-login-browser.ts | 24 ++++-- src/remote-login-manager.ts | 16 +++- src/remote-login-messages.ts | 10 +++ src/remote-login-routes.ts | 16 +++- tests/remote-login-readiness.test.ts | 108 +++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 tests/remote-login-readiness.test.ts diff --git a/src/remote-login-browser.ts b/src/remote-login-browser.ts index 5e7846f..52072b7 100644 --- a/src/remote-login-browser.ts +++ b/src/remote-login-browser.ts @@ -64,6 +64,16 @@ function launchOptions(config: RemoteLoginConfig): Parameters[0] { + return { + acceptDownloads: false, + viewport: { width: config.viewportWidth, height: config.viewportHeight }, + deviceScaleFactor: config.deviceScaleFactor, + userAgent: config.userAgent, + locale: config.locale, + }; +} + async function ensureRemoteBrowser(config: RemoteLoginConfig): Promise { if (remoteBrowser?.isConnected()) return remoteBrowser; remoteBrowser = await chromium.launch(launchOptions(config)); @@ -123,13 +133,7 @@ export async function createRemoteLoginPage( onPoToken: (poToken: string) => void, ): Promise { const browser = await ensureRemoteBrowser(config); - const context = await browser.newContext({ - acceptDownloads: false, - viewport: { width: config.viewportWidth, height: config.viewportHeight }, - deviceScaleFactor: config.deviceScaleFactor, - userAgent: config.userAgent, - locale: config.locale, - }); + const context = await browser.newContext(contextOptions(config)); const page = await context.newPage(); page.on("request", (request) => { const poToken = capturePot(request.url()); @@ -145,6 +149,12 @@ export async function createRemoteLoginPage( }; } +export async function probeRemoteLoginBrowser(config: RemoteLoginConfig): Promise { + const browser = await ensureRemoteBrowser(config); + const context = await browser.newContext(contextOptions(config)); + await context.close(); +} + export async function closeRemoteLoginBrowser(): Promise { if (!remoteBrowser) return; await remoteBrowser.close().catch(() => undefined); diff --git a/src/remote-login-manager.ts b/src/remote-login-manager.ts index 12e35c1..4f5003d 100644 --- a/src/remote-login-manager.ts +++ b/src/remote-login-manager.ts @@ -1,4 +1,4 @@ -import { createRemoteLoginPage } from "./remote-login-browser.ts"; +import { createRemoteLoginPage, probeRemoteLoginBrowser } from "./remote-login-browser.ts"; import type { RemoteLoginCompletionTarget } from "./remote-login-callback.ts"; import type { RemoteLoginConfig } from "./remote-login-config.ts"; import type { RemoteLoginStartResponse } from "./remote-login-messages.ts"; @@ -14,18 +14,32 @@ export type RemoteLoginStartOptions = { target: RemoteLoginCompletionTarget; }; +export type RemoteLoginRuntimeProbe = (config: RemoteLoginConfig) => Promise; + export class RemoteLoginManager { private readonly config: RemoteLoginConfig; private readonly createPage: RemoteLoginPageFactory; + private readonly probeRuntime: RemoteLoginRuntimeProbe; private readonly sessions = new Map(); private readonly byUser = new Map(); constructor( config: RemoteLoginConfig, createPage: RemoteLoginPageFactory = createRemoteLoginPage, + probeRuntime: RemoteLoginRuntimeProbe = probeRemoteLoginBrowser, ) { this.config = config; this.createPage = createPage; + this.probeRuntime = probeRuntime; + } + + async isReady(): Promise { + try { + await this.probeRuntime(this.config); + return true; + } catch { + return false; + } } async start(options: RemoteLoginStartOptions): Promise { diff --git a/src/remote-login-messages.ts b/src/remote-login-messages.ts index 7844544..274e50a 100644 --- a/src/remote-login-messages.ts +++ b/src/remote-login-messages.ts @@ -12,6 +12,10 @@ export type RemoteLoginStartResponse = { expiresAt: number; }; +export type RemoteLoginReadinessRequest = { + callbackUrl: string; +}; + export type PointerButton = "left" | "middle" | "right"; export type RemoteLoginInput = @@ -52,6 +56,12 @@ export function parseStartRequest(value: unknown): RemoteLoginStartRequest | nul return { serverSessionId, userId, callbackUrl, ttlMs }; } +export function parseReadinessRequest(value: unknown): RemoteLoginReadinessRequest | null { + if (!isRecord(value)) return null; + const callbackUrl = stringValue(value.callbackUrl, 2048); + return callbackUrl ? { callbackUrl } : null; +} + export function parseRemoteLoginInput(raw: string): RemoteLoginInput | null { const parsed = (() => { try { diff --git a/src/remote-login-routes.ts b/src/remote-login-routes.ts index 5f60b57..7281cee 100644 --- a/src/remote-login-routes.ts +++ b/src/remote-login-routes.ts @@ -5,7 +5,7 @@ import { type RemoteLoginConfig, } from "./remote-login-config.ts"; import type { RemoteLoginManager } from "./remote-login-manager.ts"; -import { parseStartRequest } from "./remote-login-messages.ts"; +import { parseReadinessRequest, parseStartRequest } from "./remote-login-messages.ts"; import type { RemoteLoginConnection } from "./remote-login-session-types.ts"; export type RemoteLoginWebSocketData = { @@ -65,6 +65,20 @@ export function createRemoteLoginHandler(manager: RemoteLoginManager, config: Re url: URL, server?: Server, ): Promise => { + if (url.pathname === "/youtube-remote-login/readiness" && req.method === "POST") { + const denied = requireInternal(req, config); + if (denied) return denied; + const body = parseReadinessRequest(await requestBody(req)); + if (!body) return Response.json({ error: "invalid request body" }, { status: 400 }); + if (!isRemoteLoginCallbackAllowed(body.callbackUrl, config)) { + return Response.json({ error: "callbackUrl is not allowed" }, { status: 400 }); + } + if (!(await manager.isReady())) { + return Response.json({ error: "remote browser is unavailable" }, { status: 503 }); + } + return new Response(null, { status: 204 }); + } + if (url.pathname === "/youtube-remote-login/start" && req.method === "POST") { const denied = requireInternal(req, config); if (denied) return denied; diff --git a/tests/remote-login-readiness.test.ts b/tests/remote-login-readiness.test.ts new file mode 100644 index 0000000..3340d9e --- /dev/null +++ b/tests/remote-login-readiness.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from "bun:test"; +import { REMOTE_LOGIN_INTERNAL_HEADER } from "../src/remote-login-config.ts"; +import { RemoteLoginManager } from "../src/remote-login-manager.ts"; +import { createRemoteLoginHandler } from "../src/remote-login-routes.ts"; +import { fakeRemoteLoginPage, remoteLoginTestConfig } from "./remote-login-fixtures.ts"; + +const readinessUrl = new URL("http://localhost:8081/youtube-remote-login/readiness"); + +function readinessRequest(callbackUrl: string, token = "internal-secret"): Request { + return new Request(readinessUrl, { + method: "POST", + headers: { + "content-type": "application/json", + [REMOTE_LOGIN_INTERNAL_HEADER]: token, + }, + body: JSON.stringify({ callbackUrl }), + }); +} + +describe("remote login readiness", () => { + it("returns 404 when remote login is disabled", async () => { + const config = remoteLoginTestConfig({ enabled: false }); + const manager = new RemoteLoginManager(config, async () => fakeRemoteLoginPage()); + const handler = createRemoteLoginHandler(manager, config); + + const res = await handler( + readinessRequest("http://typetype-server.local/internal/callback"), + readinessUrl, + ); + + expect(res?.status).toBe(404); + }); + + it("returns 503 when the internal token is missing", async () => { + const config = remoteLoginTestConfig({ internalToken: null }); + const manager = new RemoteLoginManager(config, async () => fakeRemoteLoginPage()); + const handler = createRemoteLoginHandler(manager, config); + + const res = await handler( + readinessRequest("http://typetype-server.local/internal/callback"), + readinessUrl, + ); + + expect(res?.status).toBe(503); + }); + + it("reports readiness when the callback and browser runtime are usable", async () => { + const config = remoteLoginTestConfig(); + const manager = new RemoteLoginManager( + config, + async () => fakeRemoteLoginPage(), + async () => undefined, + ); + const handler = createRemoteLoginHandler(manager, config); + + const res = await handler( + readinessRequest("http://typetype-server.local/internal/callback"), + readinessUrl, + ); + + expect(res?.status).toBe(204); + }); + + it("rejects a wrong shared secret", async () => { + const config = remoteLoginTestConfig(); + const manager = new RemoteLoginManager(config, async () => fakeRemoteLoginPage()); + const handler = createRemoteLoginHandler(manager, config); + + const res = await handler( + readinessRequest("http://typetype-server.local/internal/callback", "bad-token"), + readinessUrl, + ); + + expect(res?.status).toBe(401); + }); + + it("rejects a callback origin outside the allowlist", async () => { + const config = remoteLoginTestConfig(); + const manager = new RemoteLoginManager(config, async () => fakeRemoteLoginPage()); + const handler = createRemoteLoginHandler(manager, config); + + const res = await handler( + readinessRequest("http://other.local/internal/callback"), + readinessUrl, + ); + + expect(res?.status).toBe(400); + }); + + it("rejects an unavailable browser runtime", async () => { + const config = remoteLoginTestConfig(); + const manager = new RemoteLoginManager( + config, + async () => fakeRemoteLoginPage(), + async () => { + throw new Error("browser unavailable"); + }, + ); + const handler = createRemoteLoginHandler(manager, config); + + const res = await handler( + readinessRequest("http://typetype-server.local/internal/callback"), + readinessUrl, + ); + + expect(res?.status).toBe(503); + }); +}); From 20a5601a6b128f2fae457d3f4e45bd3a90242971 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 8 Aug 2026 12:50:04 +0200 Subject: [PATCH 3/4] chore: update token dependencies --- Dockerfile | 2 +- bun.lock | 30 +++++++++++++++--------------- package.json | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index bd0e16a..9d2b272 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY package.json bun.lock ./ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 RUN bun install --frozen-lockfile --production -FROM mcr.microsoft.com/playwright:v1.62.0-noble AS runner +FROM mcr.microsoft.com/playwright:v1.62.1-noble AS runner ARG BUILD_VERSION=1.2.4-dev ARG BUILD_REVISION=development ARG BUILD_TIME=unknown diff --git a/bun.lock b/bun.lock index 0b795bf..5d87ae1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,13 @@ "": { "name": "typetype-token", "dependencies": { - "bgutils-js": "^4.0.2", + "bgutils-js": "^4.0.3", "googlevideo": "^4.1.1", - "playwright": "^1.62.0", + "playwright": "^1.62.1", "youtubei.js": "^17.2.0", }, "devDependencies": { - "@biomejs/biome": "^2.5.6", + "@biomejs/biome": "^2.5.7", "bun-types": "^1.3.14", }, }, @@ -20,29 +20,29 @@ "@biomejs/biome", ], "packages": { - "@biomejs/biome": ["@biomejs/biome@2.5.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.6", "@biomejs/cli-darwin-x64": "2.5.6", "@biomejs/cli-linux-arm64": "2.5.6", "@biomejs/cli-linux-arm64-musl": "2.5.6", "@biomejs/cli-linux-x64": "2.5.6", "@biomejs/cli-linux-x64-musl": "2.5.6", "@biomejs/cli-win32-arm64": "2.5.6", "@biomejs/cli-win32-x64": "2.5.6" }, "bin": { "biome": "bin/biome" } }, "sha512-lxVNjv7UF6KfhMJfL9gaUHbWdJdHbsAj6OSmwSYNdhRuG67NxNQ4Xdvh3TUxsSK9sBzJBQhEJj3AopmmNJ5pSA=="], + "@biomejs/biome": ["@biomejs/biome@2.5.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.7", "@biomejs/cli-darwin-x64": "2.5.7", "@biomejs/cli-linux-arm64": "2.5.7", "@biomejs/cli-linux-arm64-musl": "2.5.7", "@biomejs/cli-linux-x64": "2.5.7", "@biomejs/cli-linux-x64-musl": "2.5.7", "@biomejs/cli-win32-arm64": "2.5.7", "@biomejs/cli-win32-x64": "2.5.7" }, "bin": { "biome": "bin/biome" } }, "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zMOLZP4oMrjh6m1zcSj1ud2awUPgTuMVbmQhYYWL7J8HwCnbHHBvTm7VBTRuY7epT5bez76IpKYQ11ZAqHFlnw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JAC1VqzvO7Th5ZplU0G2uGfkZbxEe9uDDektPAhF0JLusoz1w+T4okp2bkykI0bbaO2vslKiRfj4gU43JaGreA=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-6XsYwCFkp5sMxl85ffhgeGpGgs6A7dRYFnkceZ7WVxvycuTnGdD5xa534Z3xfrBQ0JCMK/mujT6ZNPJoghedwg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eUa3jeeYvfMt19LBeh6E5PUZpxnTC4JqNWo+EDjTtQjAr2xLGnWaxACtVU1DQqmHYbvThlJzLX+ZsYgrqh2qVw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Pop9VXCFUhFTMfFefZ39S+u2rOPyNp5iHlxbZRwXGACHLy2r0jjiRgJHmaEKJzL3SyxlVeGShXhvvElvWowonA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2Vp13QdKysH3HIWLaYLhUUwbK+jbZonJD1K+Lr0d0RO4wH7mkYd43vJixEDm8cUWrowoRz4UUHF1nm9Ae7ym8A=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-tDGshcm6BdkZOCGnTDX0Y8/U4IfBSlnUU7T56nNDuPEfed+aHg+u8G36NB43fJVl0Os6+QURXIE1yuD7AaEofA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-WN05KwXnTO/2J45RQPvzZMXf7tZUIofHoR35xIPfCo7pQ2RFidxI8sfb5mGsaTxdMmEOzHzOPRCdA5/fCpc7xQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ=="], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.1", "", {}, "sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg=="], "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], - "bgutils-js": ["bgutils-js@4.0.2", "", {}, "sha512-PyZRxSkwC2MyVtIidD0DejgeNQfb9p3N/wY/d9sa3zAFeMTg6nDMv99wPYoUMU8JhzgG22ZVTnCzAcbfzKPVfQ=="], + "bgutils-js": ["bgutils-js@4.0.3", "", {}, "sha512-nvhrzqRYqFcC3AJbf1fENc/jzyMk95wH9UC9ruQzTZCq+itYWuHi2a9leK/bC1lLfi+R4a28wJZxpD5KiTFf+w=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -54,9 +54,9 @@ "meriyah": ["meriyah@6.1.4", "", {}, "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ=="], - "playwright": ["playwright@1.62.0", "", { "dependencies": { "playwright-core": "1.62.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw=="], + "playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], - "playwright-core": ["playwright-core@1.62.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA=="], + "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], diff --git a/package.json b/package.json index 818cdc7..cc4d8c2 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,13 @@ "format": "biome format --write src tests" }, "dependencies": { - "bgutils-js": "^4.0.2", + "bgutils-js": "^4.0.3", "googlevideo": "^4.1.1", - "playwright": "^1.62.0", + "playwright": "^1.62.1", "youtubei.js": "^17.2.0" }, "devDependencies": { - "@biomejs/biome": "^2.5.6", + "@biomejs/biome": "^2.5.7", "bun-types": "^1.3.14" }, "trustedDependencies": [ From a6e65de8a76b5ff41ea3cc1d423b063a943aa559 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Mon, 10 Aug 2026 18:39:34 +0200 Subject: [PATCH 4/4] chore: prepare token 1.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc4d8c2..b961d4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typetype-token", - "version": "1.4.0", + "version": "1.5.0", "private": true, "license": "MIT", "scripts": {