Use filesystem-based cache to get response details instead of using the cookies (only use cookies for getting the request ID [FIXME]).master
@@ -127,9 +127,10 @@ In theory, any API route may have a corresponding action route. | |||||
- [ ] `<form method="dialog">` (on hold, see https://github.com/whatwg/html/issues/9625) | - [ ] `<form method="dialog">` (on hold, see https://github.com/whatwg/html/issues/9625) | ||||
- [ ] Tests | - [ ] Tests | ||||
- [X] Form with redirects | - [X] Form with redirects | ||||
- [ ] Form with files | |||||
- [X] Form with files | |||||
- [ ] Documentation | - [ ] Documentation | ||||
- [ ] Remix support | - [ ] Remix support | ||||
- [ ] `accept-charset=""` attribute support | - [ ] `accept-charset=""` attribute support | ||||
- [X] Method override support | - [X] Method override support | ||||
- Integration with Next router (iceform-next) | |||||
- [ ] Integration with Next router (iceform-next) | |||||
- [ ] Investigate bug of not cleaning response body cache and flash messages via cookies |
@@ -4,6 +4,11 @@ const nextConfig = { | |||||
experimental: { | experimental: { | ||||
optimizeCss: true, | optimizeCss: true, | ||||
}, | }, | ||||
webpack: (config) => { | |||||
config.resolve.fallback = { fs: false }; | |||||
return config; | |||||
}, | |||||
}; | }; | ||||
module.exports = nextConfig; | module.exports = nextConfig; |
@@ -51,7 +51,7 @@ const patchNote: NextApiHandler = async (req, res) => { | |||||
return; | return; | ||||
} | } | ||||
const { title, content } = req.body; | |||||
const { title, content, image } = req.body; | |||||
const dataRaw = await fs.readFile('.db/notes.jsonl', { | const dataRaw = await fs.readFile('.db/notes.jsonl', { | ||||
encoding: 'utf-8', | encoding: 'utf-8', | ||||
@@ -75,6 +75,7 @@ const patchNote: NextApiHandler = async (req, res) => { | |||||
...note, | ...note, | ||||
title: title ?? note.title, | title: title ?? note.title, | ||||
content: content ?? note.content, | content: content ?? note.content, | ||||
image: image ?? note.image, | |||||
}; | }; | ||||
data[noteIndex] = updatedNote; | data[noteIndex] = updatedNote; | ||||
@@ -185,7 +186,7 @@ export interface NoteCollectionParams { | |||||
} | } | ||||
const createNote = (params: NoteCollectionParams): NextApiHandler => async (req, res) => { | const createNote = (params: NoteCollectionParams): NextApiHandler => async (req, res) => { | ||||
const { title, content } = req.body; | |||||
const { title, content, image } = req.body; | |||||
if (typeof title !== 'string' || typeof content !== 'string') { | if (typeof title !== 'string' || typeof content !== 'string') { | ||||
res.status(400).send('Bad Request'); | res.status(400).send('Bad Request'); | ||||
@@ -202,6 +203,7 @@ const createNote = (params: NoteCollectionParams): NextApiHandler => async (req, | |||||
id: newId, | id: newId, | ||||
title, | title, | ||||
content, | content, | ||||
image: `data:${image.type};base64,${image.toString('base64')}`, | |||||
})}\n`, | })}\n`, | ||||
{ | { | ||||
flag: 'a', | flag: 'a', | ||||
@@ -223,6 +225,7 @@ const createNote = (params: NoteCollectionParams): NextApiHandler => async (req, | |||||
id: newId, | id: newId, | ||||
title, | title, | ||||
content, | content, | ||||
image, | |||||
}); | }); | ||||
}; | }; | ||||
@@ -72,6 +72,10 @@ const NotesItemPage: Iceform.NextPage<NotesItemPageProps> = ({ | |||||
/> | /> | ||||
</div> | </div> | ||||
<div> | <div> | ||||
<img | |||||
src={body.image as string} | |||||
alt={body.title as string} | |||||
/> | |||||
<label> | <label> | ||||
<span className="after:block">Image</span> | <span className="after:block">Image</span> | ||||
<input type="file" name="image" /> | <input type="file" name="image" /> | ||||
@@ -108,13 +108,24 @@ const NotesPage: NextPage<NotesPageProps> = ({ | |||||
</div> | </div> | ||||
</Iceform.Form> | </Iceform.Form> | ||||
</div> | </div> | ||||
<div | |||||
className="font-bold" | |||||
> | |||||
{note.title} | |||||
</div> | |||||
<div> | |||||
{note.content} | |||||
<div className="grid grid-cols-3 gap-4"> | |||||
<div> | |||||
<img | |||||
className="w-full" | |||||
src={note.image} | |||||
alt={note.title} | |||||
/> | |||||
</div> | |||||
<div className="col-span-2"> | |||||
<div | |||||
className="font-bold" | |||||
> | |||||
{note.title} | |||||
</div> | |||||
<div> | |||||
{note.content} | |||||
</div> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
))} | ))} | ||||
@@ -10,7 +10,9 @@ | |||||
"react/jsx-indent": ["error", "tab"], | "react/jsx-indent": ["error", "tab"], | ||||
"react/jsx-props-no-spreading": "off", | "react/jsx-props-no-spreading": "off", | ||||
"@typescript-eslint/no-misused-promises": "off", | "@typescript-eslint/no-misused-promises": "off", | ||||
"@typescript-eslint/no-namespace": "off" | |||||
"@typescript-eslint/no-namespace": "off", | |||||
"max-classes-per-file": "off", | |||||
"import/prefer-default-export": "off" | |||||
}, | }, | ||||
"parserOptions": { | "parserOptions": { | ||||
"project": "./tsconfig.eslint.json" | "project": "./tsconfig.eslint.json" | ||||
@@ -68,13 +68,11 @@ | |||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@theoryofnekomata/formxtra": "^1.0.3", | "@theoryofnekomata/formxtra": "^1.0.3", | ||||
"@web-std/file": "^3.0.3", | |||||
"busboy": "^1.6.0", | "busboy": "^1.6.0", | ||||
"nookies": "^2.5.2", | "nookies": "^2.5.2", | ||||
"seroval": "^0.9.0" | |||||
"seroval": "^0.10.2" | |||||
}, | }, | ||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js", | |||||
"exports": { | "exports": { | ||||
".": { | ".": { | ||||
"development": { | "development": { | ||||
@@ -88,5 +86,8 @@ | |||||
}, | }, | ||||
"typesVersions": { | "typesVersions": { | ||||
"*": {} | "*": {} | ||||
} | |||||
}, | |||||
"types": "./dist/types/index.d.ts", | |||||
"main": "./dist/cjs/production/index.js", | |||||
"module": "./dist/esm/production/index.js" | |||||
} | } |
@@ -1,3 +1,3 @@ | |||||
{ | { | ||||
"target": "es2018" | "target": "es2018" | ||||
} | |||||
} |
@@ -1,148 +0,0 @@ | |||||
import { | |||||
GetServerSideProps, | |||||
NextApiHandler, | |||||
PageConfig, | |||||
NextApiRequest as DefaultNextApiRequest, PreviewData, GetServerSidePropsContext, | |||||
} from 'next'; | |||||
import { ParsedUrlQuery } from 'querystring'; | |||||
import { deserializeBody, EncTypeDeserializerMap } from '../utils/serialization'; | |||||
import { IceformNextServerResponse } from './response'; | |||||
import { | |||||
BODY_COOKIE_KEY, CONTENT_TYPE_COOKIE_KEY, | |||||
CookieManager, | |||||
STATUS_CODE_COOKIE_KEY, | |||||
STATUS_MESSAGE_COOKIE_KEY, | |||||
} from '../utils/cookies'; | |||||
import { | |||||
METHOD_FORM_KEY, METHODS_WITH_BODY, | |||||
PREVENT_REDIRECT_FORM_KEY, | |||||
} from '../common/constants'; | |||||
import { | |||||
ACTION_STATUS_CODE, | |||||
DEFAULT_METHOD, | |||||
} from './constants'; | |||||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||||
api: { | |||||
...(customConfig.api ?? {}), | |||||
bodyParser: false, | |||||
}, | |||||
}); | |||||
export type OnActionFunction< | |||||
Props extends Record<string, unknown> = Record<string, unknown>, | |||||
Params extends ParsedUrlQuery = ParsedUrlQuery, | |||||
Preview extends PreviewData = PreviewData, | |||||
> = ( | |||||
actionReq: DefaultNextApiRequest, | |||||
actionRes: IceformNextServerResponse, | |||||
context: GetServerSidePropsContext<Params, Preview>, | |||||
) => Promise<Awaited<ReturnType<GetServerSideProps<Props, Params, Preview>>> | undefined>; | |||||
export interface ActionWrapperOptions { | |||||
fn: NextApiHandler, | |||||
onAction?: OnActionFunction, | |||||
deserializers?: EncTypeDeserializerMap, | |||||
/** | |||||
* Maps the Location header from the handler response to an accessible URL. | |||||
* @param url | |||||
*/ | |||||
mapLocationToRedirectDestination?: (referer: string, url: string) => string, | |||||
} | |||||
export const wrapApiHandler = ( | |||||
options: ActionWrapperOptions, | |||||
): NextApiHandler => async (req, res) => { | |||||
const reqMut = req as unknown as Record<string, unknown>; | |||||
if (METHODS_WITH_BODY.includes(req.method?.toUpperCase() as typeof METHODS_WITH_BODY[number])) { | |||||
reqMut.body = await deserializeBody({ | |||||
req, | |||||
deserializers: options.deserializers, | |||||
}); | |||||
} | |||||
return options.fn(reqMut as unknown as DefaultNextApiRequest, res); | |||||
}; | |||||
export const getServerSideProps = ( | |||||
options: ActionWrapperOptions, | |||||
): GetServerSideProps => async (ctx) => { | |||||
const { referer = '/' } = ctx.req.headers; | |||||
const deserialized = await deserializeBody({ | |||||
req: ctx.req, | |||||
deserializers: options.deserializers, | |||||
}); | |||||
const defaultMethod = ctx.req.method ?? DEFAULT_METHOD; | |||||
let effectiveMethod = defaultMethod; | |||||
let mockReqBody: unknown = deserialized; | |||||
if (typeof deserialized === 'object' && deserialized !== null) { | |||||
const { | |||||
[METHOD_FORM_KEY]: theMethod, | |||||
...theMockReqBody | |||||
} = deserialized as { | |||||
[METHOD_FORM_KEY]?: string, | |||||
[key: string]: unknown, | |||||
}; | |||||
effectiveMethod = theMethod ?? defaultMethod; | |||||
mockReqBody = theMockReqBody; | |||||
} | |||||
const mockReq = { | |||||
...ctx.req, | |||||
body: mockReqBody, | |||||
query: { | |||||
...ctx.query, | |||||
...(ctx.params ?? {}), | |||||
}, | |||||
// ?: how to prevent malicious method spoofing? | |||||
method: effectiveMethod, | |||||
} as DefaultNextApiRequest; | |||||
const mockRes = new IceformNextServerResponse(ctx.req); | |||||
await options.fn(mockReq, mockRes); | |||||
const cookieManager = new CookieManager(ctx); | |||||
cookieManager.setCookie(STATUS_CODE_COOKIE_KEY, mockRes.statusCode.toString()); | |||||
cookieManager.setCookie(STATUS_MESSAGE_COOKIE_KEY, mockRes.statusMessage); | |||||
if (mockRes.data) { | |||||
cookieManager.setCookie(BODY_COOKIE_KEY, mockRes.data as string); | |||||
if (mockRes.contentType) { | |||||
cookieManager.setCookie(CONTENT_TYPE_COOKIE_KEY, mockRes.contentType); | |||||
} | |||||
} | |||||
if (typeof options.onAction === 'function') { | |||||
const onActionResult = await options.onAction( | |||||
mockReq, | |||||
mockRes, | |||||
ctx, | |||||
); | |||||
if (onActionResult) { | |||||
return onActionResult; | |||||
} | |||||
} | |||||
const preventRedirect = ( | |||||
typeof mockReq.body === 'object' | |||||
&& mockReq.body !== null | |||||
&& PREVENT_REDIRECT_FORM_KEY in mockReq.body | |||||
); | |||||
const redirectDestination = ( | |||||
mockRes.location | |||||
&& typeof options.mapLocationToRedirectDestination === 'function' | |||||
&& !preventRedirect | |||||
) | |||||
? options.mapLocationToRedirectDestination(referer, mockRes.location) | |||||
: referer; | |||||
return { | |||||
redirect: { | |||||
destination: redirectDestination, | |||||
statusCode: ACTION_STATUS_CODE, | |||||
}, | |||||
props: { | |||||
query: ctx.query, | |||||
body: mockRes.data, | |||||
}, | |||||
}; | |||||
}; |
@@ -0,0 +1,34 @@ | |||||
import { NextApiHandler, PageConfig } from 'next'; | |||||
import { NextApiRequest as DefaultNextApiRequest } from 'next/dist/shared/lib/utils'; | |||||
import { ActionWrapperOptions } from './common'; | |||||
import { METHODS_WITH_BODY } from '../../common/constants'; | |||||
import { deserializeFormObjectBody } from '../../utils/serialization'; | |||||
/** | |||||
* Wraps the API handler's `config` export to support all content types. | |||||
* @param customConfig | |||||
*/ | |||||
export const getApiConfig = (customConfig = {} as PageConfig) => ({ | |||||
api: { | |||||
...(customConfig.api ?? {}), | |||||
bodyParser: false, | |||||
}, | |||||
}); | |||||
/** | |||||
* Wraps the API handler to support script. | |||||
* @param options | |||||
*/ | |||||
export const wrapApiHandler = (options: ActionWrapperOptions): NextApiHandler => async ( | |||||
req, | |||||
res, | |||||
) => { | |||||
const reqMut = req as unknown as Record<string, unknown>; | |||||
if (METHODS_WITH_BODY.includes(req.method?.toUpperCase() as typeof METHODS_WITH_BODY[number])) { | |||||
reqMut.body = await deserializeFormObjectBody({ | |||||
req, | |||||
deserializers: options.deserializers, | |||||
}); | |||||
} | |||||
return options.fn(reqMut as unknown as DefaultNextApiRequest, res); | |||||
}; |
@@ -0,0 +1,31 @@ | |||||
import { | |||||
GetServerSideProps, | |||||
GetServerSidePropsContext, | |||||
NextApiHandler, | |||||
PreviewData, | |||||
} from 'next'; | |||||
import { ParsedUrlQuery } from 'querystring'; | |||||
import { NextApiRequest as DefaultNextApiRequest } from 'next/dist/shared/lib/utils'; | |||||
import { EncTypeDeserializerMap } from '../../utils/serialization'; | |||||
import { IceformNextServerResponse } from '../response'; | |||||
export type OnActionFunction< | |||||
Props extends Record<string, unknown> = Record<string, unknown>, | |||||
Params extends ParsedUrlQuery = ParsedUrlQuery, | |||||
Preview extends PreviewData = PreviewData, | |||||
> = ( | |||||
actionReq: DefaultNextApiRequest, | |||||
actionRes: IceformNextServerResponse, | |||||
context: GetServerSidePropsContext<Params, Preview>, | |||||
) => Promise<Awaited<ReturnType<GetServerSideProps<Props, Params, Preview>>> | undefined>; | |||||
export interface ActionWrapperOptions { | |||||
fn: NextApiHandler, | |||||
onAction?: OnActionFunction, | |||||
deserializers?: EncTypeDeserializerMap, | |||||
/** | |||||
* Maps the Location header from the handler response to an accessible URL. | |||||
* @param url | |||||
*/ | |||||
mapLocationToRedirectDestination?: (referer: string, url: string) => string, | |||||
} |
@@ -0,0 +1,135 @@ | |||||
import { IncomingMessage } from 'http'; | |||||
import { GetServerSideProps } from 'next'; | |||||
import { NextApiRequest as DefaultNextApiRequest } from 'next/dist/shared/lib/utils'; | |||||
import crypto from 'crypto'; | |||||
import { | |||||
DEFAULT_ENCTYPE_DESERIALIZERS, | |||||
deserializeFormObjectBody, | |||||
EncTypeDeserializerMap, | |||||
} from '../../utils/serialization'; | |||||
import { METHOD_FORM_KEY, PREVENT_REDIRECT_FORM_KEY } from '../../common/constants'; | |||||
import { ACTION_STATUS_CODE, DEFAULT_METHOD } from '../constants'; | |||||
import { getBody } from '../../utils/request'; | |||||
import { | |||||
ENCTYPE_APPLICATION_OCTET_STREAM, | |||||
ENCTYPE_MULTIPART_FORM_DATA, | |||||
} from '../../common/enctypes'; | |||||
import { ActionWrapperOptions } from './common'; | |||||
import { IceformNextServerResponse } from '../response'; | |||||
import { | |||||
REQUEST_ID_COOKIE_KEY, | |||||
CookieManager, | |||||
} from '../../utils/cookies'; | |||||
import { cacheResponse } from '../cache'; | |||||
const getFormObjectMethodAndBody = async ( | |||||
req: IncomingMessage, | |||||
deserializers: EncTypeDeserializerMap, | |||||
) => { | |||||
const deserialized = await deserializeFormObjectBody({ | |||||
req, | |||||
deserializers, | |||||
}); | |||||
const { | |||||
[METHOD_FORM_KEY]: method = req.method ?? DEFAULT_METHOD, | |||||
...body | |||||
} = deserialized as { | |||||
[METHOD_FORM_KEY]?: string, | |||||
[key: string]: unknown, | |||||
}; | |||||
return { | |||||
body, | |||||
method, | |||||
}; | |||||
}; | |||||
const getBinaryMethodAndBody = async ( | |||||
req: IncomingMessage, | |||||
) => { | |||||
const body = await getBody(req); | |||||
return { | |||||
body, | |||||
method: req.method ?? DEFAULT_METHOD, | |||||
}; | |||||
}; | |||||
const isContentTypeFormObject = ( | |||||
contentType: string, | |||||
deserializers: EncTypeDeserializerMap, | |||||
) => ( | |||||
contentType?.startsWith(`${ENCTYPE_MULTIPART_FORM_DATA};`) | |||||
|| Object.keys(deserializers).includes(contentType) | |||||
); | |||||
/** | |||||
* Wraps the `getServerSideProps` function to support no-script. | |||||
* @param options | |||||
*/ | |||||
export const getServerSideProps = (options: ActionWrapperOptions): GetServerSideProps => async ( | |||||
ctx, | |||||
) => { | |||||
const { | |||||
referer = '/', | |||||
'content-type': contentType = ENCTYPE_APPLICATION_OCTET_STREAM, | |||||
} = ctx.req.headers; | |||||
const { deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = options; | |||||
const methodAndBodyFn = ( | |||||
isContentTypeFormObject(contentType, deserializers) | |||||
? getFormObjectMethodAndBody | |||||
: getBinaryMethodAndBody | |||||
); | |||||
const { body, method } = await methodAndBodyFn(ctx.req, deserializers); | |||||
const req = { | |||||
...ctx.req, | |||||
body, | |||||
query: { | |||||
...ctx.query, | |||||
...(ctx.params ?? {}), | |||||
}, | |||||
// ?: how to prevent malicious method spoofing? | |||||
method, | |||||
} as DefaultNextApiRequest; | |||||
const res = new IceformNextServerResponse(ctx.req); | |||||
await options.fn(req, res); | |||||
const requestId = crypto.randomUUID(); | |||||
const cookieManager = new CookieManager(ctx); | |||||
cookieManager.setCookie(REQUEST_ID_COOKIE_KEY, requestId); | |||||
await cacheResponse(requestId, res); | |||||
if (typeof options.onAction === 'function') { | |||||
const onActionResult = await options.onAction( | |||||
req, | |||||
res, | |||||
ctx, | |||||
); | |||||
if (onActionResult) { | |||||
return onActionResult; | |||||
} | |||||
} | |||||
const preventRedirect = ( | |||||
typeof req.body === 'object' | |||||
&& req.body !== null | |||||
&& PREVENT_REDIRECT_FORM_KEY in req.body | |||||
); | |||||
const redirectDestination = ( | |||||
res.location | |||||
&& typeof options.mapLocationToRedirectDestination === 'function' | |||||
&& !preventRedirect | |||||
) | |||||
? options.mapLocationToRedirectDestination(referer, res.location) | |||||
: referer; | |||||
return { | |||||
redirect: { | |||||
destination: redirectDestination, | |||||
statusCode: ACTION_STATUS_CODE, | |||||
}, | |||||
props: { | |||||
query: ctx.query, | |||||
body: res.data, | |||||
}, | |||||
}; | |||||
}; |
@@ -0,0 +1,3 @@ | |||||
export * from './api'; | |||||
export * from './common'; | |||||
export * from './gssp'; |
@@ -0,0 +1,48 @@ | |||||
import { createWriteStream } from 'fs'; | |||||
import { readFile, unlink } from 'fs/promises'; | |||||
interface CacheableResponse { | |||||
statusCode: number; | |||||
statusMessage?: string; | |||||
contentType?: string; | |||||
data?: unknown; | |||||
} | |||||
const getFilePathFromRequestId = (requestId: string) => `${requestId}`; | |||||
export const cacheResponse = async (requestId: string, res: CacheableResponse) => { | |||||
const filePath = getFilePathFromRequestId(requestId); | |||||
const cacheStream = createWriteStream(filePath, { encoding: 'utf-8' }); | |||||
cacheStream.write(`${res.statusCode.toString()} ${res.statusMessage || ''}\n`); | |||||
if (res.contentType) { | |||||
cacheStream.write(`Content-Type: ${res.contentType}\n`); | |||||
} | |||||
if (res.data) { | |||||
cacheStream.write('\n'); | |||||
cacheStream.write(res.data as string); | |||||
} | |||||
return new Promise((resolve) => { | |||||
cacheStream.close(resolve); | |||||
}); | |||||
}; | |||||
export const retrieveCache = async (requestId: string) => { | |||||
const filePath = getFilePathFromRequestId(requestId); | |||||
const requestBuffer = await readFile(filePath, 'utf-8'); | |||||
await unlink(filePath); | |||||
const [statusLine, ...headersAndBody] = requestBuffer.split('\n'); | |||||
const [statusCode, ...statusMessageWords] = statusLine.split(' '); | |||||
const statusMessage = statusMessageWords.join(' '); | |||||
const bodyStart = headersAndBody.findIndex((line) => line === ''); | |||||
const headers = headersAndBody.slice(0, bodyStart); | |||||
const body = headersAndBody.slice(bodyStart + 1).join('\n'); | |||||
const contentTypeHeader = headers.find((header) => header.toLowerCase().startsWith('content-type:')); | |||||
const contentType = contentTypeHeader?.split(':')[1].trim(); | |||||
return { | |||||
statusCode: parseInt(statusCode, 10), | |||||
statusMessage, | |||||
contentType, | |||||
body, | |||||
}; | |||||
}; |
@@ -2,20 +2,12 @@ import { ParsedUrlQuery } from 'querystring'; | |||||
import { GetServerSideProps, GetServerSidePropsContext, PreviewData } from 'next'; | import { GetServerSideProps, GetServerSidePropsContext, PreviewData } from 'next'; | ||||
import { deserialize } from 'seroval'; | import { deserialize } from 'seroval'; | ||||
import { NextApiRequest, NextApiResponse } from '../common/types'; | import { NextApiRequest, NextApiResponse } from '../common/types'; | ||||
import { | |||||
DEFAULT_ENCODING, | |||||
DEFAULT_METHOD, | |||||
DEFAULT_RESPONSE_STATUS_CODE, | |||||
} from './constants'; | |||||
import { DEFAULT_ENCODING, DEFAULT_METHOD } from './constants'; | |||||
import { getBody } from '../utils/request'; | import { getBody } from '../utils/request'; | ||||
import { | |||||
BODY_COOKIE_KEY, CONTENT_TYPE_COOKIE_KEY, | |||||
CookieManager, | |||||
STATUS_CODE_COOKIE_KEY, | |||||
STATUS_MESSAGE_COOKIE_KEY, | |||||
} from '../utils/cookies'; | |||||
import { REQUEST_ID_COOKIE_KEY, CookieManager } from '../utils/cookies'; | |||||
import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | ||||
import { METHODS_WITH_BODY } from '../common/constants'; | import { METHODS_WITH_BODY } from '../common/constants'; | ||||
import { retrieveCache } from './cache'; | |||||
export type DestinationGetServerSideProps< | export type DestinationGetServerSideProps< | ||||
Props extends Record<string, unknown> = Record<string, unknown>, | Props extends Record<string, unknown> = Record<string, unknown>, | ||||
@@ -48,24 +40,20 @@ export const getServerSideProps = ( | |||||
req.body = body.toString(DEFAULT_ENCODING); | req.body = body.toString(DEFAULT_ENCODING); | ||||
} | } | ||||
const cookieManager = new CookieManager(ctx); | |||||
// TODO how to properly remove cookies without leftovers? | |||||
if (cookieManager.hasCookie(STATUS_CODE_COOKIE_KEY)) { | |||||
ctx.res.statusCode = Number( | |||||
cookieManager.getCookie(STATUS_CODE_COOKIE_KEY) || DEFAULT_RESPONSE_STATUS_CODE, | |||||
); | |||||
cookieManager.unsetCookie(STATUS_CODE_COOKIE_KEY); | |||||
} | |||||
if (cookieManager.hasCookie(STATUS_MESSAGE_COOKIE_KEY)) { | |||||
ctx.res.statusMessage = cookieManager.getCookie(STATUS_MESSAGE_COOKIE_KEY) || ''; | |||||
cookieManager.unsetCookie(STATUS_MESSAGE_COOKIE_KEY); | |||||
} | |||||
const res: NextApiResponse = {}; | const res: NextApiResponse = {}; | ||||
if (cookieManager.hasCookie(BODY_COOKIE_KEY)) { | |||||
const resBody = cookieManager.getCookie(BODY_COOKIE_KEY); | |||||
const contentType = cookieManager.getCookie(CONTENT_TYPE_COOKIE_KEY); | |||||
const cookieManager = new CookieManager(ctx); | |||||
const resRequestId = cookieManager.getCookie(REQUEST_ID_COOKIE_KEY); | |||||
cookieManager.unsetCookie(REQUEST_ID_COOKIE_KEY); | |||||
if (resRequestId) { | |||||
const { | |||||
statusCode, | |||||
statusMessage, | |||||
contentType, | |||||
body: resBody, | |||||
} = await retrieveCache(resRequestId); | |||||
ctx.res.statusCode = statusCode; | |||||
ctx.res.statusMessage = statusMessage; | |||||
if (contentType === ENCTYPE_APPLICATION_JSON) { | if (contentType === ENCTYPE_APPLICATION_JSON) { | ||||
res.body = deserialize(resBody); | res.body = deserialize(resBody); | ||||
} else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | } else if (contentType === ENCTYPE_APPLICATION_OCTET_STREAM) { | ||||
@@ -75,8 +63,6 @@ export const getServerSideProps = ( | |||||
c.warn('Could not parse response body, returning nothing'); | c.warn('Could not parse response body, returning nothing'); | ||||
res.body = null; | res.body = null; | ||||
} | } | ||||
cookieManager.unsetCookie(BODY_COOKIE_KEY); | |||||
cookieManager.unsetCookie(CONTENT_TYPE_COOKIE_KEY); | |||||
} | } | ||||
const gspResult = ( | const gspResult = ( | ||||
@@ -1,9 +1,13 @@ | |||||
import { ServerResponse } from 'http'; | import { ServerResponse } from 'http'; | ||||
import { NextApiResponse as DefaultNextApiResponse } from 'next/dist/shared/lib/utils'; | import { NextApiResponse as DefaultNextApiResponse } from 'next/dist/shared/lib/utils'; | ||||
import { serialize } from 'seroval'; | import { serialize } from 'seroval'; | ||||
import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | import { ENCTYPE_APPLICATION_JSON, ENCTYPE_APPLICATION_OCTET_STREAM } from '../common/enctypes'; | ||||
import { ACTION_STATUS_CODE, CONTENT_TYPE_HEADER_KEY, LOCATION_HEADER_KEY } from './constants'; | |||||
import { | |||||
ACTION_STATUS_CODE, CONTENT_TYPE_HEADER_KEY, LOCATION_HEADER_KEY, DEFAULT_RESPONSE_STATUS_CODE, | |||||
} from './constants'; | |||||
// for client-side | |||||
class DummyServerResponse {} | class DummyServerResponse {} | ||||
const EffectiveServerResponse = ServerResponse ?? DummyServerResponse; | const EffectiveServerResponse = ServerResponse ?? DummyServerResponse; | ||||
@@ -19,6 +23,11 @@ export class IceformNextServerResponse | |||||
private readonly revalidateResponse = Promise.resolve(undefined); | private readonly revalidateResponse = Promise.resolve(undefined); | ||||
constructor(...args: ConstructorParameters<typeof EffectiveServerResponse>) { | |||||
super(...args); | |||||
this.statusCode = DEFAULT_RESPONSE_STATUS_CODE; | |||||
} | |||||
setHeader(name: string, value: number | string | readonly string[]): this { | setHeader(name: string, value: number | string | readonly string[]): this { | ||||
super.setHeader(name, value); | super.setHeader(name, value); | ||||
@@ -38,9 +47,13 @@ export class IceformNextServerResponse | |||||
return this as unknown as DefaultNextApiResponse; | return this as unknown as DefaultNextApiResponse; | ||||
} | } | ||||
private static serialize(body: unknown) { | |||||
return serialize(body); | |||||
} | |||||
json(body: unknown): void { | json(body: unknown): void { | ||||
this.contentType = ENCTYPE_APPLICATION_JSON; | this.contentType = ENCTYPE_APPLICATION_JSON; | ||||
this.data = serialize(body); | |||||
this.data = IceformNextServerResponse.serialize(body); | |||||
} | } | ||||
revalidate(): Promise<void> { | revalidate(): Promise<void> { | ||||
@@ -59,7 +72,7 @@ export class IceformNextServerResponse | |||||
this.contentType = ENCTYPE_APPLICATION_OCTET_STREAM; | this.contentType = ENCTYPE_APPLICATION_OCTET_STREAM; | ||||
} | } | ||||
this.data = serialize(body); | |||||
this.data = IceformNextServerResponse.serialize(body); | |||||
} | } | ||||
setDraftMode(): DefaultNextApiResponse { | setDraftMode(): DefaultNextApiResponse { | ||||
@@ -1,5 +1,6 @@ | |||||
import { IncomingMessage, ServerResponse } from 'http'; | import { IncomingMessage, ServerResponse } from 'http'; | ||||
import * as nookies from 'nookies'; | import * as nookies from 'nookies'; | ||||
import * as crypto from 'crypto'; | |||||
const COMMON_COOKIE_CONFIG = { | const COMMON_COOKIE_CONFIG = { | ||||
path: '/', | path: '/', | ||||
@@ -13,11 +14,7 @@ const COMMON_SET_COOKIE_CONFIG = { | |||||
const cookieKeys: Record<string, string> = {}; | const cookieKeys: Record<string, string> = {}; | ||||
export const BODY_COOKIE_KEY = 'b' as const; | |||||
export const STATUS_CODE_COOKIE_KEY = 'sc' as const; | |||||
export const STATUS_MESSAGE_COOKIE_KEY = 'sm' as const; | |||||
export const CONTENT_TYPE_COOKIE_KEY = 'ct' as const; | |||||
export const REQUEST_ID_COOKIE_KEY = 'b' as const; | |||||
export class CookieManager { | export class CookieManager { | ||||
private readonly ctx: { req: IncomingMessage, res: ServerResponse<IncomingMessage> }; | private readonly ctx: { req: IncomingMessage, res: ServerResponse<IncomingMessage> }; | ||||
@@ -27,13 +24,17 @@ export class CookieManager { | |||||
} | } | ||||
private static generateCookieKey(key: string) { | private static generateCookieKey(key: string) { | ||||
return `if${key}${Date.now()}`; | |||||
const random = crypto.randomBytes(16).toString('hex'); | |||||
return `if${key}${random}`; | |||||
} | } | ||||
setCookie(key: string, value: string) { | setCookie(key: string, value: string) { | ||||
// cleanup previous cookie | |||||
this.unsetCookie(key); | |||||
cookieKeys[key] = CookieManager.generateCookieKey(key); | |||||
nookies.setCookie( | nookies.setCookie( | ||||
this.ctx, | this.ctx, | ||||
cookieKeys[key] = CookieManager.generateCookieKey(key), | |||||
cookieKeys[key], | |||||
value, | value, | ||||
COMMON_SET_COOKIE_CONFIG, | COMMON_SET_COOKIE_CONFIG, | ||||
); | ); | ||||
@@ -0,0 +1,44 @@ | |||||
interface AbstractFormDataItem<T> { | |||||
value: T | null; | |||||
} | |||||
export interface FileItem extends AbstractFormDataItem<Buffer> { | |||||
kind: 'file'; | |||||
mimeType: string; | |||||
filename: string; | |||||
} | |||||
export interface FieldItem extends AbstractFormDataItem<string> { | |||||
kind: 'field'; | |||||
} | |||||
export type FormDataItem = FileItem | FieldItem; | |||||
export type FormDataMap = Record<string, FormDataItem>; | |||||
export const addFormObjectBodyHelpers = (deserialized: Record<string, unknown>): FormDataMap => ( | |||||
Object.fromEntries( | |||||
Object | |||||
.entries(deserialized) | |||||
.filter(([, value]) => typeof value !== 'undefined') | |||||
.map(([key, value]) => [ | |||||
key, | |||||
{ | |||||
kind: 'field', | |||||
value: value?.toString() ?? null, | |||||
}, | |||||
] satisfies [string, FieldItem]), | |||||
) | |||||
); | |||||
export const removeFormObjectBodyHelpers = (formObject: FormDataMap): Record<string, unknown> => ( | |||||
Object.fromEntries( | |||||
Object | |||||
.entries(formObject) | |||||
.filter(([, value]) => typeof value !== 'undefined') | |||||
.map(([key, valueWithHelper]) => [ | |||||
key, | |||||
valueWithHelper.value, | |||||
]), | |||||
) | |||||
); |
@@ -28,24 +28,26 @@ export const parseMultipartFormData = async ( | |||||
bb.on('file', (name, file, info) => { | bb.on('file', (name, file, info) => { | ||||
const { | const { | ||||
filename, | filename, | ||||
mimeType: mimetype, | |||||
mimeType, | |||||
} = info; | } = info; | ||||
let fileData = Buffer.from(''); | |||||
let buffer = Buffer.from(''); | |||||
file.on('data', (data) => { | file.on('data', (data) => { | ||||
fileData = Buffer.concat([fileData, data]); | |||||
buffer = Buffer.concat([buffer, data]); | |||||
}); | }); | ||||
file.on('close', () => { | file.on('close', () => { | ||||
const newFile = fileData.buffer as unknown as Record<string, unknown>; | |||||
newFile.name = filename; | |||||
newFile.type = mimetype; | |||||
body[name] = newFile; | |||||
const bufferMut = buffer as unknown as Record<string, unknown>; | |||||
bufferMut.name = filename; | |||||
bufferMut.type = mimeType; | |||||
bufferMut.size = buffer.length; | |||||
body[name] = bufferMut; | |||||
}); | }); | ||||
}); | }); | ||||
bb.on('field', (name, value) => { | bb.on('field', (name, value) => { | ||||
// TODO max length for long values, convert to reference instead | |||||
body[name] = value; | body[name] = value; | ||||
}); | }); | ||||
@@ -7,6 +7,7 @@ import { | |||||
} from '../common/enctypes'; | } from '../common/enctypes'; | ||||
import { getBody, parseMultipartFormData } from './request'; | import { getBody, parseMultipartFormData } from './request'; | ||||
import { METHOD_FORM_KEY, PREVENT_REDIRECT_FORM_KEY } from '../common/constants'; | import { METHOD_FORM_KEY, PREVENT_REDIRECT_FORM_KEY } from '../common/constants'; | ||||
import { DEFAULT_ENCODING } from '../server/constants'; | |||||
export type EncTypeSerializer = (data: unknown) => string; | export type EncTypeSerializer = (data: unknown) => string; | ||||
@@ -43,9 +44,16 @@ export const serializeBody = (params: SerializeBodyParams) => { | |||||
if (encType === ENCTYPE_MULTIPART_FORM_DATA) { | if (encType === ENCTYPE_MULTIPART_FORM_DATA) { | ||||
// type error when provided a submitter element for some reason... | // type error when provided a submitter element for some reason... | ||||
const FormDataUnknown = FormData as unknown as { | const FormDataUnknown = FormData as unknown as { | ||||
new(formElement?: HTMLElement, submitter?: HTMLElement): FormData; | |||||
new(formElement?: HTMLElement, submitter?: HTMLElement): FormData & { | |||||
entries: () => IterableIterator<[string, unknown]>; | |||||
}; | |||||
}; | }; | ||||
const formData = new FormDataUnknown(form, options?.submitter); | const formData = new FormDataUnknown(form, options?.submitter); | ||||
const emptyFiles = Array.from(formData.entries()) | |||||
.filter(([, value]) => ( | |||||
value instanceof File && value.size === 0 && value.name === '' | |||||
)); | |||||
emptyFiles.forEach(([key]) => formData.delete(key)); | |||||
formData.delete(METHOD_FORM_KEY); | formData.delete(METHOD_FORM_KEY); | ||||
formData.delete(PREVENT_REDIRECT_FORM_KEY); | formData.delete(PREVENT_REDIRECT_FORM_KEY); | ||||
return formData; | return formData; | ||||
@@ -75,9 +83,12 @@ export interface DeserializeBodyParams { | |||||
export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = { | export const DEFAULT_ENCTYPE_DESERIALIZERS: EncTypeDeserializerMap = { | ||||
[ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record<string, unknown>, | [ENCTYPE_APPLICATION_JSON]: (data: string) => JSON.parse(data) as Record<string, unknown>, | ||||
[ENCTYPE_X_WWW_FORM_URLENCODED]: (data: string) => Object.fromEntries( | |||||
new URLSearchParams(data).entries(), | |||||
), | |||||
}; | }; | ||||
export const deserializeBody = async (params: DeserializeBodyParams): Promise<unknown> => { | |||||
export const deserializeFormObjectBody = async (params: DeserializeBodyParams) => { | |||||
const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | const { req, deserializers = DEFAULT_ENCTYPE_DESERIALIZERS } = params; | ||||
const contentType = req.headers['content-type'] ?? ENCTYPE_APPLICATION_OCTET_STREAM; | const contentType = req.headers['content-type'] ?? ENCTYPE_APPLICATION_OCTET_STREAM; | ||||
@@ -85,18 +96,16 @@ export const deserializeBody = async (params: DeserializeBodyParams): Promise<un | |||||
return parseMultipartFormData(req); | return parseMultipartFormData(req); | ||||
} | } | ||||
const encoding = (req.headers['content-encoding'] ?? 'utf-8') as BufferEncoding; | |||||
const bodyRaw = await getBody(req); | const bodyRaw = await getBody(req); | ||||
const encoding = ( | |||||
req.headers['content-encoding'] ?? DEFAULT_ENCODING | |||||
) as BufferEncoding; | |||||
if (contentType === ENCTYPE_X_WWW_FORM_URLENCODED) { | |||||
return Object.fromEntries( | |||||
new URLSearchParams(bodyRaw.toString(encoding)).entries(), | |||||
); | |||||
} | |||||
const { [contentType]: theDeserializer } = deserializers; | |||||
if (typeof deserializers[contentType] === 'function') { | |||||
return deserializers[contentType](bodyRaw.toString(encoding)); | |||||
if (typeof theDeserializer !== 'function') { | |||||
throw new Error(`Could not deserialize body with content type: ${contentType}`); | |||||
} | } | ||||
return bodyRaw.toString('binary'); | |||||
return theDeserializer(bodyRaw.toString(encoding)) as Record<string, unknown>; | |||||
}; | }; |
@@ -11,6 +11,9 @@ importers: | |||||
'@theoryofnekomata/formxtra': | '@theoryofnekomata/formxtra': | ||||
specifier: ^1.0.3 | specifier: ^1.0.3 | ||||
version: 1.0.3 | version: 1.0.3 | ||||
'@web-std/file': | |||||
specifier: ^3.0.3 | |||||
version: 3.0.3 | |||||
busboy: | busboy: | ||||
specifier: ^1.6.0 | specifier: ^1.6.0 | ||||
version: 1.6.0 | version: 1.6.0 | ||||
@@ -18,8 +21,8 @@ importers: | |||||
specifier: ^2.5.2 | specifier: ^2.5.2 | ||||
version: 2.5.2 | version: 2.5.2 | ||||
seroval: | seroval: | ||||
specifier: ^0.9.0 | |||||
version: 0.9.0 | |||||
specifier: ^0.10.2 | |||||
version: 0.10.2 | |||||
devDependencies: | devDependencies: | ||||
'@testing-library/jest-dom': | '@testing-library/jest-dom': | ||||
specifier: ^5.16.5 | specifier: ^5.16.5 | ||||
@@ -1306,6 +1309,7 @@ packages: | |||||
typescript: 4.9.5 | typescript: 4.9.5 | ||||
transitivePeerDependencies: | transitivePeerDependencies: | ||||
- supports-color | - supports-color | ||||
dev: true | |||||
/@typescript-eslint/parser@6.7.0(eslint@8.49.0)(typescript@5.2.2): | /@typescript-eslint/parser@6.7.0(eslint@8.49.0)(typescript@5.2.2): | ||||
resolution: {integrity: sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==} | resolution: {integrity: sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng==} | ||||
@@ -1334,6 +1338,7 @@ packages: | |||||
dependencies: | dependencies: | ||||
'@typescript-eslint/types': 5.62.0 | '@typescript-eslint/types': 5.62.0 | ||||
'@typescript-eslint/visitor-keys': 5.62.0 | '@typescript-eslint/visitor-keys': 5.62.0 | ||||
dev: true | |||||
/@typescript-eslint/scope-manager@6.7.0: | /@typescript-eslint/scope-manager@6.7.0: | ||||
resolution: {integrity: sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==} | resolution: {integrity: sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA==} | ||||
@@ -1366,6 +1371,7 @@ packages: | |||||
/@typescript-eslint/types@5.62.0: | /@typescript-eslint/types@5.62.0: | ||||
resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} | resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} | ||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} | ||||
dev: true | |||||
/@typescript-eslint/types@6.7.0: | /@typescript-eslint/types@6.7.0: | ||||
resolution: {integrity: sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==} | resolution: {integrity: sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q==} | ||||
@@ -1391,6 +1397,7 @@ packages: | |||||
typescript: 4.9.5 | typescript: 4.9.5 | ||||
transitivePeerDependencies: | transitivePeerDependencies: | ||||
- supports-color | - supports-color | ||||
dev: true | |||||
/@typescript-eslint/typescript-estree@6.7.0(typescript@5.2.2): | /@typescript-eslint/typescript-estree@6.7.0(typescript@5.2.2): | ||||
resolution: {integrity: sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==} | resolution: {integrity: sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ==} | ||||
@@ -1439,6 +1446,7 @@ packages: | |||||
dependencies: | dependencies: | ||||
'@typescript-eslint/types': 5.62.0 | '@typescript-eslint/types': 5.62.0 | ||||
eslint-visitor-keys: 3.4.3 | eslint-visitor-keys: 3.4.3 | ||||
dev: true | |||||
/@typescript-eslint/visitor-keys@6.7.0: | /@typescript-eslint/visitor-keys@6.7.0: | ||||
resolution: {integrity: sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==} | resolution: {integrity: sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ==} | ||||
@@ -1507,6 +1515,31 @@ packages: | |||||
pretty-format: 29.7.0 | pretty-format: 29.7.0 | ||||
dev: true | dev: true | ||||
/@web-std/blob@3.0.5: | |||||
resolution: {integrity: sha512-Lm03qr0eT3PoLBuhkvFBLf0EFkAsNz/G/AYCzpOdi483aFaVX86b4iQs0OHhzHJfN5C15q17UtDbyABjlzM96A==} | |||||
dependencies: | |||||
'@web-std/stream': 1.0.0 | |||||
web-encoding: 1.1.5 | |||||
dev: false | |||||
/@web-std/file@3.0.3: | |||||
resolution: {integrity: sha512-X7YYyvEERBbaDfJeC9lBKC5Q5lIEWYCP1SNftJNwNH/VbFhdHm+3neKOQP+kWEYJmosbDFq+NEUG7+XIvet/Jw==} | |||||
dependencies: | |||||
'@web-std/blob': 3.0.5 | |||||
dev: false | |||||
/@web-std/stream@1.0.0: | |||||
resolution: {integrity: sha512-jyIbdVl+0ZJyKGTV0Ohb9E6UnxP+t7ZzX4Do3AHjZKxUXKMs9EmqnBDQgHF7bEw0EzbQygOjtt/7gvtmi//iCQ==} | |||||
dependencies: | |||||
web-streams-polyfill: 3.2.1 | |||||
dev: false | |||||
/@zxing/text-encoding@0.9.0: | |||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} | |||||
requiresBuild: true | |||||
dev: false | |||||
optional: true | |||||
/abab@2.0.6: | /abab@2.0.6: | ||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} | resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} | ||||
dev: true | dev: true | ||||
@@ -2677,6 +2710,7 @@ packages: | |||||
- eslint-import-resolver-node | - eslint-import-resolver-node | ||||
- eslint-import-resolver-webpack | - eslint-import-resolver-webpack | ||||
- supports-color | - supports-color | ||||
dev: true | |||||
/eslint-import-resolver-typescript@3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.49.0): | /eslint-import-resolver-typescript@3.6.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.49.0): | ||||
resolution: {integrity: sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg==} | resolution: {integrity: sha512-QTHR9ddNnn35RTxlaEnx2gCxqFlF2SEN0SE2d17SqwyM7YOSI2GHWRYp5BiRkObTUNYPupC/3Fq2a0PpT+EKpg==} | ||||
@@ -2729,6 +2763,7 @@ packages: | |||||
eslint-import-resolver-typescript: 3.6.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.49.0) | eslint-import-resolver-typescript: 3.6.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.28.1)(eslint@8.49.0) | ||||
transitivePeerDependencies: | transitivePeerDependencies: | ||||
- supports-color | - supports-color | ||||
dev: true | |||||
/eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): | /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): | ||||
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} | resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} | ||||
@@ -2809,6 +2844,7 @@ packages: | |||||
- eslint-import-resolver-typescript | - eslint-import-resolver-typescript | ||||
- eslint-import-resolver-webpack | - eslint-import-resolver-webpack | ||||
- supports-color | - supports-color | ||||
dev: true | |||||
/eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): | /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0): | ||||
resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} | resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} | ||||
@@ -2829,7 +2865,7 @@ packages: | |||||
doctrine: 2.1.0 | doctrine: 2.1.0 | ||||
eslint: 8.49.0 | eslint: 8.49.0 | ||||
eslint-import-resolver-node: 0.3.9 | eslint-import-resolver-node: 0.3.9 | ||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) | |||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.0)(eslint@8.49.0) | |||||
has: 1.0.3 | has: 1.0.3 | ||||
is-core-module: 2.13.0 | is-core-module: 2.13.0 | ||||
is-glob: 4.0.3 | is-glob: 4.0.3 | ||||
@@ -3542,7 +3578,6 @@ packages: | |||||
dependencies: | dependencies: | ||||
call-bind: 1.0.2 | call-bind: 1.0.2 | ||||
has-tostringtag: 1.0.0 | has-tostringtag: 1.0.0 | ||||
dev: true | |||||
/is-array-buffer@3.0.2: | /is-array-buffer@3.0.2: | ||||
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} | resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} | ||||
@@ -4892,8 +4927,8 @@ packages: | |||||
- supports-color | - supports-color | ||||
dev: true | dev: true | ||||
/seroval@0.9.0: | |||||
resolution: {integrity: sha512-Ttr96/8czi3SXjbFFzpRc2Xpp1wvBufmaNuTviUL8eGQhUr1mdeiQ6YYSaLnMwMc4YWSeBggq72bKEBVu6/IFA==} | |||||
/seroval@0.10.2: | |||||
resolution: {integrity: sha512-aa9Tmthjs1wdSdtwr+USjeKHtML55ZJw2wPR6qjzsW9o+Og3eDNL+vJhjkIBH/pQT4bm4TUJ8+DcLlZq8RwzCg==} | |||||
engines: {node: '>=10'} | engines: {node: '>=10'} | ||||
dev: false | dev: false | ||||
@@ -5278,6 +5313,7 @@ packages: | |||||
/tslib@1.14.1: | /tslib@1.14.1: | ||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} | ||||
dev: true | |||||
/tslib@2.5.0: | /tslib@2.5.0: | ||||
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} | resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} | ||||
@@ -5290,6 +5326,7 @@ packages: | |||||
dependencies: | dependencies: | ||||
tslib: 1.14.1 | tslib: 1.14.1 | ||||
typescript: 4.9.5 | typescript: 4.9.5 | ||||
dev: true | |||||
/type-check@0.4.0: | /type-check@0.4.0: | ||||
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} | ||||
@@ -5358,6 +5395,7 @@ packages: | |||||
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} | resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} | ||||
engines: {node: '>=4.2.0'} | engines: {node: '>=4.2.0'} | ||||
hasBin: true | hasBin: true | ||||
dev: true | |||||
/typescript@5.2.2: | /typescript@5.2.2: | ||||
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} | resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} | ||||
@@ -5419,6 +5457,16 @@ packages: | |||||
/util-deprecate@1.0.2: | /util-deprecate@1.0.2: | ||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} | ||||
/util@0.12.5: | |||||
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} | |||||
dependencies: | |||||
inherits: 2.0.4 | |||||
is-arguments: 1.1.1 | |||||
is-generator-function: 1.0.10 | |||||
is-typed-array: 1.1.12 | |||||
which-typed-array: 1.1.11 | |||||
dev: false | |||||
/utils-merge@1.0.1: | /utils-merge@1.0.1: | ||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} | ||||
engines: {node: '>= 0.4.0'} | engines: {node: '>= 0.4.0'} | ||||
@@ -5600,6 +5648,19 @@ packages: | |||||
defaults: 1.0.4 | defaults: 1.0.4 | ||||
dev: true | dev: true | ||||
/web-encoding@1.1.5: | |||||
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} | |||||
dependencies: | |||||
util: 0.12.5 | |||||
optionalDependencies: | |||||
'@zxing/text-encoding': 0.9.0 | |||||
dev: false | |||||
/web-streams-polyfill@3.2.1: | |||||
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} | |||||
engines: {node: '>= 8'} | |||||
dev: false | |||||
/webidl-conversions@7.0.0: | /webidl-conversions@7.0.0: | ||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} | ||||
engines: {node: '>=12'} | engines: {node: '>=12'} | ||||