refactor: better-auth (#319)
* refactor: better-auth * chore: pr feedback * chore: lower + trim usernames in db
This commit is contained in:
parent
8f915ea8bf
commit
99932a8522
59 changed files with 10126 additions and 6582 deletions
|
|
@ -21,3 +21,4 @@
|
||||||
!NOTICES.md
|
!NOTICES.md
|
||||||
!LICENSES/**
|
!LICENSES/**
|
||||||
|
|
||||||
|
node_modules/**
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,3 +13,4 @@ CLAUDE.md
|
||||||
mutagen.yml.lock
|
mutagen.yml.lock
|
||||||
notes.md
|
notes.md
|
||||||
smb-password.txt
|
smb-password.txt
|
||||||
|
cache.db
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,12 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { type ClientOptions, type Config, createClient, createConfig } from './client';
|
import {
|
||||||
import type { ClientOptions as ClientOptions2 } from './types.gen';
|
type ClientOptions,
|
||||||
|
type Config,
|
||||||
|
createClient,
|
||||||
|
createConfig,
|
||||||
|
} from "./client";
|
||||||
|
import type { ClientOptions as ClientOptions2 } from "./types.gen";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `createClientConfig()` function will be called on client initialization
|
* The `createClientConfig()` function will be called on client initialization
|
||||||
|
|
@ -11,6 +16,10 @@ import type { ClientOptions as ClientOptions2 } from './types.gen';
|
||||||
* `setConfig()`. This is useful for example if you're using Next.js
|
* `setConfig()`. This is useful for example if you're using Next.js
|
||||||
* to ensure your client always has the correct values.
|
* to ensure your client always has the correct values.
|
||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
||||||
|
override?: Config<ClientOptions & T>,
|
||||||
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:4096' }));
|
export const client = createClient(
|
||||||
|
createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,301 +1,301 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { createSseClient } from '../core/serverSentEvents.gen';
|
import { createSseClient } from "../core/serverSentEvents.gen";
|
||||||
import type { HttpMethod } from '../core/types.gen';
|
import type { HttpMethod } from "../core/types.gen";
|
||||||
import { getValidRequestBody } from '../core/utils.gen';
|
import { getValidRequestBody } from "../core/utils.gen";
|
||||||
import type {
|
import type {
|
||||||
Client,
|
Client,
|
||||||
Config,
|
Config,
|
||||||
RequestOptions,
|
RequestOptions,
|
||||||
ResolvedRequestOptions,
|
ResolvedRequestOptions,
|
||||||
} from './types.gen';
|
} from "./types.gen";
|
||||||
import {
|
import {
|
||||||
buildUrl,
|
buildUrl,
|
||||||
createConfig,
|
createConfig,
|
||||||
createInterceptors,
|
createInterceptors,
|
||||||
getParseAs,
|
getParseAs,
|
||||||
mergeConfigs,
|
mergeConfigs,
|
||||||
mergeHeaders,
|
mergeHeaders,
|
||||||
setAuthParams,
|
setAuthParams,
|
||||||
} from './utils.gen';
|
} from "./utils.gen";
|
||||||
|
|
||||||
type ReqInit = Omit<RequestInit, 'body' | 'headers'> & {
|
type ReqInit = Omit<RequestInit, "body" | "headers"> & {
|
||||||
body?: any;
|
body?: any;
|
||||||
headers: ReturnType<typeof mergeHeaders>;
|
headers: ReturnType<typeof mergeHeaders>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createClient = (config: Config = {}): Client => {
|
export const createClient = (config: Config = {}): Client => {
|
||||||
let _config = mergeConfigs(createConfig(), config);
|
let _config = mergeConfigs(createConfig(), config);
|
||||||
|
|
||||||
const getConfig = (): Config => ({ ..._config });
|
const getConfig = (): Config => ({ ..._config });
|
||||||
|
|
||||||
const setConfig = (config: Config): Config => {
|
const setConfig = (config: Config): Config => {
|
||||||
_config = mergeConfigs(_config, config);
|
_config = mergeConfigs(_config, config);
|
||||||
return getConfig();
|
return getConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
const interceptors = createInterceptors<
|
const interceptors = createInterceptors<
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
unknown,
|
unknown,
|
||||||
ResolvedRequestOptions
|
ResolvedRequestOptions
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const beforeRequest = async (options: RequestOptions) => {
|
const beforeRequest = async (options: RequestOptions) => {
|
||||||
const opts = {
|
const opts = {
|
||||||
..._config,
|
..._config,
|
||||||
...options,
|
...options,
|
||||||
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
|
||||||
headers: mergeHeaders(_config.headers, options.headers),
|
headers: mergeHeaders(_config.headers, options.headers),
|
||||||
serializedBody: undefined,
|
serializedBody: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opts.security) {
|
if (opts.security) {
|
||||||
await setAuthParams({
|
await setAuthParams({
|
||||||
...opts,
|
...opts,
|
||||||
security: opts.security,
|
security: opts.security,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.requestValidator) {
|
if (opts.requestValidator) {
|
||||||
await opts.requestValidator(opts);
|
await opts.requestValidator(opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.body !== undefined && opts.bodySerializer) {
|
if (opts.body !== undefined && opts.bodySerializer) {
|
||||||
opts.serializedBody = opts.bodySerializer(opts.body);
|
opts.serializedBody = opts.bodySerializer(opts.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove Content-Type header if body is empty to avoid sending invalid requests
|
// remove Content-Type header if body is empty to avoid sending invalid requests
|
||||||
if (opts.body === undefined || opts.serializedBody === '') {
|
if (opts.body === undefined || opts.serializedBody === "") {
|
||||||
opts.headers.delete('Content-Type');
|
opts.headers.delete("Content-Type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = buildUrl(opts);
|
const url = buildUrl(opts);
|
||||||
|
|
||||||
return { opts, url };
|
return { opts, url };
|
||||||
};
|
};
|
||||||
|
|
||||||
const request: Client['request'] = async (options) => {
|
const request: Client["request"] = async (options) => {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const { opts, url } = await beforeRequest(options);
|
const { opts, url } = await beforeRequest(options);
|
||||||
const requestInit: ReqInit = {
|
const requestInit: ReqInit = {
|
||||||
redirect: 'follow',
|
redirect: "follow",
|
||||||
...opts,
|
...opts,
|
||||||
body: getValidRequestBody(opts),
|
body: getValidRequestBody(opts),
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = new Request(url, requestInit);
|
let request = new Request(url, requestInit);
|
||||||
|
|
||||||
for (const fn of interceptors.request.fns) {
|
for (const fn of interceptors.request.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
request = await fn(request, opts);
|
request = await fn(request, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
// fetch must be assigned here, otherwise it would throw the error:
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
const _fetch = opts.fetch!;
|
const _fetch = opts.fetch!;
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await _fetch(request);
|
response = await _fetch(request);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle fetch exceptions (AbortError, network errors, etc.)
|
// Handle fetch exceptions (AbortError, network errors, etc.)
|
||||||
let finalError = error;
|
let finalError = error;
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
for (const fn of interceptors.error.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
finalError = (await fn(
|
finalError = (await fn(
|
||||||
error,
|
error,
|
||||||
undefined as any,
|
undefined as any,
|
||||||
request,
|
request,
|
||||||
opts,
|
opts,
|
||||||
)) as unknown;
|
)) as unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalError = finalError || ({} as unknown);
|
finalError = finalError || ({} as unknown);
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
if (opts.throwOnError) {
|
||||||
throw finalError;
|
throw finalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return error response
|
// Return error response
|
||||||
return opts.responseStyle === 'data'
|
return opts.responseStyle === "data"
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
error: finalError,
|
error: finalError,
|
||||||
request,
|
request,
|
||||||
response: undefined as any,
|
response: undefined as any,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const fn of interceptors.response.fns) {
|
for (const fn of interceptors.response.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
response = await fn(response, request, opts);
|
response = await fn(response, request, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const parseAs =
|
const parseAs =
|
||||||
(opts.parseAs === 'auto'
|
(opts.parseAs === "auto"
|
||||||
? getParseAs(response.headers.get('Content-Type'))
|
? getParseAs(response.headers.get("Content-Type"))
|
||||||
: opts.parseAs) ?? 'json';
|
: opts.parseAs) ?? "json";
|
||||||
|
|
||||||
if (
|
if (
|
||||||
response.status === 204 ||
|
response.status === 204 ||
|
||||||
response.headers.get('Content-Length') === '0'
|
response.headers.get("Content-Length") === "0"
|
||||||
) {
|
) {
|
||||||
let emptyData: any;
|
let emptyData: any;
|
||||||
switch (parseAs) {
|
switch (parseAs) {
|
||||||
case 'arrayBuffer':
|
case "arrayBuffer":
|
||||||
case 'blob':
|
case "blob":
|
||||||
case 'text':
|
case "text":
|
||||||
emptyData = await response[parseAs]();
|
emptyData = await response[parseAs]();
|
||||||
break;
|
break;
|
||||||
case 'formData':
|
case "formData":
|
||||||
emptyData = new FormData();
|
emptyData = new FormData();
|
||||||
break;
|
break;
|
||||||
case 'stream':
|
case "stream":
|
||||||
emptyData = response.body;
|
emptyData = response.body;
|
||||||
break;
|
break;
|
||||||
case 'json':
|
case "json":
|
||||||
default:
|
default:
|
||||||
emptyData = {};
|
emptyData = {};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return opts.responseStyle === 'data'
|
return opts.responseStyle === "data"
|
||||||
? emptyData
|
? emptyData
|
||||||
: {
|
: {
|
||||||
data: emptyData,
|
data: emptyData,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: any;
|
let data: any;
|
||||||
switch (parseAs) {
|
switch (parseAs) {
|
||||||
case 'arrayBuffer':
|
case "arrayBuffer":
|
||||||
case 'blob':
|
case "blob":
|
||||||
case 'formData':
|
case "formData":
|
||||||
case 'json':
|
case "json":
|
||||||
case 'text':
|
case "text":
|
||||||
data = await response[parseAs]();
|
data = await response[parseAs]();
|
||||||
break;
|
break;
|
||||||
case 'stream':
|
case "stream":
|
||||||
return opts.responseStyle === 'data'
|
return opts.responseStyle === "data"
|
||||||
? response.body
|
? response.body
|
||||||
: {
|
: {
|
||||||
data: response.body,
|
data: response.body,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parseAs === 'json') {
|
if (parseAs === "json") {
|
||||||
if (opts.responseValidator) {
|
if (opts.responseValidator) {
|
||||||
await opts.responseValidator(data);
|
await opts.responseValidator(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.responseTransformer) {
|
if (opts.responseTransformer) {
|
||||||
data = await opts.responseTransformer(data);
|
data = await opts.responseTransformer(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return opts.responseStyle === 'data'
|
return opts.responseStyle === "data"
|
||||||
? data
|
? data
|
||||||
: {
|
: {
|
||||||
data,
|
data,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const textError = await response.text();
|
const textError = await response.text();
|
||||||
let jsonError: unknown;
|
let jsonError: unknown;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jsonError = JSON.parse(textError);
|
jsonError = JSON.parse(textError);
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = jsonError ?? textError;
|
const error = jsonError ?? textError;
|
||||||
let finalError = error;
|
let finalError = error;
|
||||||
|
|
||||||
for (const fn of interceptors.error.fns) {
|
for (const fn of interceptors.error.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
finalError = (await fn(error, response, request, opts)) as string;
|
finalError = (await fn(error, response, request, opts)) as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
finalError = finalError || ({} as string);
|
finalError = finalError || ({} as string);
|
||||||
|
|
||||||
if (opts.throwOnError) {
|
if (opts.throwOnError) {
|
||||||
throw finalError;
|
throw finalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: we probably want to return error and improve types
|
// TODO: we probably want to return error and improve types
|
||||||
return opts.responseStyle === 'data'
|
return opts.responseStyle === "data"
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
error: finalError,
|
error: finalError,
|
||||||
...result,
|
...result,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeMethodFn =
|
const makeMethodFn =
|
||||||
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
(method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
|
||||||
request({ ...options, method });
|
request({ ...options, method });
|
||||||
|
|
||||||
const makeSseFn =
|
const makeSseFn =
|
||||||
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
(method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
|
||||||
const { opts, url } = await beforeRequest(options);
|
const { opts, url } = await beforeRequest(options);
|
||||||
return createSseClient({
|
return createSseClient({
|
||||||
...opts,
|
...opts,
|
||||||
body: opts.body as BodyInit | null | undefined,
|
body: opts.body as BodyInit | null | undefined,
|
||||||
headers: opts.headers as unknown as Record<string, string>,
|
headers: opts.headers as unknown as Record<string, string>,
|
||||||
method,
|
method,
|
||||||
onRequest: async (url, init) => {
|
onRequest: async (url, init) => {
|
||||||
let request = new Request(url, init);
|
let request = new Request(url, init);
|
||||||
for (const fn of interceptors.request.fns) {
|
for (const fn of interceptors.request.fns) {
|
||||||
if (fn) {
|
if (fn) {
|
||||||
request = await fn(request, opts);
|
request = await fn(request, opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
url,
|
url,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buildUrl,
|
buildUrl,
|
||||||
connect: makeMethodFn('CONNECT'),
|
connect: makeMethodFn("CONNECT"),
|
||||||
delete: makeMethodFn('DELETE'),
|
delete: makeMethodFn("DELETE"),
|
||||||
get: makeMethodFn('GET'),
|
get: makeMethodFn("GET"),
|
||||||
getConfig,
|
getConfig,
|
||||||
head: makeMethodFn('HEAD'),
|
head: makeMethodFn("HEAD"),
|
||||||
interceptors,
|
interceptors,
|
||||||
options: makeMethodFn('OPTIONS'),
|
options: makeMethodFn("OPTIONS"),
|
||||||
patch: makeMethodFn('PATCH'),
|
patch: makeMethodFn("PATCH"),
|
||||||
post: makeMethodFn('POST'),
|
post: makeMethodFn("POST"),
|
||||||
put: makeMethodFn('PUT'),
|
put: makeMethodFn("PUT"),
|
||||||
request,
|
request,
|
||||||
setConfig,
|
setConfig,
|
||||||
sse: {
|
sse: {
|
||||||
connect: makeSseFn('CONNECT'),
|
connect: makeSseFn("CONNECT"),
|
||||||
delete: makeSseFn('DELETE'),
|
delete: makeSseFn("DELETE"),
|
||||||
get: makeSseFn('GET'),
|
get: makeSseFn("GET"),
|
||||||
head: makeSseFn('HEAD'),
|
head: makeSseFn("HEAD"),
|
||||||
options: makeSseFn('OPTIONS'),
|
options: makeSseFn("OPTIONS"),
|
||||||
patch: makeSseFn('PATCH'),
|
patch: makeSseFn("PATCH"),
|
||||||
post: makeSseFn('POST'),
|
post: makeSseFn("POST"),
|
||||||
put: makeSseFn('PUT'),
|
put: makeSseFn("PUT"),
|
||||||
trace: makeSseFn('TRACE'),
|
trace: makeSseFn("TRACE"),
|
||||||
},
|
},
|
||||||
trace: makeMethodFn('TRACE'),
|
trace: makeMethodFn("TRACE"),
|
||||||
} as Client;
|
} as Client;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
export type { Auth } from '../core/auth.gen';
|
export type { Auth } from "../core/auth.gen";
|
||||||
export type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
export type { QuerySerializerOptions } from "../core/bodySerializer.gen";
|
||||||
export {
|
export {
|
||||||
formDataBodySerializer,
|
formDataBodySerializer,
|
||||||
jsonBodySerializer,
|
jsonBodySerializer,
|
||||||
urlSearchParamsBodySerializer,
|
urlSearchParamsBodySerializer,
|
||||||
} from '../core/bodySerializer.gen';
|
} from "../core/bodySerializer.gen";
|
||||||
export { buildClientParams } from '../core/params.gen';
|
export { buildClientParams } from "../core/params.gen";
|
||||||
export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
|
export { serializeQueryKeyValue } from "../core/queryKeySerializer.gen";
|
||||||
export { createClient } from './client.gen';
|
export { createClient } from "./client.gen";
|
||||||
export type {
|
export type {
|
||||||
Client,
|
Client,
|
||||||
ClientOptions,
|
ClientOptions,
|
||||||
Config,
|
Config,
|
||||||
CreateClientConfig,
|
CreateClientConfig,
|
||||||
Options,
|
Options,
|
||||||
RequestOptions,
|
RequestOptions,
|
||||||
RequestResult,
|
RequestResult,
|
||||||
ResolvedRequestOptions,
|
ResolvedRequestOptions,
|
||||||
ResponseStyle,
|
ResponseStyle,
|
||||||
TDataShape,
|
TDataShape,
|
||||||
} from './types.gen';
|
} from "./types.gen";
|
||||||
export { createConfig, mergeHeaders } from './utils.gen';
|
export { createConfig, mergeHeaders } from "./utils.gen";
|
||||||
|
|
|
||||||
|
|
@ -1,210 +1,210 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Auth } from '../core/auth.gen';
|
import type { Auth } from "../core/auth.gen";
|
||||||
import type {
|
import type {
|
||||||
ServerSentEventsOptions,
|
ServerSentEventsOptions,
|
||||||
ServerSentEventsResult,
|
ServerSentEventsResult,
|
||||||
} from '../core/serverSentEvents.gen';
|
} from "../core/serverSentEvents.gen";
|
||||||
import type {
|
import type {
|
||||||
Client as CoreClient,
|
Client as CoreClient,
|
||||||
Config as CoreConfig,
|
Config as CoreConfig,
|
||||||
} from '../core/types.gen';
|
} from "../core/types.gen";
|
||||||
import type { Middleware } from './utils.gen';
|
import type { Middleware } from "./utils.gen";
|
||||||
|
|
||||||
export type ResponseStyle = 'data' | 'fields';
|
export type ResponseStyle = "data" | "fields";
|
||||||
|
|
||||||
export interface Config<T extends ClientOptions = ClientOptions>
|
export interface Config<T extends ClientOptions = ClientOptions>
|
||||||
extends Omit<RequestInit, 'body' | 'headers' | 'method'>,
|
extends Omit<RequestInit, "body" | "headers" | "method">,
|
||||||
CoreConfig {
|
CoreConfig {
|
||||||
/**
|
/**
|
||||||
* Base URL for all requests made by this client.
|
* Base URL for all requests made by this client.
|
||||||
*/
|
*/
|
||||||
baseUrl?: T['baseUrl'];
|
baseUrl?: T["baseUrl"];
|
||||||
/**
|
/**
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
* Fetch API implementation. You can use this option to provide a custom
|
||||||
* fetch instance.
|
* fetch instance.
|
||||||
*
|
*
|
||||||
* @default globalThis.fetch
|
* @default globalThis.fetch
|
||||||
*/
|
*/
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
/**
|
/**
|
||||||
* Please don't use the Fetch client for Next.js applications. The `next`
|
* Please don't use the Fetch client for Next.js applications. The `next`
|
||||||
* options won't have any effect.
|
* options won't have any effect.
|
||||||
*
|
*
|
||||||
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
* Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead.
|
||||||
*/
|
*/
|
||||||
next?: never;
|
next?: never;
|
||||||
/**
|
/**
|
||||||
* Return the response data parsed in a specified format. By default, `auto`
|
* Return the response data parsed in a specified format. By default, `auto`
|
||||||
* will infer the appropriate method from the `Content-Type` response header.
|
* will infer the appropriate method from the `Content-Type` response header.
|
||||||
* You can override this behavior with any of the {@link Body} methods.
|
* You can override this behavior with any of the {@link Body} methods.
|
||||||
* Select `stream` if you don't want to parse response data at all.
|
* Select `stream` if you don't want to parse response data at all.
|
||||||
*
|
*
|
||||||
* @default 'auto'
|
* @default 'auto'
|
||||||
*/
|
*/
|
||||||
parseAs?:
|
parseAs?:
|
||||||
| 'arrayBuffer'
|
| "arrayBuffer"
|
||||||
| 'auto'
|
| "auto"
|
||||||
| 'blob'
|
| "blob"
|
||||||
| 'formData'
|
| "formData"
|
||||||
| 'json'
|
| "json"
|
||||||
| 'stream'
|
| "stream"
|
||||||
| 'text';
|
| "text";
|
||||||
/**
|
/**
|
||||||
* Should we return only data or multiple fields (data, error, response, etc.)?
|
* Should we return only data or multiple fields (data, error, response, etc.)?
|
||||||
*
|
*
|
||||||
* @default 'fields'
|
* @default 'fields'
|
||||||
*/
|
*/
|
||||||
responseStyle?: ResponseStyle;
|
responseStyle?: ResponseStyle;
|
||||||
/**
|
/**
|
||||||
* Throw an error instead of returning it in the response?
|
* Throw an error instead of returning it in the response?
|
||||||
*
|
*
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
throwOnError?: T['throwOnError'];
|
throwOnError?: T["throwOnError"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestOptions<
|
export interface RequestOptions<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
Url extends string = string,
|
Url extends string = string,
|
||||||
> extends Config<{
|
> extends Config<{
|
||||||
responseStyle: TResponseStyle;
|
responseStyle: TResponseStyle;
|
||||||
throwOnError: ThrowOnError;
|
throwOnError: ThrowOnError;
|
||||||
}>,
|
}>,
|
||||||
Pick<
|
Pick<
|
||||||
ServerSentEventsOptions<TData>,
|
ServerSentEventsOptions<TData>,
|
||||||
| 'onSseError'
|
| "onSseError"
|
||||||
| 'onSseEvent'
|
| "onSseEvent"
|
||||||
| 'sseDefaultRetryDelay'
|
| "sseDefaultRetryDelay"
|
||||||
| 'sseMaxRetryAttempts'
|
| "sseMaxRetryAttempts"
|
||||||
| 'sseMaxRetryDelay'
|
| "sseMaxRetryDelay"
|
||||||
> {
|
> {
|
||||||
/**
|
/**
|
||||||
* Any body that you want to add to your request.
|
* Any body that you want to add to your request.
|
||||||
*
|
*
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#body}
|
||||||
*/
|
*/
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
path?: Record<string, unknown>;
|
path?: Record<string, unknown>;
|
||||||
query?: Record<string, unknown>;
|
query?: Record<string, unknown>;
|
||||||
/**
|
/**
|
||||||
* Security mechanism(s) to use for the request.
|
* Security mechanism(s) to use for the request.
|
||||||
*/
|
*/
|
||||||
security?: ReadonlyArray<Auth>;
|
security?: ReadonlyArray<Auth>;
|
||||||
url: Url;
|
url: Url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResolvedRequestOptions<
|
export interface ResolvedRequestOptions<
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
Url extends string = string,
|
Url extends string = string,
|
||||||
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
|
||||||
serializedBody?: string;
|
serializedBody?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RequestResult<
|
export type RequestResult<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
> = ThrowOnError extends true
|
> = ThrowOnError extends true
|
||||||
? Promise<
|
? Promise<
|
||||||
TResponseStyle extends 'data'
|
TResponseStyle extends "data"
|
||||||
? TData extends Record<string, unknown>
|
? TData extends Record<string, unknown>
|
||||||
? TData[keyof TData]
|
? TData[keyof TData]
|
||||||
: TData
|
: TData
|
||||||
: {
|
: {
|
||||||
data: TData extends Record<string, unknown>
|
data: TData extends Record<string, unknown>
|
||||||
? TData[keyof TData]
|
? TData[keyof TData]
|
||||||
: TData;
|
: TData;
|
||||||
request: Request;
|
request: Request;
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
: Promise<
|
: Promise<
|
||||||
TResponseStyle extends 'data'
|
TResponseStyle extends "data"
|
||||||
?
|
?
|
||||||
| (TData extends Record<string, unknown>
|
| (TData extends Record<string, unknown>
|
||||||
? TData[keyof TData]
|
? TData[keyof TData]
|
||||||
: TData)
|
: TData)
|
||||||
| undefined
|
| undefined
|
||||||
: (
|
: (
|
||||||
| {
|
| {
|
||||||
data: TData extends Record<string, unknown>
|
data: TData extends Record<string, unknown>
|
||||||
? TData[keyof TData]
|
? TData[keyof TData]
|
||||||
: TData;
|
: TData;
|
||||||
error: undefined;
|
error: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
data: undefined;
|
data: undefined;
|
||||||
error: TError extends Record<string, unknown>
|
error: TError extends Record<string, unknown>
|
||||||
? TError[keyof TError]
|
? TError[keyof TError]
|
||||||
: TError;
|
: TError;
|
||||||
}
|
}
|
||||||
) & {
|
) & {
|
||||||
request: Request;
|
request: Request;
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface ClientOptions {
|
export interface ClientOptions {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
responseStyle?: ResponseStyle;
|
responseStyle?: ResponseStyle;
|
||||||
throwOnError?: boolean;
|
throwOnError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MethodFn = <
|
type MethodFn = <
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||||
|
|
||||||
type SseFn = <
|
type SseFn = <
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
|
||||||
) => Promise<ServerSentEventsResult<TData, TError>>;
|
) => Promise<ServerSentEventsResult<TData, TError>>;
|
||||||
|
|
||||||
type RequestFn = <
|
type RequestFn = <
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TError = unknown,
|
TError = unknown,
|
||||||
ThrowOnError extends boolean = false,
|
ThrowOnError extends boolean = false,
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
>(
|
>(
|
||||||
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
|
options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
|
||||||
Pick<
|
Pick<
|
||||||
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
|
Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
|
||||||
'method'
|
"method"
|
||||||
>,
|
>,
|
||||||
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
|
||||||
|
|
||||||
type BuildUrlFn = <
|
type BuildUrlFn = <
|
||||||
TData extends {
|
TData extends {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
path?: Record<string, unknown>;
|
path?: Record<string, unknown>;
|
||||||
query?: Record<string, unknown>;
|
query?: Record<string, unknown>;
|
||||||
url: string;
|
url: string;
|
||||||
},
|
},
|
||||||
>(
|
>(
|
||||||
options: TData & Options<TData>,
|
options: TData & Options<TData>,
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
export type Client = CoreClient<
|
export type Client = CoreClient<
|
||||||
RequestFn,
|
RequestFn,
|
||||||
Config,
|
Config,
|
||||||
MethodFn,
|
MethodFn,
|
||||||
BuildUrlFn,
|
BuildUrlFn,
|
||||||
SseFn
|
SseFn
|
||||||
> & {
|
> & {
|
||||||
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -216,26 +216,26 @@ export type Client = CoreClient<
|
||||||
* to ensure your client always has the correct values.
|
* to ensure your client always has the correct values.
|
||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
|
||||||
override?: Config<ClientOptions & T>,
|
override?: Config<ClientOptions & T>,
|
||||||
) => Config<Required<ClientOptions> & T>;
|
) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export interface TDataShape {
|
export interface TDataShape {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
headers?: unknown;
|
headers?: unknown;
|
||||||
path?: unknown;
|
path?: unknown;
|
||||||
query?: unknown;
|
query?: unknown;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
|
||||||
|
|
||||||
export type Options<
|
export type Options<
|
||||||
TData extends TDataShape = TDataShape,
|
TData extends TDataShape = TDataShape,
|
||||||
ThrowOnError extends boolean = boolean,
|
ThrowOnError extends boolean = boolean,
|
||||||
TResponse = unknown,
|
TResponse = unknown,
|
||||||
TResponseStyle extends ResponseStyle = 'fields',
|
TResponseStyle extends ResponseStyle = "fields",
|
||||||
> = OmitKeys<
|
> = OmitKeys<
|
||||||
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
|
RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
|
||||||
'body' | 'path' | 'query' | 'url'
|
"body" | "path" | "query" | "url"
|
||||||
> &
|
> &
|
||||||
([TData] extends [never] ? unknown : Omit<TData, 'url'>);
|
([TData] extends [never] ? unknown : Omit<TData, "url">);
|
||||||
|
|
|
||||||
|
|
@ -1,332 +1,337 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { getAuthToken } from '../core/auth.gen';
|
import { getAuthToken } from "../core/auth.gen";
|
||||||
import type { QuerySerializerOptions } from '../core/bodySerializer.gen';
|
import type { QuerySerializerOptions } from "../core/bodySerializer.gen";
|
||||||
import { jsonBodySerializer } from '../core/bodySerializer.gen';
|
import { jsonBodySerializer } from "../core/bodySerializer.gen";
|
||||||
import {
|
import {
|
||||||
serializeArrayParam,
|
serializeArrayParam,
|
||||||
serializeObjectParam,
|
serializeObjectParam,
|
||||||
serializePrimitiveParam,
|
serializePrimitiveParam,
|
||||||
} from '../core/pathSerializer.gen';
|
} from "../core/pathSerializer.gen";
|
||||||
import { getUrl } from '../core/utils.gen';
|
import { getUrl } from "../core/utils.gen";
|
||||||
import type { Client, ClientOptions, Config, RequestOptions } from './types.gen';
|
import type {
|
||||||
|
Client,
|
||||||
|
ClientOptions,
|
||||||
|
Config,
|
||||||
|
RequestOptions,
|
||||||
|
} from "./types.gen";
|
||||||
|
|
||||||
export const createQuerySerializer = <T = unknown>({
|
export const createQuerySerializer = <T = unknown>({
|
||||||
parameters = {},
|
parameters = {},
|
||||||
...args
|
...args
|
||||||
}: QuerySerializerOptions = {}) => {
|
}: QuerySerializerOptions = {}) => {
|
||||||
const querySerializer = (queryParams: T) => {
|
const querySerializer = (queryParams: T) => {
|
||||||
const search: string[] = [];
|
const search: string[] = [];
|
||||||
if (queryParams && typeof queryParams === 'object') {
|
if (queryParams && typeof queryParams === "object") {
|
||||||
for (const name in queryParams) {
|
for (const name in queryParams) {
|
||||||
const value = queryParams[name];
|
const value = queryParams[name];
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = parameters[name] || args;
|
const options = parameters[name] || args;
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const serializedArray = serializeArrayParam({
|
const serializedArray = serializeArrayParam({
|
||||||
allowReserved: options.allowReserved,
|
allowReserved: options.allowReserved,
|
||||||
explode: true,
|
explode: true,
|
||||||
name,
|
name,
|
||||||
style: 'form',
|
style: "form",
|
||||||
value,
|
value,
|
||||||
...options.array,
|
...options.array,
|
||||||
});
|
});
|
||||||
if (serializedArray) search.push(serializedArray);
|
if (serializedArray) search.push(serializedArray);
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === "object") {
|
||||||
const serializedObject = serializeObjectParam({
|
const serializedObject = serializeObjectParam({
|
||||||
allowReserved: options.allowReserved,
|
allowReserved: options.allowReserved,
|
||||||
explode: true,
|
explode: true,
|
||||||
name,
|
name,
|
||||||
style: 'deepObject',
|
style: "deepObject",
|
||||||
value: value as Record<string, unknown>,
|
value: value as Record<string, unknown>,
|
||||||
...options.object,
|
...options.object,
|
||||||
});
|
});
|
||||||
if (serializedObject) search.push(serializedObject);
|
if (serializedObject) search.push(serializedObject);
|
||||||
} else {
|
} else {
|
||||||
const serializedPrimitive = serializePrimitiveParam({
|
const serializedPrimitive = serializePrimitiveParam({
|
||||||
allowReserved: options.allowReserved,
|
allowReserved: options.allowReserved,
|
||||||
name,
|
name,
|
||||||
value: value as string,
|
value: value as string,
|
||||||
});
|
});
|
||||||
if (serializedPrimitive) search.push(serializedPrimitive);
|
if (serializedPrimitive) search.push(serializedPrimitive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return search.join('&');
|
return search.join("&");
|
||||||
};
|
};
|
||||||
return querySerializer;
|
return querySerializer;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Infers parseAs value from provided Content-Type header.
|
* Infers parseAs value from provided Content-Type header.
|
||||||
*/
|
*/
|
||||||
export const getParseAs = (
|
export const getParseAs = (
|
||||||
contentType: string | null,
|
contentType: string | null,
|
||||||
): Exclude<Config['parseAs'], 'auto'> => {
|
): Exclude<Config["parseAs"], "auto"> => {
|
||||||
if (!contentType) {
|
if (!contentType) {
|
||||||
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
// If no Content-Type header is provided, the best we can do is return the raw response body,
|
||||||
// which is effectively the same as the 'stream' option.
|
// which is effectively the same as the 'stream' option.
|
||||||
return 'stream';
|
return "stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanContent = contentType.split(';')[0]?.trim();
|
const cleanContent = contentType.split(";")[0]?.trim();
|
||||||
|
|
||||||
if (!cleanContent) {
|
if (!cleanContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cleanContent.startsWith('application/json') ||
|
cleanContent.startsWith("application/json") ||
|
||||||
cleanContent.endsWith('+json')
|
cleanContent.endsWith("+json")
|
||||||
) {
|
) {
|
||||||
return 'json';
|
return "json";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanContent === 'multipart/form-data') {
|
if (cleanContent === "multipart/form-data") {
|
||||||
return 'formData';
|
return "formData";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
['application/', 'audio/', 'image/', 'video/'].some((type) =>
|
["application/", "audio/", "image/", "video/"].some((type) =>
|
||||||
cleanContent.startsWith(type),
|
cleanContent.startsWith(type),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return 'blob';
|
return "blob";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanContent.startsWith('text/')) {
|
if (cleanContent.startsWith("text/")) {
|
||||||
return 'text';
|
return "text";
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkForExistence = (
|
const checkForExistence = (
|
||||||
options: Pick<RequestOptions, 'auth' | 'query'> & {
|
options: Pick<RequestOptions, "auth" | "query"> & {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
},
|
},
|
||||||
name?: string,
|
name?: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
options.headers.has(name) ||
|
options.headers.has(name) ||
|
||||||
options.query?.[name] ||
|
options.query?.[name] ||
|
||||||
options.headers.get('Cookie')?.includes(`${name}=`)
|
options.headers.get("Cookie")?.includes(`${name}=`)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setAuthParams = async ({
|
export const setAuthParams = async ({
|
||||||
security,
|
security,
|
||||||
...options
|
...options
|
||||||
}: Pick<Required<RequestOptions>, 'security'> &
|
}: Pick<Required<RequestOptions>, "security"> &
|
||||||
Pick<RequestOptions, 'auth' | 'query'> & {
|
Pick<RequestOptions, "auth" | "query"> & {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
}) => {
|
}) => {
|
||||||
for (const auth of security) {
|
for (const auth of security) {
|
||||||
if (checkForExistence(options, auth.name)) {
|
if (checkForExistence(options, auth.name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getAuthToken(auth, options.auth);
|
const token = await getAuthToken(auth, options.auth);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = auth.name ?? 'Authorization';
|
const name = auth.name ?? "Authorization";
|
||||||
|
|
||||||
switch (auth.in) {
|
switch (auth.in) {
|
||||||
case 'query':
|
case "query":
|
||||||
if (!options.query) {
|
if (!options.query) {
|
||||||
options.query = {};
|
options.query = {};
|
||||||
}
|
}
|
||||||
options.query[name] = token;
|
options.query[name] = token;
|
||||||
break;
|
break;
|
||||||
case 'cookie':
|
case "cookie":
|
||||||
options.headers.append('Cookie', `${name}=${token}`);
|
options.headers.append("Cookie", `${name}=${token}`);
|
||||||
break;
|
break;
|
||||||
case 'header':
|
case "header":
|
||||||
default:
|
default:
|
||||||
options.headers.set(name, token);
|
options.headers.set(name, token);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildUrl: Client['buildUrl'] = (options) =>
|
export const buildUrl: Client["buildUrl"] = (options) =>
|
||||||
getUrl({
|
getUrl({
|
||||||
baseUrl: options.baseUrl as string,
|
baseUrl: options.baseUrl as string,
|
||||||
path: options.path,
|
path: options.path,
|
||||||
query: options.query,
|
query: options.query,
|
||||||
querySerializer:
|
querySerializer:
|
||||||
typeof options.querySerializer === 'function'
|
typeof options.querySerializer === "function"
|
||||||
? options.querySerializer
|
? options.querySerializer
|
||||||
: createQuerySerializer(options.querySerializer),
|
: createQuerySerializer(options.querySerializer),
|
||||||
url: options.url,
|
url: options.url,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mergeConfigs = (a: Config, b: Config): Config => {
|
export const mergeConfigs = (a: Config, b: Config): Config => {
|
||||||
const config = { ...a, ...b };
|
const config = { ...a, ...b };
|
||||||
if (config.baseUrl?.endsWith('/')) {
|
if (config.baseUrl?.endsWith("/")) {
|
||||||
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
|
||||||
}
|
}
|
||||||
config.headers = mergeHeaders(a.headers, b.headers);
|
config.headers = mergeHeaders(a.headers, b.headers);
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
const headersEntries = (headers: Headers): Array<[string, string]> => {
|
||||||
const entries: Array<[string, string]> = [];
|
const entries: Array<[string, string]> = [];
|
||||||
headers.forEach((value, key) => {
|
headers.forEach((value, key) => {
|
||||||
entries.push([key, value]);
|
entries.push([key, value]);
|
||||||
});
|
});
|
||||||
return entries;
|
return entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mergeHeaders = (
|
export const mergeHeaders = (
|
||||||
...headers: Array<Required<Config>['headers'] | undefined>
|
...headers: Array<Required<Config>["headers"] | undefined>
|
||||||
): Headers => {
|
): Headers => {
|
||||||
const mergedHeaders = new Headers();
|
const mergedHeaders = new Headers();
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
if (!header) {
|
if (!header) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const iterator =
|
const iterator =
|
||||||
header instanceof Headers
|
header instanceof Headers
|
||||||
? headersEntries(header)
|
? headersEntries(header)
|
||||||
: Object.entries(header);
|
: Object.entries(header);
|
||||||
|
|
||||||
for (const [key, value] of iterator) {
|
for (const [key, value] of iterator) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
mergedHeaders.delete(key);
|
mergedHeaders.delete(key);
|
||||||
} else if (Array.isArray(value)) {
|
} else if (Array.isArray(value)) {
|
||||||
for (const v of value) {
|
for (const v of value) {
|
||||||
mergedHeaders.append(key, v as string);
|
mergedHeaders.append(key, v as string);
|
||||||
}
|
}
|
||||||
} else if (value !== undefined) {
|
} else if (value !== undefined) {
|
||||||
// assume object headers are meant to be JSON stringified, i.e. their
|
// assume object headers are meant to be JSON stringified, i.e. their
|
||||||
// content value in OpenAPI specification is 'application/json'
|
// content value in OpenAPI specification is 'application/json'
|
||||||
mergedHeaders.set(
|
mergedHeaders.set(
|
||||||
key,
|
key,
|
||||||
typeof value === 'object' ? JSON.stringify(value) : (value as string),
|
typeof value === "object" ? JSON.stringify(value) : (value as string),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mergedHeaders;
|
return mergedHeaders;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ErrInterceptor<Err, Res, Req, Options> = (
|
type ErrInterceptor<Err, Res, Req, Options> = (
|
||||||
error: Err,
|
error: Err,
|
||||||
response: Res,
|
response: Res,
|
||||||
request: Req,
|
request: Req,
|
||||||
options: Options,
|
options: Options,
|
||||||
) => Err | Promise<Err>;
|
) => Err | Promise<Err>;
|
||||||
|
|
||||||
type ReqInterceptor<Req, Options> = (
|
type ReqInterceptor<Req, Options> = (
|
||||||
request: Req,
|
request: Req,
|
||||||
options: Options,
|
options: Options,
|
||||||
) => Req | Promise<Req>;
|
) => Req | Promise<Req>;
|
||||||
|
|
||||||
type ResInterceptor<Res, Req, Options> = (
|
type ResInterceptor<Res, Req, Options> = (
|
||||||
response: Res,
|
response: Res,
|
||||||
request: Req,
|
request: Req,
|
||||||
options: Options,
|
options: Options,
|
||||||
) => Res | Promise<Res>;
|
) => Res | Promise<Res>;
|
||||||
|
|
||||||
class Interceptors<Interceptor> {
|
class Interceptors<Interceptor> {
|
||||||
fns: Array<Interceptor | null> = [];
|
fns: Array<Interceptor | null> = [];
|
||||||
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.fns = [];
|
this.fns = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
eject(id: number | Interceptor): void {
|
eject(id: number | Interceptor): void {
|
||||||
const index = this.getInterceptorIndex(id);
|
const index = this.getInterceptorIndex(id);
|
||||||
if (this.fns[index]) {
|
if (this.fns[index]) {
|
||||||
this.fns[index] = null;
|
this.fns[index] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exists(id: number | Interceptor): boolean {
|
exists(id: number | Interceptor): boolean {
|
||||||
const index = this.getInterceptorIndex(id);
|
const index = this.getInterceptorIndex(id);
|
||||||
return Boolean(this.fns[index]);
|
return Boolean(this.fns[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInterceptorIndex(id: number | Interceptor): number {
|
getInterceptorIndex(id: number | Interceptor): number {
|
||||||
if (typeof id === 'number') {
|
if (typeof id === "number") {
|
||||||
return this.fns[id] ? id : -1;
|
return this.fns[id] ? id : -1;
|
||||||
}
|
}
|
||||||
return this.fns.indexOf(id);
|
return this.fns.indexOf(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(
|
||||||
id: number | Interceptor,
|
id: number | Interceptor,
|
||||||
fn: Interceptor,
|
fn: Interceptor,
|
||||||
): number | Interceptor | false {
|
): number | Interceptor | false {
|
||||||
const index = this.getInterceptorIndex(id);
|
const index = this.getInterceptorIndex(id);
|
||||||
if (this.fns[index]) {
|
if (this.fns[index]) {
|
||||||
this.fns[index] = fn;
|
this.fns[index] = fn;
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
use(fn: Interceptor): number {
|
use(fn: Interceptor): number {
|
||||||
this.fns.push(fn);
|
this.fns.push(fn);
|
||||||
return this.fns.length - 1;
|
return this.fns.length - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Middleware<Req, Res, Err, Options> {
|
export interface Middleware<Req, Res, Err, Options> {
|
||||||
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
error: Interceptors<ErrInterceptor<Err, Res, Req, Options>>;
|
||||||
request: Interceptors<ReqInterceptor<Req, Options>>;
|
request: Interceptors<ReqInterceptor<Req, Options>>;
|
||||||
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
response: Interceptors<ResInterceptor<Res, Req, Options>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
|
export const createInterceptors = <Req, Res, Err, Options>(): Middleware<
|
||||||
Req,
|
Req,
|
||||||
Res,
|
Res,
|
||||||
Err,
|
Err,
|
||||||
Options
|
Options
|
||||||
> => ({
|
> => ({
|
||||||
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
error: new Interceptors<ErrInterceptor<Err, Res, Req, Options>>(),
|
||||||
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
request: new Interceptors<ReqInterceptor<Req, Options>>(),
|
||||||
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
response: new Interceptors<ResInterceptor<Res, Req, Options>>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultQuerySerializer = createQuerySerializer({
|
const defaultQuerySerializer = createQuerySerializer({
|
||||||
allowReserved: false,
|
allowReserved: false,
|
||||||
array: {
|
array: {
|
||||||
explode: true,
|
explode: true,
|
||||||
style: 'form',
|
style: "form",
|
||||||
},
|
},
|
||||||
object: {
|
object: {
|
||||||
explode: true,
|
explode: true,
|
||||||
style: 'deepObject',
|
style: "deepObject",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
export const createConfig = <T extends ClientOptions = ClientOptions>(
|
||||||
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
override: Config<Omit<ClientOptions, keyof T> & T> = {},
|
||||||
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
): Config<Omit<ClientOptions, keyof T> & T> => ({
|
||||||
...jsonBodySerializer,
|
...jsonBodySerializer,
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
parseAs: 'auto',
|
parseAs: "auto",
|
||||||
querySerializer: defaultQuerySerializer,
|
querySerializer: defaultQuerySerializer,
|
||||||
...override,
|
...override,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,40 @@
|
||||||
export type AuthToken = string | undefined;
|
export type AuthToken = string | undefined;
|
||||||
|
|
||||||
export interface Auth {
|
export interface Auth {
|
||||||
/**
|
/**
|
||||||
* Which part of the request do we use to send the auth?
|
* Which part of the request do we use to send the auth?
|
||||||
*
|
*
|
||||||
* @default 'header'
|
* @default 'header'
|
||||||
*/
|
*/
|
||||||
in?: 'header' | 'query' | 'cookie';
|
in?: "header" | "query" | "cookie";
|
||||||
/**
|
/**
|
||||||
* Header or query parameter name.
|
* Header or query parameter name.
|
||||||
*
|
*
|
||||||
* @default 'Authorization'
|
* @default 'Authorization'
|
||||||
*/
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
scheme?: 'basic' | 'bearer';
|
scheme?: "basic" | "bearer";
|
||||||
type: 'apiKey' | 'http';
|
type: "apiKey" | "http";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAuthToken = async (
|
export const getAuthToken = async (
|
||||||
auth: Auth,
|
auth: Auth,
|
||||||
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
callback: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken,
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const token =
|
const token =
|
||||||
typeof callback === 'function' ? await callback(auth) : callback;
|
typeof callback === "function" ? await callback(auth) : callback;
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.scheme === 'bearer') {
|
if (auth.scheme === "bearer") {
|
||||||
return `Bearer ${token}`;
|
return `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auth.scheme === 'basic') {
|
if (auth.scheme === "basic") {
|
||||||
return `Basic ${btoa(token)}`;
|
return `Basic ${btoa(token)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,100 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ArrayStyle,
|
ArrayStyle,
|
||||||
ObjectStyle,
|
ObjectStyle,
|
||||||
SerializerOptions,
|
SerializerOptions,
|
||||||
} from './pathSerializer.gen';
|
} from "./pathSerializer.gen";
|
||||||
|
|
||||||
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
export type QuerySerializer = (query: Record<string, unknown>) => string;
|
||||||
|
|
||||||
export type BodySerializer = (body: any) => any;
|
export type BodySerializer = (body: any) => any;
|
||||||
|
|
||||||
type QuerySerializerOptionsObject = {
|
type QuerySerializerOptionsObject = {
|
||||||
allowReserved?: boolean;
|
allowReserved?: boolean;
|
||||||
array?: Partial<SerializerOptions<ArrayStyle>>;
|
array?: Partial<SerializerOptions<ArrayStyle>>;
|
||||||
object?: Partial<SerializerOptions<ObjectStyle>>;
|
object?: Partial<SerializerOptions<ObjectStyle>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
export type QuerySerializerOptions = QuerySerializerOptionsObject & {
|
||||||
/**
|
/**
|
||||||
* Per-parameter serialization overrides. When provided, these settings
|
* Per-parameter serialization overrides. When provided, these settings
|
||||||
* override the global array/object settings for specific parameter names.
|
* override the global array/object settings for specific parameter names.
|
||||||
*/
|
*/
|
||||||
parameters?: Record<string, QuerySerializerOptionsObject>;
|
parameters?: Record<string, QuerySerializerOptionsObject>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializeFormDataPair = (
|
const serializeFormDataPair = (
|
||||||
data: FormData,
|
data: FormData,
|
||||||
key: string,
|
key: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): void => {
|
): void => {
|
||||||
if (typeof value === 'string' || value instanceof Blob) {
|
if (typeof value === "string" || value instanceof Blob) {
|
||||||
data.append(key, value);
|
data.append(key, value);
|
||||||
} else if (value instanceof Date) {
|
} else if (value instanceof Date) {
|
||||||
data.append(key, value.toISOString());
|
data.append(key, value.toISOString());
|
||||||
} else {
|
} else {
|
||||||
data.append(key, JSON.stringify(value));
|
data.append(key, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const serializeUrlSearchParamsPair = (
|
const serializeUrlSearchParamsPair = (
|
||||||
data: URLSearchParams,
|
data: URLSearchParams,
|
||||||
key: string,
|
key: string,
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): void => {
|
): void => {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === "string") {
|
||||||
data.append(key, value);
|
data.append(key, value);
|
||||||
} else {
|
} else {
|
||||||
data.append(key, JSON.stringify(value));
|
data.append(key, JSON.stringify(value));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formDataBodySerializer = {
|
export const formDataBodySerializer = {
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||||
body: T,
|
body: T,
|
||||||
): FormData => {
|
): FormData => {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
Object.entries(body).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((v) => serializeFormDataPair(data, key, v));
|
value.forEach((v) => serializeFormDataPair(data, key, v));
|
||||||
} else {
|
} else {
|
||||||
serializeFormDataPair(data, key, value);
|
serializeFormDataPair(data, key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jsonBodySerializer = {
|
export const jsonBodySerializer = {
|
||||||
bodySerializer: <T>(body: T): string =>
|
bodySerializer: <T>(body: T): string =>
|
||||||
JSON.stringify(body, (_key, value) =>
|
JSON.stringify(body, (_key, value) =>
|
||||||
typeof value === 'bigint' ? value.toString() : value,
|
typeof value === "bigint" ? value.toString() : value,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const urlSearchParamsBodySerializer = {
|
export const urlSearchParamsBodySerializer = {
|
||||||
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
bodySerializer: <T extends Record<string, any> | Array<Record<string, any>>>(
|
||||||
body: T,
|
body: T,
|
||||||
): string => {
|
): string => {
|
||||||
const data = new URLSearchParams();
|
const data = new URLSearchParams();
|
||||||
|
|
||||||
Object.entries(body).forEach(([key, value]) => {
|
Object.entries(body).forEach(([key, value]) => {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
value.forEach((v) => serializeUrlSearchParamsPair(data, key, v));
|
||||||
} else {
|
} else {
|
||||||
serializeUrlSearchParamsPair(data, key, value);
|
serializeUrlSearchParamsPair(data, key, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return data.toString();
|
return data.toString();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,176 +1,176 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
type Slot = 'body' | 'headers' | 'path' | 'query';
|
type Slot = "body" | "headers" | "path" | "query";
|
||||||
|
|
||||||
export type Field =
|
export type Field =
|
||||||
| {
|
| {
|
||||||
in: Exclude<Slot, 'body'>;
|
in: Exclude<Slot, "body">;
|
||||||
/**
|
/**
|
||||||
* Field name. This is the name we want the user to see and use.
|
* Field name. This is the name we want the user to see and use.
|
||||||
*/
|
*/
|
||||||
key: string;
|
key: string;
|
||||||
/**
|
/**
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
* If omitted, we use the same value as `key`.
|
* If omitted, we use the same value as `key`.
|
||||||
*/
|
*/
|
||||||
map?: string;
|
map?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
in: Extract<Slot, 'body'>;
|
in: Extract<Slot, "body">;
|
||||||
/**
|
/**
|
||||||
* Key isn't required for bodies.
|
* Key isn't required for bodies.
|
||||||
*/
|
*/
|
||||||
key?: string;
|
key?: string;
|
||||||
map?: string;
|
map?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
/**
|
/**
|
||||||
* Field name. This is the name we want the user to see and use.
|
* Field name. This is the name we want the user to see and use.
|
||||||
*/
|
*/
|
||||||
key: string;
|
key: string;
|
||||||
/**
|
/**
|
||||||
* Field mapped name. This is the name we want to use in the request.
|
* Field mapped name. This is the name we want to use in the request.
|
||||||
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
* If `in` is omitted, `map` aliases `key` to the transport layer.
|
||||||
*/
|
*/
|
||||||
map: Slot;
|
map: Slot;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Fields {
|
export interface Fields {
|
||||||
allowExtra?: Partial<Record<Slot, boolean>>;
|
allowExtra?: Partial<Record<Slot, boolean>>;
|
||||||
args?: ReadonlyArray<Field>;
|
args?: ReadonlyArray<Field>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
export type FieldsConfig = ReadonlyArray<Field | Fields>;
|
||||||
|
|
||||||
const extraPrefixesMap: Record<string, Slot> = {
|
const extraPrefixesMap: Record<string, Slot> = {
|
||||||
$body_: 'body',
|
$body_: "body",
|
||||||
$headers_: 'headers',
|
$headers_: "headers",
|
||||||
$path_: 'path',
|
$path_: "path",
|
||||||
$query_: 'query',
|
$query_: "query",
|
||||||
};
|
};
|
||||||
const extraPrefixes = Object.entries(extraPrefixesMap);
|
const extraPrefixes = Object.entries(extraPrefixesMap);
|
||||||
|
|
||||||
type KeyMap = Map<
|
type KeyMap = Map<
|
||||||
string,
|
string,
|
||||||
| {
|
| {
|
||||||
in: Slot;
|
in: Slot;
|
||||||
map?: string;
|
map?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
in?: never;
|
in?: never;
|
||||||
map: Slot;
|
map: Slot;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
|
||||||
if (!map) {
|
if (!map) {
|
||||||
map = new Map();
|
map = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const config of fields) {
|
for (const config of fields) {
|
||||||
if ('in' in config) {
|
if ("in" in config) {
|
||||||
if (config.key) {
|
if (config.key) {
|
||||||
map.set(config.key, {
|
map.set(config.key, {
|
||||||
in: config.in,
|
in: config.in,
|
||||||
map: config.map,
|
map: config.map,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if ('key' in config) {
|
} else if ("key" in config) {
|
||||||
map.set(config.key, {
|
map.set(config.key, {
|
||||||
map: config.map,
|
map: config.map,
|
||||||
});
|
});
|
||||||
} else if (config.args) {
|
} else if (config.args) {
|
||||||
buildKeyMap(config.args, map);
|
buildKeyMap(config.args, map);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Params {
|
interface Params {
|
||||||
body: unknown;
|
body: unknown;
|
||||||
headers: Record<string, unknown>;
|
headers: Record<string, unknown>;
|
||||||
path: Record<string, unknown>;
|
path: Record<string, unknown>;
|
||||||
query: Record<string, unknown>;
|
query: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripEmptySlots = (params: Params) => {
|
const stripEmptySlots = (params: Params) => {
|
||||||
for (const [slot, value] of Object.entries(params)) {
|
for (const [slot, value] of Object.entries(params)) {
|
||||||
if (value && typeof value === 'object' && !Object.keys(value).length) {
|
if (value && typeof value === "object" && !Object.keys(value).length) {
|
||||||
delete params[slot as Slot];
|
delete params[slot as Slot];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const buildClientParams = (
|
export const buildClientParams = (
|
||||||
args: ReadonlyArray<unknown>,
|
args: ReadonlyArray<unknown>,
|
||||||
fields: FieldsConfig,
|
fields: FieldsConfig,
|
||||||
) => {
|
) => {
|
||||||
const params: Params = {
|
const params: Params = {
|
||||||
body: {},
|
body: {},
|
||||||
headers: {},
|
headers: {},
|
||||||
path: {},
|
path: {},
|
||||||
query: {},
|
query: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const map = buildKeyMap(fields);
|
const map = buildKeyMap(fields);
|
||||||
|
|
||||||
let config: FieldsConfig[number] | undefined;
|
let config: FieldsConfig[number] | undefined;
|
||||||
|
|
||||||
for (const [index, arg] of args.entries()) {
|
for (const [index, arg] of args.entries()) {
|
||||||
if (fields[index]) {
|
if (fields[index]) {
|
||||||
config = fields[index];
|
config = fields[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('in' in config) {
|
if ("in" in config) {
|
||||||
if (config.key) {
|
if (config.key) {
|
||||||
const field = map.get(config.key)!;
|
const field = map.get(config.key)!;
|
||||||
const name = field.map || config.key;
|
const name = field.map || config.key;
|
||||||
if (field.in) {
|
if (field.in) {
|
||||||
(params[field.in] as Record<string, unknown>)[name] = arg;
|
(params[field.in] as Record<string, unknown>)[name] = arg;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
params.body = arg;
|
params.body = arg;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const [key, value] of Object.entries(arg ?? {})) {
|
for (const [key, value] of Object.entries(arg ?? {})) {
|
||||||
const field = map.get(key);
|
const field = map.get(key);
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
if (field.in) {
|
if (field.in) {
|
||||||
const name = field.map || key;
|
const name = field.map || key;
|
||||||
(params[field.in] as Record<string, unknown>)[name] = value;
|
(params[field.in] as Record<string, unknown>)[name] = value;
|
||||||
} else {
|
} else {
|
||||||
params[field.map] = value;
|
params[field.map] = value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const extra = extraPrefixes.find(([prefix]) =>
|
const extra = extraPrefixes.find(([prefix]) =>
|
||||||
key.startsWith(prefix),
|
key.startsWith(prefix),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (extra) {
|
if (extra) {
|
||||||
const [prefix, slot] = extra;
|
const [prefix, slot] = extra;
|
||||||
(params[slot] as Record<string, unknown>)[
|
(params[slot] as Record<string, unknown>)[
|
||||||
key.slice(prefix.length)
|
key.slice(prefix.length)
|
||||||
] = value;
|
] = value;
|
||||||
} else if ('allowExtra' in config && config.allowExtra) {
|
} else if ("allowExtra" in config && config.allowExtra) {
|
||||||
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
for (const [slot, allowed] of Object.entries(config.allowExtra)) {
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
(params[slot as Slot] as Record<string, unknown>)[key] = value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stripEmptySlots(params);
|
stripEmptySlots(params);
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,181 +1,181 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
interface SerializeOptions<T>
|
interface SerializeOptions<T>
|
||||||
extends SerializePrimitiveOptions,
|
extends SerializePrimitiveOptions,
|
||||||
SerializerOptions<T> {}
|
SerializerOptions<T> {}
|
||||||
|
|
||||||
interface SerializePrimitiveOptions {
|
interface SerializePrimitiveOptions {
|
||||||
allowReserved?: boolean;
|
allowReserved?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerializerOptions<T> {
|
export interface SerializerOptions<T> {
|
||||||
/**
|
/**
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
explode: boolean;
|
explode: boolean;
|
||||||
style: T;
|
style: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
|
export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
|
||||||
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
export type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
|
||||||
type MatrixStyle = 'label' | 'matrix' | 'simple';
|
type MatrixStyle = "label" | "matrix" | "simple";
|
||||||
export type ObjectStyle = 'form' | 'deepObject';
|
export type ObjectStyle = "form" | "deepObject";
|
||||||
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
|
||||||
|
|
||||||
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
interface SerializePrimitiveParam extends SerializePrimitiveOptions {
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'label':
|
case "label":
|
||||||
return '.';
|
return ".";
|
||||||
case 'matrix':
|
case "matrix":
|
||||||
return ';';
|
return ";";
|
||||||
case 'simple':
|
case "simple":
|
||||||
return ',';
|
return ",";
|
||||||
default:
|
default:
|
||||||
return '&';
|
return "&";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'form':
|
case "form":
|
||||||
return ',';
|
return ",";
|
||||||
case 'pipeDelimited':
|
case "pipeDelimited":
|
||||||
return '|';
|
return "|";
|
||||||
case 'spaceDelimited':
|
case "spaceDelimited":
|
||||||
return '%20';
|
return "%20";
|
||||||
default:
|
default:
|
||||||
return ',';
|
return ",";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'label':
|
case "label":
|
||||||
return '.';
|
return ".";
|
||||||
case 'matrix':
|
case "matrix":
|
||||||
return ';';
|
return ";";
|
||||||
case 'simple':
|
case "simple":
|
||||||
return ',';
|
return ",";
|
||||||
default:
|
default:
|
||||||
return '&';
|
return "&";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializeArrayParam = ({
|
export const serializeArrayParam = ({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
explode,
|
explode,
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
value,
|
value,
|
||||||
}: SerializeOptions<ArraySeparatorStyle> & {
|
}: SerializeOptions<ArraySeparatorStyle> & {
|
||||||
value: unknown[];
|
value: unknown[];
|
||||||
}) => {
|
}) => {
|
||||||
if (!explode) {
|
if (!explode) {
|
||||||
const joinedValues = (
|
const joinedValues = (
|
||||||
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
|
||||||
).join(separatorArrayNoExplode(style));
|
).join(separatorArrayNoExplode(style));
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'label':
|
case "label":
|
||||||
return `.${joinedValues}`;
|
return `.${joinedValues}`;
|
||||||
case 'matrix':
|
case "matrix":
|
||||||
return `;${name}=${joinedValues}`;
|
return `;${name}=${joinedValues}`;
|
||||||
case 'simple':
|
case "simple":
|
||||||
return joinedValues;
|
return joinedValues;
|
||||||
default:
|
default:
|
||||||
return `${name}=${joinedValues}`;
|
return `${name}=${joinedValues}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const separator = separatorArrayExplode(style);
|
const separator = separatorArrayExplode(style);
|
||||||
const joinedValues = value
|
const joinedValues = value
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
if (style === 'label' || style === 'simple') {
|
if (style === "label" || style === "simple") {
|
||||||
return allowReserved ? v : encodeURIComponent(v as string);
|
return allowReserved ? v : encodeURIComponent(v as string);
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializePrimitiveParam({
|
return serializePrimitiveParam({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
name,
|
name,
|
||||||
value: v as string,
|
value: v as string,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.join(separator);
|
.join(separator);
|
||||||
return style === 'label' || style === 'matrix'
|
return style === "label" || style === "matrix"
|
||||||
? separator + joinedValues
|
? separator + joinedValues
|
||||||
: joinedValues;
|
: joinedValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializePrimitiveParam = ({
|
export const serializePrimitiveParam = ({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
}: SerializePrimitiveParam) => {
|
}: SerializePrimitiveParam) => {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
if (typeof value === "object") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.',
|
"Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const serializeObjectParam = ({
|
export const serializeObjectParam = ({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
explode,
|
explode,
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
value,
|
value,
|
||||||
valueOnly,
|
valueOnly,
|
||||||
}: SerializeOptions<ObjectSeparatorStyle> & {
|
}: SerializeOptions<ObjectSeparatorStyle> & {
|
||||||
value: Record<string, unknown> | Date;
|
value: Record<string, unknown> | Date;
|
||||||
valueOnly?: boolean;
|
valueOnly?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style !== 'deepObject' && !explode) {
|
if (style !== "deepObject" && !explode) {
|
||||||
let values: string[] = [];
|
let values: string[] = [];
|
||||||
Object.entries(value).forEach(([key, v]) => {
|
Object.entries(value).forEach(([key, v]) => {
|
||||||
values = [
|
values = [
|
||||||
...values,
|
...values,
|
||||||
key,
|
key,
|
||||||
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
allowReserved ? (v as string) : encodeURIComponent(v as string),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
const joinedValues = values.join(',');
|
const joinedValues = values.join(",");
|
||||||
switch (style) {
|
switch (style) {
|
||||||
case 'form':
|
case "form":
|
||||||
return `${name}=${joinedValues}`;
|
return `${name}=${joinedValues}`;
|
||||||
case 'label':
|
case "label":
|
||||||
return `.${joinedValues}`;
|
return `.${joinedValues}`;
|
||||||
case 'matrix':
|
case "matrix":
|
||||||
return `;${name}=${joinedValues}`;
|
return `;${name}=${joinedValues}`;
|
||||||
default:
|
default:
|
||||||
return joinedValues;
|
return joinedValues;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const separator = separatorObjectExplode(style);
|
const separator = separatorObjectExplode(style);
|
||||||
const joinedValues = Object.entries(value)
|
const joinedValues = Object.entries(value)
|
||||||
.map(([key, v]) =>
|
.map(([key, v]) =>
|
||||||
serializePrimitiveParam({
|
serializePrimitiveParam({
|
||||||
allowReserved,
|
allowReserved,
|
||||||
name: style === 'deepObject' ? `${name}[${key}]` : key,
|
name: style === "deepObject" ? `${name}[${key}]` : key,
|
||||||
value: v as string,
|
value: v as string,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.join(separator);
|
.join(separator);
|
||||||
return style === 'label' || style === 'matrix'
|
return style === "label" || style === "matrix"
|
||||||
? separator + joinedValues
|
? separator + joinedValues
|
||||||
: joinedValues;
|
: joinedValues;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,133 +4,133 @@
|
||||||
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
* JSON-friendly union that mirrors what Pinia Colada can hash.
|
||||||
*/
|
*/
|
||||||
export type JsonValue =
|
export type JsonValue =
|
||||||
| null
|
| null
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| JsonValue[]
|
| JsonValue[]
|
||||||
| { [key: string]: JsonValue };
|
| { [key: string]: JsonValue };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
* Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
|
||||||
*/
|
*/
|
||||||
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
|
||||||
if (
|
if (
|
||||||
value === undefined ||
|
value === undefined ||
|
||||||
typeof value === 'function' ||
|
typeof value === "function" ||
|
||||||
typeof value === 'symbol'
|
typeof value === "symbol"
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (typeof value === 'bigint') {
|
if (typeof value === "bigint") {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toISOString();
|
return value.toISOString();
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely stringifies a value and parses it back into a JsonValue.
|
* Safely stringifies a value and parses it back into a JsonValue.
|
||||||
*/
|
*/
|
||||||
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => {
|
||||||
try {
|
try {
|
||||||
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
const json = JSON.stringify(input, queryKeyJsonReplacer);
|
||||||
if (json === undefined) {
|
if (json === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return JSON.parse(json) as JsonValue;
|
return JSON.parse(json) as JsonValue;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects plain objects (including objects with a null prototype).
|
* Detects plain objects (including objects with a null prototype).
|
||||||
*/
|
*/
|
||||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||||
if (value === null || typeof value !== 'object') {
|
if (value === null || typeof value !== "object") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const prototype = Object.getPrototypeOf(value as object);
|
const prototype = Object.getPrototypeOf(value as object);
|
||||||
return prototype === Object.prototype || prototype === null;
|
return prototype === Object.prototype || prototype === null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
* Turns URLSearchParams into a sorted JSON object for deterministic keys.
|
||||||
*/
|
*/
|
||||||
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
const serializeSearchParams = (params: URLSearchParams): JsonValue => {
|
||||||
const entries = Array.from(params.entries()).sort(([a], [b]) =>
|
const entries = Array.from(params.entries()).sort(([a], [b]) =>
|
||||||
a.localeCompare(b),
|
a.localeCompare(b),
|
||||||
);
|
);
|
||||||
const result: Record<string, JsonValue> = {};
|
const result: Record<string, JsonValue> = {};
|
||||||
|
|
||||||
for (const [key, value] of entries) {
|
for (const [key, value] of entries) {
|
||||||
const existing = result[key];
|
const existing = result[key];
|
||||||
if (existing === undefined) {
|
if (existing === undefined) {
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(existing)) {
|
if (Array.isArray(existing)) {
|
||||||
(existing as string[]).push(value);
|
(existing as string[]).push(value);
|
||||||
} else {
|
} else {
|
||||||
result[key] = [existing, value];
|
result[key] = [existing, value];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
* Normalizes any accepted value into a JSON-friendly shape for query keys.
|
||||||
*/
|
*/
|
||||||
export const serializeQueryKeyValue = (
|
export const serializeQueryKeyValue = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): JsonValue | undefined => {
|
): JsonValue | undefined => {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof value === 'string' ||
|
typeof value === "string" ||
|
||||||
typeof value === 'number' ||
|
typeof value === "number" ||
|
||||||
typeof value === 'boolean'
|
typeof value === "boolean"
|
||||||
) {
|
) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
value === undefined ||
|
value === undefined ||
|
||||||
typeof value === 'function' ||
|
typeof value === "function" ||
|
||||||
typeof value === 'symbol'
|
typeof value === "symbol"
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'bigint') {
|
if (typeof value === "bigint") {
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return value.toISOString();
|
return value.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return stringifyToJsonValue(value);
|
return stringifyToJsonValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof URLSearchParams !== 'undefined' &&
|
typeof URLSearchParams !== "undefined" &&
|
||||||
value instanceof URLSearchParams
|
value instanceof URLSearchParams
|
||||||
) {
|
) {
|
||||||
return serializeSearchParams(value);
|
return serializeSearchParams(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
if (isPlainObject(value)) {
|
||||||
return stringifyToJsonValue(value);
|
return stringifyToJsonValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,266 +1,266 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Config } from './types.gen';
|
import type { Config } from "./types.gen";
|
||||||
|
|
||||||
export type ServerSentEventsOptions<TData = unknown> = Omit<
|
export type ServerSentEventsOptions<TData = unknown> = Omit<
|
||||||
RequestInit,
|
RequestInit,
|
||||||
'method'
|
"method"
|
||||||
> &
|
> &
|
||||||
Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
|
Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
|
||||||
/**
|
/**
|
||||||
* Fetch API implementation. You can use this option to provide a custom
|
* Fetch API implementation. You can use this option to provide a custom
|
||||||
* fetch instance.
|
* fetch instance.
|
||||||
*
|
*
|
||||||
* @default globalThis.fetch
|
* @default globalThis.fetch
|
||||||
*/
|
*/
|
||||||
fetch?: typeof fetch;
|
fetch?: typeof fetch;
|
||||||
/**
|
/**
|
||||||
* Implementing clients can call request interceptors inside this hook.
|
* Implementing clients can call request interceptors inside this hook.
|
||||||
*/
|
*/
|
||||||
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
onRequest?: (url: string, init: RequestInit) => Promise<Request>;
|
||||||
/**
|
/**
|
||||||
* Callback invoked when a network or parsing error occurs during streaming.
|
* Callback invoked when a network or parsing error occurs during streaming.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @param error The error that occurred.
|
* @param error The error that occurred.
|
||||||
*/
|
*/
|
||||||
onSseError?: (error: unknown) => void;
|
onSseError?: (error: unknown) => void;
|
||||||
/**
|
/**
|
||||||
* Callback invoked when an event is streamed from the server.
|
* Callback invoked when an event is streamed from the server.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @param event Event streamed from the server.
|
* @param event Event streamed from the server.
|
||||||
* @returns Nothing (void).
|
* @returns Nothing (void).
|
||||||
*/
|
*/
|
||||||
onSseEvent?: (event: StreamEvent<TData>) => void;
|
onSseEvent?: (event: StreamEvent<TData>) => void;
|
||||||
serializedBody?: RequestInit['body'];
|
serializedBody?: RequestInit["body"];
|
||||||
/**
|
/**
|
||||||
* Default retry delay in milliseconds.
|
* Default retry delay in milliseconds.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @default 3000
|
* @default 3000
|
||||||
*/
|
*/
|
||||||
sseDefaultRetryDelay?: number;
|
sseDefaultRetryDelay?: number;
|
||||||
/**
|
/**
|
||||||
* Maximum number of retry attempts before giving up.
|
* Maximum number of retry attempts before giving up.
|
||||||
*/
|
*/
|
||||||
sseMaxRetryAttempts?: number;
|
sseMaxRetryAttempts?: number;
|
||||||
/**
|
/**
|
||||||
* Maximum retry delay in milliseconds.
|
* Maximum retry delay in milliseconds.
|
||||||
*
|
*
|
||||||
* Applies only when exponential backoff is used.
|
* Applies only when exponential backoff is used.
|
||||||
*
|
*
|
||||||
* This option applies only if the endpoint returns a stream of events.
|
* This option applies only if the endpoint returns a stream of events.
|
||||||
*
|
*
|
||||||
* @default 30000
|
* @default 30000
|
||||||
*/
|
*/
|
||||||
sseMaxRetryDelay?: number;
|
sseMaxRetryDelay?: number;
|
||||||
/**
|
/**
|
||||||
* Optional sleep function for retry backoff.
|
* Optional sleep function for retry backoff.
|
||||||
*
|
*
|
||||||
* Defaults to using `setTimeout`.
|
* Defaults to using `setTimeout`.
|
||||||
*/
|
*/
|
||||||
sseSleepFn?: (ms: number) => Promise<void>;
|
sseSleepFn?: (ms: number) => Promise<void>;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StreamEvent<TData = unknown> {
|
export interface StreamEvent<TData = unknown> {
|
||||||
data: TData;
|
data: TData;
|
||||||
event?: string;
|
event?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
retry?: number;
|
retry?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerSentEventsResult<
|
export type ServerSentEventsResult<
|
||||||
TData = unknown,
|
TData = unknown,
|
||||||
TReturn = void,
|
TReturn = void,
|
||||||
TNext = unknown,
|
TNext = unknown,
|
||||||
> = {
|
> = {
|
||||||
stream: AsyncGenerator<
|
stream: AsyncGenerator<
|
||||||
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
TData extends Record<string, unknown> ? TData[keyof TData] : TData,
|
||||||
TReturn,
|
TReturn,
|
||||||
TNext
|
TNext
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSseClient = <TData = unknown>({
|
export const createSseClient = <TData = unknown>({
|
||||||
onRequest,
|
onRequest,
|
||||||
onSseError,
|
onSseError,
|
||||||
onSseEvent,
|
onSseEvent,
|
||||||
responseTransformer,
|
responseTransformer,
|
||||||
responseValidator,
|
responseValidator,
|
||||||
sseDefaultRetryDelay,
|
sseDefaultRetryDelay,
|
||||||
sseMaxRetryAttempts,
|
sseMaxRetryAttempts,
|
||||||
sseMaxRetryDelay,
|
sseMaxRetryDelay,
|
||||||
sseSleepFn,
|
sseSleepFn,
|
||||||
url,
|
url,
|
||||||
...options
|
...options
|
||||||
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
|
||||||
let lastEventId: string | undefined;
|
let lastEventId: string | undefined;
|
||||||
|
|
||||||
const sleep =
|
const sleep =
|
||||||
sseSleepFn ??
|
sseSleepFn ??
|
||||||
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||||
|
|
||||||
const createStream = async function* () {
|
const createStream = async function* () {
|
||||||
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
let retryDelay: number = sseDefaultRetryDelay ?? 3000;
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
const signal = options.signal ?? new AbortController().signal;
|
const signal = options.signal ?? new AbortController().signal;
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (signal.aborted) break;
|
if (signal.aborted) break;
|
||||||
|
|
||||||
attempt++;
|
attempt++;
|
||||||
|
|
||||||
const headers =
|
const headers =
|
||||||
options.headers instanceof Headers
|
options.headers instanceof Headers
|
||||||
? options.headers
|
? options.headers
|
||||||
: new Headers(options.headers as Record<string, string> | undefined);
|
: new Headers(options.headers as Record<string, string> | undefined);
|
||||||
|
|
||||||
if (lastEventId !== undefined) {
|
if (lastEventId !== undefined) {
|
||||||
headers.set('Last-Event-ID', lastEventId);
|
headers.set("Last-Event-ID", lastEventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestInit: RequestInit = {
|
const requestInit: RequestInit = {
|
||||||
redirect: 'follow',
|
redirect: "follow",
|
||||||
...options,
|
...options,
|
||||||
body: options.serializedBody,
|
body: options.serializedBody,
|
||||||
headers,
|
headers,
|
||||||
signal,
|
signal,
|
||||||
};
|
};
|
||||||
let request = new Request(url, requestInit);
|
let request = new Request(url, requestInit);
|
||||||
if (onRequest) {
|
if (onRequest) {
|
||||||
request = await onRequest(url, requestInit);
|
request = await onRequest(url, requestInit);
|
||||||
}
|
}
|
||||||
// fetch must be assigned here, otherwise it would throw the error:
|
// fetch must be assigned here, otherwise it would throw the error:
|
||||||
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
// TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation
|
||||||
const _fetch = options.fetch ?? globalThis.fetch;
|
const _fetch = options.fetch ?? globalThis.fetch;
|
||||||
const response = await _fetch(request);
|
const response = await _fetch(request);
|
||||||
|
|
||||||
if (!response.ok)
|
if (!response.ok)
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`SSE failed: ${response.status} ${response.statusText}`,
|
`SSE failed: ${response.status} ${response.statusText}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.body) throw new Error('No body in SSE response');
|
if (!response.body) throw new Error("No body in SSE response");
|
||||||
|
|
||||||
const reader = response.body
|
const reader = response.body
|
||||||
.pipeThrough(new TextDecoderStream())
|
.pipeThrough(new TextDecoderStream())
|
||||||
.getReader();
|
.getReader();
|
||||||
|
|
||||||
let buffer = '';
|
let buffer = "";
|
||||||
|
|
||||||
const abortHandler = () => {
|
const abortHandler = () => {
|
||||||
try {
|
try {
|
||||||
reader.cancel();
|
reader.cancel();
|
||||||
} catch {
|
} catch {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
signal.addEventListener('abort', abortHandler);
|
signal.addEventListener("abort", abortHandler);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
if (done) break;
|
||||||
buffer += value;
|
buffer += value;
|
||||||
// Normalize line endings: CRLF -> LF, then CR -> LF
|
// Normalize line endings: CRLF -> LF, then CR -> LF
|
||||||
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||||||
|
|
||||||
const chunks = buffer.split('\n\n');
|
const chunks = buffer.split("\n\n");
|
||||||
buffer = chunks.pop() ?? '';
|
buffer = chunks.pop() ?? "";
|
||||||
|
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
const lines = chunk.split('\n');
|
const lines = chunk.split("\n");
|
||||||
const dataLines: Array<string> = [];
|
const dataLines: Array<string> = [];
|
||||||
let eventName: string | undefined;
|
let eventName: string | undefined;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data:')) {
|
if (line.startsWith("data:")) {
|
||||||
dataLines.push(line.replace(/^data:\s*/, ''));
|
dataLines.push(line.replace(/^data:\s*/, ""));
|
||||||
} else if (line.startsWith('event:')) {
|
} else if (line.startsWith("event:")) {
|
||||||
eventName = line.replace(/^event:\s*/, '');
|
eventName = line.replace(/^event:\s*/, "");
|
||||||
} else if (line.startsWith('id:')) {
|
} else if (line.startsWith("id:")) {
|
||||||
lastEventId = line.replace(/^id:\s*/, '');
|
lastEventId = line.replace(/^id:\s*/, "");
|
||||||
} else if (line.startsWith('retry:')) {
|
} else if (line.startsWith("retry:")) {
|
||||||
const parsed = Number.parseInt(
|
const parsed = Number.parseInt(
|
||||||
line.replace(/^retry:\s*/, ''),
|
line.replace(/^retry:\s*/, ""),
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
if (!Number.isNaN(parsed)) {
|
if (!Number.isNaN(parsed)) {
|
||||||
retryDelay = parsed;
|
retryDelay = parsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: unknown;
|
let data: unknown;
|
||||||
let parsedJson = false;
|
let parsedJson = false;
|
||||||
|
|
||||||
if (dataLines.length) {
|
if (dataLines.length) {
|
||||||
const rawData = dataLines.join('\n');
|
const rawData = dataLines.join("\n");
|
||||||
try {
|
try {
|
||||||
data = JSON.parse(rawData);
|
data = JSON.parse(rawData);
|
||||||
parsedJson = true;
|
parsedJson = true;
|
||||||
} catch {
|
} catch {
|
||||||
data = rawData;
|
data = rawData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedJson) {
|
if (parsedJson) {
|
||||||
if (responseValidator) {
|
if (responseValidator) {
|
||||||
await responseValidator(data);
|
await responseValidator(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseTransformer) {
|
if (responseTransformer) {
|
||||||
data = await responseTransformer(data);
|
data = await responseTransformer(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSseEvent?.({
|
onSseEvent?.({
|
||||||
data,
|
data,
|
||||||
event: eventName,
|
event: eventName,
|
||||||
id: lastEventId,
|
id: lastEventId,
|
||||||
retry: retryDelay,
|
retry: retryDelay,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (dataLines.length) {
|
if (dataLines.length) {
|
||||||
yield data as any;
|
yield data as any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
signal.removeEventListener('abort', abortHandler);
|
signal.removeEventListener("abort", abortHandler);
|
||||||
reader.releaseLock();
|
reader.releaseLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
break; // exit loop on normal completion
|
break; // exit loop on normal completion
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// connection failed or aborted; retry after delay
|
// connection failed or aborted; retry after delay
|
||||||
onSseError?.(error);
|
onSseError?.(error);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
sseMaxRetryAttempts !== undefined &&
|
sseMaxRetryAttempts !== undefined &&
|
||||||
attempt >= sseMaxRetryAttempts
|
attempt >= sseMaxRetryAttempts
|
||||||
) {
|
) {
|
||||||
break; // stop after firing error
|
break; // stop after firing error
|
||||||
}
|
}
|
||||||
|
|
||||||
// exponential backoff: double retry each attempt, cap at 30s
|
// exponential backoff: double retry each attempt, cap at 30s
|
||||||
const backoff = Math.min(
|
const backoff = Math.min(
|
||||||
retryDelay * 2 ** (attempt - 1),
|
retryDelay * 2 ** (attempt - 1),
|
||||||
sseMaxRetryDelay ?? 30000,
|
sseMaxRetryDelay ?? 30000,
|
||||||
);
|
);
|
||||||
await sleep(backoff);
|
await sleep(backoff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stream = createStream();
|
const stream = createStream();
|
||||||
|
|
||||||
return { stream };
|
return { stream };
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,118 +1,118 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { Auth, AuthToken } from './auth.gen';
|
import type { Auth, AuthToken } from "./auth.gen";
|
||||||
import type {
|
import type {
|
||||||
BodySerializer,
|
BodySerializer,
|
||||||
QuerySerializer,
|
QuerySerializer,
|
||||||
QuerySerializerOptions,
|
QuerySerializerOptions,
|
||||||
} from './bodySerializer.gen';
|
} from "./bodySerializer.gen";
|
||||||
|
|
||||||
export type HttpMethod =
|
export type HttpMethod =
|
||||||
| 'connect'
|
| "connect"
|
||||||
| 'delete'
|
| "delete"
|
||||||
| 'get'
|
| "get"
|
||||||
| 'head'
|
| "head"
|
||||||
| 'options'
|
| "options"
|
||||||
| 'patch'
|
| "patch"
|
||||||
| 'post'
|
| "post"
|
||||||
| 'put'
|
| "put"
|
||||||
| 'trace';
|
| "trace";
|
||||||
|
|
||||||
export type Client<
|
export type Client<
|
||||||
RequestFn = never,
|
RequestFn = never,
|
||||||
Config = unknown,
|
Config = unknown,
|
||||||
MethodFn = never,
|
MethodFn = never,
|
||||||
BuildUrlFn = never,
|
BuildUrlFn = never,
|
||||||
SseFn = never,
|
SseFn = never,
|
||||||
> = {
|
> = {
|
||||||
/**
|
/**
|
||||||
* Returns the final request URL.
|
* Returns the final request URL.
|
||||||
*/
|
*/
|
||||||
buildUrl: BuildUrlFn;
|
buildUrl: BuildUrlFn;
|
||||||
getConfig: () => Config;
|
getConfig: () => Config;
|
||||||
request: RequestFn;
|
request: RequestFn;
|
||||||
setConfig: (config: Config) => Config;
|
setConfig: (config: Config) => Config;
|
||||||
} & {
|
} & {
|
||||||
[K in HttpMethod]: MethodFn;
|
[K in HttpMethod]: MethodFn;
|
||||||
} & ([SseFn] extends [never]
|
} & ([SseFn] extends [never]
|
||||||
? { sse?: never }
|
? { sse?: never }
|
||||||
: { sse: { [K in HttpMethod]: SseFn } });
|
: { sse: { [K in HttpMethod]: SseFn } });
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
/**
|
/**
|
||||||
* Auth token or a function returning auth token. The resolved value will be
|
* Auth token or a function returning auth token. The resolved value will be
|
||||||
* added to the request payload as defined by its `security` array.
|
* added to the request payload as defined by its `security` array.
|
||||||
*/
|
*/
|
||||||
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
auth?: ((auth: Auth) => Promise<AuthToken> | AuthToken) | AuthToken;
|
||||||
/**
|
/**
|
||||||
* A function for serializing request body parameter. By default,
|
* A function for serializing request body parameter. By default,
|
||||||
* {@link JSON.stringify()} will be used.
|
* {@link JSON.stringify()} will be used.
|
||||||
*/
|
*/
|
||||||
bodySerializer?: BodySerializer | null;
|
bodySerializer?: BodySerializer | null;
|
||||||
/**
|
/**
|
||||||
* An object containing any HTTP headers that you want to pre-populate your
|
* An object containing any HTTP headers that you want to pre-populate your
|
||||||
* `Headers` object with.
|
* `Headers` object with.
|
||||||
*
|
*
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
* {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more}
|
||||||
*/
|
*/
|
||||||
headers?:
|
headers?:
|
||||||
| RequestInit['headers']
|
| RequestInit["headers"]
|
||||||
| Record<
|
| Record<
|
||||||
string,
|
string,
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| boolean
|
| boolean
|
||||||
| (string | number | boolean)[]
|
| (string | number | boolean)[]
|
||||||
| null
|
| null
|
||||||
| undefined
|
| undefined
|
||||||
| unknown
|
| unknown
|
||||||
>;
|
>;
|
||||||
/**
|
/**
|
||||||
* The request method.
|
* The request method.
|
||||||
*
|
*
|
||||||
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
* {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
|
||||||
*/
|
*/
|
||||||
method?: Uppercase<HttpMethod>;
|
method?: Uppercase<HttpMethod>;
|
||||||
/**
|
/**
|
||||||
* A function for serializing request query parameters. By default, arrays
|
* A function for serializing request query parameters. By default, arrays
|
||||||
* will be exploded in form style, objects will be exploded in deepObject
|
* will be exploded in form style, objects will be exploded in deepObject
|
||||||
* style, and reserved characters are percent-encoded.
|
* style, and reserved characters are percent-encoded.
|
||||||
*
|
*
|
||||||
* This method will have no effect if the native `paramsSerializer()` Axios
|
* This method will have no effect if the native `paramsSerializer()` Axios
|
||||||
* API function is used.
|
* API function is used.
|
||||||
*
|
*
|
||||||
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
* {@link https://swagger.io/docs/specification/serialization/#query View examples}
|
||||||
*/
|
*/
|
||||||
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
querySerializer?: QuerySerializer | QuerySerializerOptions;
|
||||||
/**
|
/**
|
||||||
* A function validating request data. This is useful if you want to ensure
|
* A function validating request data. This is useful if you want to ensure
|
||||||
* the request conforms to the desired shape, so it can be safely sent to
|
* the request conforms to the desired shape, so it can be safely sent to
|
||||||
* the server.
|
* the server.
|
||||||
*/
|
*/
|
||||||
requestValidator?: (data: unknown) => Promise<unknown>;
|
requestValidator?: (data: unknown) => Promise<unknown>;
|
||||||
/**
|
/**
|
||||||
* A function transforming response data before it's returned. This is useful
|
* A function transforming response data before it's returned. This is useful
|
||||||
* for post-processing data, e.g. converting ISO strings into Date objects.
|
* for post-processing data, e.g. converting ISO strings into Date objects.
|
||||||
*/
|
*/
|
||||||
responseTransformer?: (data: unknown) => Promise<unknown>;
|
responseTransformer?: (data: unknown) => Promise<unknown>;
|
||||||
/**
|
/**
|
||||||
* A function validating response data. This is useful if you want to ensure
|
* A function validating response data. This is useful if you want to ensure
|
||||||
* the response conforms to the desired shape, so it can be safely passed to
|
* the response conforms to the desired shape, so it can be safely passed to
|
||||||
* the transformers and returned to the user.
|
* the transformers and returned to the user.
|
||||||
*/
|
*/
|
||||||
responseValidator?: (data: unknown) => Promise<unknown>;
|
responseValidator?: (data: unknown) => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
|
||||||
? true
|
? true
|
||||||
: [T] extends [never | undefined]
|
: [T] extends [never | undefined]
|
||||||
? [undefined] extends [T]
|
? [undefined] extends [T]
|
||||||
? false
|
? false
|
||||||
: true
|
: true
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
export type OmitNever<T extends Record<string, unknown>> = {
|
export type OmitNever<T extends Record<string, unknown>> = {
|
||||||
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
[K in keyof T as IsExactlyNeverOrNeverUndefined<T[K]> extends true
|
||||||
? never
|
? never
|
||||||
: K]: T[K];
|
: K]: T[K];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,143 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import type { BodySerializer, QuerySerializer } from './bodySerializer.gen';
|
import type { BodySerializer, QuerySerializer } from "./bodySerializer.gen";
|
||||||
import {
|
import {
|
||||||
type ArraySeparatorStyle,
|
type ArraySeparatorStyle,
|
||||||
serializeArrayParam,
|
serializeArrayParam,
|
||||||
serializeObjectParam,
|
serializeObjectParam,
|
||||||
serializePrimitiveParam,
|
serializePrimitiveParam,
|
||||||
} from './pathSerializer.gen';
|
} from "./pathSerializer.gen";
|
||||||
|
|
||||||
export interface PathSerializer {
|
export interface PathSerializer {
|
||||||
path: Record<string, unknown>;
|
path: Record<string, unknown>;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
export const PATH_PARAM_RE = /\{[^{}]+\}/g;
|
||||||
|
|
||||||
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
|
||||||
let url = _url;
|
let url = _url;
|
||||||
const matches = _url.match(PATH_PARAM_RE);
|
const matches = _url.match(PATH_PARAM_RE);
|
||||||
if (matches) {
|
if (matches) {
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
let explode = false;
|
let explode = false;
|
||||||
let name = match.substring(1, match.length - 1);
|
let name = match.substring(1, match.length - 1);
|
||||||
let style: ArraySeparatorStyle = 'simple';
|
let style: ArraySeparatorStyle = "simple";
|
||||||
|
|
||||||
if (name.endsWith('*')) {
|
if (name.endsWith("*")) {
|
||||||
explode = true;
|
explode = true;
|
||||||
name = name.substring(0, name.length - 1);
|
name = name.substring(0, name.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name.startsWith('.')) {
|
if (name.startsWith(".")) {
|
||||||
name = name.substring(1);
|
name = name.substring(1);
|
||||||
style = 'label';
|
style = "label";
|
||||||
} else if (name.startsWith(';')) {
|
} else if (name.startsWith(";")) {
|
||||||
name = name.substring(1);
|
name = name.substring(1);
|
||||||
style = 'matrix';
|
style = "matrix";
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = path[name];
|
const value = path[name];
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
serializeArrayParam({ explode, name, style, value }),
|
serializeArrayParam({ explode, name, style, value }),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
if (typeof value === "object") {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
serializeObjectParam({
|
serializeObjectParam({
|
||||||
explode,
|
explode,
|
||||||
name,
|
name,
|
||||||
style,
|
style,
|
||||||
value: value as Record<string, unknown>,
|
value: value as Record<string, unknown>,
|
||||||
valueOnly: true,
|
valueOnly: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (style === 'matrix') {
|
if (style === "matrix") {
|
||||||
url = url.replace(
|
url = url.replace(
|
||||||
match,
|
match,
|
||||||
`;${serializePrimitiveParam({
|
`;${serializePrimitiveParam({
|
||||||
name,
|
name,
|
||||||
value: value as string,
|
value: value as string,
|
||||||
})}`,
|
})}`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceValue = encodeURIComponent(
|
const replaceValue = encodeURIComponent(
|
||||||
style === 'label' ? `.${value as string}` : (value as string),
|
style === "label" ? `.${value as string}` : (value as string),
|
||||||
);
|
);
|
||||||
url = url.replace(match, replaceValue);
|
url = url.replace(match, replaceValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUrl = ({
|
export const getUrl = ({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path,
|
path,
|
||||||
query,
|
query,
|
||||||
querySerializer,
|
querySerializer,
|
||||||
url: _url,
|
url: _url,
|
||||||
}: {
|
}: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
path?: Record<string, unknown>;
|
path?: Record<string, unknown>;
|
||||||
query?: Record<string, unknown>;
|
query?: Record<string, unknown>;
|
||||||
querySerializer: QuerySerializer;
|
querySerializer: QuerySerializer;
|
||||||
url: string;
|
url: string;
|
||||||
}) => {
|
}) => {
|
||||||
const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
|
const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
|
||||||
let url = (baseUrl ?? '') + pathUrl;
|
let url = (baseUrl ?? "") + pathUrl;
|
||||||
if (path) {
|
if (path) {
|
||||||
url = defaultPathSerializer({ path, url });
|
url = defaultPathSerializer({ path, url });
|
||||||
}
|
}
|
||||||
let search = query ? querySerializer(query) : '';
|
let search = query ? querySerializer(query) : "";
|
||||||
if (search.startsWith('?')) {
|
if (search.startsWith("?")) {
|
||||||
search = search.substring(1);
|
search = search.substring(1);
|
||||||
}
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
url += `?${search}`;
|
url += `?${search}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getValidRequestBody(options: {
|
export function getValidRequestBody(options: {
|
||||||
body?: unknown;
|
body?: unknown;
|
||||||
bodySerializer?: BodySerializer | null;
|
bodySerializer?: BodySerializer | null;
|
||||||
serializedBody?: unknown;
|
serializedBody?: unknown;
|
||||||
}) {
|
}) {
|
||||||
const hasBody = options.body !== undefined;
|
const hasBody = options.body !== undefined;
|
||||||
const isSerializedBody = hasBody && options.bodySerializer;
|
const isSerializedBody = hasBody && options.bodySerializer;
|
||||||
|
|
||||||
if (isSerializedBody) {
|
if (isSerializedBody) {
|
||||||
if ('serializedBody' in options) {
|
if ("serializedBody" in options) {
|
||||||
const hasSerializedBody =
|
const hasSerializedBody =
|
||||||
options.serializedBody !== undefined && options.serializedBody !== '';
|
options.serializedBody !== undefined && options.serializedBody !== "";
|
||||||
|
|
||||||
return hasSerializedBody ? options.serializedBody : null;
|
return hasSerializedBody ? options.serializedBody : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// not all clients implement a serializedBody property (i.e. client-axios)
|
// not all clients implement a serializedBody property (i.e. client-axios)
|
||||||
return options.body !== '' ? options.body : null;
|
return options.body !== "" ? options.body : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// plain/text body
|
// plain/text body
|
||||||
if (hasBody) {
|
if (hasBody) {
|
||||||
return options.body;
|
return options.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no body was provided
|
// no body was provided
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
export type * from './types.gen';
|
export type * from "./types.gen";
|
||||||
export * from './sdk.gen';
|
export * from "./sdk.gen";
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { LifeBuoy } from "lucide-react";
|
import { LifeBuoy } from "lucide-react";
|
||||||
import { Outlet, redirect, useNavigate } from "react-router";
|
import { Outlet, redirect, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -10,7 +9,7 @@ import { GridBackground } from "./grid-background";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { SidebarProvider, SidebarTrigger } from "./ui/sidebar";
|
import { SidebarProvider, SidebarTrigger } from "./ui/sidebar";
|
||||||
import { AppSidebar } from "./app-sidebar";
|
import { AppSidebar } from "./app-sidebar";
|
||||||
import { logoutMutation } from "../api-client/@tanstack/react-query.gen";
|
import { authClient } from "../lib/auth-client";
|
||||||
|
|
||||||
export const clientMiddleware = [authMiddleware];
|
export const clientMiddleware = [authMiddleware];
|
||||||
|
|
||||||
|
|
@ -27,16 +26,18 @@ export async function clientLoader({ context }: Route.LoaderArgs) {
|
||||||
export default function Layout({ loaderData }: Route.ComponentProps) {
|
export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const logout = useMutation({
|
const handleLogout = async () => {
|
||||||
...logoutMutation(),
|
await authClient.signOut({
|
||||||
onSuccess: async () => {
|
fetchOptions: {
|
||||||
void navigate("/login", { replace: true });
|
onSuccess: () => {
|
||||||
},
|
void navigate("/login", { replace: true });
|
||||||
onError: (error) => {
|
},
|
||||||
console.error(error);
|
onError: ({ error }) => {
|
||||||
toast.error("Logout failed", { description: error.message });
|
toast.error("Logout failed", { description: error.message });
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider defaultOpen={true}>
|
<SidebarProvider defaultOpen={true}>
|
||||||
|
|
@ -54,7 +55,7 @@ export default function Layout({ loaderData }: Route.ComponentProps) {
|
||||||
Welcome,
|
Welcome,
|
||||||
<span className="text-strong-accent">{loaderData.user?.username}</span>
|
<span className="text-strong-accent">{loaderData.user?.username}</span>
|
||||||
</span>
|
</span>
|
||||||
<Button variant="default" size="sm" onClick={() => logout.mutate({})} loading={logout.isPending}>
|
<Button variant="default" size="sm" onClick={handleLogout}>
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" size="sm" className="relative overflow-hidden hidden lg:inline-flex">
|
<Button variant="default" size="sm" className="relative overflow-hidden hidden lg:inline-flex">
|
||||||
|
|
|
||||||
8
app/client/lib/auth-client.ts
Normal file
8
app/client/lib/auth-client.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
import { usernameClient } from "better-auth/client/plugins";
|
||||||
|
import { inferAdditionalFields } from "better-auth/client/plugins";
|
||||||
|
import type { auth } from "~/lib/auth";
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
plugins: [inferAdditionalFields<typeof auth>(), usernameClient()],
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import type {
|
import type {
|
||||||
GetBackupScheduleResponse,
|
GetBackupScheduleResponse,
|
||||||
GetMeResponse,
|
|
||||||
GetRepositoryResponse,
|
GetRepositoryResponse,
|
||||||
GetVolumeResponse,
|
GetVolumeResponse,
|
||||||
ListNotificationDestinationsResponse,
|
ListNotificationDestinationsResponse,
|
||||||
|
|
@ -11,8 +10,6 @@ export type Volume = GetVolumeResponse["volume"];
|
||||||
export type StatFs = GetVolumeResponse["statfs"];
|
export type StatFs = GetVolumeResponse["statfs"];
|
||||||
export type VolumeStatus = Volume["status"];
|
export type VolumeStatus = Volume["status"];
|
||||||
|
|
||||||
export type User = GetMeResponse["user"];
|
|
||||||
|
|
||||||
export type Repository = GetRepositoryResponse;
|
export type Repository = GetRepositoryResponse;
|
||||||
|
|
||||||
export type BackupSchedule = GetBackupScheduleResponse;
|
export type BackupSchedule = GetBackupScheduleResponse;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
@ -11,8 +10,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { authMiddleware } from "~/middleware/auth";
|
import { authMiddleware } from "~/middleware/auth";
|
||||||
import type { Route } from "./+types/login";
|
import type { Route } from "./+types/login";
|
||||||
import { loginMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
|
||||||
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
|
||||||
export const clientMiddleware = [authMiddleware];
|
export const clientMiddleware = [authMiddleware];
|
||||||
|
|
||||||
|
|
@ -36,6 +35,7 @@ type LoginFormValues = typeof loginSchema.inferIn;
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [showResetDialog, setShowResetDialog] = useState(false);
|
const [showResetDialog, setShowResetDialog] = useState(false);
|
||||||
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||||
|
|
||||||
const form = useForm<LoginFormValues>({
|
const form = useForm<LoginFormValues>({
|
||||||
resolver: arktypeResolver(loginSchema),
|
resolver: arktypeResolver(loginSchema),
|
||||||
|
|
@ -45,28 +45,32 @@ export default function LoginPage() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const login = useMutation({
|
const onSubmit = async (values: LoginFormValues) => {
|
||||||
...loginMutation(),
|
const { data, error } = await authClient.signIn.username({
|
||||||
onSuccess: async (data) => {
|
username: values.username.toLowerCase().trim(),
|
||||||
if (data.user && !data.user.hasDownloadedResticPassword) {
|
password: values.password,
|
||||||
void navigate("/download-recovery-key");
|
fetchOptions: {
|
||||||
} else {
|
onRequest: () => {
|
||||||
void navigate("/volumes");
|
setIsLoggingIn(true);
|
||||||
}
|
},
|
||||||
},
|
onResponse: () => {
|
||||||
onError: (error) => {
|
setIsLoggingIn(false);
|
||||||
console.error(error);
|
},
|
||||||
toast.error("Login failed", { description: error.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (values: LoginFormValues) => {
|
|
||||||
login.mutate({
|
|
||||||
body: {
|
|
||||||
username: values.username.trim(),
|
|
||||||
password: values.password.trim(),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Login failed", { description: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = await authClient.getSession();
|
||||||
|
if (data.user && !d.data?.user.hasDownloadedResticPassword) {
|
||||||
|
void navigate("/download-recovery-key");
|
||||||
|
} else {
|
||||||
|
void navigate("/volumes");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -80,7 +84,7 @@ export default function LoginPage() {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="text" placeholder="admin" disabled={login.isPending} autoFocus />
|
<Input {...field} type="text" placeholder="admin" disabled={isLoggingIn} autoFocus />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
@ -102,13 +106,13 @@ export default function LoginPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" disabled={login.isPending} />
|
<Input {...field} type="password" disabled={isLoggingIn} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" className="w-full" loading={login.isPending}>
|
<Button type="submit" className="w-full" loading={isLoggingIn}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
@ -18,7 +17,8 @@ import type { Route } from "./+types/onboarding";
|
||||||
import { AuthLayout } from "~/client/components/auth-layout";
|
import { AuthLayout } from "~/client/components/auth-layout";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { registerMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export const clientMiddleware = [authMiddleware];
|
export const clientMiddleware = [authMiddleware];
|
||||||
|
|
||||||
|
|
@ -33,7 +33,8 @@ export function meta(_: Route.MetaArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onboardingSchema = type({
|
const onboardingSchema = type({
|
||||||
username: "2<=string<=50",
|
username: type("2<=string<=30").pipe((str) => str.trim().toLowerCase()),
|
||||||
|
email: type("string.email").pipe((str) => str.trim().toLowerCase()),
|
||||||
password: "string>=8",
|
password: "string>=8",
|
||||||
confirmPassword: "string>=1",
|
confirmPassword: "string>=1",
|
||||||
});
|
});
|
||||||
|
|
@ -42,6 +43,7 @@ type OnboardingFormValues = typeof onboardingSchema.inferIn;
|
||||||
|
|
||||||
export default function OnboardingPage() {
|
export default function OnboardingPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
const form = useForm<OnboardingFormValues>({
|
const form = useForm<OnboardingFormValues>({
|
||||||
resolver: arktypeResolver(onboardingSchema),
|
resolver: arktypeResolver(onboardingSchema),
|
||||||
|
|
@ -49,22 +51,11 @@ export default function OnboardingPage() {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
|
email: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const registerUser = useMutation({
|
const onSubmit = async (values: OnboardingFormValues) => {
|
||||||
...registerMutation(),
|
|
||||||
onSuccess: async () => {
|
|
||||||
toast.success("Admin user created successfully!");
|
|
||||||
void navigate("/download-recovery-key");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("Failed to create admin user", { description: error.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = (values: OnboardingFormValues) => {
|
|
||||||
if (values.password !== values.confirmPassword) {
|
if (values.password !== values.confirmPassword) {
|
||||||
form.setError("confirmPassword", {
|
form.setError("confirmPassword", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
|
|
@ -73,18 +64,50 @@ export default function OnboardingPage() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerUser.mutate({
|
const { data, error } = await authClient.signUp.email({
|
||||||
body: {
|
username: values.username.toLowerCase().trim(),
|
||||||
username: values.username.trim(),
|
password: values.password,
|
||||||
password: values.password.trim(),
|
email: values.email.toLowerCase().trim(),
|
||||||
|
name: values.username,
|
||||||
|
displayUsername: values.username,
|
||||||
|
hasDownloadedResticPassword: false,
|
||||||
|
fetchOptions: {
|
||||||
|
onRequest: () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
},
|
||||||
|
onResponse: () => {
|
||||||
|
setSubmitting(false);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data?.token) {
|
||||||
|
toast.success("Admin user created successfully!");
|
||||||
|
void navigate("/download-recovery-key");
|
||||||
|
} else if (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Failed to create admin user", { description: error.message });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout title="Welcome to Zerobyte" description="Create the admin user to get started">
|
<AuthLayout title="Welcome to Zerobyte" description="Create the admin user to get started">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} type="email" placeholder="you@example.com" disabled={submitting} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Enter your email address</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="username"
|
name="username"
|
||||||
|
|
@ -92,7 +115,7 @@ export default function OnboardingPage() {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="text" placeholder="admin" disabled={registerUser.isPending} autoFocus />
|
<Input {...field} type="text" placeholder="admin" disabled={submitting} autoFocus />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Choose a username for the admin account</FormDescription>
|
<FormDescription>Choose a username for the admin account</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -106,12 +129,7 @@ export default function OnboardingPage() {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Password</FormLabel>
|
<FormLabel>Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} type="password" placeholder="Enter a secure password" disabled={submitting} />
|
||||||
{...field}
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter a secure password"
|
|
||||||
disabled={registerUser.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>Password must be at least 8 characters long.</FormDescription>
|
<FormDescription>Password must be at least 8 characters long.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -125,19 +143,14 @@ export default function OnboardingPage() {
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Confirm Password</FormLabel>
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} type="password" placeholder="Re-enter your password" disabled={submitting} />
|
||||||
{...field}
|
|
||||||
type="password"
|
|
||||||
placeholder="Re-enter your password"
|
|
||||||
disabled={registerUser.isPending}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" className="w-full" loading={registerUser.isPending}>
|
<Button type="submit" className="w-full" loading={submitting}>
|
||||||
Create Admin User
|
Create admin user
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,8 @@ import { Input } from "~/client/components/ui/input";
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { appContext } from "~/context";
|
import { appContext } from "~/context";
|
||||||
import type { Route } from "./+types/settings";
|
import type { Route } from "./+types/settings";
|
||||||
import {
|
import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
changePasswordMutation,
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
downloadResticPasswordMutation,
|
|
||||||
logoutMutation,
|
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: () => [{ label: "Settings" }],
|
breadcrumb: () => [{ label: "Settings" }],
|
||||||
|
|
@ -49,31 +46,22 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
const [confirmPassword, setConfirmPassword] = useState("");
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
||||||
const [downloadPassword, setDownloadPassword] = useState("");
|
const [downloadPassword, setDownloadPassword] = useState("");
|
||||||
|
const [isChangingPassword, setIsChangingPassword] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const logout = useMutation({
|
const handleLogout = async () => {
|
||||||
...logoutMutation(),
|
await authClient.signOut({
|
||||||
onSuccess: () => {
|
fetchOptions: {
|
||||||
void navigate("/login", { replace: true });
|
onSuccess: () => {
|
||||||
},
|
void navigate("/login", { replace: true });
|
||||||
});
|
},
|
||||||
|
onError: ({ error }) => {
|
||||||
const changePassword = useMutation({
|
console.error(error);
|
||||||
...changePasswordMutation(),
|
toast.error("Logout failed", { description: error.message });
|
||||||
onSuccess: (data) => {
|
},
|
||||||
if (data.success) {
|
},
|
||||||
toast.success("Password changed successfully. You will be logged out.");
|
});
|
||||||
setTimeout(() => {
|
};
|
||||||
logout.mutate({});
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
toast.error("Failed to change password", { description: data.message });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error("Failed to change password", { description: error.message });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadResticPassword = useMutation({
|
const downloadResticPassword = useMutation({
|
||||||
...downloadResticPasswordMutation(),
|
...downloadResticPasswordMutation(),
|
||||||
|
|
@ -97,7 +85,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleChangePassword = (e: React.FormEvent) => {
|
const handleChangePassword = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (newPassword !== confirmPassword) {
|
if (newPassword !== confirmPassword) {
|
||||||
|
|
@ -110,10 +98,26 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
changePassword.mutate({
|
await authClient.changePassword({
|
||||||
body: {
|
newPassword,
|
||||||
currentPassword,
|
currentPassword: currentPassword,
|
||||||
newPassword,
|
revokeOtherSessions: true,
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Password changed successfully. You will be logged out.");
|
||||||
|
setTimeout(() => {
|
||||||
|
void handleLogout();
|
||||||
|
}, 1500);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to change password", { description: error.error.message });
|
||||||
|
},
|
||||||
|
onRequest: () => {
|
||||||
|
setIsChangingPassword(true);
|
||||||
|
},
|
||||||
|
onResponse: () => {
|
||||||
|
setIsChangingPassword(false);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -194,7 +198,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
||||||
minLength={8}
|
minLength={8}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" loading={changePassword.isPending} className="mt-4">
|
<Button type="submit" loading={isChangingPassword} className="mt-4">
|
||||||
<KeyRound className="h-4 w-4 mr-2" />
|
<KeyRound className="h-4 w-4 mr-2" />
|
||||||
Change Password
|
Change Password
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { createContext } from "react-router";
|
import { createContext } from "react-router";
|
||||||
import type { User } from "./client/lib/types";
|
|
||||||
|
type User = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
hasDownloadedResticPassword: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type AppContext = {
|
type AppContext = {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
|
|
|
||||||
65
app/drizzle/0029_boring_luke_cage.sql
Normal file
65
app/drizzle/0029_boring_luke_cage.sql
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
CREATE TABLE `account` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`account_id` text NOT NULL,
|
||||||
|
`provider_id` text NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`access_token` text,
|
||||||
|
`refresh_token` text,
|
||||||
|
`id_token` text,
|
||||||
|
`access_token_expires_at` integer,
|
||||||
|
`refresh_token_expires_at` integer,
|
||||||
|
`scope` text,
|
||||||
|
`password` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `verification` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`identifier` text NOT NULL,
|
||||||
|
`value` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_sessions_table` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`ip_address` text,
|
||||||
|
`user_agent` text,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DROP TABLE `sessions_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_sessions_table` RENAME TO `sessions_table`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `sessions_table_token_unique` ON `sessions_table` (`token`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_users_table` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`username` text NOT NULL,
|
||||||
|
`password_hash` text,
|
||||||
|
`has_downloaded_restic_password` integer DEFAULT false NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||||
|
`name` text NOT NULL,
|
||||||
|
`email` text NOT NULL,
|
||||||
|
`email_verified` integer DEFAULT false NOT NULL,
|
||||||
|
`image` text,
|
||||||
|
`display_username` text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_users_table`("id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at", "name", "email", "email_verified", "image", "display_username") SELECT "id", "username", "password_hash", "has_downloaded_restic_password", "created_at", "updated_at", "username", "username" || '@placeholder.local', false, "image", "username" FROM `users_table`;--> statement-breakpoint
|
||||||
|
|
||||||
|
DROP TABLE `users_table`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_users_table` RENAME TO `users_table`;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_table_username_unique` ON `users_table` (`username`);--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `users_table_email_unique` ON `users_table` (`email`);
|
||||||
2
app/drizzle/0030_lower-trim-username.sql
Normal file
2
app/drizzle/0030_lower-trim-username.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Custom SQL migration file, put your code below! --
|
||||||
|
UPDATE users_table SET username = LOWER(TRIM(username));
|
||||||
1113
app/drizzle/meta/0029_snapshot.json
Normal file
1113
app/drizzle/meta/0029_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1113
app/drizzle/meta/0030_snapshot.json
Normal file
1113
app/drizzle/meta/0030_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,209 +1,223 @@
|
||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1755765658194,
|
"when": 1755765658194,
|
||||||
"tag": "0000_known_madelyne_pryor",
|
"tag": "0000_known_madelyne_pryor",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1755775437391,
|
"when": 1755775437391,
|
||||||
"tag": "0001_far_frank_castle",
|
"tag": "0001_far_frank_castle",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
"idx": 2,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1756930554198,
|
"when": 1756930554198,
|
||||||
"tag": "0002_cheerful_randall",
|
"tag": "0002_cheerful_randall",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 3,
|
"idx": 3,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1758653407064,
|
"when": 1758653407064,
|
||||||
"tag": "0003_mature_hellcat",
|
"tag": "0003_mature_hellcat",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 4,
|
"idx": 4,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1758961535488,
|
"when": 1758961535488,
|
||||||
"tag": "0004_wealthy_tomas",
|
"tag": "0004_wealthy_tomas",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 5,
|
"idx": 5,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1759416698274,
|
"when": 1759416698274,
|
||||||
"tag": "0005_simple_alice",
|
"tag": "0005_simple_alice",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 6,
|
"idx": 6,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1760734377440,
|
"when": 1760734377440,
|
||||||
"tag": "0006_secret_micromacro",
|
"tag": "0006_secret_micromacro",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 7,
|
"idx": 7,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1761224911352,
|
"when": 1761224911352,
|
||||||
"tag": "0007_watery_sersi",
|
"tag": "0007_watery_sersi",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 8,
|
"idx": 8,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1761414054481,
|
"when": 1761414054481,
|
||||||
"tag": "0008_silent_lady_bullseye",
|
"tag": "0008_silent_lady_bullseye",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 9,
|
"idx": 9,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1762095226041,
|
"when": 1762095226041,
|
||||||
"tag": "0009_little_adam_warlock",
|
"tag": "0009_little_adam_warlock",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 10,
|
"idx": 10,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1762610065889,
|
"when": 1762610065889,
|
||||||
"tag": "0010_perfect_proemial_gods",
|
"tag": "0010_perfect_proemial_gods",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 11,
|
"idx": 11,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1763644043601,
|
"when": 1763644043601,
|
||||||
"tag": "0011_familiar_stone_men",
|
"tag": "0011_familiar_stone_men",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 12,
|
"idx": 12,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764100562084,
|
"when": 1764100562084,
|
||||||
"tag": "0012_add_short_ids",
|
"tag": "0012_add_short_ids",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 13,
|
"idx": 13,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764182159797,
|
"when": 1764182159797,
|
||||||
"tag": "0013_elite_sprite",
|
"tag": "0013_elite_sprite",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 14,
|
"idx": 14,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764182405089,
|
"when": 1764182405089,
|
||||||
"tag": "0014_wild_echo",
|
"tag": "0014_wild_echo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 15,
|
"idx": 15,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764182465287,
|
"when": 1764182465287,
|
||||||
"tag": "0015_jazzy_sersi",
|
"tag": "0015_jazzy_sersi",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 16,
|
"idx": 16,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764194697035,
|
"when": 1764194697035,
|
||||||
"tag": "0016_fix-timestamps-to-ms",
|
"tag": "0016_fix-timestamps-to-ms",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 17,
|
"idx": 17,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764357897219,
|
"when": 1764357897219,
|
||||||
"tag": "0017_fix-compression-modes",
|
"tag": "0017_fix-compression-modes",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 18,
|
"idx": 18,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764794371040,
|
"when": 1764794371040,
|
||||||
"tag": "0018_breezy_invaders",
|
"tag": "0018_breezy_invaders",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 19,
|
"idx": 19,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764839917446,
|
"when": 1764839917446,
|
||||||
"tag": "0019_secret_nomad",
|
"tag": "0019_secret_nomad",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 20,
|
"idx": 20,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1764847918249,
|
"when": 1764847918249,
|
||||||
"tag": "0020_even_dexter_bennett",
|
"tag": "0020_even_dexter_bennett",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 21,
|
"idx": 21,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1765307881092,
|
"when": 1765307881092,
|
||||||
"tag": "0021_steady_viper",
|
"tag": "0021_steady_viper",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 22,
|
"idx": 22,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1765794552191,
|
"when": 1765794552191,
|
||||||
"tag": "0022_woozy_shen",
|
"tag": "0022_woozy_shen",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 23,
|
"idx": 23,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1766320570509,
|
"when": 1766320570509,
|
||||||
"tag": "0023_special_thor",
|
"tag": "0023_special_thor",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 24,
|
"idx": 24,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1766325504548,
|
"when": 1766325504548,
|
||||||
"tag": "0024_schedules-one-fs",
|
"tag": "0024_schedules-one-fs",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 25,
|
"idx": 25,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1766431021321,
|
"when": 1766431021321,
|
||||||
"tag": "0025_remarkable_pete_wisdom",
|
"tag": "0025_remarkable_pete_wisdom",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 26,
|
"idx": 26,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1766765013108,
|
"when": 1766765013108,
|
||||||
"tag": "0026_migrate-local-repo-paths",
|
"tag": "0026_migrate-local-repo-paths",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 27,
|
"idx": 27,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1766778073418,
|
"when": 1766778073418,
|
||||||
"tag": "0027_careful_cammi",
|
"tag": "0027_careful_cammi",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 28,
|
"idx": 28,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1766778162985,
|
"when": 1766778162985,
|
||||||
"tag": "0028_third_amazoness",
|
"tag": "0028_third_amazoness",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1767819883495,
|
||||||
|
"tag": "0029_boring_luke_cage",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 30,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1767821088612,
|
||||||
|
"tag": "0030_lower-trim-username",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
50
app/lib/auth-middlewares/convert-legacy-user.ts
Normal file
50
app/lib/auth-middlewares/convert-legacy-user.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { hashPassword } from "better-auth/crypto";
|
||||||
|
import { and, eq, ne } from "drizzle-orm";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { account, usersTable } from "~/server/db/schema";
|
||||||
|
import type { AuthMiddlewareContext } from "../auth";
|
||||||
|
|
||||||
|
export const convertLegacyUserOnFirstLogin = async (ctx: AuthMiddlewareContext) => {
|
||||||
|
const { path, body } = ctx;
|
||||||
|
|
||||||
|
if (path !== "/sign-in/username") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyUser = await db.query.usersTable.findFirst({
|
||||||
|
where: and(eq(usersTable.username, body.username.trim().toLowerCase()), ne(usersTable.passwordHash, "")),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (legacyUser) {
|
||||||
|
const isValid = await Bun.password.verify(body.password, legacyUser.passwordHash ?? "");
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const newUserId = crypto.randomUUID();
|
||||||
|
const accountId = crypto.randomUUID();
|
||||||
|
|
||||||
|
await tx.delete(usersTable).where(eq(usersTable.id, legacyUser.id));
|
||||||
|
|
||||||
|
await tx.insert(usersTable).values({
|
||||||
|
id: newUserId,
|
||||||
|
username: legacyUser.username,
|
||||||
|
email: legacyUser.email,
|
||||||
|
name: legacyUser.name,
|
||||||
|
hasDownloadedResticPassword: legacyUser.hasDownloadedResticPassword,
|
||||||
|
emailVerified: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.insert(account).values({
|
||||||
|
id: accountId,
|
||||||
|
providerId: "credential",
|
||||||
|
accountId: legacyUser.username,
|
||||||
|
userId: newUserId,
|
||||||
|
password: await hashPassword(body.password),
|
||||||
|
createdAt: new Date(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid credentials");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
17
app/lib/auth-middlewares/only-one-user.ts
Normal file
17
app/lib/auth-middlewares/only-one-user.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import type { AuthMiddlewareContext } from "../auth";
|
||||||
|
import { logger } from "~/server/utils/logger";
|
||||||
|
|
||||||
|
export const ensureOnlyOneUser = async (ctx: AuthMiddlewareContext) => {
|
||||||
|
const { path } = ctx;
|
||||||
|
|
||||||
|
if (path !== "/sign-up/email") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await db.query.usersTable.findFirst();
|
||||||
|
if (existingUser) {
|
||||||
|
logger.error("Attempt to create a second administrator account blocked.");
|
||||||
|
throw new Error("An administrator account already exists");
|
||||||
|
}
|
||||||
|
};
|
||||||
49
app/lib/auth.ts
Normal file
49
app/lib/auth.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
betterAuth,
|
||||||
|
type AuthContext,
|
||||||
|
type BetterAuthOptions,
|
||||||
|
type MiddlewareContext,
|
||||||
|
type MiddlewareOptions,
|
||||||
|
} from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { createAuthMiddleware, username } from "better-auth/plugins";
|
||||||
|
import { convertLegacyUserOnFirstLogin } from "./auth-middlewares/convert-legacy-user";
|
||||||
|
import { cryptoUtils } from "~/server/utils/crypto";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { ensureOnlyOneUser } from "./auth-middlewares/only-one-user";
|
||||||
|
|
||||||
|
export type AuthMiddlewareContext = MiddlewareContext<MiddlewareOptions, AuthContext<BetterAuthOptions>>;
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
secret: await cryptoUtils.deriveSecret("better-auth"),
|
||||||
|
hooks: {
|
||||||
|
before: createAuthMiddleware(async (ctx) => {
|
||||||
|
await ensureOnlyOneUser(ctx);
|
||||||
|
await convertLegacyUserOnFirstLogin(ctx);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "sqlite",
|
||||||
|
}),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
modelName: "usersTable",
|
||||||
|
additionalFields: {
|
||||||
|
username: {
|
||||||
|
type: "string",
|
||||||
|
returned: true,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hasDownloadedResticPassword: {
|
||||||
|
type: "boolean",
|
||||||
|
returned: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
modelName: "sessionsTable",
|
||||||
|
},
|
||||||
|
plugins: [username({})],
|
||||||
|
});
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { redirect, type MiddlewareFunction } from "react-router";
|
import { redirect, type MiddlewareFunction } from "react-router";
|
||||||
import { getMe, getStatus } from "~/client/api-client";
|
import { getStatus } from "~/client/api-client";
|
||||||
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import { appContext } from "~/context";
|
import { appContext } from "~/context";
|
||||||
|
|
||||||
export const authMiddleware: MiddlewareFunction = async ({ context, request }) => {
|
export const authMiddleware: MiddlewareFunction = async ({ context, request }) => {
|
||||||
const session = await getMe();
|
const { data: session } = await authClient.getSession();
|
||||||
|
|
||||||
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
|
const isAuthRoute = ["/login", "/onboarding"].includes(new URL(request.url).pathname);
|
||||||
|
|
||||||
if (!session.data?.user?.id && !isAuthRoute) {
|
if (!session?.user?.id && !isAuthRoute) {
|
||||||
const status = await getStatus();
|
const status = await getStatus();
|
||||||
if (!status.data?.hasUsers) {
|
if (!status.data?.hasUsers) {
|
||||||
throw redirect("/onboarding");
|
throw redirect("/onboarding");
|
||||||
|
|
@ -16,8 +17,8 @@ export const authMiddleware: MiddlewareFunction = async ({ context, request }) =
|
||||||
throw redirect("/login");
|
throw redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.data?.user?.id) {
|
if (session?.user?.id) {
|
||||||
context.set(appContext, { user: session.data.user, hasUsers: true });
|
context.set(appContext, { user: session.user, hasUsers: true });
|
||||||
|
|
||||||
if (isAuthRoute) {
|
if (isAuthRoute) {
|
||||||
throw redirect("/");
|
throw redirect("/");
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { notificationsController } from "./modules/notifications/notifications.c
|
||||||
import { handleServiceError } from "./utils/errors";
|
import { handleServiceError } from "./utils/errors";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
import { config } from "./core/config";
|
import { config } from "./core/config";
|
||||||
|
import { auth } from "~/lib/auth";
|
||||||
|
|
||||||
export const generalDescriptor = (app: Hono) =>
|
export const generalDescriptor = (app: Hono) =>
|
||||||
openAPIRouteHandler(app, {
|
openAPIRouteHandler(app, {
|
||||||
|
|
@ -62,6 +63,7 @@ export const createApp = () => {
|
||||||
.route("/api/v1/system", systemController)
|
.route("/api/v1/system", systemController)
|
||||||
.route("/api/v1/events", eventsController);
|
.route("/api/v1/events", eventsController);
|
||||||
|
|
||||||
|
app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));
|
||||||
app.get("/api/v1/openapi.json", generalDescriptor(app));
|
app.get("/api/v1/openapi.json", generalDescriptor(app));
|
||||||
app.get("/api/v1/docs", requireAuth, scalarDescriptor);
|
app.get("/api/v1/docs", requireAuth, scalarDescriptor);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,45 @@
|
||||||
import { Command } from "commander";
|
|
||||||
import { password, select } from "@inquirer/prompts";
|
import { password, select } from "@inquirer/prompts";
|
||||||
import { eq } from "drizzle-orm";
|
import { hashPassword } from "better-auth/crypto";
|
||||||
|
import { Command } from "commander";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { toMessage } from "~/server/utils/errors";
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { sessionsTable, usersTable } from "../../db/schema";
|
import { account, sessionsTable, usersTable } from "../../db/schema";
|
||||||
|
|
||||||
const listUsers = () => {
|
const listUsers = () => {
|
||||||
return db.select({ id: usersTable.id, username: usersTable.username }).from(usersTable);
|
return db
|
||||||
|
.select({ id: usersTable.id, username: usersTable.username })
|
||||||
|
.from(usersTable);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetPassword = async (username: string, newPassword: string) => {
|
const resetPassword = async (username: string, newPassword: string) => {
|
||||||
const [user] = await db.select().from(usersTable).where(eq(usersTable.username, username));
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(usersTable)
|
||||||
|
.where(eq(usersTable.username, username));
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`User "${username}" not found`);
|
throw new Error(`User "${username}" not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPasswordHash = await Bun.password.hash(newPassword, {
|
const newPasswordHash = await hashPassword(newPassword);
|
||||||
algorithm: "argon2id",
|
|
||||||
memoryCost: 19456,
|
|
||||||
timeCost: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx.update(usersTable).set({ passwordHash: newPasswordHash }).where(eq(usersTable.id, user.id));
|
await tx
|
||||||
|
.update(account)
|
||||||
|
.set({ password: newPasswordHash })
|
||||||
|
.where(
|
||||||
|
and(eq(account.userId, user.id), eq(account.providerId, "credential")),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user.passwordHash) {
|
||||||
|
const legacyHash = await Bun.password.hash(newPassword);
|
||||||
|
await tx
|
||||||
|
.update(usersTable)
|
||||||
|
.set({ passwordHash: legacyHash })
|
||||||
|
.where(eq(usersTable.id, user.id));
|
||||||
|
}
|
||||||
|
|
||||||
await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id));
|
await tx.delete(sessionsTable).where(eq(sessionsTable.userId, user.id));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -42,7 +59,9 @@ export const resetPasswordCommand = new Command("reset-password")
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
console.error("❌ No users found in the database.");
|
console.error("❌ No users found in the database.");
|
||||||
console.log(" Please create a user first by starting the application.");
|
console.log(
|
||||||
|
" Please create a user first by starting the application.",
|
||||||
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,10 +99,12 @@ export const resetPasswordCommand = new Command("reset-password")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await resetPassword(username, newPassword);
|
await resetPassword(username, newPassword);
|
||||||
console.log(`\n✅ Password for user "${username}" has been reset successfully.`);
|
console.log(
|
||||||
|
`\n✅ Password for user "${username}" has been reset successfully.`,
|
||||||
|
);
|
||||||
console.log(" All existing sessions have been invalidated.");
|
console.log(" All existing sessions have been invalidated.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`\n❌ Failed to reset password: ${error instanceof Error ? error.message : "Unknown error"}`);
|
console.error(`\n❌ Failed to reset password: ${toMessage(error)}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,51 @@
|
||||||
import "dotenv/config";
|
|
||||||
import { Database } from "bun:sqlite";
|
import { Database } from "bun:sqlite";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
import { DATABASE_URL } from "../core/constants";
|
import { DATABASE_URL } from "../core/constants";
|
||||||
import * as schema from "./schema";
|
import fs from "node:fs";
|
||||||
import fs from "node:fs/promises";
|
|
||||||
import { config } from "../core/config";
|
import { config } from "../core/config";
|
||||||
|
import type * as schemaTypes from "./schema";
|
||||||
|
|
||||||
await fs.mkdir(path.dirname(DATABASE_URL), { recursive: true });
|
/**
|
||||||
|
* TODO: try to remove this if moving away from react-router.
|
||||||
|
* The rr vite plugin doesn't let us customize the chunk names
|
||||||
|
* to isolate the db initialization code from the rest of the server code.
|
||||||
|
*/
|
||||||
|
let _sqlite: Database | undefined;
|
||||||
|
let _db: ReturnType<typeof drizzle<typeof schemaTypes>> | undefined;
|
||||||
|
let _schema: typeof schemaTypes | undefined;
|
||||||
|
|
||||||
const sqlite = new Database(DATABASE_URL);
|
/**
|
||||||
export const db = drizzle({ client: sqlite, schema });
|
* Sets the database schema. This must be called before any database operations.
|
||||||
|
*/
|
||||||
|
export const setSchema = (schema: typeof schemaTypes) => {
|
||||||
|
_schema = schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initDb = () => {
|
||||||
|
if (!_schema) {
|
||||||
|
throw new Error("Database schema not set. Call setSchema() before accessing the database.");
|
||||||
|
}
|
||||||
|
fs.mkdirSync(path.dirname(DATABASE_URL), { recursive: true });
|
||||||
|
_sqlite = new Database(DATABASE_URL);
|
||||||
|
return drizzle({ client: _sqlite, schema: _schema });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database instance (Proxy for lazy initialization)
|
||||||
|
*/
|
||||||
|
export const db = new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_, prop, receiver) {
|
||||||
|
if (!_db) {
|
||||||
|
_db = initDb();
|
||||||
|
}
|
||||||
|
return Reflect.get(_db, prop, receiver);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) as ReturnType<typeof drizzle<typeof schemaTypes>>;
|
||||||
|
|
||||||
export const runDbMigrations = () => {
|
export const runDbMigrations = () => {
|
||||||
let migrationsFolder: string;
|
let migrationsFolder: string;
|
||||||
|
|
@ -26,5 +60,9 @@ export const runDbMigrations = () => {
|
||||||
|
|
||||||
migrate(db, { migrationsFolder });
|
migrate(db, { migrationsFolder });
|
||||||
|
|
||||||
sqlite.run("PRAGMA foreign_keys = ON;");
|
if (!_sqlite) {
|
||||||
|
throw new Error("Database not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
_sqlite.run("PRAGMA foreign_keys = ON;");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { relations, sql } from "drizzle-orm";
|
import { relations, sql } from "drizzle-orm";
|
||||||
import { int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core";
|
import { index, int, integer, sqliteTable, text, primaryKey, unique } from "drizzle-orm/sqlite-core";
|
||||||
import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryBackend, repositoryConfigSchema, RepositoryStatus } from "~/schemas/restic";
|
||||||
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
|
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
|
||||||
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
|
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
|
||||||
|
|
@ -14,9 +14,16 @@ export const volumesTable = sqliteTable("volumes_table", {
|
||||||
type: text().$type<BackendType>().notNull(),
|
type: text().$type<BackendType>().notNull(),
|
||||||
status: text().$type<BackendStatus>().notNull().default("unmounted"),
|
status: text().$type<BackendStatus>().notNull().default("unmounted"),
|
||||||
lastError: text("last_error"),
|
lastError: text("last_error"),
|
||||||
lastHealthCheck: integer("last_health_check", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
lastHealthCheck: integer("last_health_check", { mode: "number" })
|
||||||
createdAt: integer("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
updatedAt: integer("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
createdAt: integer("created_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.$onUpdate(() => Date.now())
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
config: text("config", { mode: "json" }).$type<typeof volumeConfigSchema.inferOut>().notNull(),
|
||||||
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true),
|
||||||
});
|
});
|
||||||
|
|
@ -27,24 +34,112 @@ export type VolumeInsert = typeof volumesTable.$inferInsert;
|
||||||
* Users Table
|
* Users Table
|
||||||
*/
|
*/
|
||||||
export const usersTable = sqliteTable("users_table", {
|
export const usersTable = sqliteTable("users_table", {
|
||||||
id: int().primaryKey({ autoIncrement: true }),
|
id: text("id").primaryKey(),
|
||||||
username: text().notNull().unique(),
|
username: text().notNull().unique(),
|
||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash"),
|
||||||
hasDownloadedResticPassword: int("has_downloaded_restic_password", { mode: "boolean" }).notNull().default(false),
|
hasDownloadedResticPassword: int("has_downloaded_restic_password", { mode: "boolean" }).notNull().default(false),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "timestamp_ms" })
|
||||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: int("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.notNull()
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
emailVerified: integer("email_verified", { mode: "boolean" }).default(false).notNull(),
|
||||||
|
image: text("image"),
|
||||||
|
displayUsername: text("display_username"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type User = typeof usersTable.$inferSelect;
|
export type User = typeof usersTable.$inferSelect;
|
||||||
export const sessionsTable = sqliteTable("sessions_table", {
|
export const sessionsTable = sqliteTable("sessions_table", {
|
||||||
id: text().primaryKey(),
|
id: text().primaryKey(),
|
||||||
userId: int("user_id")
|
userId: text("user_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => usersTable.id, { onDelete: "cascade" }),
|
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||||
expiresAt: int("expires_at", { mode: "number" }).notNull(),
|
token: text("token").notNull().unique(),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
expiresAt: int("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||||
|
createdAt: int("created_at", { mode: "timestamp_ms" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.notNull()
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
});
|
});
|
||||||
export type Session = typeof sessionsTable.$inferSelect;
|
export type Session = typeof sessionsTable.$inferSelect;
|
||||||
|
|
||||||
|
export const account = sqliteTable(
|
||||||
|
"account",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
accountId: text("account_id").notNull(),
|
||||||
|
providerId: text("provider_id").notNull(),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => usersTable.id, { onDelete: "cascade" }),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
idToken: text("id_token"),
|
||||||
|
accessTokenExpiresAt: integer("access_token_expires_at", {
|
||||||
|
mode: "timestamp_ms",
|
||||||
|
}),
|
||||||
|
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
||||||
|
mode: "timestamp_ms",
|
||||||
|
}),
|
||||||
|
scope: text("scope"),
|
||||||
|
password: text("password"),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||||
|
.$onUpdate(() => new Date())
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
},
|
||||||
|
(table) => [index("account_userId_idx").on(table.userId)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const verification = sqliteTable(
|
||||||
|
"verification",
|
||||||
|
{
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
expiresAt: integer("expires_at", { mode: "number" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "number" })
|
||||||
|
.$onUpdate(() => Date.now())
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
},
|
||||||
|
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const userRelations = relations(usersTable, ({ many }) => ({
|
||||||
|
sessions: many(sessionsTable),
|
||||||
|
accounts: many(account),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const sessionRelations = relations(sessionsTable, ({ one }) => ({
|
||||||
|
user: one(usersTable, {
|
||||||
|
fields: [sessionsTable.userId],
|
||||||
|
references: [usersTable.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const accountRelations = relations(account, ({ one }) => ({
|
||||||
|
user: one(usersTable, {
|
||||||
|
fields: [account.userId],
|
||||||
|
references: [usersTable.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repositories Table
|
* Repositories Table
|
||||||
*/
|
*/
|
||||||
|
|
@ -58,8 +153,12 @@ export const repositoriesTable = sqliteTable("repositories_table", {
|
||||||
status: text().$type<RepositoryStatus>().default("unknown"),
|
status: text().$type<RepositoryStatus>().default("unknown"),
|
||||||
lastChecked: int("last_checked", { mode: "number" }),
|
lastChecked: int("last_checked", { mode: "number" }),
|
||||||
lastError: text("last_error"),
|
lastError: text("last_error"),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "number" })
|
||||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: int("updated_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
});
|
});
|
||||||
export type Repository = typeof repositoriesTable.$inferSelect;
|
export type Repository = typeof repositoriesTable.$inferSelect;
|
||||||
export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
|
export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
|
||||||
|
|
@ -97,8 +196,12 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
||||||
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
nextBackupAt: int("next_backup_at", { mode: "number" }),
|
||||||
oneFileSystem: int("one_file_system", { mode: "boolean" }).notNull().default(false),
|
oneFileSystem: int("one_file_system", { mode: "boolean" }).notNull().default(false),
|
||||||
sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
|
sortOrder: int("sort_order", { mode: "number" }).notNull().default(0),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "number" })
|
||||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: int("updated_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
});
|
});
|
||||||
export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert;
|
export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert;
|
||||||
|
|
||||||
|
|
@ -125,8 +228,12 @@ export const notificationDestinationsTable = sqliteTable("notification_destinati
|
||||||
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
type: text().$type<NotificationType>().notNull(),
|
type: text().$type<NotificationType>().notNull(),
|
||||||
config: text("config", { mode: "json" }).$type<typeof notificationConfigSchema.inferOut>().notNull(),
|
config: text("config", { mode: "json" }).$type<typeof notificationConfigSchema.inferOut>().notNull(),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "number" })
|
||||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: int("updated_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
});
|
});
|
||||||
export const notificationDestinationRelations = relations(notificationDestinationsTable, ({ many }) => ({
|
export const notificationDestinationRelations = relations(notificationDestinationsTable, ({ many }) => ({
|
||||||
schedules: many(backupScheduleNotificationsTable),
|
schedules: many(backupScheduleNotificationsTable),
|
||||||
|
|
@ -149,7 +256,9 @@ export const backupScheduleNotificationsTable = sqliteTable(
|
||||||
notifyOnSuccess: int("notify_on_success", { mode: "boolean" }).notNull().default(false),
|
notifyOnSuccess: int("notify_on_success", { mode: "boolean" }).notNull().default(false),
|
||||||
notifyOnWarning: int("notify_on_warning", { mode: "boolean" }).notNull().default(true),
|
notifyOnWarning: int("notify_on_warning", { mode: "boolean" }).notNull().default(true),
|
||||||
notifyOnFailure: int("notify_on_failure", { mode: "boolean" }).notNull().default(true),
|
notifyOnFailure: int("notify_on_failure", { mode: "boolean" }).notNull().default(true),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
},
|
},
|
||||||
(table) => [primaryKey({ columns: [table.scheduleId, table.destinationId] })],
|
(table) => [primaryKey({ columns: [table.scheduleId, table.destinationId] })],
|
||||||
);
|
);
|
||||||
|
|
@ -183,7 +292,9 @@ export const backupScheduleMirrorsTable = sqliteTable(
|
||||||
lastCopyAt: int("last_copy_at", { mode: "number" }),
|
lastCopyAt: int("last_copy_at", { mode: "number" }),
|
||||||
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(),
|
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(),
|
||||||
lastCopyError: text("last_copy_error"),
|
lastCopyError: text("last_copy_error"),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
},
|
},
|
||||||
(table) => [unique().on(table.scheduleId, table.repositoryId)],
|
(table) => [unique().on(table.scheduleId, table.repositoryId)],
|
||||||
);
|
);
|
||||||
|
|
@ -207,7 +318,11 @@ export type BackupScheduleMirror = typeof backupScheduleMirrorsTable.$inferSelec
|
||||||
export const appMetadataTable = sqliteTable("app_metadata", {
|
export const appMetadataTable = sqliteTable("app_metadata", {
|
||||||
key: text().primaryKey(),
|
key: text().primaryKey(),
|
||||||
value: text().notNull(),
|
value: text().notNull(),
|
||||||
createdAt: int("created_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
createdAt: int("created_at", { mode: "number" })
|
||||||
updatedAt: int("updated_at", { mode: "number" }).notNull().default(sql`(unixepoch() * 1000)`),
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
|
updatedAt: int("updated_at", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch() * 1000)`),
|
||||||
});
|
});
|
||||||
export type AppMetadata = typeof appMetadataTable.$inferSelect;
|
export type AppMetadata = typeof appMetadataTable.$inferSelect;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { createHonoServer } from "react-router-hono-server/bun";
|
import { createHonoServer } from "react-router-hono-server/bun";
|
||||||
import { runDbMigrations } from "./db/db";
|
import * as schema from "./db/schema";
|
||||||
|
import { setSchema, runDbMigrations } from "./db/db";
|
||||||
import { startup } from "./modules/lifecycle/startup";
|
import { startup } from "./modules/lifecycle/startup";
|
||||||
import { retagSnapshots } from "./modules/lifecycle/migration";
|
import { retagSnapshots } from "./modules/lifecycle/migration";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
@ -10,6 +11,8 @@ import { createApp } from "./app";
|
||||||
import { config } from "./core/config";
|
import { config } from "./core/config";
|
||||||
import { runCLI } from "./cli";
|
import { runCLI } from "./cli";
|
||||||
|
|
||||||
|
setSchema(schema);
|
||||||
|
|
||||||
const cliRun = await runCLI(Bun.argv);
|
const cliRun = await runCLI(Bun.argv);
|
||||||
if (cliRun) {
|
if (cliRun) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
import { Job } from "../core/scheduler";
|
|
||||||
import { authService } from "../modules/auth/auth.service";
|
|
||||||
|
|
||||||
export class CleanupSessionsJob extends Job {
|
|
||||||
async run() {
|
|
||||||
await authService.cleanupExpiredSessions();
|
|
||||||
|
|
||||||
return { done: true, timestamp: new Date() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,157 +1,8 @@
|
||||||
import { validator } from "hono-openapi";
|
|
||||||
import { rateLimiter } from "hono-rate-limiter";
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
import { getStatusDto, type GetStatusDto } from "./auth.dto";
|
||||||
import {
|
|
||||||
changePasswordBodySchema,
|
|
||||||
changePasswordDto,
|
|
||||||
getMeDto,
|
|
||||||
getStatusDto,
|
|
||||||
loginBodySchema,
|
|
||||||
loginDto,
|
|
||||||
logoutDto,
|
|
||||||
registerBodySchema,
|
|
||||||
registerDto,
|
|
||||||
type ChangePasswordDto,
|
|
||||||
type GetMeDto,
|
|
||||||
type GetStatusDto,
|
|
||||||
type LoginDto,
|
|
||||||
type LogoutDto,
|
|
||||||
type RegisterDto,
|
|
||||||
} from "./auth.dto";
|
|
||||||
import { authService } from "./auth.service";
|
import { authService } from "./auth.service";
|
||||||
import { toMessage } from "../../utils/errors";
|
|
||||||
import { config } from "~/server/core/config";
|
|
||||||
|
|
||||||
const COOKIE_NAME = "session_id";
|
export const authController = new Hono().get("/status", getStatusDto, async (c) => {
|
||||||
const COOKIE_OPTIONS = {
|
const hasUsers = await authService.hasUsers();
|
||||||
httpOnly: true,
|
return c.json<GetStatusDto>({ hasUsers });
|
||||||
secure: false,
|
|
||||||
sameSite: "lax" as const,
|
|
||||||
path: "/",
|
|
||||||
};
|
|
||||||
|
|
||||||
const authRateLimiter = rateLimiter({
|
|
||||||
windowMs: 15 * 60 * 1000,
|
|
||||||
limit: 20,
|
|
||||||
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? "",
|
|
||||||
skip: () => {
|
|
||||||
return config.__prod__ === false;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authController = new Hono()
|
|
||||||
.post("/register", authRateLimiter, registerDto, validator("json", registerBodySchema), async (c) => {
|
|
||||||
const body = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { user, sessionId } = await authService.register(body.username, body.password);
|
|
||||||
|
|
||||||
setCookie(c, COOKIE_NAME, sessionId, {
|
|
||||||
...COOKIE_OPTIONS,
|
|
||||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json<RegisterDto>(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
message: "User registered successfully",
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
hasDownloadedResticPassword: user.hasDownloadedResticPassword,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
201,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
return c.json<RegisterDto>({ success: false, message: toMessage(error) }, 400);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.post("/login", authRateLimiter, loginDto, validator("json", loginBodySchema), async (c) => {
|
|
||||||
const body = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { sessionId, user, expiresAt } = await authService.login(body.username, body.password);
|
|
||||||
|
|
||||||
setCookie(c, COOKIE_NAME, sessionId, {
|
|
||||||
...COOKIE_OPTIONS,
|
|
||||||
expires: new Date(expiresAt),
|
|
||||||
});
|
|
||||||
|
|
||||||
return c.json<LoginDto>({
|
|
||||||
success: true,
|
|
||||||
message: "Login successful",
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
hasDownloadedResticPassword: user.hasDownloadedResticPassword,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return c.json<LoginDto>({ success: false, message: toMessage(error) }, 401);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.post("/logout", authRateLimiter, logoutDto, async (c) => {
|
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
await authService.logout(sessionId);
|
|
||||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json<LogoutDto>({ success: true });
|
|
||||||
})
|
|
||||||
.get("/me", getMeDto, async (c) => {
|
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return c.json<GetMeDto>({ success: false, message: "Not authenticated" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await authService.verifySession(sessionId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
|
||||||
return c.json({ message: "Not authenticated" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json<GetMeDto>({
|
|
||||||
success: true,
|
|
||||||
user: session.user,
|
|
||||||
message: "Authenticated",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.get("/status", getStatusDto, async (c) => {
|
|
||||||
const hasUsers = await authService.hasUsers();
|
|
||||||
return c.json<GetStatusDto>({ hasUsers });
|
|
||||||
})
|
|
||||||
.post(
|
|
||||||
"/change-password",
|
|
||||||
authRateLimiter,
|
|
||||||
changePasswordDto,
|
|
||||||
validator("json", changePasswordBodySchema),
|
|
||||||
async (c) => {
|
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return c.json<ChangePasswordDto>({ success: false, message: "Not authenticated" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = await authService.verifySession(sessionId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
|
||||||
return c.json<ChangePasswordDto>({ success: false, message: "Not authenticated" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = c.req.valid("json");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await authService.changePassword(session.user.id, body.currentPassword, body.newPassword);
|
|
||||||
return c.json<ChangePasswordDto>({ success: true, message: "Password changed successfully" });
|
|
||||||
} catch (error) {
|
|
||||||
return c.json<ChangePasswordDto>({ success: false, message: toMessage(error) }, 400);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,6 @@
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
|
|
||||||
// Validation schemas
|
|
||||||
export const loginBodySchema = type({
|
|
||||||
username: "string>0",
|
|
||||||
password: "string>7",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const registerBodySchema = type({
|
|
||||||
username: "string>2",
|
|
||||||
password: "string>7",
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginResponseSchema = type({
|
|
||||||
message: "string",
|
|
||||||
success: "boolean",
|
|
||||||
user: type({
|
|
||||||
id: "number",
|
|
||||||
username: "string",
|
|
||||||
hasDownloadedResticPassword: "boolean",
|
|
||||||
}).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const loginDto = describeRoute({
|
|
||||||
description: "Login with username and password",
|
|
||||||
operationId: "login",
|
|
||||||
tags: ["Auth"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Login successful",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(loginResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type LoginDto = typeof loginResponseSchema.infer;
|
|
||||||
|
|
||||||
export const registerDto = describeRoute({
|
|
||||||
description: "Register a new user",
|
|
||||||
operationId: "register",
|
|
||||||
tags: ["Auth"],
|
|
||||||
responses: {
|
|
||||||
201: {
|
|
||||||
description: "User created successfully",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(loginResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type RegisterDto = typeof loginResponseSchema.infer;
|
|
||||||
|
|
||||||
const logoutResponseSchema = type({
|
|
||||||
success: "boolean",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const logoutDto = describeRoute({
|
|
||||||
description: "Logout current user",
|
|
||||||
operationId: "logout",
|
|
||||||
tags: ["Auth"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Logout successful",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(logoutResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type LogoutDto = typeof logoutResponseSchema.infer;
|
|
||||||
|
|
||||||
export const getMeDto = describeRoute({
|
|
||||||
description: "Get current authenticated user",
|
|
||||||
operationId: "getMe",
|
|
||||||
tags: ["Auth"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Current user information",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(loginResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type GetMeDto = typeof loginResponseSchema.infer;
|
|
||||||
|
|
||||||
const statusResponseSchema = type({
|
const statusResponseSchema = type({
|
||||||
hasUsers: "boolean",
|
hasUsers: "boolean",
|
||||||
});
|
});
|
||||||
|
|
@ -119,35 +22,3 @@ export const getStatusDto = describeRoute({
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GetStatusDto = typeof statusResponseSchema.infer;
|
export type GetStatusDto = typeof statusResponseSchema.infer;
|
||||||
|
|
||||||
export const changePasswordBodySchema = type({
|
|
||||||
currentPassword: "string>0",
|
|
||||||
newPassword: "string>7",
|
|
||||||
});
|
|
||||||
|
|
||||||
const changePasswordResponseSchema = type({
|
|
||||||
success: "boolean",
|
|
||||||
message: "string",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const changePasswordDto = describeRoute({
|
|
||||||
description: "Change current user password",
|
|
||||||
operationId: "changePassword",
|
|
||||||
tags: ["Auth"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Password changed successfully",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(changePasswordResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ChangePasswordDto = typeof changePasswordResponseSchema.infer;
|
|
||||||
|
|
||||||
export type LoginBody = typeof loginBodySchema.infer;
|
|
||||||
export type RegisterBody = typeof registerBodySchema.infer;
|
|
||||||
export type ChangePasswordBody = typeof changePasswordBodySchema.infer;
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,10 @@
|
||||||
import { deleteCookie, getCookie } from "hono/cookie";
|
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import { authService } from "./auth.service";
|
import { auth } from "~/lib/auth";
|
||||||
|
|
||||||
const COOKIE_NAME = "session_id";
|
|
||||||
const COOKIE_OPTIONS = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
sameSite: "lax" as const,
|
|
||||||
path: "/",
|
|
||||||
};
|
|
||||||
|
|
||||||
declare module "hono" {
|
declare module "hono" {
|
||||||
interface ContextVariableMap {
|
interface ContextVariableMap {
|
||||||
user: {
|
user: {
|
||||||
id: number;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
hasDownloadedResticPassword: boolean;
|
hasDownloadedResticPassword: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -25,40 +16,17 @@ declare module "hono" {
|
||||||
* Verifies the session cookie and attaches user to context
|
* Verifies the session cookie and attaches user to context
|
||||||
*/
|
*/
|
||||||
export const requireAuth = createMiddleware(async (c, next) => {
|
export const requireAuth = createMiddleware(async (c, next) => {
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
const session = await auth.api.getSession({
|
||||||
|
headers: c.req.raw.headers,
|
||||||
|
});
|
||||||
|
|
||||||
if (!sessionId) {
|
const { user } = session ?? {};
|
||||||
return c.json({ message: "Authentication required" }, 401);
|
|
||||||
|
if (!user) {
|
||||||
|
return c.json<unknown>({ message: "Invalid or expired session" }, 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await authService.verifySession(sessionId);
|
c.set("user", user);
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
|
||||||
return c.json({ message: "Invalid or expired session" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
c.set("user", session.user);
|
|
||||||
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware to optionally attach user if authenticated
|
|
||||||
* Does not block the request if not authenticated
|
|
||||||
*/
|
|
||||||
export const optionalAuth = createMiddleware(async (c, next) => {
|
|
||||||
const sessionId = getCookie(c, COOKIE_NAME);
|
|
||||||
|
|
||||||
if (sessionId) {
|
|
||||||
const session = await authService.verifySession(sessionId);
|
|
||||||
|
|
||||||
if (session) {
|
|
||||||
c.set("user", session.user);
|
|
||||||
} else {
|
|
||||||
deleteCookie(c, COOKIE_NAME, COOKIE_OPTIONS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,145 +1,7 @@
|
||||||
import { eq, lt } from "drizzle-orm";
|
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { sessionsTable, usersTable } from "../../db/schema";
|
import { usersTable } from "../../db/schema";
|
||||||
import { logger } from "../../utils/logger";
|
|
||||||
|
|
||||||
const SESSION_DURATION = 60 * 60 * 24 * 30 * 1000; // 30 days in milliseconds
|
|
||||||
|
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
/**
|
|
||||||
* Register a new user with username and password
|
|
||||||
*/
|
|
||||||
async register(username: string, password: string) {
|
|
||||||
const [existingUser] = await db.select().from(usersTable);
|
|
||||||
|
|
||||||
if (existingUser) {
|
|
||||||
throw new Error("Admin user already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = await Bun.password.hash(password, {
|
|
||||||
algorithm: "argon2id",
|
|
||||||
memoryCost: 19456,
|
|
||||||
timeCost: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [user] = await db.insert(usersTable).values({ username, passwordHash }).returning();
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User registration failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`User registered: ${username}`);
|
|
||||||
const sessionId = crypto.randomUUID();
|
|
||||||
const expiresAt = Date.now() + SESSION_DURATION;
|
|
||||||
|
|
||||||
await db.insert(sessionsTable).values({
|
|
||||||
id: sessionId,
|
|
||||||
userId: user.id,
|
|
||||||
expiresAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
createdAt: user.createdAt,
|
|
||||||
hasDownloadedResticPassword: user.hasDownloadedResticPassword,
|
|
||||||
},
|
|
||||||
sessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Login user with username and password
|
|
||||||
*/
|
|
||||||
async login(username: string, password: string) {
|
|
||||||
const [user] = await db.select().from(usersTable).where(eq(usersTable.username, username));
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("Invalid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await Bun.password.verify(password, user.passwordHash);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error("Invalid credentials");
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionId = crypto.randomUUID();
|
|
||||||
const expiresAt = Date.now() + SESSION_DURATION;
|
|
||||||
|
|
||||||
await db.insert(sessionsTable).values({
|
|
||||||
id: sessionId,
|
|
||||||
userId: user.id,
|
|
||||||
expiresAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(`User logged in: ${username}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
sessionId,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
hasDownloadedResticPassword: user.hasDownloadedResticPassword,
|
|
||||||
},
|
|
||||||
expiresAt,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logout user by deleting their session
|
|
||||||
*/
|
|
||||||
async logout(sessionId: string) {
|
|
||||||
await db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId));
|
|
||||||
logger.info(`User logged out: session ${sessionId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify a session and return the associated user
|
|
||||||
*/
|
|
||||||
async verifySession(sessionId: string) {
|
|
||||||
const [session] = await db
|
|
||||||
.select({
|
|
||||||
session: sessionsTable,
|
|
||||||
user: usersTable,
|
|
||||||
})
|
|
||||||
.from(sessionsTable)
|
|
||||||
.innerJoin(usersTable, eq(sessionsTable.userId, usersTable.id))
|
|
||||||
.where(eq(sessionsTable.id, sessionId));
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.session.expiresAt < Date.now()) {
|
|
||||||
await db.delete(sessionsTable).where(eq(sessionsTable.id, sessionId));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: {
|
|
||||||
id: session.user.id,
|
|
||||||
username: session.user.username,
|
|
||||||
hasDownloadedResticPassword: session.user.hasDownloadedResticPassword,
|
|
||||||
},
|
|
||||||
session: {
|
|
||||||
id: session.session.id,
|
|
||||||
expiresAt: session.session.expiresAt,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clean up expired sessions
|
|
||||||
*/
|
|
||||||
async cleanupExpiredSessions() {
|
|
||||||
const result = await db.delete(sessionsTable).where(lt(sessionsTable.expiresAt, Date.now())).returning();
|
|
||||||
if (result.length > 0) {
|
|
||||||
logger.info(`Cleaned up ${result.length} expired sessions`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if any users exist in the system
|
* Check if any users exist in the system
|
||||||
*/
|
*/
|
||||||
|
|
@ -147,33 +9,6 @@ export class AuthService {
|
||||||
const [user] = await db.select({ id: usersTable.id }).from(usersTable).limit(1);
|
const [user] = await db.select({ id: usersTable.id }).from(usersTable).limit(1);
|
||||||
return !!user;
|
return !!user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Change password for a user
|
|
||||||
*/
|
|
||||||
async changePassword(userId: number, currentPassword: string, newPassword: string) {
|
|
||||||
const [user] = await db.select().from(usersTable).where(eq(usersTable.id, userId));
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await Bun.password.verify(currentPassword, user.passwordHash);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
throw new Error("Current password is incorrect");
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPasswordHash = await Bun.password.hash(newPassword, {
|
|
||||||
algorithm: "argon2id",
|
|
||||||
memoryCost: 19456,
|
|
||||||
timeCost: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db.update(usersTable).set({ passwordHash: newPasswordHash }).where(eq(usersTable.id, userId));
|
|
||||||
|
|
||||||
logger.info(`Password changed for user: ${user.username}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authService = new AuthService();
|
export const authService = new AuthService();
|
||||||
|
|
|
||||||
26
app/server/modules/auth/helpers.ts
Normal file
26
app/server/modules/auth/helpers.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { verifyPassword } from "better-auth/crypto";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import { account } from "~/server/db/schema";
|
||||||
|
|
||||||
|
type PasswordVerificationBody = {
|
||||||
|
userId: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyUserPassword = async ({ password, userId }: PasswordVerificationBody) => {
|
||||||
|
const userAccount = await db.query.account.findFirst({
|
||||||
|
where: eq(account.userId, userId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userAccount || !userAccount.password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await verifyPassword({ password: password, hash: userAccount.password });
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
@ -9,28 +9,26 @@ describe("backups security", () => {
|
||||||
const res = await app.request("/api/v1/backups");
|
const res = await app.request("/api/v1/backups");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 if session is invalid", async () => {
|
test("should return 401 if session is invalid", async () => {
|
||||||
const res = await app.request("/api/v1/backups", {
|
const res = await app.request("/api/v1/backups", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: "session_id=invalid-session",
|
Cookie: "better-auth.session_token=invalid-session",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Invalid or expired session");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
|
||||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 200 if session is valid", async () => {
|
test("should return 200 if session is valid", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/backups", {
|
const res = await app.request("/api/v1/backups", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -61,7 +59,7 @@ describe("backups security", () => {
|
||||||
const res = await app.request(path, { method });
|
const res = await app.request(path, { method });
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -71,23 +69,23 @@ describe("backups security", () => {
|
||||||
const res = await app.request("/api/v1/backups/999999");
|
const res = await app.request("/api/v1/backups/999999");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should not disclose if a volume exists when unauthenticated", async () => {
|
test("should not disclose if a volume exists when unauthenticated", async () => {
|
||||||
const res = await app.request("/api/v1/backups/volume/999999");
|
const res = await app.request("/api/v1/backups/volume/999999");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
test("should return 404 for malformed schedule ID", async () => {
|
test("should return 404 for malformed schedule ID", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/backups/not-a-number", {
|
const res = await app.request("/api/v1/backups/not-a-number", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -95,10 +93,10 @@ describe("backups security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 404 for non-existent schedule ID", async () => {
|
test("should return 404 for non-existent schedule ID", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/backups/999999", {
|
const res = await app.request("/api/v1/backups/999999", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -108,11 +106,11 @@ describe("backups security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 400 for invalid payload on create", async () => {
|
test("should return 400 for invalid payload on create", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/backups", {
|
const res = await app.request("/api/v1/backups", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,26 @@ describe("events security", () => {
|
||||||
const res = await app.request("/api/v1/events");
|
const res = await app.request("/api/v1/events");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 if session is invalid", async () => {
|
test("should return 401 if session is invalid", async () => {
|
||||||
const res = await app.request("/api/v1/events", {
|
const res = await app.request("/api/v1/events", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: "session_id=invalid-session",
|
Cookie: "better-auth.session_token=invalid-session",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Invalid or expired session");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
|
||||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 200 if session is valid", async () => {
|
test("should return 200 if session is valid", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/events", {
|
const res = await app.request("/api/v1/events", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -46,7 +44,7 @@ describe("events security", () => {
|
||||||
const res = await app.request(path, { method });
|
const res = await app.request(path, { method });
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { CleanupDanglingMountsJob } from "../../jobs/cleanup-dangling";
|
||||||
import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
import { VolumeHealthCheckJob } from "../../jobs/healthchecks";
|
||||||
import { RepositoryHealthCheckJob } from "../../jobs/repository-healthchecks";
|
import { RepositoryHealthCheckJob } from "../../jobs/repository-healthchecks";
|
||||||
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
import { BackupExecutionJob } from "../../jobs/backup-execution";
|
||||||
import { CleanupSessionsJob } from "../../jobs/cleanup-sessions";
|
|
||||||
import { repositoriesService } from "../repositories/repositories.service";
|
import { repositoriesService } from "../repositories/repositories.service";
|
||||||
import { notificationsService } from "../notifications/notifications.service";
|
import { notificationsService } from "../notifications/notifications.service";
|
||||||
import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
|
import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount";
|
||||||
|
|
@ -82,6 +81,5 @@ export const startup = async () => {
|
||||||
Scheduler.build(VolumeHealthCheckJob).schedule("*/30 * * * *");
|
Scheduler.build(VolumeHealthCheckJob).schedule("*/30 * * * *");
|
||||||
Scheduler.build(RepositoryHealthCheckJob).schedule("50 12 * * *");
|
Scheduler.build(RepositoryHealthCheckJob).schedule("50 12 * * *");
|
||||||
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
|
Scheduler.build(BackupExecutionJob).schedule("* * * * *");
|
||||||
Scheduler.build(CleanupSessionsJob).schedule("0 0 * * *");
|
|
||||||
Scheduler.build(VolumeAutoRemountJob).schedule("*/5 * * * *");
|
Scheduler.build(VolumeAutoRemountJob).schedule("*/5 * * * *");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,26 @@ describe("notifications security", () => {
|
||||||
const res = await app.request("/api/v1/notifications/destinations");
|
const res = await app.request("/api/v1/notifications/destinations");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 if session is invalid", async () => {
|
test("should return 401 if session is invalid", async () => {
|
||||||
const res = await app.request("/api/v1/notifications/destinations", {
|
const res = await app.request("/api/v1/notifications/destinations", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: "session_id=invalid-session",
|
Cookie: "better-auth.session_token=invalid-session",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Invalid or expired session");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
|
||||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 200 if session is valid", async () => {
|
test("should return 200 if session is valid", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/notifications/destinations", {
|
const res = await app.request("/api/v1/notifications/destinations", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -52,7 +50,7 @@ describe("notifications security", () => {
|
||||||
const res = await app.request(path, { method });
|
const res = await app.request(path, { method });
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -62,16 +60,16 @@ describe("notifications security", () => {
|
||||||
const res = await app.request("/api/v1/notifications/destinations/999999");
|
const res = await app.request("/api/v1/notifications/destinations/999999");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
test("should return 404 for malformed destination ID", async () => {
|
test("should return 404 for malformed destination ID", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/notifications/destinations/not-a-number", {
|
const res = await app.request("/api/v1/notifications/destinations/not-a-number", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -79,10 +77,10 @@ describe("notifications security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 404 for non-existent destination ID", async () => {
|
test("should return 404 for non-existent destination ID", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/notifications/destinations/999999", {
|
const res = await app.request("/api/v1/notifications/destinations/999999", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -92,11 +90,12 @@ describe("notifications security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 400 for invalid payload on create", async () => {
|
test("should return 400 for invalid payload on create", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/notifications/destinations", {
|
const res = await app.request("/api/v1/notifications/destinations", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,26 @@ describe("repositories security", () => {
|
||||||
const res = await app.request("/api/v1/repositories");
|
const res = await app.request("/api/v1/repositories");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 if session is invalid", async () => {
|
test("should return 401 if session is invalid", async () => {
|
||||||
const res = await app.request("/api/v1/repositories", {
|
const res = await app.request("/api/v1/repositories", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: "session_id=invalid-session",
|
Cookie: "better-auth.session_token=invalid-session",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Invalid or expired session");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
|
||||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 200 if session is valid", async () => {
|
test("should return 200 if session is valid", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/repositories", {
|
const res = await app.request("/api/v1/repositories", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -59,7 +57,7 @@ describe("repositories security", () => {
|
||||||
const res = await app.request(path, { method });
|
const res = await app.request(path, { method });
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -69,16 +67,16 @@ describe("repositories security", () => {
|
||||||
const res = await app.request("/api/v1/repositories/non-existent-repo");
|
const res = await app.request("/api/v1/repositories/non-existent-repo");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
test("should return 404 for non-existent repository", async () => {
|
test("should return 404 for non-existent repository", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/repositories/non-existent-repo", {
|
const res = await app.request("/api/v1/repositories/non-existent-repo", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -88,11 +86,11 @@ describe("repositories security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 400 for invalid payload on create", async () => {
|
test("should return 400 for invalid payload on create", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/repositories", {
|
const res = await app.request("/api/v1/repositories", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,26 @@ describe("system security", () => {
|
||||||
const res = await app.request("/api/v1/system/info");
|
const res = await app.request("/api/v1/system/info");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 if session is invalid", async () => {
|
test("should return 401 if session is invalid", async () => {
|
||||||
const res = await app.request("/api/v1/system/info", {
|
const res = await app.request("/api/v1/system/info", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: "session_id=invalid-session",
|
Cookie: "better-auth.session_token=invalid-session",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Invalid or expired session");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
|
||||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 200 if session is valid", async () => {
|
test("should return 200 if session is valid", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/system/info", {
|
const res = await app.request("/api/v1/system/info", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -48,18 +46,18 @@ describe("system security", () => {
|
||||||
const res = await app.request(path, { method });
|
const res = await app.request(path, { method });
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
test("should return 400 for invalid payload on restic-password", async () => {
|
test("should return 400 for invalid payload on restic-password", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/system/restic-password", {
|
const res = await app.request("/api/v1/system/restic-password", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({}),
|
body: JSON.stringify({}),
|
||||||
|
|
@ -69,11 +67,11 @@ describe("system security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 for incorrect password on restic-password", async () => {
|
test("should return 401 for incorrect password on restic-password", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/system/restic-password", {
|
const res = await app.request("/api/v1/system/restic-password", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -83,7 +81,7 @@ describe("system security", () => {
|
||||||
|
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Incorrect password");
|
expect(body.message).toBe("Invalid password");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { RESTIC_PASS_FILE } from "../../core/constants";
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { usersTable } from "../../db/schema";
|
import { usersTable } from "../../db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { verifyUserPassword } from "../auth/helpers";
|
||||||
|
|
||||||
export const systemController = new Hono()
|
export const systemController = new Hono()
|
||||||
.use(requireAuth)
|
.use(requireAuth)
|
||||||
|
|
@ -35,16 +36,9 @@ export const systemController = new Hono()
|
||||||
const user = c.get("user");
|
const user = c.get("user");
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
const [dbUser] = await db.select().from(usersTable).where(eq(usersTable.id, user.id));
|
const isPasswordValid = await verifyUserPassword({ password: body.password, userId: user.id });
|
||||||
|
if (!isPasswordValid) {
|
||||||
if (!dbUser) {
|
return c.json({ message: "Invalid password" }, 401);
|
||||||
return c.json({ message: "User not found" }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await Bun.password.verify(body.password, dbUser.passwordHash);
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
return c.json({ message: "Incorrect password" }, 401);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,26 @@ describe("volumes security", () => {
|
||||||
const res = await app.request("/api/v1/volumes");
|
const res = await app.request("/api/v1/volumes");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 401 if session is invalid", async () => {
|
test("should return 401 if session is invalid", async () => {
|
||||||
const res = await app.request("/api/v1/volumes", {
|
const res = await app.request("/api/v1/volumes", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: "session_id=invalid-session",
|
Cookie: "better-auth.session_token=invalid-session",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Invalid or expired session");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
|
|
||||||
expect(res.headers.get("Set-Cookie")).toContain("session_id=;");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 200 if session is valid", async () => {
|
test("should return 200 if session is valid", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
|
|
||||||
const res = await app.request("/api/v1/volumes", {
|
const res = await app.request("/api/v1/volumes", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -57,7 +55,7 @@ describe("volumes security", () => {
|
||||||
const res = await app.request(path, { method });
|
const res = await app.request(path, { method });
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -67,16 +65,16 @@ describe("volumes security", () => {
|
||||||
const res = await app.request("/api/v1/volumes/non-existent-volume");
|
const res = await app.request("/api/v1/volumes/non-existent-volume");
|
||||||
expect(res.status).toBe(401);
|
expect(res.status).toBe(401);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.message).toBe("Authentication required");
|
expect(body.message).toBe("Invalid or expired session");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
test("should return 404 for non-existent volume", async () => {
|
test("should return 404 for non-existent volume", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/volumes/non-existent-volume", {
|
const res = await app.request("/api/v1/volumes/non-existent-volume", {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -86,11 +84,11 @@ describe("volumes security", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return 400 for invalid payload on create", async () => {
|
test("should return 400 for invalid payload on create", async () => {
|
||||||
const { sessionId } = await createTestSession();
|
const { token } = await createTestSession();
|
||||||
const res = await app.request("/api/v1/volumes", {
|
const res = await app.request("/api/v1/volumes", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `session_id=${sessionId}`,
|
Cookie: `better-auth.session_token=${token}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { RESTIC_PASS_FILE } from "../core/constants";
|
import { RESTIC_PASS_FILE } from "../core/constants";
|
||||||
import { isNodeJSErrnoException } from "./fs";
|
import { isNodeJSErrnoException } from "./fs";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
const hkdf = promisify(crypto.hkdf);
|
||||||
|
|
||||||
const algorithm = "aes-256-gcm" as const;
|
const algorithm = "aes-256-gcm" as const;
|
||||||
const keyLength = 32;
|
const keyLength = 32;
|
||||||
|
|
@ -183,7 +186,16 @@ const sealSecret = async (value: string): Promise<string> => {
|
||||||
return encrypt(value);
|
return encrypt(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function deriveSecret(label: string) {
|
||||||
|
const masterSecret = await Bun.file(RESTIC_PASS_FILE).text();
|
||||||
|
|
||||||
|
const derivedKey = await hkdf("sha256", masterSecret, "", label, 32);
|
||||||
|
|
||||||
|
return Buffer.from(derivedKey).toString("hex");
|
||||||
|
}
|
||||||
|
|
||||||
export const cryptoUtils = {
|
export const cryptoUtils = {
|
||||||
resolveSecret,
|
resolveSecret,
|
||||||
sealSecret,
|
sealSecret,
|
||||||
|
deriveSecret,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,53 @@
|
||||||
import { authService } from "~/server/modules/auth/auth.service";
|
|
||||||
import { db } from "~/server/db/db";
|
import { db } from "~/server/db/db";
|
||||||
import { usersTable, sessionsTable } from "~/server/db/schema";
|
import { sessionsTable, usersTable, account } from "~/server/db/schema";
|
||||||
|
import { hashPassword } from "better-auth/crypto";
|
||||||
|
import { createHmac } from "node:crypto";
|
||||||
|
|
||||||
export async function createTestSession() {
|
export async function createTestSession() {
|
||||||
const [existingUser] = await db.select().from(usersTable);
|
const [existingUser] = await db.select().from(usersTable);
|
||||||
|
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
await authService.register("testadmin", "testpassword");
|
await db.insert(usersTable).values({
|
||||||
|
username: "testuser",
|
||||||
|
email: "test@test.com",
|
||||||
|
name: "Test User",
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [user] = await db.select().from(usersTable);
|
const [user] = await db.select().from(usersTable);
|
||||||
|
|
||||||
const sessionId = crypto.randomUUID();
|
const token = crypto.randomUUID().replace(/-/g, "");
|
||||||
const expiresAt = Date.now() + 1000 * 60 * 60 * 24; // 24 hours
|
const sessionId = token;
|
||||||
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
await db.insert(sessionsTable).values({
|
await db.insert(sessionsTable).values({
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
|
token: token,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { sessionId, user };
|
// Better Auth signs the token using HMAC-SHA256 with the secret
|
||||||
|
// The secret is "test-secret" because we mocked cryptoUtils.deriveSecret
|
||||||
|
const signature = createHmac("sha256", "test-secret").update(token).digest("base64");
|
||||||
|
|
||||||
|
const signedToken = `${token}.${signature}`;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(account)
|
||||||
|
.values({
|
||||||
|
userId: user.id,
|
||||||
|
accountId: "testuser",
|
||||||
|
password: await hashPassword("password123"),
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
providerId: "credentials",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.onConflictDoNothing();
|
||||||
|
|
||||||
|
return { token: encodeURIComponent(signedToken), user };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ import { beforeAll, mock } from "bun:test";
|
||||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { cwd } from "node:process";
|
import { cwd } from "node:process";
|
||||||
import { db } from "~/server/db/db";
|
import * as schema from "~/server/db/schema";
|
||||||
|
import { db, setSchema } from "~/server/db/db";
|
||||||
|
|
||||||
|
setSchema(schema);
|
||||||
|
|
||||||
void mock.module("~/server/utils/logger", () => ({
|
void mock.module("~/server/utils/logger", () => ({
|
||||||
logger: {
|
logger: {
|
||||||
|
|
@ -13,6 +16,14 @@ void mock.module("~/server/utils/logger", () => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
void mock.module("~/server/utils/crypto", () => ({
|
||||||
|
cryptoUtils: {
|
||||||
|
deriveSecret: async () => "test-secret",
|
||||||
|
sealSecret: async (v: string) => v,
|
||||||
|
resolveSecret: async (v: string) => v,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const migrationsFolder = path.join(cwd(), "app", "drizzle");
|
const migrationsFolder = path.join(cwd(), "app", "drizzle");
|
||||||
migrate(db, { migrationsFolder });
|
migrate(db, { migrationsFolder });
|
||||||
|
|
|
||||||
117
bun.lock
117
bun.lock
|
|
@ -30,6 +30,7 @@
|
||||||
"@scalar/hono-api-reference": "^0.9.25",
|
"@scalar/hono-api-reference": "^0.9.25",
|
||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"arktype": "^2.1.28",
|
"arktype": "^2.1.28",
|
||||||
|
"better-auth": "^1.4.10",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
|
|
@ -160,6 +161,14 @@
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
|
"@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="],
|
||||||
|
|
||||||
|
"@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
|
||||||
|
|
||||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
|
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
|
||||||
|
|
@ -304,6 +313,10 @@
|
||||||
|
|
||||||
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
"@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.2.0", "", {}, "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.22.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dhz2m2uLrHT3MwM+LAdvr97EojJZTwaZ6BuMTRftJzqa9dHYDG/MtSBuDD2DpGpZ0SM2iVwni2wCzCYGKTojbA=="],
|
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.22.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dhz2m2uLrHT3MwM+LAdvr97EojJZTwaZ6BuMTRftJzqa9dHYDG/MtSBuDD2DpGpZ0SM2iVwni2wCzCYGKTojbA=="],
|
||||||
|
|
||||||
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.22.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-VykUbibvqSOG5YIFUMpHtZVrY1YKDl9Il2SvFemUfR5Ac1t1BFZOnazYe98jtZGFY4sEdEORs0ImBARnyMX/hw=="],
|
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.22.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-VykUbibvqSOG5YIFUMpHtZVrY1YKDl9Il2SvFemUfR5Ac1t1BFZOnazYe98jtZGFY4sEdEORs0ImBARnyMX/hw=="],
|
||||||
|
|
@ -348,6 +361,8 @@
|
||||||
|
|
||||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-J+Vc00Utcf8p77lZPruQgb0QnQXuKnFogN88kCnOqs2a83I+vTBB8ILr0+L9sTwVRvIDMSC0pLdeQH4svWGFZg=="],
|
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-J+Vc00Utcf8p77lZPruQgb0QnQXuKnFogN88kCnOqs2a83I+vTBB8ILr0+L9sTwVRvIDMSC0pLdeQH4svWGFZg=="],
|
||||||
|
|
||||||
|
"@prisma/client": ["@prisma/client@5.22.0", "", { "peerDependencies": { "prisma": "*" }, "optionalPeers": ["prisma"] }, "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA=="],
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
@ -584,6 +599,8 @@
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
||||||
|
|
||||||
|
"@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
@ -632,16 +649,30 @@
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
|
||||||
|
|
||||||
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="],
|
||||||
|
|
||||||
|
"better-sqlite3": ["better-sqlite3@12.5.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg=="],
|
||||||
|
|
||||||
|
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
|
||||||
|
|
||||||
|
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||||
|
|
||||||
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
|
"body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="],
|
||||||
|
|
||||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
|
||||||
|
|
||||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
"bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="],
|
||||||
|
|
@ -674,6 +705,8 @@
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
|
||||||
|
|
||||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||||
|
|
||||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
@ -756,8 +789,12 @@
|
||||||
|
|
||||||
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
|
||||||
|
|
||||||
|
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
|
||||||
|
|
||||||
"dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="],
|
"dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="],
|
||||||
|
|
||||||
|
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
|
||||||
|
|
||||||
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
"default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="],
|
||||||
|
|
||||||
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
||||||
|
|
@ -806,6 +843,8 @@
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
"enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="],
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
@ -836,6 +875,8 @@
|
||||||
|
|
||||||
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
"exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||||
|
|
||||||
|
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
|
||||||
|
|
||||||
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
"express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
|
||||||
|
|
||||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||||
|
|
@ -846,6 +887,8 @@
|
||||||
|
|
||||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||||
|
|
||||||
|
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
|
||||||
|
|
||||||
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
"finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="],
|
||||||
|
|
||||||
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
|
||||||
|
|
@ -854,6 +897,8 @@
|
||||||
|
|
||||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||||
|
|
||||||
|
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
@ -874,6 +919,8 @@
|
||||||
|
|
||||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||||
|
|
||||||
|
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||||
|
|
||||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||||
|
|
||||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
@ -906,10 +953,14 @@
|
||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
"iconv-lite": ["iconv-lite@0.7.1", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||||
|
|
||||||
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
"inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
|
||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
@ -944,6 +995,8 @@
|
||||||
|
|
||||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
|
|
@ -954,6 +1007,8 @@
|
||||||
|
|
||||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.9", "", {}, "sha512-3BeXMoiOhpOwu62CiVpO6lxfq4eS6KMYfQdMsN/2kUCRNuF2YiEr7u0HLHaQU+O4Xu8YXE3bHVkwaQ85i72EuA=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
"lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
|
||||||
|
|
@ -1096,10 +1151,14 @@
|
||||||
|
|
||||||
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
|
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
|
|
||||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||||
|
|
||||||
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
|
"morgan": ["morgan@1.10.1", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.1.0" } }, "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
@ -1108,10 +1167,16 @@
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="],
|
||||||
|
|
||||||
|
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||||
|
|
||||||
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||||
|
|
||||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
|
"node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="],
|
||||||
|
|
||||||
"node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="],
|
"node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="],
|
||||||
|
|
||||||
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||||
|
|
@ -1134,6 +1199,8 @@
|
||||||
|
|
||||||
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
|
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
|
||||||
|
|
||||||
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
"open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
|
||||||
|
|
@ -1160,6 +1227,22 @@
|
||||||
|
|
||||||
"perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
|
"perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="],
|
||||||
|
|
||||||
|
"pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
|
||||||
|
|
||||||
|
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
|
||||||
|
|
||||||
|
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
|
||||||
|
|
||||||
|
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||||
|
|
||||||
|
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
|
||||||
|
|
||||||
|
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
|
||||||
|
|
||||||
|
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||||
|
|
||||||
|
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
@ -1170,8 +1253,18 @@
|
||||||
|
|
||||||
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
"postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="],
|
||||||
|
|
||||||
|
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||||
|
|
||||||
|
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||||
|
|
||||||
|
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||||
|
|
||||||
|
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||||
|
|
||||||
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
|
||||||
|
|
||||||
|
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
|
|
@ -1180,6 +1273,8 @@
|
||||||
|
|
||||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
|
||||||
|
|
||||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||||
|
|
||||||
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
|
||||||
|
|
@ -1190,6 +1285,8 @@
|
||||||
|
|
||||||
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||||
|
|
||||||
|
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||||
|
|
||||||
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
"rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="],
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
@ -1240,6 +1337,8 @@
|
||||||
|
|
||||||
"rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="],
|
"rollup": ["rollup@4.54.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.54.0", "@rollup/rollup-android-arm64": "4.54.0", "@rollup/rollup-darwin-arm64": "4.54.0", "@rollup/rollup-darwin-x64": "4.54.0", "@rollup/rollup-freebsd-arm64": "4.54.0", "@rollup/rollup-freebsd-x64": "4.54.0", "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", "@rollup/rollup-linux-arm-musleabihf": "4.54.0", "@rollup/rollup-linux-arm64-gnu": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-loong64-gnu": "4.54.0", "@rollup/rollup-linux-ppc64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-gnu": "4.54.0", "@rollup/rollup-linux-riscv64-musl": "4.54.0", "@rollup/rollup-linux-s390x-gnu": "4.54.0", "@rollup/rollup-linux-x64-gnu": "4.54.0", "@rollup/rollup-linux-x64-musl": "4.54.0", "@rollup/rollup-openharmony-arm64": "4.54.0", "@rollup/rollup-win32-arm64-msvc": "4.54.0", "@rollup/rollup-win32-ia32-msvc": "4.54.0", "@rollup/rollup-win32-x64-gnu": "4.54.0", "@rollup/rollup-win32-x64-msvc": "4.54.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||||
|
|
||||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||||
|
|
||||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
@ -1274,6 +1373,10 @@
|
||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
|
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
|
||||||
|
|
||||||
|
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
|
||||||
|
|
||||||
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
|
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
|
||||||
|
|
||||||
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
@ -1286,6 +1389,8 @@
|
||||||
|
|
||||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||||
|
|
||||||
|
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||||
|
|
||||||
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
@ -1298,6 +1403,8 @@
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
"style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
|
||||||
|
|
||||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||||
|
|
@ -1310,6 +1417,10 @@
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
|
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||||
|
|
||||||
|
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||||
|
|
||||||
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
@ -1334,6 +1445,8 @@
|
||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
"type-fest": ["type-fest@5.0.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA=="],
|
"type-fest": ["type-fest@5.0.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA=="],
|
||||||
|
|
@ -1404,10 +1517,14 @@
|
||||||
|
|
||||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||||
|
|
||||||
"wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="],
|
"wsl-utils": ["wsl-utils@0.3.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-3sFIGLiaDP7rTO4xh3g+b3AzhYDIUGGywE/WsmqzJWDxus5aJXVnPTNC/6L+r2WzrwXqVOdD262OaO+cEyPMSQ=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||||
|
|
|
||||||
225
package.json
225
package.json
|
|
@ -1,114 +1,115 @@
|
||||||
{
|
{
|
||||||
"name": "zerobyte",
|
"name": "zerobyte",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.5",
|
"packageManager": "bun@1.3.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "oxlint --type-aware",
|
"lint": "oxlint --type-aware",
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"dev": "bunx --bun vite",
|
"dev": "bunx --bun vite",
|
||||||
"start": "bun ./dist/server/index.js",
|
"start": "bun ./dist/server/index.js",
|
||||||
"cli:dev": "bun run app/server/cli/main.ts",
|
"cli:dev": "bun run app/server/cli/main.ts",
|
||||||
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
"cli": "ZEROBYTE_CLI=1 bun ./dist/server/index.js",
|
||||||
"tsc": "react-router typegen && tsc",
|
"tsc": "react-router typegen && tsc",
|
||||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||||
"gen:api-client": "openapi-ts",
|
"gen:api-client": "openapi-ts",
|
||||||
"gen:migrations": "drizzle-kit generate",
|
"gen:migrations": "drizzle-kit generate",
|
||||||
"studio": "drizzle-kit studio",
|
"studio": "drizzle-kit studio",
|
||||||
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
|
"test:server": "dotenv -e .env.test -- bun test app/server --preload ./app/test/setup.ts",
|
||||||
"test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts",
|
"test:client": "dotenv -e .env.test -- bun test app/client --preload ./app/test/setup-client.ts",
|
||||||
"test": "bun run test:server && bun run test:client"
|
"test": "bun run test:server && bun run test:client"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"esbuild": "^0.27.2"
|
"esbuild": "^0.27.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hono/standard-validator": "^0.2.0",
|
"@hono/standard-validator": "^0.2.0",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@inquirer/prompts": "^8.0.2",
|
"@inquirer/prompts": "^8.0.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-hover-card": "^1.1.15",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-router/node": "^7.10.0",
|
"@react-router/node": "^7.10.0",
|
||||||
"@react-router/serve": "^7.10.0",
|
"@react-router/serve": "^7.10.0",
|
||||||
"@scalar/hono-api-reference": "^0.9.25",
|
"@scalar/hono-api-reference": "^0.9.25",
|
||||||
"@tanstack/react-query": "^5.90.11",
|
"@tanstack/react-query": "^5.90.11",
|
||||||
"arktype": "^2.1.28",
|
"arktype": "^2.1.28",
|
||||||
"class-variance-authority": "^0.7.1",
|
"better-auth": "^1.4.10",
|
||||||
"clsx": "^2.1.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"commander": "^14.0.2",
|
"clsx": "^2.1.1",
|
||||||
"cron-parser": "^5.4.0",
|
"commander": "^14.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"cron-parser": "^5.4.0",
|
||||||
"dither-plugin": "^1.1.1",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.2.3",
|
"dither-plugin": "^1.1.1",
|
||||||
"drizzle-orm": "^0.44.7",
|
"dotenv": "^17.2.3",
|
||||||
"es-toolkit": "^1.42.0",
|
"drizzle-orm": "^0.44.7",
|
||||||
"hono": "4.10.5",
|
"es-toolkit": "^1.42.0",
|
||||||
"hono-openapi": "^1.1.1",
|
"hono": "4.10.5",
|
||||||
"hono-rate-limiter": "^0.5.0",
|
"hono-openapi": "^1.1.1",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"hono-rate-limiter": "^0.5.0",
|
||||||
"isbot": "^5.1.32",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
"lucide-react": "^0.555.0",
|
"isbot": "^5.1.32",
|
||||||
"next-themes": "^0.4.6",
|
"lucide-react": "^0.555.0",
|
||||||
"node-cron": "^4.2.1",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-hook-form": "^7.68.0",
|
"react-dom": "^19.2.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-hook-form": "^7.68.0",
|
||||||
"react-router": "^7.10.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-hono-server": "^2.22.0",
|
"react-router": "^7.10.0",
|
||||||
"recharts": "3.5.1",
|
"react-router-hono-server": "^2.22.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"recharts": "3.5.1",
|
||||||
"semver": "^7.7.3",
|
"remark-gfm": "^4.0.1",
|
||||||
"slugify": "^1.6.6",
|
"semver": "^7.7.3",
|
||||||
"sonner": "^2.0.7",
|
"slugify": "^1.6.6",
|
||||||
"tailwind-merge": "^3.4.0",
|
"sonner": "^2.0.7",
|
||||||
"tiny-typed-emitter": "^2.1.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"winston": "^3.18.3",
|
"tiny-typed-emitter": "^2.1.0",
|
||||||
"yaml": "^2.8.2"
|
"winston": "^3.18.3",
|
||||||
},
|
"yaml": "^2.8.2"
|
||||||
"devDependencies": {
|
},
|
||||||
"@faker-js/faker": "^10.1.0",
|
"devDependencies": {
|
||||||
"@happy-dom/global-registrator": "^20.0.11",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@hey-api/openapi-ts": "^0.88.0",
|
"@happy-dom/global-registrator": "^20.0.11",
|
||||||
"@react-router/dev": "^7.10.0",
|
"@hey-api/openapi-ts": "^0.88.0",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@react-router/dev": "^7.10.0",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query-devtools": "^5.91.1",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@tanstack/react-query-devtools": "^5.91.1",
|
||||||
"@testing-library/react": "^16.3.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/bun": "^1.3.4",
|
"@testing-library/react": "^16.3.1",
|
||||||
"@types/node": "^25.0.3",
|
"@types/bun": "^1.3.4",
|
||||||
"@types/react": "^19.2.7",
|
"@types/node": "^25.0.3",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react": "^19.2.7",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/react-dom": "^19.2.3",
|
||||||
"dotenv-cli": "^11.0.0",
|
"@types/semver": "^7.7.1",
|
||||||
"drizzle-kit": "^0.31.7",
|
"dotenv-cli": "^11.0.0",
|
||||||
"lightningcss": "^1.30.2",
|
"drizzle-kit": "^0.31.7",
|
||||||
"oxfmt": "^0.22.0",
|
"lightningcss": "^1.30.2",
|
||||||
"oxlint": "^1.36.0",
|
"oxfmt": "^0.22.0",
|
||||||
"oxlint-tsgolint": "^0.10.1",
|
"oxlint": "^1.36.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"oxlint-tsgolint": "^0.10.1",
|
||||||
"tinyglobby": "^0.2.15",
|
"tailwindcss": "^4.1.17",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tinyglobby": "^0.2.15",
|
||||||
"typescript": "^5.9.3",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite": "^7.2.6",
|
"typescript": "^5.9.3",
|
||||||
"vite-bundle-analyzer": "^1.2.3",
|
"vite": "^7.2.6",
|
||||||
"vite-tsconfig-paths": "^6.0.3"
|
"vite-bundle-analyzer": "^1.2.3",
|
||||||
}
|
"vite-tsconfig-paths": "^6.0.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue