@meekojs/meeko 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/dist/autoimports-ts.d.ts +26 -0
- package/dist/autoimports-ts.js +127 -0
- package/dist/autoimports-ts.js.map +1 -0
- package/dist/autoimports.d.ts +21 -0
- package/dist/autoimports.js +285 -0
- package/dist/autoimports.js.map +1 -0
- package/dist/check.js +35 -23
- package/dist/check.js.map +1 -0
- package/dist/cli.js +59 -19
- package/dist/cli.js.map +1 -0
- package/dist/compiler/import-graph.d.ts +8 -0
- package/dist/compiler/import-graph.js +33 -0
- package/dist/compiler/import-graph.js.map +1 -0
- package/dist/compiler/mapping.js +1 -0
- package/dist/compiler/mapping.js.map +1 -0
- package/dist/compiler/project-root.d.ts +2 -0
- package/dist/compiler/project-root.js +18 -0
- package/dist/compiler/project-root.js.map +1 -0
- package/dist/compiler/sfc.js +1 -0
- package/dist/compiler/sfc.js.map +1 -0
- package/dist/compiler/sourcemap.d.ts +3 -0
- package/dist/compiler/sourcemap.js +75 -0
- package/dist/compiler/sourcemap.js.map +1 -0
- package/dist/compiler/template.js +1 -0
- package/dist/compiler/template.js.map +1 -0
- package/dist/compiler.js +22 -11
- package/dist/compiler.js.map +1 -0
- package/dist/devtools-ui/index.js +56 -6
- package/dist/devtools-ui/index.js.map +1 -0
- package/dist/devtools.d.ts +9 -0
- package/dist/devtools.js +16 -6
- package/dist/devtools.js.map +1 -0
- package/dist/dom.d.ts +17 -6
- package/dist/dom.js +86 -19
- package/dist/dom.js.map +1 -0
- package/dist/framework/check-app.d.ts +5 -0
- package/dist/framework/check-app.js +59 -0
- package/dist/framework/check-app.js.map +1 -0
- package/dist/framework/client.d.ts +45 -0
- package/dist/framework/client.js +146 -0
- package/dist/framework/client.js.map +1 -0
- package/dist/framework/clients.d.ts +82 -0
- package/dist/framework/clients.js +182 -0
- package/dist/framework/clients.js.map +1 -0
- package/dist/framework/config-type.d.ts +10 -0
- package/dist/framework/config-type.js +4 -0
- package/dist/framework/config-type.js.map +1 -0
- package/dist/framework/config.d.ts +15 -0
- package/dist/framework/config.js +138 -0
- package/dist/framework/config.js.map +1 -0
- package/dist/framework/contracts-generate.d.ts +8 -0
- package/dist/framework/contracts-generate.js +227 -0
- package/dist/framework/contracts-generate.js.map +1 -0
- package/dist/framework/generate.d.ts +7 -0
- package/dist/framework/generate.js +67 -0
- package/dist/framework/generate.js.map +1 -0
- package/dist/framework/init.d.ts +2 -0
- package/dist/framework/init.js +362 -0
- package/dist/framework/init.js.map +1 -0
- package/dist/framework/modules.d.ts +22 -0
- package/dist/framework/modules.js +29 -0
- package/dist/framework/modules.js.map +1 -0
- package/dist/framework/page.d.ts +60 -0
- package/dist/framework/page.js +42 -0
- package/dist/framework/page.js.map +1 -0
- package/dist/framework/prerender.d.ts +14 -0
- package/dist/framework/prerender.js +104 -0
- package/dist/framework/prerender.js.map +1 -0
- package/dist/framework/router.d.ts +39 -0
- package/dist/framework/router.js +173 -0
- package/dist/framework/router.js.map +1 -0
- package/dist/framework/server.d.ts +51 -0
- package/dist/framework/server.js +192 -0
- package/dist/framework/server.js.map +1 -0
- package/dist/framework/socket-contracts-generate.d.ts +5 -0
- package/dist/framework/socket-contracts-generate.js +159 -0
- package/dist/framework/socket-contracts-generate.js.map +1 -0
- package/dist/framework/socket.d.ts +70 -0
- package/dist/framework/socket.js +150 -0
- package/dist/framework/socket.js.map +1 -0
- package/dist/framework/state.d.ts +15 -0
- package/dist/framework/state.js +72 -0
- package/dist/framework/state.js.map +1 -0
- package/dist/framework-server/auto-clients.d.ts +3 -0
- package/dist/framework-server/auto-clients.js +27 -0
- package/dist/framework-server/auto-clients.js.map +1 -0
- package/dist/framework-server/context.d.ts +21 -0
- package/dist/framework-server/context.js +22 -0
- package/dist/framework-server/context.js.map +1 -0
- package/dist/framework-server/contracts.d.ts +64 -0
- package/dist/framework-server/contracts.js +129 -0
- package/dist/framework-server/contracts.js.map +1 -0
- package/dist/framework-server/discovery.d.ts +40 -0
- package/dist/framework-server/discovery.js +252 -0
- package/dist/framework-server/discovery.js.map +1 -0
- package/dist/framework-server/local.d.ts +19 -0
- package/dist/framework-server/local.js +98 -0
- package/dist/framework-server/local.js.map +1 -0
- package/dist/framework-server/node-upgrade.d.ts +23 -0
- package/dist/framework-server/node-upgrade.js +78 -0
- package/dist/framework-server/node-upgrade.js.map +1 -0
- package/dist/framework-server/node-websocket.d.ts +16 -0
- package/dist/framework-server/node-websocket.js +125 -0
- package/dist/framework-server/node-websocket.js.map +1 -0
- package/dist/framework-server/openapi.d.ts +17 -0
- package/dist/framework-server/openapi.js +118 -0
- package/dist/framework-server/openapi.js.map +1 -0
- package/dist/framework-server/session-stores.d.ts +66 -0
- package/dist/framework-server/session-stores.js +96 -0
- package/dist/framework-server/session-stores.js.map +1 -0
- package/dist/framework-server/sessions.d.ts +64 -0
- package/dist/framework-server/sessions.js +136 -0
- package/dist/framework-server/sessions.js.map +1 -0
- package/dist/framework-server/socket.d.ts +83 -0
- package/dist/framework-server/socket.js +161 -0
- package/dist/framework-server/socket.js.map +1 -0
- package/dist/framework-server.d.ts +21 -0
- package/dist/framework-server.js +12 -0
- package/dist/framework-server.js.map +1 -0
- package/dist/framework.d.ts +29 -0
- package/dist/framework.js +13 -0
- package/dist/framework.js.map +1 -0
- package/dist/hmr.js +1 -0
- package/dist/hmr.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.js +209 -0
- package/dist/init.js.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.js +52 -0
- package/dist/node.js.map +1 -0
- package/dist/reactivity.d.ts +25 -1
- package/dist/reactivity.js +69 -10
- package/dist/reactivity.js.map +1 -0
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -0
- package/dist/types.js +23 -8
- package/dist/types.js.map +1 -0
- package/dist/vite.js +91 -6
- package/dist/vite.js.map +1 -0
- package/package.json +19 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/framework-server/openapi.ts"],"names":[],"mappings":"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","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport type { ApiRouteDefinition, RpcDefinition, Schema } from \"./contracts.js\";\nimport type { ServerManifest } from \"./discovery.js\";\nimport { serializeJson } from \"../server.js\";\n\nexport interface OpenApiOptions {\n manifest: ServerManifest;\n info: { title: string; version: string };\n /** Only schemas explicitly exported from shared/schemas belong here. */\n publicSchemas: Readonly<Record<string, Schema>>;\n /** For Valibot, supply a converter from @valibot/to-json-schema. */\n toJsonSchema(schema: Schema): unknown;\n load?: (file: string) => Promise<unknown>;\n cookieName?: string;\n}\n\ntype JsonSchema = Record<string, unknown>;\ntype PathItem = Record<string, unknown>;\n\nfunction asSchema(value: unknown, name: string): JsonSchema {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`OpenAPI schema ${name} is not representable as JSON Schema`);\n serializeJson(value);\n return value as JsonSchema;\n}\n\nfunction properties(value: JsonSchema, name: string): { properties: Record<string, JsonSchema>; required: Set<string> } {\n if (value.type !== \"object\" || !value.properties || typeof value.properties !== \"object\" || Array.isArray(value.properties)) throw new Error(`OpenAPI ${name} must convert to an object schema with properties`);\n const entries = Object.entries(value.properties as Record<string, unknown>);\n const result: Record<string, JsonSchema> = {};\n for (const [key, schema] of entries) result[key] = asSchema(schema, `${name}.${key}`);\n const required = new Set(Array.isArray(value.required) ? value.required.filter((key): key is string => typeof key === \"string\") : []);\n return { properties: result, required };\n}\n\nfunction publicPath(pathname: string): string {\n return pathname.replace(/\\[\\.\\.\\.[^\\]]+\\]/g, () => { throw new Error(`OpenAPI cannot represent catch-all route ${pathname}`); })\n .replace(/\\[([A-Za-z_$][\\w$]*)\\]/g, \"{$1}\");\n}\n\n/** Generates OpenAPI only from a caller-provided public schema registry. */\nexport async function generateOpenApi(options: OpenApiOptions): Promise<Record<string, unknown>> {\n const load = options.load ?? (async (file: string) => import(pathToFileURL(file).href) as Promise<unknown>);\n const registry = new Map<Schema, string>();\n for (const [name, schema] of Object.entries(options.publicSchemas)) {\n if (!/^[A-Za-z_$][\\w$]*$/.test(name)) throw new Error(`Invalid public schema name: ${name}`);\n if (registry.has(schema)) throw new Error(`Duplicate public schema instance: ${name}`);\n registry.set(schema, name);\n }\n const schemas: Record<string, JsonSchema> = {};\n const ref = (schema: Schema | undefined, label: string): { $ref: string } => {\n if (!schema) throw new Error(`OpenAPI ${label} requires a schema`);\n const name = registry.get(schema);\n if (!name) throw new Error(`OpenAPI ${label} uses a schema outside publicSchemas`);\n if (!schemas[name]) {\n let converted: unknown;\n try { converted = options.toJsonSchema(schema); }\n catch (error) { throw new Error(`OpenAPI schema ${name} cannot be converted: ${error instanceof Error ? error.message : String(error)}`); }\n schemas[name] = asSchema(converted, name);\n }\n return { $ref: `#/components/schemas/${name}` };\n };\n const paths: Record<string, PathItem> = {};\n const usePath = (path: string): PathItem => paths[path] ??= {};\n const requireDefinition = async (file: string): Promise<Record<string, unknown>> => {\n const module = await load(file);\n if (!module || typeof module !== \"object\" || !(\"default\" in module) || !module.default || typeof module.default !== \"object\") throw new Error(`OpenAPI handler has no default definition: ${file}`);\n return module.default as Record<string, unknown>;\n };\n for (const endpoint of options.manifest.api) {\n const definition = await requireDefinition(endpoint.file) as unknown as ApiRouteDefinition<Schema | undefined, Schema | undefined, Schema | undefined, Schema | undefined>;\n if (definition.contentType === \"response\") throw new Error(`OpenAPI non-JSON response requires an explicit representation: ${endpoint.file}`);\n const operation: Record<string, unknown> = { operationId: `${endpoint.clientKey.replaceAll(\".\", \"_\")}_${endpoint.method.toLowerCase()}`, responses: { \"200\": { description: \"OK\", content: { \"application/json\": { schema: ref(definition.response, `${endpoint.file} response`) } } } } };\n const parameters: Array<Record<string, unknown>> = [];\n const routeParams = [...endpoint.pathname.matchAll(/\\[([A-Za-z_$][\\w$]*)\\]/g)].map((match) => match[1]!);\n if (routeParams.length) {\n const { $ref } = ref(definition.params, `${endpoint.file} params`);\n const converted = schemas[$ref.slice(\"#/components/schemas/\".length)]!;\n const object = properties(converted, `${endpoint.file} params`);\n for (const name of routeParams) {\n const schema = object.properties[name];\n if (!schema) throw new Error(`OpenAPI path parameter ${name} is missing from ${endpoint.file} params schema`);\n parameters.push({ in: \"path\", name, required: true, schema });\n }\n }\n if (definition.query) {\n const { $ref } = ref(definition.query, `${endpoint.file} query`);\n const converted = schemas[$ref.slice(\"#/components/schemas/\".length)]!;\n const object = properties(converted, `${endpoint.file} query`);\n for (const [name, schema] of Object.entries(object.properties)) parameters.push({ in: \"query\", name, required: object.required.has(name), schema });\n }\n if (parameters.length) operation.parameters = parameters;\n if (definition.body) operation.requestBody = { required: true, content: { \"application/json\": { schema: ref(definition.body, `${endpoint.file} body`) } } };\n if (definition.auth === \"required\") operation.security = [{ sessionCookie: [] }];\n const path = publicPath(endpoint.pathname);\n const item = usePath(path);\n const method = endpoint.method.toLowerCase();\n if (item[method]) throw new Error(`OpenAPI duplicate operation: ${endpoint.method} ${path}`);\n item[method] = operation;\n }\n for (const endpoint of options.manifest.rpc) {\n const definition = await requireDefinition(endpoint.file) as unknown as RpcDefinition<Schema, Schema>;\n const item = usePath(endpoint.pathname);\n if (item.post) throw new Error(`OpenAPI duplicate operation: POST ${endpoint.pathname}`);\n item.post = {\n operationId: `rpc_${endpoint.id.replaceAll(\".\", \"_\")}`,\n requestBody: { required: true, content: { \"application/json\": { schema: ref(definition.input, `${endpoint.file} input`) } } },\n responses: { \"200\": { description: \"OK\", content: { \"application/json\": { schema: ref(definition.output, `${endpoint.file} output`) } } } },\n ...(definition.auth === \"required\" ? { security: [{ sessionCookie: [] }] } : {}),\n };\n }\n return {\n openapi: \"3.1.0\",\n info: options.info,\n paths,\n components: { schemas, securitySchemes: { sessionCookie: { type: \"apiKey\", in: \"cookie\", name: options.cookieName ?? \"__Host-meeko.sid\" } } },\n };\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SessionRecord, SessionStore } from "./sessions.js";
|
|
2
|
+
/** Small interface compatible with Redis clients that support SET PX. */
|
|
3
|
+
export interface RedisSessionClient {
|
|
4
|
+
get(key: string): Promise<string | null>;
|
|
5
|
+
set(key: string, value: string, options: {
|
|
6
|
+
PX: number;
|
|
7
|
+
}): Promise<unknown>;
|
|
8
|
+
del(key: string): Promise<unknown>;
|
|
9
|
+
eval(script: string, options: {
|
|
10
|
+
keys: string[];
|
|
11
|
+
arguments: string[];
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
quit?(): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface RedisSessionStoreOptions {
|
|
16
|
+
/** Use a unique prefix for each application and environment. */
|
|
17
|
+
prefix: string;
|
|
18
|
+
/** Close the Redis client when this store owns its connection. */
|
|
19
|
+
closeClient?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Persistent store with Redis-managed expiration and explicit connection ownership. */
|
|
22
|
+
export declare class RedisSessionStore<T = unknown> implements SessionStore<T> {
|
|
23
|
+
private readonly client;
|
|
24
|
+
private readonly options;
|
|
25
|
+
readonly persistent = true;
|
|
26
|
+
constructor(client: RedisSessionClient, options: RedisSessionStoreOptions);
|
|
27
|
+
private key;
|
|
28
|
+
get(id: string): Promise<SessionRecord<T> | undefined>;
|
|
29
|
+
set(record: SessionRecord<T>): Promise<void>;
|
|
30
|
+
delete(id: string): Promise<void>;
|
|
31
|
+
replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/** A driver can implement these operations using any SQL client and dialect. */
|
|
35
|
+
export interface SqlSessionRow {
|
|
36
|
+
id: string;
|
|
37
|
+
csrfToken: string;
|
|
38
|
+
dataJson: string;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
expiresAt: number;
|
|
41
|
+
}
|
|
42
|
+
export interface SqlSessionDriver {
|
|
43
|
+
read(id: string): Promise<SqlSessionRow | undefined>;
|
|
44
|
+
/** Insert or update one row, keyed by its opaque id. */
|
|
45
|
+
write(row: SqlSessionRow): Promise<void>;
|
|
46
|
+
remove(id: string): Promise<void>;
|
|
47
|
+
/** Atomically replace an unexpired row in one transaction; return false if absent or expired. */
|
|
48
|
+
replace(previousId: string, row: SqlSessionRow, now: number): Promise<boolean>;
|
|
49
|
+
close?(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export interface SqlSessionStoreOptions {
|
|
52
|
+
/** Close the SQL driver when this store owns its connection. */
|
|
53
|
+
closeDriver?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Generic SQL store. The application supplies SQL statements through its driver. */
|
|
56
|
+
export declare class SqlSessionStore<T = unknown> implements SessionStore<T> {
|
|
57
|
+
private readonly driver;
|
|
58
|
+
private readonly options;
|
|
59
|
+
readonly persistent = true;
|
|
60
|
+
constructor(driver: SqlSessionDriver, options?: SqlSessionStoreOptions);
|
|
61
|
+
get(id: string): Promise<SessionRecord<T> | undefined>;
|
|
62
|
+
set(record: SessionRecord<T>): Promise<void>;
|
|
63
|
+
delete(id: string): Promise<void>;
|
|
64
|
+
replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
|
|
65
|
+
close(): Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { serializeJson } from "../server.js";
|
|
2
|
+
function decodeRecord(value, id) {
|
|
3
|
+
let record;
|
|
4
|
+
try {
|
|
5
|
+
record = JSON.parse(value);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error("Corrupt session record");
|
|
9
|
+
}
|
|
10
|
+
if (!record || typeof record !== "object")
|
|
11
|
+
throw new Error("Corrupt session record");
|
|
12
|
+
const candidate = record;
|
|
13
|
+
if (candidate.id !== id || typeof candidate.csrfToken !== "string" || !Object.hasOwn(candidate, "data") ||
|
|
14
|
+
typeof candidate.createdAt !== "number" || typeof candidate.expiresAt !== "number" ||
|
|
15
|
+
!Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt)) {
|
|
16
|
+
throw new Error("Corrupt session record");
|
|
17
|
+
}
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
/** Persistent store with Redis-managed expiration and explicit connection ownership. */
|
|
21
|
+
export class RedisSessionStore {
|
|
22
|
+
client;
|
|
23
|
+
options;
|
|
24
|
+
persistent = true;
|
|
25
|
+
constructor(client, options) {
|
|
26
|
+
this.client = client;
|
|
27
|
+
this.options = options;
|
|
28
|
+
if (!options.prefix || !options.prefix.endsWith(":") || /\s/.test(options.prefix))
|
|
29
|
+
throw new TypeError("Session Redis prefix must be nonempty, contain no whitespace, and end with ':'");
|
|
30
|
+
}
|
|
31
|
+
key(id) { return `${this.options.prefix}${id}`; }
|
|
32
|
+
async get(id) {
|
|
33
|
+
const value = await this.client.get(this.key(id));
|
|
34
|
+
if (value === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const record = decodeRecord(value, id);
|
|
37
|
+
if (record.expiresAt > Date.now())
|
|
38
|
+
return record;
|
|
39
|
+
await this.delete(id);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
async set(record) {
|
|
43
|
+
const ttlMs = record.expiresAt - Date.now();
|
|
44
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
45
|
+
throw new RangeError("Session must have a future expiration");
|
|
46
|
+
await this.client.set(this.key(record.id), serializeJson(record), { PX: ttlMs });
|
|
47
|
+
}
|
|
48
|
+
async delete(id) { await this.client.del(this.key(id)); }
|
|
49
|
+
async replace(previousId, record) {
|
|
50
|
+
const ttlMs = record.expiresAt - Date.now();
|
|
51
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0)
|
|
52
|
+
throw new RangeError("Session must have a future expiration");
|
|
53
|
+
const result = await this.client.eval("if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[2]) redis.call('DEL', KEYS[1]) return 1", { keys: [this.key(previousId), this.key(record.id)], arguments: [serializeJson(record), String(ttlMs)] });
|
|
54
|
+
if (result !== 0 && result !== 1)
|
|
55
|
+
throw new Error("Unexpected Redis rotation result");
|
|
56
|
+
return result === 1;
|
|
57
|
+
}
|
|
58
|
+
async close() { if (this.options.closeClient)
|
|
59
|
+
await this.client.quit?.(); }
|
|
60
|
+
}
|
|
61
|
+
/** Generic SQL store. The application supplies SQL statements through its driver. */
|
|
62
|
+
export class SqlSessionStore {
|
|
63
|
+
driver;
|
|
64
|
+
options;
|
|
65
|
+
persistent = true;
|
|
66
|
+
constructor(driver, options = {}) {
|
|
67
|
+
this.driver = driver;
|
|
68
|
+
this.options = options;
|
|
69
|
+
}
|
|
70
|
+
async get(id) {
|
|
71
|
+
const row = await this.driver.read(id);
|
|
72
|
+
if (!row)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (row.id !== id || typeof row.dataJson !== "string")
|
|
75
|
+
throw new Error("Corrupt session record");
|
|
76
|
+
const record = decodeRecord(serializeJson({ id: row.id, csrfToken: row.csrfToken, data: JSON.parse(row.dataJson), createdAt: row.createdAt, expiresAt: row.expiresAt }), id);
|
|
77
|
+
if (record.expiresAt > Date.now())
|
|
78
|
+
return record;
|
|
79
|
+
await this.delete(id);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
async set(record) {
|
|
83
|
+
if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now())
|
|
84
|
+
throw new RangeError("Session must have a future expiration");
|
|
85
|
+
await this.driver.write({ id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt });
|
|
86
|
+
}
|
|
87
|
+
async delete(id) { await this.driver.remove(id); }
|
|
88
|
+
async replace(previousId, record) {
|
|
89
|
+
if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now())
|
|
90
|
+
throw new RangeError("Session must have a future expiration");
|
|
91
|
+
return this.driver.replace(previousId, { id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt }, Date.now());
|
|
92
|
+
}
|
|
93
|
+
async close() { if (this.options.closeDriver)
|
|
94
|
+
await this.driver.close?.(); }
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=session-stores.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-stores.js","sourceRoot":"","sources":["../../src/framework-server/session-stores.ts"],"names":[],"mappings":"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","sourcesContent":["import { serializeJson } from \"../server.js\";\nimport type { SessionRecord, SessionStore } from \"./sessions.js\";\n\n/** Small interface compatible with Redis clients that support SET PX. */\nexport interface RedisSessionClient {\n get(key: string): Promise<string | null>;\n set(key: string, value: string, options: { PX: number }): Promise<unknown>;\n del(key: string): Promise<unknown>;\n eval(script: string, options: { keys: string[]; arguments: string[] }): Promise<unknown>;\n quit?(): Promise<unknown>;\n}\n\nexport interface RedisSessionStoreOptions {\n /** Use a unique prefix for each application and environment. */\n prefix: string;\n /** Close the Redis client when this store owns its connection. */\n closeClient?: boolean;\n}\n\nfunction decodeRecord<T>(value: string, id: string): SessionRecord<T> {\n let record: unknown;\n try { record = JSON.parse(value) as unknown; }\n catch { throw new Error(\"Corrupt session record\"); }\n if (!record || typeof record !== \"object\") throw new Error(\"Corrupt session record\");\n const candidate = record as Partial<SessionRecord<T>>;\n if (candidate.id !== id || typeof candidate.csrfToken !== \"string\" || !Object.hasOwn(candidate, \"data\") ||\n typeof candidate.createdAt !== \"number\" || typeof candidate.expiresAt !== \"number\" ||\n !Number.isFinite(candidate.createdAt) || !Number.isFinite(candidate.expiresAt)) {\n throw new Error(\"Corrupt session record\");\n }\n return candidate as SessionRecord<T>;\n}\n\n/** Persistent store with Redis-managed expiration and explicit connection ownership. */\nexport class RedisSessionStore<T = unknown> implements SessionStore<T> {\n readonly persistent = true;\n constructor(private readonly client: RedisSessionClient, private readonly options: RedisSessionStoreOptions) {\n if (!options.prefix || !options.prefix.endsWith(\":\") || /\\s/.test(options.prefix)) throw new TypeError(\"Session Redis prefix must be nonempty, contain no whitespace, and end with ':'\");\n }\n private key(id: string): string { return `${this.options.prefix}${id}`; }\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const value = await this.client.get(this.key(id));\n if (value === null) return undefined;\n const record = decodeRecord<T>(value, id);\n if (record.expiresAt > Date.now()) return record;\n await this.delete(id);\n return undefined;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n const ttlMs = record.expiresAt - Date.now();\n if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"Session must have a future expiration\");\n await this.client.set(this.key(record.id), serializeJson(record), { PX: ttlMs });\n }\n async delete(id: string): Promise<void> { await this.client.del(this.key(id)); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n const ttlMs = record.expiresAt - Date.now();\n if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0) throw new RangeError(\"Session must have a future expiration\");\n const result = await this.client.eval(\n \"if redis.call('EXISTS', KEYS[1]) == 0 then return 0 end redis.call('SET', KEYS[2], ARGV[1], 'PX', ARGV[2]) redis.call('DEL', KEYS[1]) return 1\",\n { keys: [this.key(previousId), this.key(record.id)], arguments: [serializeJson(record), String(ttlMs)] },\n );\n if (result !== 0 && result !== 1) throw new Error(\"Unexpected Redis rotation result\");\n return result === 1;\n }\n async close(): Promise<void> { if (this.options.closeClient) await this.client.quit?.(); }\n}\n\n/** A driver can implement these operations using any SQL client and dialect. */\nexport interface SqlSessionRow {\n id: string;\n csrfToken: string;\n dataJson: string;\n createdAt: number;\n expiresAt: number;\n}\n\nexport interface SqlSessionDriver {\n read(id: string): Promise<SqlSessionRow | undefined>;\n /** Insert or update one row, keyed by its opaque id. */\n write(row: SqlSessionRow): Promise<void>;\n remove(id: string): Promise<void>;\n /** Atomically replace an unexpired row in one transaction; return false if absent or expired. */\n replace(previousId: string, row: SqlSessionRow, now: number): Promise<boolean>;\n close?(): Promise<void>;\n}\n\nexport interface SqlSessionStoreOptions {\n /** Close the SQL driver when this store owns its connection. */\n closeDriver?: boolean;\n}\n\n/** Generic SQL store. The application supplies SQL statements through its driver. */\nexport class SqlSessionStore<T = unknown> implements SessionStore<T> {\n readonly persistent = true;\n constructor(private readonly driver: SqlSessionDriver, private readonly options: SqlSessionStoreOptions = {}) {}\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const row = await this.driver.read(id);\n if (!row) return undefined;\n if (row.id !== id || typeof row.dataJson !== \"string\") throw new Error(\"Corrupt session record\");\n const record = decodeRecord<T>(serializeJson({ id: row.id, csrfToken: row.csrfToken, data: JSON.parse(row.dataJson) as unknown, createdAt: row.createdAt, expiresAt: row.expiresAt }), id);\n if (record.expiresAt > Date.now()) return record;\n await this.delete(id);\n return undefined;\n }\n async set(record: SessionRecord<T>): Promise<void> {\n if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now()) throw new RangeError(\"Session must have a future expiration\");\n await this.driver.write({ id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt });\n }\n async delete(id: string): Promise<void> { await this.driver.remove(id); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n if (!Number.isSafeInteger(record.expiresAt) || record.expiresAt <= Date.now()) throw new RangeError(\"Session must have a future expiration\");\n return this.driver.replace(previousId, { id: record.id, csrfToken: record.csrfToken, dataJson: serializeJson(record.data), createdAt: record.createdAt, expiresAt: record.expiresAt }, Date.now());\n }\n async close(): Promise<void> { if (this.options.closeDriver) await this.driver.close?.(); }\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface SessionRecord<T = unknown> {
|
|
2
|
+
id: string;
|
|
3
|
+
csrfToken: string;
|
|
4
|
+
data: T;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
expiresAt: number;
|
|
7
|
+
}
|
|
8
|
+
export interface SessionStore<T = unknown> {
|
|
9
|
+
/** Explicitly acknowledge that records survive application process restarts. */
|
|
10
|
+
readonly persistent?: boolean;
|
|
11
|
+
get(id: string): Promise<SessionRecord<T> | undefined>;
|
|
12
|
+
set(record: SessionRecord<T>): Promise<void>;
|
|
13
|
+
delete(id: string): Promise<void>;
|
|
14
|
+
/** Atomically replace an existing id; false means it no longer exists. */
|
|
15
|
+
replace?(previousId: string, record: SessionRecord<T>): Promise<boolean>;
|
|
16
|
+
close?(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare class MemorySessionStore<T = unknown> implements SessionStore<T> {
|
|
19
|
+
readonly developmentOnly = true;
|
|
20
|
+
readonly persistent = false;
|
|
21
|
+
private readonly records;
|
|
22
|
+
get(id: string): Promise<SessionRecord<T> | undefined>;
|
|
23
|
+
set(record: SessionRecord<T>): Promise<void>;
|
|
24
|
+
delete(id: string): Promise<void>;
|
|
25
|
+
replace(previousId: string, record: SessionRecord<T>): Promise<boolean>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export interface SessionOptions<T> {
|
|
29
|
+
store: SessionStore<T>;
|
|
30
|
+
production?: boolean;
|
|
31
|
+
ttlMs?: number;
|
|
32
|
+
secure?: boolean;
|
|
33
|
+
sameSite?: "Lax" | "Strict" | "None";
|
|
34
|
+
cookieName?: string;
|
|
35
|
+
allowedOrigins?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
export declare class SessionHandle<T = unknown> {
|
|
38
|
+
private readonly manager;
|
|
39
|
+
current?: SessionRecord<T> | undefined;
|
|
40
|
+
private pendingCookie?;
|
|
41
|
+
constructor(manager: SessionManager<T>, current?: SessionRecord<T> | undefined);
|
|
42
|
+
get cookieHeader(): string | undefined;
|
|
43
|
+
create(data: T): Promise<SessionRecord<T>>;
|
|
44
|
+
rotate(): Promise<SessionRecord<T>>;
|
|
45
|
+
revoke(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export declare class SessionManager<T = unknown> {
|
|
48
|
+
readonly store: SessionStore<T>;
|
|
49
|
+
readonly ttlMs: number;
|
|
50
|
+
readonly cookieName: string;
|
|
51
|
+
readonly allowedOrigins: readonly string[];
|
|
52
|
+
private readonly secure;
|
|
53
|
+
private readonly sameSite;
|
|
54
|
+
constructor(options: SessionOptions<T>);
|
|
55
|
+
serializeCookie(value: string, ttlMs: number): string;
|
|
56
|
+
load(request: Request): Promise<SessionHandle<T>>;
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
/** Apply both Origin and token checks to authenticated cookie mutations. */
|
|
59
|
+
assertMutationAllowed(request: Request, session: SessionHandle<T>, mutating?: boolean): void;
|
|
60
|
+
}
|
|
61
|
+
export declare class SessionSecurityError extends Error {
|
|
62
|
+
readonly code: "INVALID_ORIGIN" | "INVALID_CSRF";
|
|
63
|
+
constructor(code: "INVALID_ORIGIN" | "INVALID_CSRF");
|
|
64
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
export class MemorySessionStore {
|
|
3
|
+
developmentOnly = true;
|
|
4
|
+
persistent = false;
|
|
5
|
+
records = new Map();
|
|
6
|
+
async get(id) {
|
|
7
|
+
const record = this.records.get(id);
|
|
8
|
+
if (record && record.expiresAt <= Date.now()) {
|
|
9
|
+
this.records.delete(id);
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return record;
|
|
13
|
+
}
|
|
14
|
+
async set(record) { this.records.set(record.id, record); }
|
|
15
|
+
async delete(id) { this.records.delete(id); }
|
|
16
|
+
async replace(previousId, record) {
|
|
17
|
+
const previous = this.records.get(previousId);
|
|
18
|
+
if (!previous || previous.expiresAt <= Date.now()) {
|
|
19
|
+
this.records.delete(previousId);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
this.records.set(record.id, record);
|
|
23
|
+
this.records.delete(previousId);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
async close() { this.records.clear(); }
|
|
27
|
+
}
|
|
28
|
+
function token() { return randomBytes(32).toString("base64url"); }
|
|
29
|
+
function cookieValue(header, name) {
|
|
30
|
+
for (const cookie of (header ?? "").split(";")) {
|
|
31
|
+
const separator = cookie.indexOf("=");
|
|
32
|
+
if (separator < 0 || cookie.slice(0, separator).trim() !== name)
|
|
33
|
+
continue;
|
|
34
|
+
const value = cookie.slice(separator + 1).trim();
|
|
35
|
+
return /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
export class SessionHandle {
|
|
40
|
+
manager;
|
|
41
|
+
current;
|
|
42
|
+
pendingCookie;
|
|
43
|
+
constructor(manager, current) {
|
|
44
|
+
this.manager = manager;
|
|
45
|
+
this.current = current;
|
|
46
|
+
}
|
|
47
|
+
get cookieHeader() { return this.pendingCookie; }
|
|
48
|
+
async create(data) {
|
|
49
|
+
const previous = this.current;
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const record = { id: token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };
|
|
52
|
+
if (previous) {
|
|
53
|
+
if (!this.manager.store.replace)
|
|
54
|
+
throw new Error("Session storage does not support atomic rotation");
|
|
55
|
+
if (!(await this.manager.store.replace(previous.id, record)))
|
|
56
|
+
throw new Error("Session has expired or been revoked");
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
await this.manager.store.set(record);
|
|
60
|
+
this.current = record;
|
|
61
|
+
this.pendingCookie = this.manager.serializeCookie(record.id, this.manager.ttlMs);
|
|
62
|
+
return record;
|
|
63
|
+
}
|
|
64
|
+
async rotate() {
|
|
65
|
+
if (!this.current)
|
|
66
|
+
throw new Error("No active session to rotate");
|
|
67
|
+
return this.create(this.current.data);
|
|
68
|
+
}
|
|
69
|
+
async revoke() {
|
|
70
|
+
if (this.current)
|
|
71
|
+
await this.manager.store.delete(this.current.id);
|
|
72
|
+
this.current = undefined;
|
|
73
|
+
this.pendingCookie = this.manager.serializeCookie("", 0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class SessionManager {
|
|
77
|
+
store;
|
|
78
|
+
ttlMs;
|
|
79
|
+
cookieName;
|
|
80
|
+
allowedOrigins;
|
|
81
|
+
secure;
|
|
82
|
+
sameSite;
|
|
83
|
+
constructor(options) {
|
|
84
|
+
if (options.production && (options.store.persistent !== true || !options.store.replace))
|
|
85
|
+
throw new Error("Persistent session storage with atomic rotation is required in production");
|
|
86
|
+
this.store = options.store;
|
|
87
|
+
this.ttlMs = options.ttlMs ?? 7 * 24 * 60 * 60 * 1000;
|
|
88
|
+
if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0)
|
|
89
|
+
throw new RangeError("Invalid session TTL");
|
|
90
|
+
this.secure = options.secure ?? true;
|
|
91
|
+
this.sameSite = options.sameSite ?? "Lax";
|
|
92
|
+
if (this.sameSite === "None" && !this.secure)
|
|
93
|
+
throw new Error("SameSite=None requires Secure");
|
|
94
|
+
this.cookieName = options.cookieName ?? (this.secure ? "__Host-meeko.sid" : "meeko.sid");
|
|
95
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(this.cookieName))
|
|
96
|
+
throw new TypeError("Invalid cookie name");
|
|
97
|
+
if (this.cookieName.startsWith("__Host-") && !this.secure)
|
|
98
|
+
throw new Error("__Host- cookies require Secure");
|
|
99
|
+
this.allowedOrigins = options.allowedOrigins ?? [];
|
|
100
|
+
}
|
|
101
|
+
serializeCookie(value, ttlMs) {
|
|
102
|
+
const parts = [`${this.cookieName}=${value}`, "Path=/", "HttpOnly", `SameSite=${this.sameSite}`, `Max-Age=${Math.floor(ttlMs / 1000)}`];
|
|
103
|
+
if (this.secure)
|
|
104
|
+
parts.push("Secure");
|
|
105
|
+
return parts.join("; ");
|
|
106
|
+
}
|
|
107
|
+
async load(request) {
|
|
108
|
+
const id = cookieValue(request.headers.get("cookie"), this.cookieName);
|
|
109
|
+
const record = id ? await this.store.get(id) : undefined;
|
|
110
|
+
if (record && record.expiresAt <= Date.now()) {
|
|
111
|
+
await this.store.delete(record.id);
|
|
112
|
+
return new SessionHandle(this);
|
|
113
|
+
}
|
|
114
|
+
return new SessionHandle(this, record);
|
|
115
|
+
}
|
|
116
|
+
async close() { await this.store.close?.(); }
|
|
117
|
+
/** Apply both Origin and token checks to authenticated cookie mutations. */
|
|
118
|
+
assertMutationAllowed(request, session, mutating = !["GET", "HEAD", "OPTIONS"].includes(request.method)) {
|
|
119
|
+
if (!session.current || !mutating)
|
|
120
|
+
return;
|
|
121
|
+
const origin = request.headers.get("origin");
|
|
122
|
+
const expected = new URL(request.url).origin;
|
|
123
|
+
if (!origin || (origin !== expected && !this.allowedOrigins.includes(origin)))
|
|
124
|
+
throw new SessionSecurityError("INVALID_ORIGIN");
|
|
125
|
+
if (request.headers.get("x-meeko-csrf") !== session.current.csrfToken)
|
|
126
|
+
throw new SessionSecurityError("INVALID_CSRF");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export class SessionSecurityError extends Error {
|
|
130
|
+
code;
|
|
131
|
+
constructor(code) {
|
|
132
|
+
super(code);
|
|
133
|
+
this.code = code;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../../src/framework-server/sessions.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\n\nexport interface SessionRecord<T = unknown> {\n id: string;\n csrfToken: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n}\n\nexport interface SessionStore<T = unknown> {\n /** Explicitly acknowledge that records survive application process restarts. */\n readonly persistent?: boolean;\n get(id: string): Promise<SessionRecord<T> | undefined>;\n set(record: SessionRecord<T>): Promise<void>;\n delete(id: string): Promise<void>;\n /** Atomically replace an existing id; false means it no longer exists. */\n replace?(previousId: string, record: SessionRecord<T>): Promise<boolean>;\n close?(): Promise<void>;\n}\n\nexport class MemorySessionStore<T = unknown> implements SessionStore<T> {\n readonly developmentOnly = true;\n readonly persistent = false;\n private readonly records = new Map<string, SessionRecord<T>>();\n async get(id: string): Promise<SessionRecord<T> | undefined> {\n const record = this.records.get(id);\n if (record && record.expiresAt <= Date.now()) {\n this.records.delete(id);\n return undefined;\n }\n return record;\n }\n async set(record: SessionRecord<T>): Promise<void> { this.records.set(record.id, record); }\n async delete(id: string): Promise<void> { this.records.delete(id); }\n async replace(previousId: string, record: SessionRecord<T>): Promise<boolean> {\n const previous = this.records.get(previousId);\n if (!previous || previous.expiresAt <= Date.now()) {\n this.records.delete(previousId);\n return false;\n }\n this.records.set(record.id, record);\n this.records.delete(previousId);\n return true;\n }\n async close(): Promise<void> { this.records.clear(); }\n}\n\nexport interface SessionOptions<T> {\n store: SessionStore<T>;\n production?: boolean;\n ttlMs?: number;\n secure?: boolean;\n sameSite?: \"Lax\" | \"Strict\" | \"None\";\n cookieName?: string;\n allowedOrigins?: readonly string[];\n}\n\nfunction token(): string { return randomBytes(32).toString(\"base64url\"); }\n\nfunction cookieValue(header: string | null, name: string): string | undefined {\n for (const cookie of (header ?? \"\").split(\";\")) {\n const separator = cookie.indexOf(\"=\");\n if (separator < 0 || cookie.slice(0, separator).trim() !== name) continue;\n const value = cookie.slice(separator + 1).trim();\n return /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;\n }\n return undefined;\n}\n\nexport class SessionHandle<T = unknown> {\n private pendingCookie?: string;\n constructor(private readonly manager: SessionManager<T>, public current?: SessionRecord<T>) {}\n get cookieHeader(): string | undefined { return this.pendingCookie; }\n async create(data: T): Promise<SessionRecord<T>> {\n const previous = this.current;\n const now = Date.now();\n const record = { id: token(), csrfToken: token(), data, createdAt: now, expiresAt: now + this.manager.ttlMs };\n if (previous) {\n if (!this.manager.store.replace) throw new Error(\"Session storage does not support atomic rotation\");\n if (!(await this.manager.store.replace(previous.id, record))) throw new Error(\"Session has expired or been revoked\");\n } else await this.manager.store.set(record);\n this.current = record;\n this.pendingCookie = this.manager.serializeCookie(record.id, this.manager.ttlMs);\n return record;\n }\n async rotate(): Promise<SessionRecord<T>> {\n if (!this.current) throw new Error(\"No active session to rotate\");\n return this.create(this.current.data);\n }\n async revoke(): Promise<void> {\n if (this.current) await this.manager.store.delete(this.current.id);\n this.current = undefined;\n this.pendingCookie = this.manager.serializeCookie(\"\", 0);\n }\n}\n\nexport class SessionManager<T = unknown> {\n readonly store: SessionStore<T>;\n readonly ttlMs: number;\n readonly cookieName: string;\n readonly allowedOrigins: readonly string[];\n private readonly secure: boolean;\n private readonly sameSite: \"Lax\" | \"Strict\" | \"None\";\n constructor(options: SessionOptions<T>) {\n if (options.production && (options.store.persistent !== true || !options.store.replace)) throw new Error(\"Persistent session storage with atomic rotation is required in production\");\n this.store = options.store;\n this.ttlMs = options.ttlMs ?? 7 * 24 * 60 * 60 * 1000;\n if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) throw new RangeError(\"Invalid session TTL\");\n this.secure = options.secure ?? true;\n this.sameSite = options.sameSite ?? \"Lax\";\n if (this.sameSite === \"None\" && !this.secure) throw new Error(\"SameSite=None requires Secure\");\n this.cookieName = options.cookieName ?? (this.secure ? \"__Host-meeko.sid\" : \"meeko.sid\");\n if (!/^[A-Za-z0-9_.-]+$/.test(this.cookieName)) throw new TypeError(\"Invalid cookie name\");\n if (this.cookieName.startsWith(\"__Host-\") && !this.secure) throw new Error(\"__Host- cookies require Secure\");\n this.allowedOrigins = options.allowedOrigins ?? [];\n }\n serializeCookie(value: string, ttlMs: number): string {\n const parts = [`${this.cookieName}=${value}`, \"Path=/\", \"HttpOnly\", `SameSite=${this.sameSite}`, `Max-Age=${Math.floor(ttlMs / 1000)}`];\n if (this.secure) parts.push(\"Secure\");\n return parts.join(\"; \");\n }\n async load(request: Request): Promise<SessionHandle<T>> {\n const id = cookieValue(request.headers.get(\"cookie\"), this.cookieName);\n const record = id ? await this.store.get(id) : undefined;\n if (record && record.expiresAt <= Date.now()) {\n await this.store.delete(record.id);\n return new SessionHandle(this);\n }\n return new SessionHandle(this, record);\n }\n async close(): Promise<void> { await this.store.close?.(); }\n /** Apply both Origin and token checks to authenticated cookie mutations. */\n assertMutationAllowed(request: Request, session: SessionHandle<T>, mutating = ![\"GET\", \"HEAD\", \"OPTIONS\"].includes(request.method)): void {\n if (!session.current || !mutating) return;\n const origin = request.headers.get(\"origin\");\n const expected = new URL(request.url).origin;\n if (!origin || (origin !== expected && !this.allowedOrigins.includes(origin))) throw new SessionSecurityError(\"INVALID_ORIGIN\");\n if (request.headers.get(\"x-meeko-csrf\") !== session.current.csrfToken) throw new SessionSecurityError(\"INVALID_CSRF\");\n }\n}\n\nexport class SessionSecurityError extends Error {\n constructor(public readonly code: \"INVALID_ORIGIN\" | \"INVALID_CSRF\") { super(code); }\n}\n"]}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type RequestContext } from "./context.js";
|
|
2
|
+
import { type Schema } from "./contracts.js";
|
|
3
|
+
import type { SessionManager } from "./sessions.js";
|
|
4
|
+
type Output<S> = S extends Schema<unknown, infer O> ? O : never;
|
|
5
|
+
type SchemaMap = Readonly<Record<string, Schema>>;
|
|
6
|
+
export interface SocketPeer {
|
|
7
|
+
send(message: string): void | Promise<void>;
|
|
8
|
+
close(code?: number, reason?: string): void;
|
|
9
|
+
readonly bufferedAmount?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SocketChannelDefinition<C extends SchemaMap, S extends SchemaMap, TServices = unknown> {
|
|
12
|
+
clientToServer: C;
|
|
13
|
+
serverToClient: S;
|
|
14
|
+
auth?: "required" | "optional";
|
|
15
|
+
authorize?: (args: {
|
|
16
|
+
context: RequestContext<TServices>;
|
|
17
|
+
roomId?: string;
|
|
18
|
+
}) => boolean | Promise<boolean>;
|
|
19
|
+
authorizeEvent?: (args: {
|
|
20
|
+
event: keyof C & string;
|
|
21
|
+
payload: unknown;
|
|
22
|
+
context: RequestContext<TServices>;
|
|
23
|
+
roomId?: string;
|
|
24
|
+
}) => boolean | Promise<boolean>;
|
|
25
|
+
authorizeSend?: (args: {
|
|
26
|
+
event: keyof S & string;
|
|
27
|
+
payload: unknown;
|
|
28
|
+
context: RequestContext<TServices>;
|
|
29
|
+
roomId?: string;
|
|
30
|
+
}) => boolean | Promise<boolean>;
|
|
31
|
+
handlers?: {
|
|
32
|
+
[K in keyof C]?: (args: {
|
|
33
|
+
payload: Output<C[K]>;
|
|
34
|
+
context: RequestContext<TServices>;
|
|
35
|
+
roomId?: string;
|
|
36
|
+
emit: <E extends keyof S & string>(event: E, payload: Output<S[E]>) => Promise<void>;
|
|
37
|
+
}) => void | Promise<void>;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export declare function defineSocketChannel<C extends SchemaMap, S extends SchemaMap, TServices = unknown>(definition: SocketChannelDefinition<C, S, TServices>): SocketChannelDefinition<C, S, TServices>;
|
|
41
|
+
export interface SocketPubSub {
|
|
42
|
+
publish(channel: string, room: string, message: {
|
|
43
|
+
event: string;
|
|
44
|
+
payload: unknown;
|
|
45
|
+
}): Promise<void>;
|
|
46
|
+
subscribe(channel: string, room: string, listener: (message: {
|
|
47
|
+
event: string;
|
|
48
|
+
payload: unknown;
|
|
49
|
+
}) => void | Promise<void>): () => void;
|
|
50
|
+
}
|
|
51
|
+
export declare class LocalSocketPubSub implements SocketPubSub {
|
|
52
|
+
private readonly listeners;
|
|
53
|
+
publish(channel: string, room: string, message: {
|
|
54
|
+
event: string;
|
|
55
|
+
payload: unknown;
|
|
56
|
+
}): Promise<void>;
|
|
57
|
+
subscribe(channel: string, room: string, listener: (message: {
|
|
58
|
+
event: string;
|
|
59
|
+
payload: unknown;
|
|
60
|
+
}) => void | Promise<void>): () => void;
|
|
61
|
+
}
|
|
62
|
+
export interface SocketConnection {
|
|
63
|
+
receive(message: string | Uint8Array): Promise<void>;
|
|
64
|
+
emit(event: string, payload: unknown): Promise<void>;
|
|
65
|
+
close(code?: number, reason?: string): void;
|
|
66
|
+
}
|
|
67
|
+
export interface SocketHubOptions<TServices = unknown> {
|
|
68
|
+
channels: Readonly<Record<string, unknown>>;
|
|
69
|
+
sessions: SessionManager;
|
|
70
|
+
services: TServices | ((request: Request) => TServices | Promise<TServices>);
|
|
71
|
+
allowedOrigins?: readonly string[];
|
|
72
|
+
maxMessageBytes?: number;
|
|
73
|
+
maxBufferBytes?: number;
|
|
74
|
+
pubsub?: SocketPubSub;
|
|
75
|
+
}
|
|
76
|
+
export interface SocketHub {
|
|
77
|
+
preflight(name: string, request: Request, roomId?: string): Promise<void>;
|
|
78
|
+
accept(name: string, request: Request, peer: SocketPeer, roomId?: string): Promise<SocketConnection>;
|
|
79
|
+
broadcast(name: string, roomId: string, event: string, payload: unknown): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/** The host performs the HTTP upgrade, then passes its connected peer here. */
|
|
82
|
+
export declare function createSocketHub<TServices>(options: SocketHubOptions<TServices>): SocketHub;
|
|
83
|
+
export {};
|