Separate JSON API backend (vm_api.py) + React+TanStack frontend scaffold
- vm_api.py: pure FastAPI JSON API on :8098 with CORS - vm-api.service: systemd unit for API (replaces inline HTML vm_web.py for frontend) - vm_web.py: kept intact as fallback; Apache now proxies /api/ -> :8098 - frontend/: Vite + React + TanStack Query scaffold - Apache vhost updated: DocumentRoot -> frontend/dist, /api/ proxy, SPA fallback - All original HTML pages still functional via :8099 during transition
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
MIT License
|
||||
|
||||
Cookie-es copyright (c) Pooya Parsa <[email protected]>
|
||||
|
||||
Cookie parsing based on https://github.com/jshttp/cookie
|
||||
Copyright (c) 2012-2014 Roman Shtylman <[email protected]>
|
||||
Copyright (c) 2015 Douglas Christopher Wilson <[email protected]>
|
||||
|
||||
Set-Cookie parsing based on https://github.com/nfriedly/set-cookie-parser
|
||||
Copyright (c) 2015 Nathan Friedly <[email protected]> (http://nfriedly.com/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# 🍪 cookie-es
|
||||
|
||||
<!-- automd:badges bundlejs packagephobia codecov -->
|
||||
|
||||
[](https://npmjs.com/package/cookie-es)
|
||||
[](https://npm.chart.dev/cookie-es)
|
||||
[](https://bundlejs.com/?q=cookie-es)
|
||||
[](https://packagephobia.com/result?p=cookie-es)
|
||||
[](https://codecov.io/gh/unjs/cookie-es)
|
||||
|
||||
<!-- /automd -->
|
||||
|
||||
ESM-ready [`Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie) and [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) parser and serializer based on [cookie](https://github.com/jshttp/cookie) and [set-cookie-parser](https://github.com/nfriedly/set-cookie-parser) with built-in TypeScript types. Compliant with [RFC 6265bis](https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html).
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
# ✨ Auto-detect (npm, yarn, pnpm, bun, deno)
|
||||
npx nypm install cookie-es
|
||||
```
|
||||
|
||||
## Import
|
||||
|
||||
```js
|
||||
import {
|
||||
parseCookie,
|
||||
parseSetCookie,
|
||||
serializeCookie,
|
||||
stringifyCookie,
|
||||
splitSetCookieString,
|
||||
} from "cookie-es";
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `parseCookie(str, options?)`
|
||||
|
||||
Parse a `Cookie` header string into an object. First occurrence wins for duplicate names.
|
||||
|
||||
```js
|
||||
parseCookie("foo=bar; equation=E%3Dmc%5E2");
|
||||
// { foo: "bar", equation: "E=mc^2" }
|
||||
|
||||
// Custom decoder
|
||||
parseCookie("foo=bar", { decode: (v) => v });
|
||||
|
||||
// Only parse specific keys
|
||||
parseCookie("a=1; b=2; c=3", { filter: (key) => key !== "b" });
|
||||
// { a: "1", c: "3" }
|
||||
```
|
||||
|
||||
### `parseSetCookie(str, options?)`
|
||||
|
||||
Parse a `Set-Cookie` header string into an object with all cookie attributes.
|
||||
|
||||
```js
|
||||
parseSetCookie(
|
||||
"id=abc; Domain=example.com; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600; Partitioned; Priority=High",
|
||||
);
|
||||
// {
|
||||
// name: "id",
|
||||
// value: "abc",
|
||||
// domain: "example.com",
|
||||
// path: "/",
|
||||
// httpOnly: true,
|
||||
// secure: true,
|
||||
// sameSite: "lax",
|
||||
// maxAge: 3600,
|
||||
// partitioned: true,
|
||||
// priority: "high",
|
||||
// }
|
||||
```
|
||||
|
||||
Supports `decode` option (custom function or `false` to skip decoding). Returns `undefined` for cookies with forbidden names (prototype pollution protection) or when both name and value are empty ([RFC 6265bis](https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html) sec 5.7).
|
||||
|
||||
### `serializeCookie(name, value, options?)`
|
||||
|
||||
Serialize a cookie name-value pair into a `Set-Cookie` header string.
|
||||
|
||||
```js
|
||||
serializeCookie("foo", "bar", { httpOnly: true, secure: true, maxAge: 3600 });
|
||||
// "foo=bar; Max-Age=3600; HttpOnly; Secure"
|
||||
|
||||
// Also accepts a cookie object
|
||||
serializeCookie({
|
||||
name: "foo",
|
||||
value: "bar",
|
||||
domain: "example.com",
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
});
|
||||
// "foo=bar; Domain=example.com; Path=/; SameSite=Lax"
|
||||
```
|
||||
|
||||
Non-string values are coerced to strings (`null` and `undefined` become empty string).
|
||||
|
||||
Supported attributes: `maxAge`, `expires`, `domain`, `path`, `httpOnly`, `secure`, `sameSite`, `priority`, `partitioned`. Use `encode` option for custom value encoding (default: `encodeURIComponent`).
|
||||
|
||||
> [!NOTE]
|
||||
> `parse` and `serialize` are available as shorter aliases for `parseCookie` and `serializeCookie`.
|
||||
|
||||
### `stringifyCookie(cookies, options?)`
|
||||
|
||||
Stringify a cookies object into an HTTP `Cookie` header string.
|
||||
|
||||
```js
|
||||
stringifyCookie({ foo: "bar", baz: "qux" });
|
||||
// "foo=bar; baz=qux"
|
||||
```
|
||||
|
||||
### `splitSetCookieString(input)`
|
||||
|
||||
Split comma-joined `Set-Cookie` headers into individual strings. Correctly handles commas within cookie attributes like `Expires` dates.
|
||||
|
||||
```js
|
||||
splitSetCookieString(
|
||||
"foo=bar; Expires=Thu, 01 Jan 2026 00:00:00 GMT, baz=qux",
|
||||
);
|
||||
// ["foo=bar; Expires=Thu, 01 Jan 2026 00:00:00 GMT", "baz=qux"]
|
||||
|
||||
// Also accepts an array
|
||||
splitSetCookieString(["a=1, b=2", "c=3"]);
|
||||
// ["a=1", "b=2", "c=3"]
|
||||
```
|
||||
|
||||
## Parsing Options
|
||||
|
||||
### `allowMultiple`
|
||||
|
||||
By default, when a cookie name appears more than once, only the first value is kept. Set `allowMultiple: true` to collect all values into an array:
|
||||
|
||||
```js
|
||||
import { parseCookie } from "cookie-es";
|
||||
|
||||
// Default: first value wins
|
||||
parseCookie("foo=a;bar=b;foo=c");
|
||||
// => { foo: "a", bar: "b" }
|
||||
|
||||
// With allowMultiple: duplicates return arrays
|
||||
parseCookie("foo=a;bar=b;foo=c", { allowMultiple: true });
|
||||
// => { foo: ["a", "c"], bar: "b" }
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
Based on [jshttp/cookie](https://github.com/jshttp/cookie) (Roman Shtylman and Douglas Christopher Wilson) and [nfriedly/set-cookie-parser](https://github.com/nfriedly/set-cookie-parser) (Nathan Friedly).
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Parse options.
|
||||
*/
|
||||
interface CookieParseOptions {
|
||||
/**
|
||||
* Specifies a function that will be used to decode a [cookie-value](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1).
|
||||
* Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to decode
|
||||
* a previously-encoded cookie value into a JavaScript string.
|
||||
*
|
||||
* The default function is the global `decodeURIComponent`, wrapped in a `try..catch`. If an error
|
||||
* is thrown it will return the cookie's original value. If you provide your own encode/decode
|
||||
* scheme you must ensure errors are appropriately handled.
|
||||
*
|
||||
* @default decode
|
||||
*/
|
||||
decode?: (str: string) => string | undefined;
|
||||
/**
|
||||
* Custom function to filter parsing specific keys.
|
||||
*/
|
||||
filter?(key: string): boolean;
|
||||
/**
|
||||
* When enabled, duplicate cookie names will return an array of values
|
||||
* instead of only the first value.
|
||||
*/
|
||||
allowMultiple?: boolean;
|
||||
}
|
||||
/**
|
||||
* Cookies object.
|
||||
*/
|
||||
type Cookies = Record<string, string | undefined>;
|
||||
/**
|
||||
* Cookies object when `allowMultiple` is enabled.
|
||||
*/
|
||||
type MultiCookies = Record<string, string | string[] | undefined>;
|
||||
/**
|
||||
* Stringify options.
|
||||
*/
|
||||
interface CookieStringifyOptions {
|
||||
/**
|
||||
* Specifies a function that will be used to encode a [cookie-value](https://datatracker.ietf.org/doc/html/rfc6265#section-4.1.1).
|
||||
* Since value of a cookie has a limited character set (and must be a simple string), this function can be used to encode
|
||||
* a value into a string suited for a cookie's value, and should mirror `decode` when parsing.
|
||||
*
|
||||
* @default encodeURIComponent
|
||||
*/
|
||||
encode?: (str: string) => string;
|
||||
/**
|
||||
* Specifies a function that will be used to coerce non-string values to a string.
|
||||
*
|
||||
* @default JSON.stringify
|
||||
*/
|
||||
stringify?: (value: unknown) => string;
|
||||
}
|
||||
/**
|
||||
* Set-Cookie object.
|
||||
*/
|
||||
interface SetCookie {
|
||||
/**
|
||||
* Specifies the name of the cookie.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Specifies the string to be the value for the cookie.
|
||||
*/
|
||||
value: string | undefined;
|
||||
/**
|
||||
* Specifies the `number` (in seconds) to be the value for the [`Max-Age` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2).
|
||||
*
|
||||
* The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
|
||||
* `maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
|
||||
* so if both are set, they should point to the same date and time.
|
||||
*/
|
||||
maxAge?: number;
|
||||
/**
|
||||
* Specifies the `Date` object to be the value for the [`Expires` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1).
|
||||
* When no expiration is set, clients consider this a "non-persistent cookie" and delete it when the current session is over.
|
||||
*
|
||||
* The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and
|
||||
* `maxAge` are set, then `maxAge` takes precedence, but it is possible not all clients by obey this,
|
||||
* so if both are set, they should point to the same date and time.
|
||||
*/
|
||||
expires?: Date;
|
||||
/**
|
||||
* Specifies the value for the [`Domain` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3).
|
||||
* When no domain is set, clients consider the cookie to apply to the current domain only.
|
||||
*/
|
||||
domain?: string;
|
||||
/**
|
||||
* Specifies the value for the [`Path` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4).
|
||||
* When no path is set, the path is considered the ["default path"](https://tools.ietf.org/html/rfc6265#section-5.1.4).
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
* Enables the [`HttpOnly` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6).
|
||||
* When enabled, clients will not allow client-side JavaScript to see the cookie in `document.cookie`.
|
||||
*/
|
||||
httpOnly?: boolean;
|
||||
/**
|
||||
* Enables the [`Secure` `Set-Cookie` attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5).
|
||||
* When enabled, clients will only send the cookie back if the browser has an HTTPS connection.
|
||||
*/
|
||||
secure?: boolean;
|
||||
/**
|
||||
* Enables the [`Partitioned` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-cutler-httpbis-partitioned-cookies/).
|
||||
* When enabled, clients will only send the cookie back when the current domain _and_ top-level domain matches.
|
||||
*
|
||||
* This is an attribute that has not yet been fully standardized, and may change in the future.
|
||||
* This also means clients may ignore this attribute until they understand it. More information
|
||||
* about can be found in [the proposal](https://github.com/privacycg/CHIPS).
|
||||
*/
|
||||
partitioned?: boolean;
|
||||
/**
|
||||
* Specifies the value for the [`Priority` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1).
|
||||
*
|
||||
* - `'low'` will set the `Priority` attribute to `Low`.
|
||||
* - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
|
||||
* - `'high'` will set the `Priority` attribute to `High`.
|
||||
*
|
||||
* More information about priority levels can be found in [the specification](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1).
|
||||
*/
|
||||
priority?: "low" | "medium" | "high";
|
||||
/**
|
||||
* Specifies the value for the [`SameSite` `Set-Cookie` attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7).
|
||||
*
|
||||
* - `true` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
|
||||
* - `'lax'` will set the `SameSite` attribute to `Lax` for lax same site enforcement.
|
||||
* - `'none'` will set the `SameSite` attribute to `None` for an explicit cross-site cookie.
|
||||
* - `'strict'` will set the `SameSite` attribute to `Strict` for strict same site enforcement.
|
||||
*
|
||||
* More information about enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7).
|
||||
*/
|
||||
sameSite?: boolean | "lax" | "strict" | "none";
|
||||
}
|
||||
/**
|
||||
* Backward compatibility serialize options.
|
||||
*/
|
||||
type CookieSerializeOptions = CookieStringifyOptions & Omit<SetCookie, "name" | "value">;
|
||||
/**
|
||||
* Parse a `Cookie` header string into an object.
|
||||
*
|
||||
* The object has cookie names as keys and decoded values as values.
|
||||
* First occurrence wins for duplicate names unless `allowMultiple` is set.
|
||||
*
|
||||
* @param str - The `Cookie` header string to parse.
|
||||
* @param options - Parsing options (`decode`, `filter`, `allowMultiple`).
|
||||
* @returns A prototype-less object of cookie name-value pairs.
|
||||
*/
|
||||
declare function parse(str: string, options: CookieParseOptions & {
|
||||
allowMultiple: true;
|
||||
}): MultiCookies;
|
||||
declare function parse(str: string, options?: CookieParseOptions): Cookies;
|
||||
/**
|
||||
* Stringify a cookies object into an HTTP `Cookie` header string.
|
||||
*
|
||||
* @param cookie - An object of cookie name-value pairs.
|
||||
* @param options - Stringify options (`encode`).
|
||||
* @returns A `Cookie` header string (e.g. `"foo=bar; baz=qux"`).
|
||||
*/
|
||||
declare function stringifyCookie(cookie: Cookies, options?: CookieStringifyOptions): string;
|
||||
/**
|
||||
* Serialize a cookie into a `Set-Cookie` header string.
|
||||
*
|
||||
* Accepts either a name-value pair with options or a `SetCookie` object.
|
||||
* Non-string values are coerced to strings. Validates name, value, domain,
|
||||
* and path against RFC 6265bis.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* serialize("foo", "bar", { httpOnly: true });
|
||||
* // => "foo=bar; HttpOnly"
|
||||
*
|
||||
* serialize({ name: "foo", value: "bar", secure: true });
|
||||
* // => "foo=bar; Secure"
|
||||
* ```
|
||||
*/
|
||||
declare function serialize(cookie: SetCookie, options?: CookieStringifyOptions): string;
|
||||
declare function serialize(name: string, val: unknown, options?: CookieSerializeOptions): string;
|
||||
interface SetCookieParseOptions {
|
||||
/**
|
||||
* Custom decode function to use on cookie values.
|
||||
*
|
||||
* By default, `decodeURIComponent` is used.
|
||||
*
|
||||
* **Note:** If decoding fails, the original (undecoded) value will be used
|
||||
*/
|
||||
decode?: false | ((value: string) => string);
|
||||
}
|
||||
interface SetCookie$1 {
|
||||
/**
|
||||
* Cookie name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Cookie value
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* Cookie path
|
||||
*/
|
||||
path?: string | undefined;
|
||||
/**
|
||||
* Absolute expiration date for the cookie
|
||||
*/
|
||||
expires?: Date | undefined;
|
||||
/**
|
||||
* Relative max age of the cookie in seconds from when the client receives it (integer or undefined)
|
||||
*
|
||||
* Note: when using with express's res.cookie() method, multiply maxAge by 1000 to convert to milliseconds
|
||||
*/
|
||||
maxAge?: number | undefined;
|
||||
/**
|
||||
* Domain for the cookie,
|
||||
* May begin with "." to indicate the named domain or any subdomain of it
|
||||
*/
|
||||
domain?: string | undefined;
|
||||
/**
|
||||
* Indicates that this cookie should only be sent over HTTPs
|
||||
*/
|
||||
secure?: boolean | undefined;
|
||||
/**
|
||||
* Indicates that this cookie should not be accessible to client-side JavaScript
|
||||
*/
|
||||
httpOnly?: boolean | undefined;
|
||||
/**
|
||||
* Indicates a cookie ought not to be sent along with cross-site requests
|
||||
*/
|
||||
sameSite?: true | false | "lax" | "strict" | "none" | undefined;
|
||||
/**
|
||||
* Indicates that the cookie should be stored using partitioned storage
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/Privacy/Privacy_sandbox/Partitioned_cookies
|
||||
*/
|
||||
partitioned?: boolean | undefined;
|
||||
/**
|
||||
* Indicates the priority of the cookie
|
||||
*
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#prioritylow_medium_high
|
||||
*/
|
||||
priority?: "low" | "medium" | "high" | undefined;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
/**
|
||||
* Parse a [`Set-Cookie`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie) header string into an object.
|
||||
*
|
||||
* Returns `undefined` for cookies with forbidden names (prototype pollution protection)
|
||||
* or when both name and value are empty (RFC 6265bis sec 5.7).
|
||||
*
|
||||
* @param str - The `Set-Cookie` header string to parse.
|
||||
* @param options - Parsing options (`decode`).
|
||||
* @returns A `SetCookie` object with all parsed attributes, or `undefined`.
|
||||
*/
|
||||
declare function parseSetCookie(str: string, options?: SetCookieParseOptions): SetCookie$1 | undefined;
|
||||
/**
|
||||
* Split comma-joined `Set-Cookie` header strings into individual cookie strings.
|
||||
*
|
||||
* Correctly handles commas within cookie attributes like `Expires` dates
|
||||
* by checking for `=` after a comma to determine if it's a cookie separator.
|
||||
*
|
||||
* @param cookiesString - A comma-joined `Set-Cookie` string or array of strings.
|
||||
* @returns An array of individual `Set-Cookie` strings.
|
||||
*
|
||||
* @see https://tools.ietf.org/html/rfc2616#section-4.2
|
||||
*/
|
||||
declare function splitSetCookieString(cookiesString: string | string[]): string[];
|
||||
export { type CookieParseOptions, type CookieSerializeOptions, type CookieStringifyOptions, type Cookies, type MultiCookies, type SetCookie, type SetCookieParseOptions, parse, parse as parseCookie, parseSetCookie, serialize, serialize as serializeCookie, splitSetCookieString, stringifyCookie };
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
const COOKIE_MAX_AGE_LIMIT = 3456e4;
|
||||
function endIndex(str, min, len) {
|
||||
const index = str.indexOf(";", min);
|
||||
return index === -1 ? len : index;
|
||||
}
|
||||
function eqIndex(str, min, max) {
|
||||
const index = str.indexOf("=", min);
|
||||
return index < max ? index : -1;
|
||||
}
|
||||
function valueSlice(str, min, max) {
|
||||
if (min === max) return "";
|
||||
let start = min;
|
||||
let end = max;
|
||||
do {
|
||||
const code = str.charCodeAt(start);
|
||||
if (code !== 32 && code !== 9) break;
|
||||
} while (++start < end);
|
||||
while (end > start) {
|
||||
const code = str.charCodeAt(end - 1);
|
||||
if (code !== 32 && code !== 9) break;
|
||||
end--;
|
||||
}
|
||||
return str.slice(start, end);
|
||||
}
|
||||
const NullObject = /* @__PURE__ */ (() => {
|
||||
const C = function() {};
|
||||
C.prototype = Object.create(null);
|
||||
return C;
|
||||
})();
|
||||
function parse(str, options) {
|
||||
const obj = new NullObject();
|
||||
const len = str.length;
|
||||
if (len < 2) return obj;
|
||||
const dec = options?.decode || decode;
|
||||
const allowMultiple = options?.allowMultiple || false;
|
||||
let index = 0;
|
||||
do {
|
||||
const eqIdx = eqIndex(str, index, len);
|
||||
if (eqIdx === -1) break;
|
||||
const endIdx = endIndex(str, index, len);
|
||||
if (eqIdx > endIdx) {
|
||||
index = str.lastIndexOf(";", eqIdx - 1) + 1;
|
||||
continue;
|
||||
}
|
||||
const key = valueSlice(str, index, eqIdx);
|
||||
if (options?.filter && !options.filter(key)) {
|
||||
index = endIdx + 1;
|
||||
continue;
|
||||
}
|
||||
const val = dec(valueSlice(str, eqIdx + 1, endIdx));
|
||||
if (allowMultiple) {
|
||||
const existing = obj[key];
|
||||
if (existing === void 0) obj[key] = val;
|
||||
else if (Array.isArray(existing)) existing.push(val);
|
||||
else obj[key] = [existing, val];
|
||||
} else if (obj[key] === void 0) obj[key] = val;
|
||||
index = endIdx + 1;
|
||||
} while (index < len);
|
||||
return obj;
|
||||
}
|
||||
function decode(str) {
|
||||
if (!str.includes("%")) return str;
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
const cookieNameRegExp = /^[\u0021-\u003A\u003C\u003E-\u007E]+$/;
|
||||
const cookieValueRegExp = /^[\u0021-\u003A\u003C-\u007E]*$/;
|
||||
const domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
||||
const pathValueRegExp = /^[\u0020-\u003A\u003C-\u007E]*$/;
|
||||
const __toString = Object.prototype.toString;
|
||||
function stringifyCookie(cookie, options) {
|
||||
const enc = options?.encode || encodeURIComponent;
|
||||
const keys = Object.keys(cookie);
|
||||
let str = "";
|
||||
for (const [i, name] of keys.entries()) {
|
||||
const val = cookie[name];
|
||||
if (val === void 0) continue;
|
||||
if (!cookieNameRegExp.test(name)) throw new TypeError(`cookie name is invalid: ${name}`);
|
||||
const value = enc(val);
|
||||
if (!cookieValueRegExp.test(value)) throw new TypeError(`cookie val is invalid: ${val}`);
|
||||
if (i > 0) str += "; ";
|
||||
str += name + "=" + value;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function serialize(_a0, _a1, _a2) {
|
||||
const isObj = typeof _a0 === "object" && _a0 !== null;
|
||||
const options = isObj ? _a1 : _a2;
|
||||
const stringify = options?.stringify || JSON.stringify;
|
||||
const cookie = isObj ? _a0 : {
|
||||
..._a2,
|
||||
name: _a0,
|
||||
value: _a1 == void 0 ? "" : typeof _a1 === "string" ? _a1 : stringify(_a1)
|
||||
};
|
||||
const enc = options?.encode || encodeURIComponent;
|
||||
if (!cookieNameRegExp.test(cookie.name)) throw new TypeError(`argument name is invalid: ${cookie.name}`);
|
||||
const value = cookie.value ? enc(cookie.value) : "";
|
||||
if (!cookieValueRegExp.test(value)) throw new TypeError(`argument val is invalid: ${cookie.value}`);
|
||||
if (!cookie.secure) {
|
||||
if (cookie.partitioned) throw new TypeError(`Partitioned cookies must have the Secure attribute`);
|
||||
if (cookie.sameSite && String(cookie.sameSite).toLowerCase() === "none") throw new TypeError(`SameSite=None cookies must have the Secure attribute`);
|
||||
if (cookie.name.length > 9 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95) {
|
||||
const nameLower = cookie.name.toLowerCase();
|
||||
if (nameLower.startsWith("__secure-") || nameLower.startsWith("__host-")) throw new TypeError(`${cookie.name} cookies must have the Secure attribute`);
|
||||
}
|
||||
}
|
||||
if (cookie.name.length > 7 && cookie.name.charCodeAt(0) === 95 && cookie.name.charCodeAt(1) === 95 && cookie.name.toLowerCase().startsWith("__host-")) {
|
||||
if (cookie.path !== "/") throw new TypeError(`__Host- cookies must have Path=/`);
|
||||
if (cookie.domain) throw new TypeError(`__Host- cookies must not have a Domain attribute`);
|
||||
}
|
||||
let str = cookie.name + "=" + value;
|
||||
if (cookie.maxAge !== void 0) {
|
||||
if (!Number.isInteger(cookie.maxAge)) throw new TypeError(`option maxAge is invalid: ${cookie.maxAge}`);
|
||||
str += "; Max-Age=" + Math.max(0, Math.min(cookie.maxAge, COOKIE_MAX_AGE_LIMIT));
|
||||
}
|
||||
if (cookie.domain) {
|
||||
if (!domainValueRegExp.test(cookie.domain)) throw new TypeError(`option domain is invalid: ${cookie.domain}`);
|
||||
str += "; Domain=" + cookie.domain;
|
||||
}
|
||||
if (cookie.path) {
|
||||
if (!pathValueRegExp.test(cookie.path)) throw new TypeError(`option path is invalid: ${cookie.path}`);
|
||||
str += "; Path=" + cookie.path;
|
||||
}
|
||||
if (cookie.expires) {
|
||||
if (!isDate(cookie.expires) || !Number.isFinite(cookie.expires.valueOf())) throw new TypeError(`option expires is invalid: ${cookie.expires}`);
|
||||
str += "; Expires=" + cookie.expires.toUTCString();
|
||||
}
|
||||
if (cookie.httpOnly) str += "; HttpOnly";
|
||||
if (cookie.secure) str += "; Secure";
|
||||
if (cookie.partitioned) str += "; Partitioned";
|
||||
if (cookie.priority) switch (typeof cookie.priority === "string" ? cookie.priority.toLowerCase() : void 0) {
|
||||
case "low":
|
||||
str += "; Priority=Low";
|
||||
break;
|
||||
case "medium":
|
||||
str += "; Priority=Medium";
|
||||
break;
|
||||
case "high":
|
||||
str += "; Priority=High";
|
||||
break;
|
||||
default: throw new TypeError(`option priority is invalid: ${cookie.priority}`);
|
||||
}
|
||||
if (cookie.sameSite) switch (typeof cookie.sameSite === "string" ? cookie.sameSite.toLowerCase() : cookie.sameSite) {
|
||||
case true:
|
||||
case "strict":
|
||||
str += "; SameSite=Strict";
|
||||
break;
|
||||
case "lax":
|
||||
str += "; SameSite=Lax";
|
||||
break;
|
||||
case "none":
|
||||
str += "; SameSite=None";
|
||||
break;
|
||||
default: throw new TypeError(`option sameSite is invalid: ${cookie.sameSite}`);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function isDate(val) {
|
||||
return __toString.call(val) === "[object Date]";
|
||||
}
|
||||
const maxAgeRegExp = /^-?\d+$/;
|
||||
const _nullProto = /* @__PURE__ */ Object.getPrototypeOf({});
|
||||
function parseSetCookie(str, options) {
|
||||
const len = str.length;
|
||||
let _endIdx = len;
|
||||
let eqIdx = -1;
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = str.charCodeAt(i);
|
||||
if (c === 59) {
|
||||
_endIdx = i;
|
||||
break;
|
||||
}
|
||||
if (c === 61 && eqIdx === -1) eqIdx = i;
|
||||
}
|
||||
if (eqIdx >= _endIdx) eqIdx = -1;
|
||||
const name = eqIdx === -1 ? "" : _trim(str, 0, eqIdx);
|
||||
if (name && name in _nullProto) return void 0;
|
||||
let value = eqIdx === -1 ? _trim(str, 0, _endIdx) : _trim(str, eqIdx + 1, _endIdx);
|
||||
if (!name && !value) return void 0;
|
||||
if (name.length + value.length > 4096) return void 0;
|
||||
if (options?.decode !== false) value = _decode(value, options?.decode);
|
||||
const setCookie = {
|
||||
name,
|
||||
value
|
||||
};
|
||||
let index = _endIdx + 1;
|
||||
while (index < len) {
|
||||
let endIdx = len;
|
||||
let attrEqIdx = -1;
|
||||
for (let i = index; i < len; i++) {
|
||||
const c = str.charCodeAt(i);
|
||||
if (c === 59) {
|
||||
endIdx = i;
|
||||
break;
|
||||
}
|
||||
if (c === 61 && attrEqIdx === -1) attrEqIdx = i;
|
||||
}
|
||||
if (attrEqIdx >= endIdx) attrEqIdx = -1;
|
||||
const attr = attrEqIdx === -1 ? _trim(str, index, endIdx) : _trim(str, index, attrEqIdx);
|
||||
const val = attrEqIdx === -1 ? void 0 : _trim(str, attrEqIdx + 1, endIdx);
|
||||
if (val === void 0 || val.length <= 1024) switch (attr.toLowerCase()) {
|
||||
case "httponly":
|
||||
setCookie.httpOnly = true;
|
||||
break;
|
||||
case "secure":
|
||||
setCookie.secure = true;
|
||||
break;
|
||||
case "partitioned":
|
||||
setCookie.partitioned = true;
|
||||
break;
|
||||
case "domain":
|
||||
if (val) setCookie.domain = (val.charCodeAt(0) === 46 ? val.slice(1) : val).toLowerCase();
|
||||
break;
|
||||
case "path":
|
||||
setCookie.path = val;
|
||||
break;
|
||||
case "max-age":
|
||||
if (val && maxAgeRegExp.test(val)) setCookie.maxAge = Math.min(Number(val), COOKIE_MAX_AGE_LIMIT);
|
||||
break;
|
||||
case "expires": {
|
||||
if (!val) break;
|
||||
const date = new Date(val);
|
||||
if (Number.isFinite(date.valueOf())) {
|
||||
const maxDate = new Date(Date.now() + COOKIE_MAX_AGE_LIMIT * 1e3);
|
||||
setCookie.expires = date > maxDate ? maxDate : date;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "priority": {
|
||||
if (!val) break;
|
||||
const priority = val.toLowerCase();
|
||||
if (priority === "low" || priority === "medium" || priority === "high") setCookie.priority = priority;
|
||||
break;
|
||||
}
|
||||
case "samesite": {
|
||||
if (!val) break;
|
||||
const sameSite = val.toLowerCase();
|
||||
if (sameSite === "lax" || sameSite === "strict" || sameSite === "none") setCookie.sameSite = sameSite;
|
||||
else setCookie.sameSite = "lax";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const attrLower = attr.toLowerCase();
|
||||
if (attrLower && !(attrLower in _nullProto)) setCookie[attrLower] = val;
|
||||
}
|
||||
}
|
||||
index = endIdx + 1;
|
||||
}
|
||||
return setCookie;
|
||||
}
|
||||
function _trim(str, start, end) {
|
||||
if (start === end) return "";
|
||||
let s = start;
|
||||
let e = end;
|
||||
while (s < e && (str.charCodeAt(s) === 32 || str.charCodeAt(s) === 9)) s++;
|
||||
while (e > s && (str.charCodeAt(e - 1) === 32 || str.charCodeAt(e - 1) === 9)) e--;
|
||||
return str.slice(s, e);
|
||||
}
|
||||
function _decode(value, decode) {
|
||||
if (!decode && !value.includes("%")) return value;
|
||||
try {
|
||||
return (decode || decodeURIComponent)(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
function splitSetCookieString(cookiesString) {
|
||||
if (Array.isArray(cookiesString)) return cookiesString.flatMap((c) => splitSetCookieString(c));
|
||||
if (typeof cookiesString !== "string") return [];
|
||||
const cookiesStrings = [];
|
||||
let pos = 0;
|
||||
let start;
|
||||
let ch;
|
||||
let lastComma;
|
||||
let nextStart;
|
||||
let cookiesSeparatorFound;
|
||||
const skipWhitespace = () => {
|
||||
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) pos += 1;
|
||||
return pos < cookiesString.length;
|
||||
};
|
||||
const notSpecialChar = () => {
|
||||
ch = cookiesString.charAt(pos);
|
||||
return ch !== "=" && ch !== ";" && ch !== ",";
|
||||
};
|
||||
while (pos < cookiesString.length) {
|
||||
start = pos;
|
||||
cookiesSeparatorFound = false;
|
||||
while (skipWhitespace()) {
|
||||
ch = cookiesString.charAt(pos);
|
||||
if (ch === ",") {
|
||||
lastComma = pos;
|
||||
pos += 1;
|
||||
skipWhitespace();
|
||||
nextStart = pos;
|
||||
while (pos < cookiesString.length && notSpecialChar()) pos += 1;
|
||||
if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
|
||||
cookiesSeparatorFound = true;
|
||||
pos = nextStart;
|
||||
cookiesStrings.push(cookiesString.slice(start, lastComma));
|
||||
start = pos;
|
||||
} else pos = lastComma + 1;
|
||||
} else pos += 1;
|
||||
}
|
||||
if (!cookiesSeparatorFound || pos >= cookiesString.length) cookiesStrings.push(cookiesString.slice(start));
|
||||
}
|
||||
return cookiesStrings;
|
||||
}
|
||||
export { parse, parse as parseCookie, parseSetCookie, serialize, serialize as serializeCookie, splitSetCookieString, stringifyCookie };
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "cookie-es",
|
||||
"version": "3.1.1",
|
||||
"license": "MIT",
|
||||
"repository": "unjs/cookie-es",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": "./dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "obuild",
|
||||
"dev": "vitest --coverage",
|
||||
"lint": "oxlint . && oxfmt --check src test",
|
||||
"fmt": "automd && oxlint . --fix && oxfmt src test",
|
||||
"release": "pnpm test && pnpm build && changelogen --release --push && npm publish",
|
||||
"test": "pnpm lint && vitest run --coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.0",
|
||||
"@vitest/coverage-v8": "^4.1.1",
|
||||
"automd": "^0.4.3",
|
||||
"changelogen": "^0.6.2",
|
||||
"cookie": "^1.1.1",
|
||||
"eslint-config-unjs": "^0.6.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"mitata": "^1.0.34",
|
||||
"obuild": "^0.4.32",
|
||||
"oxfmt": "^0.41.0",
|
||||
"oxlint": "^1.56.0",
|
||||
"rolldown": "1.0.0-rc.11",
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.1"
|
||||
},
|
||||
"packageManager": "[email protected]"
|
||||
}
|
||||
Reference in New Issue
Block a user