diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md index 7c48740..2af4c03 100644 --- a/apps/web/CLAUDE.md +++ b/apps/web/CLAUDE.md @@ -95,13 +95,14 @@ shared → ./src/shared ### HTTP Client -`utils/api.ts` — Axios instance with interceptors (`baseURL: "/api"`). It unwraps both -the axios envelope and the backend's `{ data }` envelope, so callers get the payload -directly. All API calls proxy through the dev server (see `vite.config.ts`). +`utils/api.ts` — 薄薄一层 fetch 封装(没有 axios),前缀 `/api`,get / post / put / delete +四个方法。它剥掉后端的 `{ data }` 信封,调用方直接拿到业务数据;失败抛 `ApiError`。 +dev 下由 `vite.config.ts` 的 proxy 转给后端。 登录态是 `Authorization: Bearer` 的 access token(15 分钟)+ refresh token,都在 -localStorage,换 token 的逻辑全在 `utils/auth.ts`。**不走 axios 的请求用 `authFetch` -代替裸 `fetch`**,否则不带 token、也不会在 `token-expired` 时换一张重试。 +localStorage,换 token 的逻辑全在 `utils/auth.ts`。**所有请求都从 `authFetch` 出门** +(`utils/api` 也是),要自己发 fetch(流式、`keepalive`、下载二进制)时用它代替裸 +`fetch`,否则不带 token、也不会在 `token-expired` 时换一张重试。 WebSocket 的 token 借子协议带(浏览器设不了请求头),见契约 `WS_AUTH_PROTOCOL`。 ### Contract guard (`utils/contract.ts`) @@ -179,7 +180,7 @@ Env files: `.env`(本机)、`.env.production`(服务器)、`.env.staging | `PUBLIC_CODE_URL` | 代码分享服务(提交详情、题目页的「分享」) | | `PUBLIC_ICONIFY_URL` | 自建 Iconify 图标源,不设则走公共 CDN | -后端地址**不在这里**:`utils/api.ts` 写死 `baseURL: "/api"`,dev 由 `vite.config.ts` 的 +后端地址**不在这里**:`utils/api.ts` 写死前缀 `/api`,dev 由 `vite.config.ts` 的 proxy 转给 3000,线上由 Caddy 同源伺服。 ### Routing diff --git a/apps/web/package.json b/apps/web/package.json index 4453ee5..65196b3 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -26,7 +26,6 @@ "@vueuse/router": "^15.0.0", "@wangeditor-next/editor": "^6.5.0", "@wangeditor-next/editor-for-vue": "^6.5.0", - "axios": "^1.20.0", "canvas-confetti": "^1.9.4", "chart.js": "^4.5.1", "chartjs-chart-wordcloud": "^4.4.5", diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index 10ced12..56c3efe 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -304,9 +304,7 @@ export async function uploadImage(file: File): Promise { success: boolean filePath: string msg: string - }>("admin/upload-image", form, { - headers: { "content-type": "multipart/form-data" }, - }) + }>("admin/upload-image", form) return res.success ? res.filePath : "" } @@ -317,9 +315,7 @@ export function uploadTestcases(file: File, options: { sql?: boolean } = {}) { if (options.sql) { form.append("sql", "1") } - return api.post("admin/test-cases", form, { - headers: { "content-type": "multipart/form-data" }, - }) + return api.post("admin/test-cases", form) } // SQL 题测试点预览:后端跑一遍初始化脚本+标准答案,返回数据表和期望结果展示数据 diff --git a/apps/web/src/oj/api.ts b/apps/web/src/oj/api.ts index 66ccea0..382cf70 100644 --- a/apps/web/src/oj/api.ts +++ b/apps/web/src/oj/api.ts @@ -93,7 +93,7 @@ export function getProblemProgress() { /** * 「随便来一道」:当前知识点和类型里,先挑能用 language 做的、自己没做对的简单 / 中等题, - * 返回题号。空串的参数拦截器会去掉 + * 返回题号。空串的参数 utils/api 会去掉 */ export function getRandomProblem(params: { tag: string; type: string; language: string }) { return api.get("problems/random", { params }) @@ -357,9 +357,7 @@ export function getContestClassView(contestID: string) { export function uploadAvatar(file: File) { const form = new window.FormData() form.append("image", file) - return api.post("me/avatar", form, { - headers: { "content-type": "multipart/form-data" }, - }) + return api.post("me/avatar", form) } export function updateProfile(data: { realName?: string; mood?: string }) { @@ -551,8 +549,8 @@ export function getLearnProgress(type: "python" | "c") { /** * 上报自学留痕。`opened` 为真表示刚进这一课,否则只是补停留时长。 * - * 走裸 fetch 而不是 axios,是为了 `keepalive`:离开页面那一下的最后一次上报, - * axios 发出去也会随页面卸载被浏览器掐掉,学生每节课的最后一段时长就永远丢了。 + * 直接走 authFetch 而不是 utils/api,是为了 `keepalive`:离开页面那一下的最后一次上报, + * 不带它会随页面卸载被浏览器掐掉,学生每节课的最后一段时长就永远丢了。 * 失败一律吞掉 —— 留痕是旁路,不该让学生看到任何报错。 */ export function reportLearnProgress( diff --git a/apps/web/src/utils/api.ts b/apps/web/src/utils/api.ts index 9abceed..12b4c37 100644 --- a/apps/web/src/utils/api.ts +++ b/apps/web/src/utils/api.ts @@ -1,11 +1,6 @@ -import axios, { - type AxiosRequestConfig, - type AxiosResponse, - type InternalAxiosRequestConfig, -} from "axios" import { createDiscreteApi } from "naive-ui" import { useAuthModalStore } from "shared/store/authModal" -import { getAccessToken, refreshAccessToken } from "./auth" +import { authFetch } from "./auth" import { STORAGE_KEY } from "./constants" import storage from "./storage" @@ -20,7 +15,7 @@ interface ErrorPayload { } /** - * 拦截器 reject 出来的就是它:`error` 是错误码,`data` 是后端文案。 + * 请求失败时抛的就是它:`error` 是错误码,`data` 是后端文案。 * * 字段名沿用原来那个裸对象 `{ error, data }`,存量调用点读 `err.error` / `err.data` * 的语义不变;做成 Error 的子类是为了在 catch 里能和代码 bug(TypeError 之类)分开, @@ -50,26 +45,12 @@ export function errorMessage(err: unknown, fallback = "未知错误") { return isApiError(err) && err.data ? err.data : fallback } -/** - * 成功时直接拿到业务数据本身。后端成功响应是 `{ data }`(见 apps/api/src/http.ts - * 的 success),拦截器把 axios 外层和这层信封一起剥掉。 - * - * 失败走 reject,形状是 `{ error: 错误码, data: 文案 }` —— 和成功路径不对称是 - * 故意的:成功没有错误码可言。分支处理一律判 `err.error` 里的错误码。 - */ -interface ApiClient { - get(url: string, config?: AxiosRequestConfig): Promise - post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise - put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise - delete(url: string, config?: AxiosRequestConfig): Promise -} - /** * 这几种错误全站都是同样的处理,不放在这里的话每个调用点都得自己 catch, * 漏一个就是「点了没反应」。需要分支处理的调用方一律判 `err.error` 里的 * 错误码,**不要**去 match `err.data` 的文案 —— 文案是后端可以随时改的。 * - * 导出给不走 axios 的请求用(AI 的流式接口是裸 fetch,见 utils/stream.ts)—— + * 导出给不走 api 客户端的请求用(AI 的流式接口,见 utils/stream.ts)—— * 同一个错误码从两条路进来,学生看到的结果不该有两个样子。 */ /** handleGlobalApiError 已经替调用方提示过的错误码,调用方别再弹第二遍 */ @@ -94,68 +75,69 @@ export function handleGlobalApiError(code: string, message?: string) { } } -const instance = axios.create({ - baseURL: "/api", -}) +const NETWORK_ERROR_MESSAGE = "网络异常,请检查网络后重试" -/** 已经因为 token-expired 重试过一次的请求,别再进第二轮,也别再被请求拦截器换 token */ -type RetriableConfig = InternalAxiosRequestConfig & { authRetried?: boolean } +interface RequestConfig { + /** 拼进 query string。值是 ""、null、undefined 的键直接丢掉,调用方不用自己挑 */ + params?: Record + /** 请求体,只给 delete 用:别的方法的请求体走第二个参数 */ + data?: unknown +} -instance.interceptors.request.use(async (config: RetriableConfig) => { - if (!config.authRetried) { - const token = await getAccessToken() - if (token) config.headers.set("Authorization", `Bearer ${token}`) +function withQuery(url: string, params?: Record) { + const query = new URLSearchParams() + for (const [key, value] of Object.entries(params ?? {})) { + if (value !== "" && value !== null && value !== undefined) query.append(key, String(value)) } - if (config.params) { - config.params = Object.fromEntries( - Object.entries(config.params).filter( - ([, value]) => value !== "" && value !== null && value !== undefined, - ), - ) + const search = query.toString() + return search ? `${url}?${search}` : url +} + +async function request(method: string, url: string, body: unknown, config: RequestConfig = {}) { + const init: RequestInit = { method } + // FormData 不能手动设 Content-Type:multipart 的 boundary 得让浏览器自己写进去 + if (body instanceof FormData) { + init.body = body + } else if (body !== undefined) { + init.headers = { "Content-Type": "application/json" } + init.body = JSON.stringify(body) } - return config -}) -instance.interceptors.response.use( - // 这里**故意**不返回 AxiosResponse:把 axios 的外层和后端的 { data } 信封一起 - // 剥掉,让调用方直接拿到业务数据。类型上和 axios 的拦截器签名对不上(它期望原样 - // 返回响应),文件末尾的 `as unknown as ApiClient` 就是为了把真实形状交出去。 - ((response: AxiosResponse) => response.data.data) as unknown as ( - response: AxiosResponse, - ) => AxiosResponse, - async (error) => { - const payload = error.response?.data as ErrorPayload | undefined - const code = payload?.error?.code ?? "network-error" - // 没有响应体 = 请求根本没到后端(断网、反代挂了、超时),学生看到的就是这一句 - const message = payload?.error?.message ?? "网络异常,请检查网络后重试" + let response: Response + try { + response = await authFetch(withQuery(`/api/${url}`, config.params), init) + } catch { + // 请求根本没到后端(断网、反代挂了),学生看到的就是这一句 + throw new ApiError("network-error", NETWORK_ERROR_MESSAGE) + } + const payload = (await response.json().catch(() => null)) as ({ data?: T } & ErrorPayload) | null + if (response.ok) return payload?.data as T - // access token 不能用了(过期、被吊销):拿 refresh token 换一张,原请求重发一次。 - // 重发的结果照样走这两个拦截器,调用方拿到的和一次成功没有区别 - const config = error.config as RetriableConfig | undefined - if (code === "token-expired" && config && !config.authRetried) { - config.authRetried = true - const sent = String(config.headers.get("Authorization") ?? "").replace(/^Bearer /, "") - const outcome = await refreshAccessToken(sent) - if (outcome.token) { - config.headers.set("Authorization", `Bearer ${outcome.token}`) - return instance.request(config) - } - if (outcome.code === "network-error") { - return Promise.reject(new ApiError("network-error", "网络异常,请检查网络后重试")) - } - if (outcome.code === "account-disabled") { - handleGlobalApiError("account-disabled") - return Promise.reject(new ApiError("account-disabled", "账号已被禁用,请联系老师")) - } - // 登录真的过期了。拿掉 token 重发:要登录的接口回 login-required 弹登录框, - // 可选登录的(首页、题目列表……)照常当游客看,不平白弹框 - config.headers.delete("Authorization") - return instance.request(config) - } + // 没有 JSON 信封 = 不是后端回的(网关的错误页之类),当网络问题 + const code = payload?.error?.code ?? "network-error" + const message = payload?.error?.message ?? NETWORK_ERROR_MESSAGE + handleGlobalApiError(code, message) + throw new ApiError(code, message) +} - handleGlobalApiError(code, message) - return Promise.reject(new ApiError(code, message)) - }, -) +/** + * 成功时直接拿到业务数据本身:后端成功响应是 `{ data }`(见 apps/api/src/http.ts + * 的 success),这层信封在这里剥掉。 + * + * 失败抛 ApiError,`error` 是错误码、`data` 是文案 —— 和成功路径不对称是故意的: + * 成功没有错误码可言。分支处理一律判 `err.error` 里的错误码。 + * + * token 过期换一张重发、换不到当游客重发,都在 authFetch 里,调用方拿到的和一次 + * 成功没有区别。 + */ +const api = { + get: (url: string, config?: RequestConfig) => request("GET", url, undefined, config), + post: (url: string, data?: unknown, config?: RequestConfig) => + request("POST", url, data, config), + put: (url: string, data?: unknown, config?: RequestConfig) => + request("PUT", url, data, config), + delete: (url: string, config?: RequestConfig) => + request("DELETE", url, config?.data, config), +} -export default instance as unknown as ApiClient +export default api diff --git a/apps/web/src/utils/auth.ts b/apps/web/src/utils/auth.ts index 079ca2c..f11cba9 100644 --- a/apps/web/src/utils/auth.ts +++ b/apps/web/src/utils/auth.ts @@ -86,7 +86,7 @@ async function runRefresh(stale?: string): Promise { let response: Response try { - // 裸 fetch:走 utils/api 的 axios 会再进它的拦截器,绕回这里 + // 裸 fetch:走 authFetch / utils/api 会再来换 token,绕回这里 response = await fetch("/api/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -135,9 +135,11 @@ async function responseCode(response: Response) { } /** - * 不走 axios 的请求(AI 流式接口、离开页面时的 keepalive 上报)用它代替 fetch: - * 带上 token,后端回 `token-expired` 就换一张重试一次 —— 和 utils/api.ts 拦截器 - * 是同一套规矩。 + * 带登录态的 fetch,所有发往后端的请求都从这里出门(utils/api 也是):带上 token, + * 后端回 `token-expired` 就换一张重试一次。 + * + * 换 token 那一步断了网就照 fetch 的样子 reject —— 这时候拿掉 token 重发,要登录的 + * 接口会回 login-required,学生平白看到登录框,而真正的原因只是网络抖了一下。 */ export async function authFetch(input: string, init: RequestInit = {}) { const send = (token: string | null) => @@ -154,6 +156,7 @@ export async function authFetch(input: string, init: RequestInit = {}) { if ((await responseCode(response)) !== "token-expired") return response const outcome = await refreshAccessToken(token) + if (outcome.code === "network-error") throw new TypeError("换 token 时网络异常") // 账号被禁用:换 token 那一步就知道了,原请求不必再发。造一个和后端同形的响应, // 调用方按原样处理(弹「账号已被禁用」而不是登录框) if (outcome.code === "account-disabled") { diff --git a/apps/web/src/utils/download.ts b/apps/web/src/utils/download.ts index ec2451b..d3878ef 100644 --- a/apps/web/src/utils/download.ts +++ b/apps/web/src/utils/download.ts @@ -1,58 +1,35 @@ -import axios from "axios" import { createDiscreteApi } from "naive-ui" -import { getAccessToken } from "./auth" - -// 指向新后端的 /api/admin。响应是 zip 二进制,不走 { error, data } 信封, -// 所以不能复用 utils/api 的拦截器(它会把 response.data.data 取出来)。 -const http = axios.create({ - baseURL: "/api/admin", - responseType: "blob", -}) - -http.interceptors.request.use(async (config) => { - const token = await getAccessToken() - if (token) config.headers.set("Authorization", `Bearer ${token}`) - return config -}) +import { authFetch } from "./auth" // 脱离 n-message-provider 也能弹,理由同 utils/api.ts const { message: toast } = createDiscreteApi(["message"]) -/** - * 失败时把错误信封读出来。`responseType: "blob"` 是给成功路径(zip)设的, - * 出错时 axios 照样把 JSON 信封包成 Blob 交回来,直接 `.error.message` 取不到。 - */ -async function failureMessage(error: unknown) { - const data = (error as { response?: { data?: unknown } }).response?.data - if (data instanceof Blob) { - try { - const payload = JSON.parse(await data.text()) - const message = payload?.error?.message - if (typeof message === "string" && message) return message - } catch { - // 不是 JSON(网关的 HTML 错误页之类),走下面的兜底文案 - } - } - return "下载失败" +/** 失败时读后端的错误信封;不是 JSON(网关的 HTML 错误页之类)就用兜底文案 */ +async function failureMessage(response: Response) { + const payload = (await response.json().catch(() => null)) as { + error?: { message?: string } + } | null + const message = payload?.error?.message + return typeof message === "string" && message ? message : "下载失败" } +// 指向 /api/admin。响应是 zip 二进制,不走 { error, data } 信封,所以不经 utils/api async function download(url: string) { - let res + let response: Response try { - res = await http.get(url) - } catch (error) { + response = await authFetch(`/api/admin/${url}`) + } catch { + toast.error("下载失败") + return + } + if (!response.ok) { // 以前这里什么都不接:题目还没传测试点时后端回 404,页面上一点反应都没有 - toast.error(await failureMessage(error)) + toast.error(await failureMessage(response)) return } - const headers = res.headers const link = document.createElement("a") - link.href = window.URL.createObjectURL( - new window.Blob([res.data], { - type: String(headers["content-type"] ?? ""), - }), - ) - link.download = (headers["content-disposition"] || "").split("filename=")[1] + link.href = window.URL.createObjectURL(await response.blob()) + link.download = (response.headers.get("content-disposition") || "").split("filename=")[1] ?? "" document.body.appendChild(link) link.click() link.remove() diff --git a/apps/web/src/utils/stream.ts b/apps/web/src/utils/stream.ts index c150194..2d7166c 100644 --- a/apps/web/src/utils/stream.ts +++ b/apps/web/src/utils/stream.ts @@ -100,8 +100,8 @@ export async function consumeJSONEventStream( * AI 端点的非 2xx 响应体是 JSON 不是 SSE,直接丢给上面的解析器只会抛 * 「无法解析服务端事件数据: {...}」。后端 error.message 是英文的,按 code 换成中文。 * - * 登录失效 / 账号禁用走和 axios 拦截器同一套处理(弹登录框 / 提示禁用)—— - * 原来这里裸 fetch 绕过了拦截器,未登录时只显示一句「AI 分析生成失败」。 + * 登录失效 / 账号禁用走和 utils/api 同一套处理(弹登录框 / 提示禁用)—— + * 原来这里裸 fetch 绕过了那边,未登录时只显示一句「AI 分析生成失败」。 */ async function aiStreamError(response: Response) { const body = (await response.json().catch(() => null)) as { diff --git a/bun.lock b/bun.lock index fa93bbe..6cf4855 100644 --- a/bun.lock +++ b/bun.lock @@ -55,7 +55,6 @@ "@vueuse/router": "^15.0.0", "@wangeditor-next/editor": "^6.5.0", "@wangeditor-next/editor-for-vue": "^6.5.0", - "axios": "^1.20.0", "canvas-confetti": "^1.9.4", "chart.js": "^4.5.1", "chartjs-chart-wordcloud": "^4.4.5", @@ -839,8 +838,6 @@ "acorn": ["acorn@8.18.0", "https://registry.npmjs.com/acorn/-/acorn-8.18.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], - "agent-base": ["agent-base@6.0.2", "https://registry.npmjs.com/agent-base/-/agent-base-6.0.2.tgz", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "alien-signals": ["alien-signals@3.2.1", "https://registry.npmjs.com/alien-signals/-/alien-signals-3.2.1.tgz", {}, "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g=="], "ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmjs.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -859,10 +856,6 @@ "async-validator": ["async-validator@4.2.5", "https://registry.npmjs.com/async-validator/-/async-validator-4.2.5.tgz", {}, "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="], - "asynckit": ["asynckit@0.4.0", "https://registry.npmjs.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "axios": ["axios@1.20.0", "https://registry.npmjs.com/axios/-/axios-1.20.0.tgz", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.17", "https://registry.npmjs.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-define-polyfill-provider": "^0.6.8", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w=="], "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.14.2", "https://registry.npmjs.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.8", "core-js-compat": "^3.48.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g=="], @@ -883,8 +876,6 @@ "bun-types": ["bun-types@1.4.0", "https://registry.npmjs.com/bun-types/-/bun-types-1.4.0.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmjs.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001810", "https://registry.npmjs.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], "canvas-confetti": ["canvas-confetti@1.9.4", "https://registry.npmjs.com/canvas-confetti/-/canvas-confetti-1.9.4.tgz", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="], @@ -901,8 +892,6 @@ "codemirror": ["codemirror@6.0.2", "https://registry.npmjs.com/codemirror/-/codemirror-6.0.2.tgz", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="], - "combined-stream": ["combined-stream@1.0.8", "https://registry.npmjs.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@2.20.3", "https://registry.npmjs.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "https://registry.npmjs.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], @@ -937,8 +926,6 @@ "debug": ["debug@4.4.3", "https://registry.npmjs.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmjs.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "denque": ["denque@2.1.0", "https://registry.npmjs.com/denque/-/denque-2.1.0.tgz", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], "dequal": ["dequal@2.0.3", "https://registry.npmjs.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -953,22 +940,14 @@ "drizzle-orm": ["drizzle-orm@0.45.4", "https://registry.npmjs.com/drizzle-orm/-/drizzle-orm-0.45.4.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@netlify/db": ">=0.4.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@netlify/db", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-NiM1YZpJ2UxyeE67DpDDZ+Tsw4YLX3trSlkNT7Tm9eKoUNb1LvaAaQzXgYQsWs+AQ9TUmmwMmBOCXuqqxUWKKQ=="], - "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmjs.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - "electron-to-chromium": ["electron-to-chromium@1.5.430", "https://registry.npmjs.com/electron-to-chromium/-/electron-to-chromium-1.5.430.tgz", {}, "sha512-e1QEj72Y4zd8RlNZVmoTg+iCOSVwpk05IOiiQwdrkwCSVlZfPthevErhE+nckGd2YbsXfp1SkisznhGVIXP2NQ=="], "entities": ["entities@4.5.0", "https://registry.npmjs.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - "es-define-property": ["es-define-property@1.0.1", "https://registry.npmjs.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - "es-errors": ["es-errors@1.3.0", "https://registry.npmjs.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], "es-module-lexer": ["es-module-lexer@2.3.2", "https://registry.npmjs.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], - "es-object-atoms": ["es-object-atoms@1.1.2", "https://registry.npmjs.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmjs.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "es5-ext": ["es5-ext@0.10.64", "https://registry.npmjs.com/es5-ext/-/es5-ext-0.10.64.tgz", { "dependencies": { "es6-iterator": "^2.0.3", "es6-symbol": "^3.1.3", "esniff": "^2.0.1", "next-tick": "^1.1.0" } }, "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg=="], "es6-iterator": ["es6-iterator@2.0.3", "https://registry.npmjs.com/es6-iterator/-/es6-iterator-2.0.3.tgz", { "dependencies": { "d": "1", "es5-ext": "^0.10.35", "es6-symbol": "^3.1.1" } }, "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g=="], @@ -1003,28 +982,14 @@ "flatted": ["flatted@3.4.4", "https://registry.npmjs.com/flatted/-/flatted-3.4.4.tgz", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], - "follow-redirects": ["follow-redirects@1.16.0", "https://registry.npmjs.com/follow-redirects/-/follow-redirects-1.16.0.tgz", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], - - "form-data": ["form-data@4.0.6", "https://registry.npmjs.com/form-data/-/form-data-4.0.6.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "fsevents": ["fsevents@2.3.3", "https://registry.npmjs.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "https://registry.npmjs.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "gensync": ["gensync@1.0.0-beta.2", "https://registry.npmjs.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmjs.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-proto": ["get-proto@1.0.1", "https://registry.npmjs.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-tsconfig": ["get-tsconfig@4.14.1", "https://registry.npmjs.com/get-tsconfig/-/get-tsconfig-4.14.1.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A=="], - "gopd": ["gopd@1.2.0", "https://registry.npmjs.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "has-symbols": ["has-symbols@1.1.0", "https://registry.npmjs.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmjs.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.4", "https://registry.npmjs.com/hasown/-/hasown-2.0.4.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "highlight.js": ["highlight.js@11.12.0", "https://registry.npmjs.com/highlight.js/-/highlight.js-11.12.0.tgz", {}, "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg=="], @@ -1035,8 +1000,6 @@ "html-void-elements": ["html-void-elements@3.0.0", "https://registry.npmjs.com/html-void-elements/-/html-void-elements-3.0.0.tgz", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "https-proxy-agent": ["https-proxy-agent@5.0.1", "https://registry.npmjs.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - "i18next": ["i18next@23.16.8", "https://registry.npmjs.com/i18next/-/i18next-23.16.8.tgz", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], "ioredis": ["ioredis@6.0.0", "https://registry.npmjs.com/ioredis/-/ioredis-6.0.0.tgz", { "dependencies": { "@ioredis/commands": "2.0.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "standard-as-callback": "2.1.0" } }, "sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w=="], @@ -1115,18 +1078,12 @@ "markdown-it": ["markdown-it@14.3.0", "https://registry.npmjs.com/markdown-it/-/markdown-it-14.3.0.tgz", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmjs.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "mdurl": ["mdurl@2.1.0", "https://registry.npmjs.com/mdurl/-/mdurl-2.1.0.tgz", {}, "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="], "meow": ["meow@13.2.0", "https://registry.npmjs.com/meow/-/meow-13.2.0.tgz", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], - "mime-db": ["mime-db@1.52.0", "https://registry.npmjs.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "mime-match": ["mime-match@1.0.2", "https://registry.npmjs.com/mime-match/-/mime-match-1.0.2.tgz", { "dependencies": { "wildcard": "^1.1.0" } }, "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg=="], - "mime-types": ["mime-types@2.1.35", "https://registry.npmjs.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "mlly": ["mlly@1.8.2", "https://registry.npmjs.com/mlly/-/mlly-1.8.2.tgz", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], "mrmime": ["mrmime@2.0.1", "https://registry.npmjs.com/mrmime/-/mrmime-2.0.1.tgz", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], @@ -1177,7 +1134,7 @@ "pathe": ["pathe@2.0.3", "https://registry.npmjs.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pele": ["pele@1.6.0", "", {}, "sha512-VfAiVItsOGlGhvnBfphdT28VCG7mXMZxQNOwAGsCtxI2ztbUsT39MraEOYjsapLxVETkdUZscm0DdotYDQMLdw=="], + "pele": ["pele@1.6.0", "https://registry.npmjs.com/pele/-/pele-1.6.0.tgz", {}, "sha512-VfAiVItsOGlGhvnBfphdT28VCG7mXMZxQNOwAGsCtxI2ztbUsT39MraEOYjsapLxVETkdUZscm0DdotYDQMLdw=="], "perfect-debounce": ["perfect-debounce@2.1.0", "https://registry.npmjs.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], @@ -1203,8 +1160,6 @@ "prismjs": ["prismjs@1.30.0", "https://registry.npmjs.com/prismjs/-/prismjs-1.30.0.tgz", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], - "proxy-from-env": ["proxy-from-env@2.1.0", "https://registry.npmjs.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], - "punycode.js": ["punycode.js@2.3.1", "https://registry.npmjs.com/punycode.js/-/punycode.js-2.3.1.tgz", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], "quansync": ["quansync@0.2.11", "https://registry.npmjs.com/quansync/-/quansync-0.2.11.tgz", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],