@nifrajs/core 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/brand.d.ts +11 -0
- package/dist/internal/brand.d.ts.map +1 -0
- package/dist/internal/brand.js +10 -0
- package/dist/internal/brand.js.map +1 -0
- package/dist/internal/secret.d.ts +9 -0
- package/dist/internal/secret.d.ts.map +1 -0
- package/dist/internal/secret.js +19 -0
- package/dist/internal/secret.js.map +1 -0
- package/dist/router/router.d.ts +61 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/router.js +324 -0
- package/dist/router/router.js.map +1 -0
- package/dist/schema/standard.d.ts +46 -0
- package/dist/schema/standard.d.ts.map +1 -0
- package/dist/schema/standard.js +20 -0
- package/dist/schema/standard.js.map +1 -0
- package/dist/seo.d.ts +48 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +109 -0
- package/dist/seo.js.map +1 -0
- package/dist/server/body.d.ts +39 -0
- package/dist/server/body.d.ts.map +1 -0
- package/dist/server/body.js +85 -0
- package/dist/server/body.js.map +1 -0
- package/dist/server/context.d.ts +111 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +2 -0
- package/dist/server/context.js.map +1 -0
- package/dist/server/contract.d.ts +142 -0
- package/dist/server/contract.d.ts.map +1 -0
- package/dist/server/contract.js +59 -0
- package/dist/server/contract.js.map +1 -0
- package/dist/server/cookies.d.ts +41 -0
- package/dist/server/cookies.d.ts.map +1 -0
- package/dist/server/cookies.js +181 -0
- package/dist/server/cookies.js.map +1 -0
- package/dist/server/logger.d.ts +49 -0
- package/dist/server/logger.d.ts.map +1 -0
- package/dist/server/logger.js +125 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/registry.d.ts +46 -0
- package/dist/server/registry.d.ts.map +1 -0
- package/dist/server/registry.js +2 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/server.d.ts +528 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1660 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/sse.d.ts +54 -0
- package/dist/server/sse.d.ts.map +1 -0
- package/dist/server/sse.js +126 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/server/webhook.d.ts +42 -0
- package/dist/server/webhook.d.ts.map +1 -0
- package/dist/server/webhook.js +149 -0
- package/dist/server/webhook.js.map +1 -0
- package/dist/server/websocket.d.ts +152 -0
- package/dist/server/websocket.d.ts.map +1 -0
- package/dist/server/websocket.js +178 -0
- package/dist/server/websocket.js.map +1 -0
- package/dist/server/ws-bun.d.ts +27 -0
- package/dist/server/ws-bun.d.ts.map +1 -0
- package/dist/server/ws-bun.js +82 -0
- package/dist/server/ws-bun.js.map +1 -0
- package/dist/server/ws-hook.d.ts +36 -0
- package/dist/server/ws-hook.d.ts.map +1 -0
- package/dist/server/ws-hook.js +18 -0
- package/dist/server/ws-hook.js.map +1 -0
- package/dist/ws.d.ts +19 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +26 -0
- package/dist/ws.js.map +1 -0
- package/package.json +63 -0
- package/src/errors.ts +41 -0
- package/src/index.ts +115 -0
- package/src/internal/brand.ts +11 -0
- package/src/internal/secret.ts +24 -0
- package/src/router/router.ts +429 -0
- package/src/schema/standard.ts +76 -0
- package/src/seo.ts +155 -0
- package/src/server/body.ts +89 -0
- package/src/server/context.ts +124 -0
- package/src/server/contract.ts +209 -0
- package/src/server/cookies.ts +198 -0
- package/src/server/logger.ts +167 -0
- package/src/server/registry.ts +63 -0
- package/src/server/server.ts +2479 -0
- package/src/server/sse.ts +161 -0
- package/src/server/webhook.ts +207 -0
- package/src/server/websocket.ts +308 -0
- package/src/server/ws-bun.ts +106 -0
- package/src/server/ws-hook.ts +53 -0
- package/src/ws.ts +40 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { type Method } from "../router/router.js";
|
|
2
|
+
import type { InferOutput, StandardSchemaV1 } from "../schema/standard.js";
|
|
3
|
+
import type { Context, Params, RouteSchema } from "./context.js";
|
|
4
|
+
import type { OutputOf } from "./registry.js";
|
|
5
|
+
import { Server } from "./server.js";
|
|
6
|
+
/** An additional (non-success) response a contract operation can document, e.g. a `404`. */
|
|
7
|
+
export interface ResponseDef {
|
|
8
|
+
readonly description?: string;
|
|
9
|
+
/** Response body schema (any Standard Schema; a `t` schema yields full JSON Schema in OpenAPI). */
|
|
10
|
+
readonly schema?: StandardSchemaV1;
|
|
11
|
+
/** Content type. Default `application/json`. */
|
|
12
|
+
readonly contentType?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One operation in a contract. Input schemas are any Standard Schema; `response` is optional.
|
|
16
|
+
*
|
|
17
|
+
* The fields below `response` are **optional OpenAPI metadata** — they don't affect runtime
|
|
18
|
+
* validation or the inferred handler types, they enrich the document `toOpenAPI` emits. A contract is
|
|
19
|
+
* the natural home for them: it's the versionable description of the API, decoupled from the impl.
|
|
20
|
+
*/
|
|
21
|
+
export interface OperationDef {
|
|
22
|
+
readonly method: Method;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly body?: StandardSchemaV1;
|
|
25
|
+
readonly query?: StandardSchemaV1;
|
|
26
|
+
readonly response?: StandardSchemaV1;
|
|
27
|
+
/** Short summary (OpenAPI `summary`). */
|
|
28
|
+
readonly summary?: string;
|
|
29
|
+
/** Longer description (OpenAPI `description`, CommonMark). */
|
|
30
|
+
readonly description?: string;
|
|
31
|
+
/** Grouping tags (OpenAPI `tags`). */
|
|
32
|
+
readonly tags?: readonly string[];
|
|
33
|
+
/** Mark the operation deprecated. */
|
|
34
|
+
readonly deprecated?: boolean;
|
|
35
|
+
/** Security requirements (names ref `securitySchemes`); `[]` = explicitly public. Omit ⇒ inherit the document default. */
|
|
36
|
+
readonly security?: ReadonlyArray<Readonly<Record<string, readonly string[]>>>;
|
|
37
|
+
/** Request body content type. Default `application/json`. */
|
|
38
|
+
readonly requestContentType?: string;
|
|
39
|
+
/** Content type of the success (`200`) response. Default `application/json`. */
|
|
40
|
+
readonly responseContentType?: string;
|
|
41
|
+
/** Additional responses by status code, e.g. `{ "404": { description: "Not found" } }`. */
|
|
42
|
+
readonly responses?: Readonly<Record<string, ResponseDef>>;
|
|
43
|
+
}
|
|
44
|
+
/** A contract: named operations. Names are the handler keys and OpenAPI operationIds. */
|
|
45
|
+
export type ContractShape = Record<string, OperationDef>;
|
|
46
|
+
type OpBody<O extends OperationDef> = O extends {
|
|
47
|
+
body: infer B extends StandardSchemaV1;
|
|
48
|
+
} ? InferOutput<B> : never;
|
|
49
|
+
type OpQuery<O extends OperationDef> = O extends {
|
|
50
|
+
query: infer Q extends StandardSchemaV1;
|
|
51
|
+
} ? InferOutput<Q> : never;
|
|
52
|
+
type OpResponse<O extends OperationDef> = O extends {
|
|
53
|
+
response: infer R extends StandardSchemaV1;
|
|
54
|
+
} ? InferOutput<R> : unknown;
|
|
55
|
+
/**
|
|
56
|
+
* RouteInfo as a *decoupled consumer* sees it from the contract alone: the
|
|
57
|
+
* `output` is the declared `response` schema's type, or `unknown` when none is
|
|
58
|
+
* declared (the consumer can't know the response without the server). Path/method
|
|
59
|
+
* levels are mutable and fields are `readonly` — matching the inline registry, so
|
|
60
|
+
* the two are mutually assignable.
|
|
61
|
+
*/
|
|
62
|
+
type RouteInfoForOp<O extends OperationDef> = {
|
|
63
|
+
readonly params: Params<O["path"]>;
|
|
64
|
+
readonly query: OpQuery<O>;
|
|
65
|
+
readonly body: OpBody<O>;
|
|
66
|
+
readonly output: OpResponse<O>;
|
|
67
|
+
};
|
|
68
|
+
/** Re-key the name-keyed ops into the `path → method → RouteInfo` registry. */
|
|
69
|
+
export type RegistryFor<C extends ContractShape> = {
|
|
70
|
+
[P in C[keyof C]["path"]]: {
|
|
71
|
+
[K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]: RouteInfoForOp<C[K]>;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/** The schema shape an op contributes to its handler context (mirrors inline `RouteSchema`). */
|
|
75
|
+
type SchemaForOp<O extends OperationDef> = (O extends {
|
|
76
|
+
body: infer B extends StandardSchemaV1;
|
|
77
|
+
} ? {
|
|
78
|
+
body: B;
|
|
79
|
+
} : Record<never, never>) & (O extends {
|
|
80
|
+
query: infer Q extends StandardSchemaV1;
|
|
81
|
+
} ? {
|
|
82
|
+
query: Q;
|
|
83
|
+
} : Record<never, never>);
|
|
84
|
+
/**
|
|
85
|
+
* The handler context for an op — identical to the inline `Context<Path, S>`, so a
|
|
86
|
+
* handler written for an inline route type-checks unchanged under `implement`
|
|
87
|
+
* (the graduation guarantee).
|
|
88
|
+
*/
|
|
89
|
+
export type ContextForOp<O extends OperationDef> = Context<O["path"], SchemaForOp<O> & RouteSchema>;
|
|
90
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
91
|
+
/**
|
|
92
|
+
* What a handler may return for an op. When the op declares a `response`, the return is constrained to
|
|
93
|
+
* that contract shape (or a raw `Response`) — so an `implement`ed backend can't drift from the response
|
|
94
|
+
* the contract's client was built against. With no `response` it's unconstrained (`unknown`), identical
|
|
95
|
+
* to before. Purely type-level — erased at compile time, zero runtime cost — and mirrors the inline
|
|
96
|
+
* route's `ResponseOf` constraint, so a handler graduates inline↔contract unchanged.
|
|
97
|
+
*/
|
|
98
|
+
type HandlerReturnForOp<O extends OperationDef> = O extends {
|
|
99
|
+
response: infer R extends StandardSchemaV1;
|
|
100
|
+
} ? Response | InferOutput<R> : unknown;
|
|
101
|
+
/** The handlers `implement` requires: one per operation, typed from the op's input + response contract. */
|
|
102
|
+
export type HandlersFor<C extends ContractShape> = {
|
|
103
|
+
[K in keyof C]: (context: ContextForOp<C[K]>) => MaybePromise<HandlerReturnForOp<C[K]>>;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Define a standalone, versionable contract. Identity at runtime (it returns the
|
|
107
|
+
* contract for type inference via the `const` type parameter, which preserves the
|
|
108
|
+
* path/method literals) plus boot-time (L2) validation: each operation must use a
|
|
109
|
+
* known method, a path starting with `/`, and no two operations may share a
|
|
110
|
+
* `(method, path)`. Deeper path validation (param names, wildcard position) runs
|
|
111
|
+
* when the contract is `implement`ed.
|
|
112
|
+
*/
|
|
113
|
+
export declare function defineContract<const C extends ContractShape>(contract: C): C;
|
|
114
|
+
type AnyFn = (...args: never[]) => unknown;
|
|
115
|
+
/**
|
|
116
|
+
* The registry produced by `implement`: input from the contract op; `output` is the declared `response`
|
|
117
|
+
* contract when present (it wins — exactly as in the inline path), else the bound HANDLER's return — so
|
|
118
|
+
* the implemented server stays route-for-route identical to the equivalent inline server (the
|
|
119
|
+
* mode-conformance guarantee), and a contract-typed client and a `typeof app`-typed client agree.
|
|
120
|
+
*/
|
|
121
|
+
export type RegistryFromImpl<C extends ContractShape, H extends HandlersFor<C>> = {
|
|
122
|
+
[P in C[keyof C]["path"]]: {
|
|
123
|
+
[K in keyof C as C[K]["path"] extends P ? C[K]["method"] : never]: {
|
|
124
|
+
readonly params: Params<C[K]["path"]>;
|
|
125
|
+
readonly query: OpQuery<C[K]>;
|
|
126
|
+
readonly body: OpBody<C[K]>;
|
|
127
|
+
readonly output: C[K] extends {
|
|
128
|
+
response: infer R extends StandardSchemaV1;
|
|
129
|
+
} ? InferOutput<R> : H[K] extends AnyFn ? OutputOf<H[K]> : unknown;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Bind handlers to a contract, producing a real {@link Server} you can `.listen()`
|
|
135
|
+
* or `.fetch()`. Each op is registered through the same path as the inline
|
|
136
|
+
* builder, so the result is identical to writing the routes inline — handlers
|
|
137
|
+
* lift over **unchanged** ("graduation"), and body/query schemas validate at the
|
|
138
|
+
* request boundary exactly as in inline mode.
|
|
139
|
+
*/
|
|
140
|
+
export declare function implement<const C extends ContractShape, H extends HandlersFor<C>>(contract: C, handlers: H): Server<RegistryFromImpl<C, H>>;
|
|
141
|
+
export {};
|
|
142
|
+
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../src/server/contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RouteConfigError } from "../errors.js";
|
|
2
|
+
import { METHODS } from "../router/router.js";
|
|
3
|
+
import { Server } from "./server.js";
|
|
4
|
+
const METHOD_SET = new Set(METHODS);
|
|
5
|
+
/**
|
|
6
|
+
* Define a standalone, versionable contract. Identity at runtime (it returns the
|
|
7
|
+
* contract for type inference via the `const` type parameter, which preserves the
|
|
8
|
+
* path/method literals) plus boot-time (L2) validation: each operation must use a
|
|
9
|
+
* known method, a path starting with `/`, and no two operations may share a
|
|
10
|
+
* `(method, path)`. Deeper path validation (param names, wildcard position) runs
|
|
11
|
+
* when the contract is `implement`ed.
|
|
12
|
+
*/
|
|
13
|
+
export function defineContract(contract) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const [name, op] of Object.entries(contract)) {
|
|
16
|
+
const method = op.method.toUpperCase();
|
|
17
|
+
if (!METHOD_SET.has(method)) {
|
|
18
|
+
throw new RouteConfigError("INVALID_METHOD", `operation "${name}": unsupported method "${op.method}"`);
|
|
19
|
+
}
|
|
20
|
+
if (op.path.length === 0 || op.path.charCodeAt(0) !== 47 /* "/" */) {
|
|
21
|
+
throw new RouteConfigError("INVALID_PATH", `operation "${name}": path must start with "/": "${op.path}"`);
|
|
22
|
+
}
|
|
23
|
+
const key = `${method} ${op.path}`;
|
|
24
|
+
if (seen.has(key)) {
|
|
25
|
+
throw new RouteConfigError("DUPLICATE_ROUTE", `duplicate operation route: ${key}`);
|
|
26
|
+
}
|
|
27
|
+
seen.add(key);
|
|
28
|
+
}
|
|
29
|
+
return contract;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Bind handlers to a contract, producing a real {@link Server} you can `.listen()`
|
|
33
|
+
* or `.fetch()`. Each op is registered through the same path as the inline
|
|
34
|
+
* builder, so the result is identical to writing the routes inline — handlers
|
|
35
|
+
* lift over **unchanged** ("graduation"), and body/query schemas validate at the
|
|
36
|
+
* request boundary exactly as in inline mode.
|
|
37
|
+
*/
|
|
38
|
+
export function implement(contract, handlers) {
|
|
39
|
+
const app = new Server();
|
|
40
|
+
for (const [name, op] of Object.entries(contract)) {
|
|
41
|
+
// body/query are validated at the request boundary; `response` is a type + introspection contract
|
|
42
|
+
// ONLY — never validated at runtime and never read on the request hot path (the lifecycle reads
|
|
43
|
+
// schema.body/query by name; the `bare`/bodyOnly/queryOnly fast-path gates ignore `response`). Built
|
|
44
|
+
// once here at bind time, not per request — it just carries a reference to the op's existing schema.
|
|
45
|
+
// A response-less op still yields `undefined`, byte-identical to before.
|
|
46
|
+
const schema = op.body !== undefined || op.query !== undefined || op.response !== undefined
|
|
47
|
+
? {
|
|
48
|
+
...(op.body !== undefined ? { body: op.body } : {}),
|
|
49
|
+
...(op.query !== undefined ? { query: op.query } : {}),
|
|
50
|
+
...(op.response !== undefined ? { response: op.response } : {}),
|
|
51
|
+
}
|
|
52
|
+
: undefined;
|
|
53
|
+
app.register(op.method, op.path, schema, handlers[name]);
|
|
54
|
+
}
|
|
55
|
+
// Runtime registered exactly the contract's routes through the inline path; the
|
|
56
|
+
// registry type is computed from the contract inputs + handler return types.
|
|
57
|
+
return app;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/server/contract.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,OAAO,EAAe,MAAM,qBAAqB,CAAA;AAI1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA6GpC,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;AAExD;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAgC,QAAW;IACvE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClD,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAA;QACtC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,gBAAgB,CACxB,gBAAgB,EAChB,cAAc,IAAI,0BAA0B,EAAE,CAAC,MAAM,GAAG,CACzD,CAAA;QACH,CAAC;QACD,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,CAAC;YACnE,MAAM,IAAI,gBAAgB,CACxB,cAAc,EACd,cAAc,IAAI,iCAAiC,EAAE,CAAC,IAAI,GAAG,CAC9D,CAAA;QACH,CAAC;QACD,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,CAAA;QAClC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,gBAAgB,CAAC,iBAAiB,EAAE,8BAA8B,GAAG,EAAE,CAAC,CAAA;QACpF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACf,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC;AAyBD;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CACvB,QAAW,EACX,QAAW;IAEX,MAAM,GAAG,GAAG,IAAI,MAAM,EAAE,CAAA;IACxB,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClD,kGAAkG;QAClG,gGAAgG;QAChG,qGAAqG;QACrG,qGAAqG;QACrG,yEAAyE;QACzE,MAAM,MAAM,GACV,EAAE,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,CAAC,QAAQ,KAAK,SAAS;YAC1E,CAAC,CAAC;gBACE,GAAG,CAAC,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnD,GAAG,CAAC,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACtD,GAAG,CAAC,EAAE,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChE;YACH,CAAC,CAAC,SAAS,CAAA;QACf,GAAG,CAAC,QAAQ,CACV,EAAE,CAAC,MAAM,EACT,EAAE,CAAC,IAAI,EACP,MAAM,EACN,QAAQ,CAAC,IAAe,CAAgC,CACzD,CAAA;IACH,CAAC;IACD,gFAAgF;IAChF,6EAA6E;IAC7E,OAAO,GAAgD,CAAA;AACzD,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie primitives — parse a request `Cookie` header, serialize a `Set-Cookie`, and sign/verify a
|
|
3
|
+
* value with HMAC-SHA256 via **WebCrypto** (`crypto.subtle`), so they're portable across Bun, Node,
|
|
4
|
+
* Deno, and workerd with no `node:crypto` dependency. Pure + runtime-agnostic: `c.cookies` (read) and
|
|
5
|
+
* `c.set.cookie` (write) build on these, and `@nifrajs/auth` builds sessions on top.
|
|
6
|
+
*/
|
|
7
|
+
/** Attributes for a `Set-Cookie`. `expires` is a `Date`; `maxAge` is in **seconds**. */
|
|
8
|
+
export interface CookieOptions {
|
|
9
|
+
/** Lifetime in seconds. `0` (with a past `expires`) deletes the cookie. */
|
|
10
|
+
readonly maxAge?: number;
|
|
11
|
+
readonly expires?: Date;
|
|
12
|
+
readonly path?: string;
|
|
13
|
+
readonly domain?: string;
|
|
14
|
+
readonly secure?: boolean;
|
|
15
|
+
readonly httpOnly?: boolean;
|
|
16
|
+
readonly sameSite?: "strict" | "lax" | "none";
|
|
17
|
+
readonly partitioned?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Parse a request `Cookie` header into a name→value map (values URL-decoded). Unparseable pairs are
|
|
20
|
+
* skipped rather than throwing — a junk `Cookie` header shouldn't fail the request.
|
|
21
|
+
*
|
|
22
|
+
* Hand-rolled index walk instead of `split(";")` + `trim()` chains: those allocated an array + up
|
|
23
|
+
* to three substrings per pair and showed up at ~6% of a realistic (auth + cookie) request. Same
|
|
24
|
+
* semantics, two slices per pair, no intermediate array. */
|
|
25
|
+
export declare function parseCookies(header: string | null | undefined): Record<string, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Serialize a `Set-Cookie` header value. Pure — applies **no** security defaults (the caller, e.g.
|
|
28
|
+
* `c.set.cookie`, layers `HttpOnly`/`Secure`/`SameSite` on). Throws on an invalid cookie name, a
|
|
29
|
+
* header-injecting `Path`/`Domain`, a non-integer `maxAge`, or an oversized result — a serialization
|
|
30
|
+
* bug should fail loudly, not silently emit a cookie the browser drops.
|
|
31
|
+
*/
|
|
32
|
+
export declare function serializeCookie(name: string, value: string, options?: CookieOptions): string;
|
|
33
|
+
/** Append an HMAC-SHA256 signature to a value → `value.signature` (base64url). For signed cookies. */
|
|
34
|
+
export declare function signValue(value: string, secret: string): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Verify a `value.signature` produced by {@link signValue} and return the value, or `null` if the
|
|
37
|
+
* signature is missing, malformed, or doesn't match. Verification is **constant-time**
|
|
38
|
+
* (`crypto.subtle.verify`), so a wrong signature can't be discovered byte-by-byte via timing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function unsignValue(signed: string, secret: string): Promise<string | null>;
|
|
41
|
+
//# sourceMappingURL=cookies.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../../src/server/cookies.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,IAAI,CAAA;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;IAC7C,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAC/B;AA2BD;;;;;4DAK4D;AAC5D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2CtF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,aAAkB,GAAG,MAAM,CAoChG;AAoCD,sGAAsG;AACtG,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9E;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASxF"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie primitives — parse a request `Cookie` header, serialize a `Set-Cookie`, and sign/verify a
|
|
3
|
+
* value with HMAC-SHA256 via **WebCrypto** (`crypto.subtle`), so they're portable across Bun, Node,
|
|
4
|
+
* Deno, and workerd with no `node:crypto` dependency. Pure + runtime-agnostic: `c.cookies` (read) and
|
|
5
|
+
* `c.set.cookie` (write) build on these, and `@nifrajs/auth` builds sessions on top.
|
|
6
|
+
*/
|
|
7
|
+
import { requireSecretBytes } from "../internal/secret.js";
|
|
8
|
+
// RFC 6265 cookie-name token: visible ASCII minus separators/whitespace/controls.
|
|
9
|
+
const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
10
|
+
const MAX_COOKIE_BYTES = 4096; // browsers cap a cookie near 4 KB — reject oversized before the wire
|
|
11
|
+
/** True if `v` contains a character illegal in a cookie attribute (control char or `;` separator) —
|
|
12
|
+
* the header-injection guard for dev-supplied `Path`/`Domain`. */
|
|
13
|
+
const hasIllegalChar = (v) => {
|
|
14
|
+
for (let i = 0; i < v.length; i++) {
|
|
15
|
+
const code = v.charCodeAt(i);
|
|
16
|
+
if (code < 0x20 || code === 0x7f || code === 0x3b /* ; */)
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
21
|
+
const safeDecode = (v) => {
|
|
22
|
+
// No `%` ⇒ nothing to decode — skip the call AND its try frame (profiled: cookie values are
|
|
23
|
+
// overwhelmingly plain; this was ~3% of a realistic request).
|
|
24
|
+
if (!v.includes("%"))
|
|
25
|
+
return v;
|
|
26
|
+
try {
|
|
27
|
+
return decodeURIComponent(v);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return v; // a malformed %-escape from a hostile/garbage cookie must not throw (it'd 500 the request)
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Parse a request `Cookie` header into a name→value map (values URL-decoded). Unparseable pairs are
|
|
34
|
+
* skipped rather than throwing — a junk `Cookie` header shouldn't fail the request.
|
|
35
|
+
*
|
|
36
|
+
* Hand-rolled index walk instead of `split(";")` + `trim()` chains: those allocated an array + up
|
|
37
|
+
* to three substrings per pair and showed up at ~6% of a realistic (auth + cookie) request. Same
|
|
38
|
+
* semantics, two slices per pair, no intermediate array. */
|
|
39
|
+
export function parseCookies(header) {
|
|
40
|
+
// Null-prototype: cookie NAMES come straight from the untrusted header, so a cookie named
|
|
41
|
+
// `constructor`/`__proto__`/`toString` must be an inert own key, not a shadow of a prototype
|
|
42
|
+
// member. Matches the query/form/params parsers (all `Object.create(null)`); values are always
|
|
43
|
+
// strings so global pollution was never possible, but this keeps the whole request-parse layer
|
|
44
|
+
// consistent and safe to treat as a plain dictionary.
|
|
45
|
+
const out = Object.create(null);
|
|
46
|
+
if (!header)
|
|
47
|
+
return out;
|
|
48
|
+
const len = header.length;
|
|
49
|
+
let pos = 0;
|
|
50
|
+
while (pos < len) {
|
|
51
|
+
let end = header.indexOf(";", pos);
|
|
52
|
+
if (end === -1)
|
|
53
|
+
end = len;
|
|
54
|
+
// trim() equivalent on the segment, without allocating the untrimmed slice
|
|
55
|
+
let start = pos;
|
|
56
|
+
while (start < end && header.charCodeAt(start) === 32)
|
|
57
|
+
start++;
|
|
58
|
+
let stop = end;
|
|
59
|
+
while (stop > start && header.charCodeAt(stop - 1) === 32)
|
|
60
|
+
stop--;
|
|
61
|
+
const eq = header.indexOf("=", start);
|
|
62
|
+
if (eq > start && eq < stop) {
|
|
63
|
+
// name: trim trailing spaces before '='; value: trim leading spaces after it
|
|
64
|
+
let nameEnd = eq;
|
|
65
|
+
while (nameEnd > start && header.charCodeAt(nameEnd - 1) === 32)
|
|
66
|
+
nameEnd--;
|
|
67
|
+
let valueStart = eq + 1;
|
|
68
|
+
while (valueStart < stop && header.charCodeAt(valueStart) === 32)
|
|
69
|
+
valueStart++;
|
|
70
|
+
if (nameEnd > start) {
|
|
71
|
+
const name = header.slice(start, nameEnd);
|
|
72
|
+
let value;
|
|
73
|
+
if (stop - valueStart >= 2 &&
|
|
74
|
+
header.charCodeAt(valueStart) === 34 /* " */ &&
|
|
75
|
+
header.charCodeAt(stop - 1) === 34) {
|
|
76
|
+
value = header.slice(valueStart + 1, stop - 1);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
value = header.slice(valueStart, stop);
|
|
80
|
+
}
|
|
81
|
+
out[name] = safeDecode(value);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
pos = end + 1;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Serialize a `Set-Cookie` header value. Pure — applies **no** security defaults (the caller, e.g.
|
|
90
|
+
* `c.set.cookie`, layers `HttpOnly`/`Secure`/`SameSite` on). Throws on an invalid cookie name, a
|
|
91
|
+
* header-injecting `Path`/`Domain`, a non-integer `maxAge`, or an oversized result — a serialization
|
|
92
|
+
* bug should fail loudly, not silently emit a cookie the browser drops.
|
|
93
|
+
*/
|
|
94
|
+
export function serializeCookie(name, value, options = {}) {
|
|
95
|
+
if (!COOKIE_NAME.test(name)) {
|
|
96
|
+
throw new Error(`[nifra] invalid cookie name ${JSON.stringify(name)}: must be an RFC 6265 token`);
|
|
97
|
+
}
|
|
98
|
+
let str = `${name}=${encodeURIComponent(value)}`;
|
|
99
|
+
if (options.maxAge !== undefined) {
|
|
100
|
+
if (!Number.isInteger(options.maxAge)) {
|
|
101
|
+
throw new Error("[nifra] cookie maxAge must be an integer number of seconds");
|
|
102
|
+
}
|
|
103
|
+
str += `; Max-Age=${options.maxAge}`;
|
|
104
|
+
}
|
|
105
|
+
if (options.domain !== undefined) {
|
|
106
|
+
if (hasIllegalChar(options.domain))
|
|
107
|
+
throw new Error("[nifra] cookie Domain contains an illegal character");
|
|
108
|
+
str += `; Domain=${options.domain}`;
|
|
109
|
+
}
|
|
110
|
+
if (options.path !== undefined) {
|
|
111
|
+
if (hasIllegalChar(options.path))
|
|
112
|
+
throw new Error("[nifra] cookie Path contains an illegal character");
|
|
113
|
+
str += `; Path=${options.path}`;
|
|
114
|
+
}
|
|
115
|
+
if (options.expires !== undefined)
|
|
116
|
+
str += `; Expires=${options.expires.toUTCString()}`;
|
|
117
|
+
if (options.httpOnly === true)
|
|
118
|
+
str += "; HttpOnly";
|
|
119
|
+
if (options.secure === true)
|
|
120
|
+
str += "; Secure";
|
|
121
|
+
if (options.partitioned === true)
|
|
122
|
+
str += "; Partitioned";
|
|
123
|
+
if (options.sameSite !== undefined) {
|
|
124
|
+
str += `; SameSite=${options.sameSite === "strict" ? "Strict" : options.sameSite === "none" ? "None" : "Lax"}`;
|
|
125
|
+
}
|
|
126
|
+
if (str.length > MAX_COOKIE_BYTES) {
|
|
127
|
+
throw new Error(`[nifra] cookie ${JSON.stringify(name)} is ${str.length}B, over the ${MAX_COOKIE_BYTES}B limit`);
|
|
128
|
+
}
|
|
129
|
+
return str;
|
|
130
|
+
}
|
|
131
|
+
const TEXT = new TextEncoder();
|
|
132
|
+
const importHmacKey = (secret) => {
|
|
133
|
+
// Reject a weak secret at the choke point both signing and verification flow through (so signed
|
|
134
|
+
// cookies AND @nifrajs/auth sessions, which delegate here via signValue/unsignValue, are covered).
|
|
135
|
+
requireSecretBytes(secret, "signed-cookie");
|
|
136
|
+
return crypto.subtle.importKey("raw", TEXT.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
137
|
+
};
|
|
138
|
+
const toBase64Url = (buf) => {
|
|
139
|
+
let bin = "";
|
|
140
|
+
for (const b of new Uint8Array(buf))
|
|
141
|
+
bin += String.fromCharCode(b);
|
|
142
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
143
|
+
};
|
|
144
|
+
// Returns `Uint8Array<ArrayBuffer>` (not the generic `Uint8Array<ArrayBufferLike>`) so it satisfies
|
|
145
|
+
// WebCrypto's `BufferSource` parameter under TS 5.7+'s typed-array generics.
|
|
146
|
+
const fromBase64Url = (s) => {
|
|
147
|
+
try {
|
|
148
|
+
const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/"));
|
|
149
|
+
const bytes = new Uint8Array(bin.length);
|
|
150
|
+
for (let i = 0; i < bin.length; i++)
|
|
151
|
+
bytes[i] = bin.charCodeAt(i);
|
|
152
|
+
return bytes;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null; // not valid base64 — treat as a bad signature
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
/** Append an HMAC-SHA256 signature to a value → `value.signature` (base64url). For signed cookies. */
|
|
159
|
+
export async function signValue(value, secret) {
|
|
160
|
+
const key = await importHmacKey(secret);
|
|
161
|
+
const sig = await crypto.subtle.sign("HMAC", key, TEXT.encode(value));
|
|
162
|
+
return `${value}.${toBase64Url(sig)}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Verify a `value.signature` produced by {@link signValue} and return the value, or `null` if the
|
|
166
|
+
* signature is missing, malformed, or doesn't match. Verification is **constant-time**
|
|
167
|
+
* (`crypto.subtle.verify`), so a wrong signature can't be discovered byte-by-byte via timing.
|
|
168
|
+
*/
|
|
169
|
+
export async function unsignValue(signed, secret) {
|
|
170
|
+
const dot = signed.lastIndexOf(".");
|
|
171
|
+
if (dot < 1)
|
|
172
|
+
return null; // no signature segment (or empty value)
|
|
173
|
+
const value = signed.slice(0, dot);
|
|
174
|
+
const sig = fromBase64Url(signed.slice(dot + 1));
|
|
175
|
+
if (sig === null)
|
|
176
|
+
return null;
|
|
177
|
+
const key = await importHmacKey(secret);
|
|
178
|
+
const ok = await crypto.subtle.verify("HMAC", key, sig, TEXT.encode(value));
|
|
179
|
+
return ok ? value : null;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=cookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookies.js","sourceRoot":"","sources":["../../src/server/cookies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured, redacting logger. The framework logs through this interface so
|
|
3
|
+
* secrets/PII are scrubbed once, centrally (per the project's logging rule), not at
|
|
4
|
+
* each call site. Bring your own by passing `logger` to `server()`.
|
|
5
|
+
*/
|
|
6
|
+
export type LogFields = Record<string, unknown>;
|
|
7
|
+
export interface Logger {
|
|
8
|
+
debug(message: string, fields?: LogFields): void;
|
|
9
|
+
info(message: string, fields?: LogFields): void;
|
|
10
|
+
warn(message: string, fields?: LogFields): void;
|
|
11
|
+
error(message: string, fields?: LogFields): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tunes redaction. Key-name redaction always runs; the rest is **opt-in**:
|
|
15
|
+
* - `keyParts` — extra case-insensitive key fragments, added to the built-in denylist.
|
|
16
|
+
* - `valuePatterns` — regexes matched against string **values** *and* the log message; each match is
|
|
17
|
+
* replaced with the placeholder. This is the value-scanning hook for secrets that land in a value or
|
|
18
|
+
* message (e.g. `err.message`), which key-name redaction can't catch. Off unless provided — the
|
|
19
|
+
* default path does no value scanning, so it stays allocation-light. See {@link commonSecretPatterns}.
|
|
20
|
+
* - `placeholder` — the replacement string (default `[REDACTED]`).
|
|
21
|
+
*/
|
|
22
|
+
export interface RedactOptions {
|
|
23
|
+
readonly keyParts?: readonly string[];
|
|
24
|
+
readonly valuePatterns?: readonly RegExp[];
|
|
25
|
+
readonly placeholder?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A conservative, high-signal set of patterns for {@link RedactOptions.valuePatterns} — opt in by
|
|
29
|
+
* passing it (or a subset) to `jsonLogger`/`redactLogFields`. Covers bearer tokens, JWTs, emails, and a
|
|
30
|
+
* few well-known key formats (Stripe, GitHub, AWS access-key ids). Chosen to minimize false positives;
|
|
31
|
+
* add your own (e.g. internal id formats) as needed. Every pattern is global so all matches are scrubbed.
|
|
32
|
+
*/
|
|
33
|
+
export declare const commonSecretPatterns: ReadonlyArray<RegExp>;
|
|
34
|
+
/**
|
|
35
|
+
* Deep-copy `fields`, replacing values under sensitive keys with the placeholder; cycle-safe. With
|
|
36
|
+
* `options.valuePatterns`, also scans string values for those patterns (opt-in). Without options, this
|
|
37
|
+
* is pure key-name redaction (the long-standing default).
|
|
38
|
+
*/
|
|
39
|
+
export declare function redactLogFields(fields: LogFields, options?: RedactOptions): LogFields;
|
|
40
|
+
/**
|
|
41
|
+
* The default logger: one redacted JSON object per line. `write` is injectable for tests or
|
|
42
|
+
* alternative sinks (defaults to stderr). `options` tunes redaction — pass `valuePatterns` (e.g.
|
|
43
|
+
* {@link commonSecretPatterns}) to also scrub secrets embedded in values + the message. Framework keys
|
|
44
|
+
* (`level`, `message`, `time`) always win over user fields of the same name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function jsonLogger(write?: (line: string) => void, options?: RedactOptions): Logger;
|
|
47
|
+
/** Discards everything — for tests, or when log output is handled elsewhere. */
|
|
48
|
+
export declare const silentLogger: Logger;
|
|
49
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE/C,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAChD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAC/C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;IAC/C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI,CAAA;CACjD;AAsBD;;;;;;;;GAQG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACrC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1C,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAOtD,CAAA;AAqDD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAErF;AAMD;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,KAAK,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAoB,EAC7C,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CAkBR;AAID,gFAAgF;AAChF,eAAO,MAAM,YAAY,EAAE,MAK1B,CAAA"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured, redacting logger. The framework logs through this interface so
|
|
3
|
+
* secrets/PII are scrubbed once, centrally (per the project's logging rule), not at
|
|
4
|
+
* each call site. Bring your own by passing `logger` to `server()`.
|
|
5
|
+
*/
|
|
6
|
+
const REDACTED = "[REDACTED]";
|
|
7
|
+
/**
|
|
8
|
+
* Field-name fragments whose values are replaced with `[REDACTED]` (case-insensitive
|
|
9
|
+
* substring match). Key-name redaction is the default + always on.
|
|
10
|
+
*/
|
|
11
|
+
const SENSITIVE_KEY_PARTS = [
|
|
12
|
+
"authorization",
|
|
13
|
+
"cookie",
|
|
14
|
+
"password",
|
|
15
|
+
"passwd",
|
|
16
|
+
"secret",
|
|
17
|
+
"token",
|
|
18
|
+
"apikey",
|
|
19
|
+
"api_key",
|
|
20
|
+
"api-key",
|
|
21
|
+
"credential",
|
|
22
|
+
"ssn",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* A conservative, high-signal set of patterns for {@link RedactOptions.valuePatterns} — opt in by
|
|
26
|
+
* passing it (or a subset) to `jsonLogger`/`redactLogFields`. Covers bearer tokens, JWTs, emails, and a
|
|
27
|
+
* few well-known key formats (Stripe, GitHub, AWS access-key ids). Chosen to minimize false positives;
|
|
28
|
+
* add your own (e.g. internal id formats) as needed. Every pattern is global so all matches are scrubbed.
|
|
29
|
+
*/
|
|
30
|
+
export const commonSecretPatterns = [
|
|
31
|
+
/Bearer\s+[A-Za-z0-9._~+/-]+=*/gi, // Authorization: Bearer <token>
|
|
32
|
+
/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, // JWT (header.payload.signature)
|
|
33
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, // email
|
|
34
|
+
/\b[sprk]k_(?:live|test)_[A-Za-z0-9]{8,}/g, // Stripe-style secret/restricted keys
|
|
35
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}/g, // GitHub personal/OAuth/refresh tokens
|
|
36
|
+
/\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id
|
|
37
|
+
];
|
|
38
|
+
// `String.replace` with a global regex replaces every match and is stateless across calls; a
|
|
39
|
+
// non-global pattern would only scrub the first occurrence, so normalize to global once up front.
|
|
40
|
+
const ensureGlobal = (re) => re.flags.includes("g") ? re : new RegExp(re.source, `${re.flags}g`);
|
|
41
|
+
function buildConfig(options) {
|
|
42
|
+
const extra = options?.keyParts;
|
|
43
|
+
return {
|
|
44
|
+
keyParts: extra !== undefined && extra.length > 0
|
|
45
|
+
? [...SENSITIVE_KEY_PARTS, ...extra.map((k) => k.toLowerCase())]
|
|
46
|
+
: SENSITIVE_KEY_PARTS,
|
|
47
|
+
valuePatterns: (options?.valuePatterns ?? []).map(ensureGlobal),
|
|
48
|
+
placeholder: options?.placeholder ?? REDACTED,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function isSensitiveKey(key, parts) {
|
|
52
|
+
const lower = key.toLowerCase();
|
|
53
|
+
return parts.some((part) => lower.includes(part));
|
|
54
|
+
}
|
|
55
|
+
function redactString(value, config) {
|
|
56
|
+
if (config.valuePatterns.length === 0)
|
|
57
|
+
return value; // default fast path: no value scanning
|
|
58
|
+
let out = value;
|
|
59
|
+
for (const pattern of config.valuePatterns)
|
|
60
|
+
out = out.replace(pattern, config.placeholder);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function redactValue(value, seen, config) {
|
|
64
|
+
if (typeof value === "string")
|
|
65
|
+
return redactString(value, config);
|
|
66
|
+
if (value === null || typeof value !== "object")
|
|
67
|
+
return value;
|
|
68
|
+
if (seen.has(value))
|
|
69
|
+
return "[Circular]";
|
|
70
|
+
seen.add(value);
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value.map((item) => redactValue(item, seen, config));
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const [key, val] of Object.entries(value)) {
|
|
75
|
+
// A sensitive key wins outright (don't scan it) — the whole value is replaced.
|
|
76
|
+
out[key] = isSensitiveKey(key, config.keyParts)
|
|
77
|
+
? config.placeholder
|
|
78
|
+
: redactValue(val, seen, config);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Deep-copy `fields`, replacing values under sensitive keys with the placeholder; cycle-safe. With
|
|
84
|
+
* `options.valuePatterns`, also scans string values for those patterns (opt-in). Without options, this
|
|
85
|
+
* is pure key-name redaction (the long-standing default).
|
|
86
|
+
*/
|
|
87
|
+
export function redactLogFields(fields, options) {
|
|
88
|
+
return redactValue(fields, new WeakSet(), buildConfig(options));
|
|
89
|
+
}
|
|
90
|
+
function writeToStderr(line) {
|
|
91
|
+
process.stderr.write(`${line}\n`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The default logger: one redacted JSON object per line. `write` is injectable for tests or
|
|
95
|
+
* alternative sinks (defaults to stderr). `options` tunes redaction — pass `valuePatterns` (e.g.
|
|
96
|
+
* {@link commonSecretPatterns}) to also scrub secrets embedded in values + the message. Framework keys
|
|
97
|
+
* (`level`, `message`, `time`) always win over user fields of the same name.
|
|
98
|
+
*/
|
|
99
|
+
export function jsonLogger(write = writeToStderr, options) {
|
|
100
|
+
const config = buildConfig(options); // normalize patterns once, reuse per entry
|
|
101
|
+
const emit = (level, message, fields) => {
|
|
102
|
+
const entry = fields === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: redactValue(fields, new WeakSet(), config);
|
|
105
|
+
entry.level = level;
|
|
106
|
+
entry.message = redactString(message, config); // scan the message too (no-op without valuePatterns)
|
|
107
|
+
entry.time = new Date().toISOString();
|
|
108
|
+
write(JSON.stringify(entry));
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
debug: (message, fields) => emit("debug", message, fields),
|
|
112
|
+
info: (message, fields) => emit("info", message, fields),
|
|
113
|
+
warn: (message, fields) => emit("warn", message, fields),
|
|
114
|
+
error: (message, fields) => emit("error", message, fields),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const noop = () => undefined;
|
|
118
|
+
/** Discards everything — for tests, or when log output is handled elsewhere. */
|
|
119
|
+
export const silentLogger = {
|
|
120
|
+
debug: noop,
|
|
121
|
+
info: noop,
|
|
122
|
+
warn: noop,
|
|
123
|
+
error: noop,
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=logger.js.map
|