lambder 2.0.18 → 3.0.0
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 +162 -41
- package/dist/Lambder.d.ts +154 -46
- package/dist/Lambder.js +312 -166
- package/dist/LambderCaller.js +6 -3
- package/dist/LambderContext.d.ts +20 -9
- package/dist/LambderContext.js +57 -17
- package/dist/LambderCors.d.ts +12 -0
- package/dist/LambderCors.js +30 -0
- package/dist/LambderHtml.d.ts +33 -0
- package/dist/LambderHtml.js +62 -0
- package/dist/LambderMSW.d.ts +16 -1
- package/dist/LambderMSW.js +5 -9
- package/dist/LambderPublicFiles.d.ts +47 -0
- package/dist/LambderPublicFiles.js +108 -0
- package/dist/LambderResolver.d.ts +30 -31
- package/dist/LambderResolver.js +29 -43
- package/dist/LambderResponse.d.ts +71 -0
- package/dist/LambderResponse.js +196 -0
- package/dist/LambderResponseBuilder.d.ts +58 -33
- package/dist/LambderResponseBuilder.js +114 -167
- package/dist/LambderRouting.d.ts +23 -0
- package/dist/LambderRouting.js +67 -0
- package/dist/LambderSessionController.d.ts +13 -1
- package/dist/LambderSessionController.js +33 -10
- package/dist/LambderSessionManager.d.ts +3 -1
- package/dist/LambderSessionManager.js +15 -6
- package/dist/LambderTemplatingEngine.d.ts +87 -0
- package/dist/LambderTemplatingEngine.js +156 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +10 -1
- package/dist/node-polyfills.d.ts +4 -2
- package/dist/node-polyfills.js +28 -0
- package/package.json +7 -5
- package/.eslintrc.cjs +0 -26
- package/.vscode/settings.json +0 -26
- package/deploy +0 -22
- package/dist/LambderUtils.d.ts +0 -10
- package/dist/LambderUtils.js +0 -70
- package/docs/DYNAMODB_SETUP.md +0 -96
- package/docs/LAMBDER_MSW.md +0 -409
- package/docs/TYPE_SAFE_QUICK_START.md +0 -77
- package/examples/msw-testing-example.ts +0 -280
- package/examples/secure-session-example.ts +0 -207
- package/examples/zod-chained-api-example.ts +0 -63
- package/src/Lambder.ts +0 -430
- package/src/LambderApiContract.ts +0 -20
- package/src/LambderCaller.ts +0 -238
- package/src/LambderContext.ts +0 -78
- package/src/LambderMSW.ts +0 -180
- package/src/LambderResolver.ts +0 -101
- package/src/LambderResponseBuilder.ts +0 -332
- package/src/LambderSessionController.ts +0 -114
- package/src/LambderSessionManager.ts +0 -217
- package/src/LambderUtils.ts +0 -75
- package/src/index.ts +0 -17
- package/src/node-polyfills.ts +0 -27
- package/tests/error-handling.test.ts +0 -585
- package/tests/file-serving.test.ts +0 -194
- package/tests/fixtures/public/index.html +0 -1
- package/tests/fixtures/public/main.css +0 -1
- package/tests/hooks.test.ts +0 -561
- package/tests/output-type-runtime.test.ts +0 -381
- package/tests/redirect.test.ts +0 -88
- package/tests/routes.test.ts +0 -543
- package/tests/session.test.ts +0 -1083
- package/tests/use-plugin.test.ts +0 -460
- package/tsconfig.json +0 -24
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type LambderHtmlValue } from "./LambderHtml.js";
|
|
2
|
+
/**
|
|
3
|
+
* LambderTemplatingEngine: a comment-only HTML template engine.
|
|
4
|
+
*
|
|
5
|
+
* Fully standalone: it has no dependency on Lambder routing or file serving,
|
|
6
|
+
* and can template any HTML: app shells, emails, error pages. res.templateFile
|
|
7
|
+
* uses it internally to render HTML files from publicPath per request.
|
|
8
|
+
*
|
|
9
|
+
* Every construct is an HTML comment. That is the whole point: templates
|
|
10
|
+
* survive HTML build pipelines (e.g. Vite) untouched, and are invisible in the
|
|
11
|
+
* browser during frontend development, where the default content between the
|
|
12
|
+
* markers renders as-is.
|
|
13
|
+
*
|
|
14
|
+
* Syntax:
|
|
15
|
+
*
|
|
16
|
+
* <!--slot:name-->default content<!--/slot:name--> replaceable region;
|
|
17
|
+
* default kept when the
|
|
18
|
+
* data omits "name"
|
|
19
|
+
* <!--slot:name/--> insert-only point
|
|
20
|
+
* <!--if:name--> ... <!--else--> ... <!--/if:name--> conditional block,
|
|
21
|
+
* shown when data.name
|
|
22
|
+
* is truthy
|
|
23
|
+
* <!--if:!name--> ... <!--/if:!name--> negated conditional
|
|
24
|
+
*
|
|
25
|
+
* Blocks nest freely (ifs in slots, slots in ifs). There are intentionally no
|
|
26
|
+
* loops or inline expressions: dynamic lists are built server-side with the
|
|
27
|
+
* html`...` tagged template and passed in as a slot value. Attribute-position
|
|
28
|
+
* values (e.g. <html lang="...">) are handled with if/else around whole-tag
|
|
29
|
+
* variants.
|
|
30
|
+
*
|
|
31
|
+
* Data is dynamically typed: one Record<string, LambderHtmlValue> shared by
|
|
32
|
+
* slots and conditions.
|
|
33
|
+
* - strings/numbers are HTML-escaped on insertion (XSS-safe by default)
|
|
34
|
+
* - html`...` / raw() / jsonScript() values are inserted verbatim
|
|
35
|
+
* - arrays are flattened; null/undefined/false render the slot default
|
|
36
|
+
* - unknown data keys are ignored, so one data object can serve several
|
|
37
|
+
* templates with different slots
|
|
38
|
+
*
|
|
39
|
+
* Templates are parsed once (construction throws on unclosed or mismatched
|
|
40
|
+
* blocks with a descriptive message); render() is a cheap tree walk, safe to
|
|
41
|
+
* call per request. Discovered names are exposed on `slotNames` and
|
|
42
|
+
* `conditionNames` for runtime validation.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* import { LambderTemplatingEngine, html } from "lambder";
|
|
47
|
+
*
|
|
48
|
+
* const template = new LambderTemplatingEngine(`
|
|
49
|
+
* <title><!--slot:title-->My Site<!--/slot:title--></title>
|
|
50
|
+
* <!--if:isBeta--><meta name="robots" content="noindex" /><!--/if:isBeta-->
|
|
51
|
+
* <!--slot:head/-->
|
|
52
|
+
* `);
|
|
53
|
+
*
|
|
54
|
+
* template.render({
|
|
55
|
+
* title: userInput, // escaped
|
|
56
|
+
* isBeta: stage === "beta",
|
|
57
|
+
* head: html`<link rel="canonical" href="${canonical}" />`, // verbatim
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* // Or load from disk (compile once, render many times):
|
|
61
|
+
* const emailTemplate = await LambderTemplatingEngine.fromFile("./templates/welcome.html");
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export type LambderTemplateData = Record<string, LambderHtmlValue>;
|
|
65
|
+
export type LambderTemplatingEngineOptions = {
|
|
66
|
+
/**
|
|
67
|
+
* For full HTML documents without declared markers: expose the <title>
|
|
68
|
+
* element content as slot "title" and the position before </head> as
|
|
69
|
+
* insert-only slot "head". Default: false.
|
|
70
|
+
*/
|
|
71
|
+
htmlVirtualSlots?: boolean;
|
|
72
|
+
};
|
|
73
|
+
export declare class LambderTemplatingEngine {
|
|
74
|
+
private nodes;
|
|
75
|
+
/** Slot names discovered at compile time (dynamic typing surface). */
|
|
76
|
+
readonly slotNames: readonly string[];
|
|
77
|
+
/** Condition names discovered at compile time. */
|
|
78
|
+
readonly conditionNames: readonly string[];
|
|
79
|
+
/** Parse `source`; throws on unclosed or mismatched blocks. */
|
|
80
|
+
constructor(source: string, options?: LambderTemplatingEngineOptions);
|
|
81
|
+
/** Read and parse a template file (compile once, render many times). */
|
|
82
|
+
static fromFile(filePath: string, options?: LambderTemplatingEngineOptions): Promise<LambderTemplatingEngine>;
|
|
83
|
+
/** True when the template declares `name` as a slot or condition. */
|
|
84
|
+
has(name: string): boolean;
|
|
85
|
+
/** Render with escaped-by-default data; unknown keys ignored, omitted slots keep defaults. */
|
|
86
|
+
render(data?: LambderTemplateData): string;
|
|
87
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { getFS } from "./node-polyfills.js";
|
|
2
|
+
import { renderHtmlValue } from "./LambderHtml.js";
|
|
3
|
+
const TOKEN_PATTERN = /<!--\s*(?:(slot:([\w-]+)\s*\/)|(slot:([\w-]+))|(\/slot:([\w-]+))|(if:(!?)([\w-]+))|(else)|(\/if:(!?)([\w-]+)))\s*-->/g;
|
|
4
|
+
/**
|
|
5
|
+
* Fail loudly on slot positions where HTML escaping cannot protect against
|
|
6
|
+
* injection: unquoted attribute values (`class=<!--slot:x/-->`) and inside
|
|
7
|
+
* <script>/<style> elements (HTML escaping is the wrong grammar there; embed
|
|
8
|
+
* data with jsonScript() into a normal slot instead).
|
|
9
|
+
*/
|
|
10
|
+
const assertSafeSlotPosition = (source, slotIndex, slotName) => {
|
|
11
|
+
const before = source.slice(0, slotIndex);
|
|
12
|
+
if (/=\s*$/.test(before)) {
|
|
13
|
+
throw new Error(`LambderTemplatingEngine: slot "${slotName}" is in an unquoted attribute position. ` +
|
|
14
|
+
`Escaping cannot prevent injection there; quote the attribute: attr="<!--slot:${slotName}/-->".`);
|
|
15
|
+
}
|
|
16
|
+
for (const tag of ["script", "style"]) {
|
|
17
|
+
const lastOpen = before.toLowerCase().lastIndexOf(`<${tag}`);
|
|
18
|
+
if (lastOpen !== -1 && before.toLowerCase().indexOf(`</${tag}`, lastOpen) === -1) {
|
|
19
|
+
throw new Error(`LambderTemplatingEngine: slot "${slotName}" is inside a <${tag}> element where HTML ` +
|
|
20
|
+
`escaping does not apply. Pass data with the jsonScript() helper in a regular slot instead.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const parseTemplate = (source) => {
|
|
25
|
+
const root = { kind: "root", name: "", negated: false, nodes: [], elseNodes: [], inElse: false };
|
|
26
|
+
const stack = [root];
|
|
27
|
+
const top = () => stack[stack.length - 1];
|
|
28
|
+
const emit = (node) => {
|
|
29
|
+
const frame = top();
|
|
30
|
+
(frame.inElse ? frame.elseNodes : frame.nodes).push(node);
|
|
31
|
+
};
|
|
32
|
+
TOKEN_PATTERN.lastIndex = 0;
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = TOKEN_PATTERN.exec(source)) !== null) {
|
|
36
|
+
if (match.index > cursor) {
|
|
37
|
+
emit({ type: "text", value: source.slice(cursor, match.index) });
|
|
38
|
+
}
|
|
39
|
+
cursor = TOKEN_PATTERN.lastIndex;
|
|
40
|
+
const [, selfClosingSlot, selfClosingName, openSlot, openSlotName, closeSlot, closeSlotName, openIf, openIfNegation, openIfName, elseTag, closeIf, closeIfNegation, closeIfName] = match;
|
|
41
|
+
if (selfClosingSlot) {
|
|
42
|
+
assertSafeSlotPosition(source, match.index, selfClosingName);
|
|
43
|
+
emit({ type: "slot", name: selfClosingName, defaultNodes: [] });
|
|
44
|
+
}
|
|
45
|
+
else if (openSlot) {
|
|
46
|
+
assertSafeSlotPosition(source, match.index, openSlotName);
|
|
47
|
+
stack.push({ kind: "slot", name: openSlotName, negated: false, nodes: [], elseNodes: [], inElse: false });
|
|
48
|
+
}
|
|
49
|
+
else if (closeSlot) {
|
|
50
|
+
const frame = stack.pop();
|
|
51
|
+
if (!frame || frame.kind !== "slot" || frame.name !== closeSlotName) {
|
|
52
|
+
throw new Error(`LambderTemplatingEngine: unexpected <!--/slot:${closeSlotName}--> (open block: ${frame ? `${frame.kind}:${frame.name}` : "none"}).`);
|
|
53
|
+
}
|
|
54
|
+
emit({ type: "slot", name: frame.name, defaultNodes: frame.nodes });
|
|
55
|
+
}
|
|
56
|
+
else if (openIf) {
|
|
57
|
+
stack.push({ kind: "if", name: openIfName, negated: openIfNegation === "!", nodes: [], elseNodes: [], inElse: false });
|
|
58
|
+
}
|
|
59
|
+
else if (elseTag) {
|
|
60
|
+
const frame = top();
|
|
61
|
+
if (frame.kind !== "if" || frame.inElse) {
|
|
62
|
+
throw new Error("LambderTemplatingEngine: <!--else--> outside of an <!--if:...--> block.");
|
|
63
|
+
}
|
|
64
|
+
frame.inElse = true;
|
|
65
|
+
}
|
|
66
|
+
else if (closeIf) {
|
|
67
|
+
const frame = stack.pop();
|
|
68
|
+
const negated = closeIfNegation === "!";
|
|
69
|
+
if (!frame || frame.kind !== "if" || frame.name !== closeIfName || frame.negated !== negated) {
|
|
70
|
+
throw new Error(`LambderTemplatingEngine: unexpected <!--/if:${closeIfNegation}${closeIfName}--> (open block: ${frame ? `${frame.kind}:${frame.name}` : "none"}).`);
|
|
71
|
+
}
|
|
72
|
+
emit({ type: "if", name: frame.name, negated: frame.negated, thenNodes: frame.nodes, elseNodes: frame.elseNodes });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (cursor < source.length) {
|
|
76
|
+
emit({ type: "text", value: source.slice(cursor) });
|
|
77
|
+
}
|
|
78
|
+
if (stack.length !== 1) {
|
|
79
|
+
const frame = top();
|
|
80
|
+
throw new Error(`LambderTemplatingEngine: unclosed <!--${frame.kind}:${frame.name}--> block.`);
|
|
81
|
+
}
|
|
82
|
+
return root.nodes;
|
|
83
|
+
};
|
|
84
|
+
const renderNodes = (nodes, data) => {
|
|
85
|
+
let out = "";
|
|
86
|
+
for (const node of nodes) {
|
|
87
|
+
if (node.type === "text") {
|
|
88
|
+
out += node.value;
|
|
89
|
+
}
|
|
90
|
+
else if (node.type === "slot") {
|
|
91
|
+
const value = Object.prototype.hasOwnProperty.call(data, node.name) ? data[node.name] : undefined;
|
|
92
|
+
out += value === undefined ? renderNodes(node.defaultNodes, data) : renderHtmlValue(value);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const condition = !!data[node.name] !== node.negated;
|
|
96
|
+
out += renderNodes(condition ? node.thenNodes : node.elseNodes, data);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
};
|
|
101
|
+
const collectNames = (nodes, slots, conditions) => {
|
|
102
|
+
for (const node of nodes) {
|
|
103
|
+
if (node.type === "slot") {
|
|
104
|
+
slots.add(node.name);
|
|
105
|
+
collectNames(node.defaultNodes, slots, conditions);
|
|
106
|
+
}
|
|
107
|
+
else if (node.type === "if") {
|
|
108
|
+
conditions.add(node.name);
|
|
109
|
+
collectNames(node.thenNodes, slots, conditions);
|
|
110
|
+
collectNames(node.elseNodes, slots, conditions);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
export class LambderTemplatingEngine {
|
|
115
|
+
nodes;
|
|
116
|
+
/** Slot names discovered at compile time (dynamic typing surface). */
|
|
117
|
+
slotNames;
|
|
118
|
+
/** Condition names discovered at compile time. */
|
|
119
|
+
conditionNames;
|
|
120
|
+
/** Parse `source`; throws on unclosed or mismatched blocks. */
|
|
121
|
+
constructor(source, options = {}) {
|
|
122
|
+
this.nodes = parseTemplate(options.htmlVirtualSlots ? applyHtmlVirtualSlots(source) : source);
|
|
123
|
+
const slots = new Set();
|
|
124
|
+
const conditions = new Set();
|
|
125
|
+
collectNames(this.nodes, slots, conditions);
|
|
126
|
+
this.slotNames = [...slots];
|
|
127
|
+
this.conditionNames = [...conditions];
|
|
128
|
+
}
|
|
129
|
+
/** Read and parse a template file (compile once, render many times). */
|
|
130
|
+
static async fromFile(filePath, options = {}) {
|
|
131
|
+
const fs = await getFS();
|
|
132
|
+
if (!fs)
|
|
133
|
+
throw new Error("LambderTemplatingEngine.fromFile requires a Node.js environment.");
|
|
134
|
+
const source = await fs.promises.readFile(filePath, "utf8");
|
|
135
|
+
return new LambderTemplatingEngine(source, options);
|
|
136
|
+
}
|
|
137
|
+
/** True when the template declares `name` as a slot or condition. */
|
|
138
|
+
has(name) {
|
|
139
|
+
return this.slotNames.includes(name) || this.conditionNames.includes(name);
|
|
140
|
+
}
|
|
141
|
+
/** Render with escaped-by-default data; unknown keys ignored, omitted slots keep defaults. */
|
|
142
|
+
render(data = {}) {
|
|
143
|
+
return renderNodes(this.nodes, data);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Wrap the <title> content and the pre-</head> position in virtual slot markers. */
|
|
147
|
+
const applyHtmlVirtualSlots = (source) => {
|
|
148
|
+
let out = source;
|
|
149
|
+
if (!/<!--\s*slot:title\b/.test(out)) {
|
|
150
|
+
out = out.replace(/(<title[^>]*>)([\s\S]*?)(<\/title>)/i, (_all, open, inner, close) => `${open}<!--slot:title-->${inner}<!--/slot:title-->${close}`);
|
|
151
|
+
}
|
|
152
|
+
if (!/<!--\s*slot:head\b/.test(out)) {
|
|
153
|
+
out = out.replace(/<\/head\s*>/i, (headClose) => `<!--slot:head/-->${headClose}`);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,19 @@ export { default as LambderCaller } from "./LambderCaller.js";
|
|
|
4
4
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
6
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
7
|
+
export { default as LambderSessionController } from "./LambderSessionController.js";
|
|
7
8
|
export { default as LambderMSW } from "./LambderMSW.js";
|
|
9
|
+
export type { LambderMswModule } from "./LambderMSW.js";
|
|
10
|
+
export { LambderResponse, finalizeResponse, acceptsEncoding, type HttpStatusCode, type LambderHttpResponse, type LambderHttpEventFormat, type LambderHeadersInput, type LambderFinalizeOptions, } from "./LambderResponse.js";
|
|
11
|
+
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml, type LambderHtmlValue } from "./LambderHtml.js";
|
|
12
|
+
export { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
13
|
+
export type { LambderTemplateData, LambderTemplatingEngineOptions } from "./LambderTemplatingEngine.js";
|
|
14
|
+
export type { LambderResponseOptions, LambderApiResponse, LambderApiResponseConfig, LambderRawResponseInit, } from "./LambderResponseBuilder.js";
|
|
15
|
+
export type { LambderRouteMatcher, LambderCorsConfig, LambderConstructorOptions, ConditionFunction, RouteCondition, PathParamsOf, LambderActionTools, LambderHandler, LambderIndexHtmlOptions, } from "./Lambder.js";
|
|
16
|
+
export { LambderPublicFilesHandler } from "./LambderPublicFiles.js";
|
|
17
|
+
export type { LambderPublicFilesOptions } from "./LambderPublicFiles.js";
|
|
18
|
+
export type { LambderSessionCookieOptions } from "./LambderSessionController.js";
|
|
19
|
+
export type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
8
20
|
export { type ApiContractShape, } from "./LambderApiContract.js";
|
|
9
|
-
export type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
10
|
-
export { createContext } from "./LambderContext.js";
|
|
21
|
+
export type { LambderRenderContext, LambderSessionRenderContext, LambderHttpEvent } from "./LambderContext.js";
|
|
22
|
+
export { createContext, isV2HttpEvent } from "./LambderContext.js";
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,14 @@ export { default as LambderCaller } from "./LambderCaller.js";
|
|
|
4
4
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
6
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
7
|
+
export { default as LambderSessionController } from "./LambderSessionController.js";
|
|
7
8
|
export { default as LambderMSW } from "./LambderMSW.js";
|
|
8
|
-
|
|
9
|
+
// Response model
|
|
10
|
+
export { LambderResponse, finalizeResponse, acceptsEncoding, } from "./LambderResponse.js";
|
|
11
|
+
// Type-safe templating (tagged templates with auto-escaping)
|
|
12
|
+
export { html, xml, raw, jsonScript, escapeHtml, renderHtmlValue, LambderSafeHtml } from "./LambderHtml.js";
|
|
13
|
+
// Comment-based HTML templating engine (build-pipeline-safe slots and conditionals, standalone)
|
|
14
|
+
export { LambderTemplatingEngine } from "./LambderTemplatingEngine.js";
|
|
15
|
+
// Public file serving
|
|
16
|
+
export { LambderPublicFilesHandler } from "./LambderPublicFiles.js";
|
|
17
|
+
export { createContext, isV2HttpEvent } from "./LambderContext.js";
|
package/dist/node-polyfills.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
export declare function getFS(): Promise<
|
|
2
|
-
export declare function getPath(): Promise<
|
|
1
|
+
export declare function getFS(): Promise<typeof import('fs') | null>;
|
|
2
|
+
export declare function getPath(): Promise<typeof import('path') | null>;
|
|
3
|
+
export declare function getZlib(): Promise<typeof import('zlib') | null>;
|
|
4
|
+
export declare function getCrypto(): Promise<typeof import('crypto') | null>;
|
package/dist/node-polyfills.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// This file provides optional Node.js modules that fail gracefully in browser environments
|
|
3
3
|
let fs = null;
|
|
4
4
|
let path = null;
|
|
5
|
+
let zlib = null;
|
|
6
|
+
let crypto = null;
|
|
5
7
|
export async function getFS() {
|
|
6
8
|
try {
|
|
7
9
|
if (fs) {
|
|
@@ -28,3 +30,29 @@ export async function getPath() {
|
|
|
28
30
|
return null;
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
export async function getZlib() {
|
|
34
|
+
try {
|
|
35
|
+
if (zlib) {
|
|
36
|
+
return zlib;
|
|
37
|
+
}
|
|
38
|
+
zlib = await import('zlib');
|
|
39
|
+
return zlib;
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
// Silently fail - we're in a browser environment
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function getCrypto() {
|
|
47
|
+
try {
|
|
48
|
+
if (crypto) {
|
|
49
|
+
return crypto;
|
|
50
|
+
}
|
|
51
|
+
crypto = await import('crypto');
|
|
52
|
+
return crypto;
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
// Silently fail - we're in a browser environment
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
"browser": {
|
|
15
15
|
"fs": false,
|
|
16
|
-
"path": false
|
|
16
|
+
"path": false,
|
|
17
|
+
"zlib": false,
|
|
18
|
+
"crypto": false
|
|
17
19
|
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
18
23
|
"scripts": {
|
|
19
24
|
"test": "vitest run",
|
|
20
25
|
"test:watch": "vitest",
|
|
@@ -31,11 +36,9 @@
|
|
|
31
36
|
"@aws-sdk/client-dynamodb": "^3.574.0",
|
|
32
37
|
"@aws-sdk/lib-dynamodb": "^3.574.0",
|
|
33
38
|
"cookie": "^1.0.2",
|
|
34
|
-
"ejs": "^3.1.9",
|
|
35
39
|
"js-cookie": "^3.0.5",
|
|
36
40
|
"mime-types": "^2.1.35",
|
|
37
41
|
"path-to-regexp": "^6.2.1",
|
|
38
|
-
"querystring": "^0.2.1",
|
|
39
42
|
"zod": "^4.1.12"
|
|
40
43
|
},
|
|
41
44
|
"peerDependencies": {
|
|
@@ -49,7 +52,6 @@
|
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"@types/aws-lambda": "^8.10.136",
|
|
51
54
|
"@types/cookie": "^0.6.0",
|
|
52
|
-
"@types/ejs": "^3.1.5",
|
|
53
55
|
"@types/js-cookie": "^3.0.6",
|
|
54
56
|
"@types/mime-types": "^2.1.4",
|
|
55
57
|
"@types/node": "^20.19.22",
|
package/.eslintrc.cjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
"env": {
|
|
3
|
-
"browser": true,
|
|
4
|
-
"es2021": true
|
|
5
|
-
},
|
|
6
|
-
"extends": "standard-with-typescript",
|
|
7
|
-
"overrides": [
|
|
8
|
-
{
|
|
9
|
-
"env": {
|
|
10
|
-
"node": true
|
|
11
|
-
},
|
|
12
|
-
"files": [
|
|
13
|
-
".eslintrc.{js,cjs}"
|
|
14
|
-
],
|
|
15
|
-
"parserOptions": {
|
|
16
|
-
"sourceType": "script"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
"parserOptions": {
|
|
21
|
-
"ecmaVersion": "latest",
|
|
22
|
-
"sourceType": "module"
|
|
23
|
-
},
|
|
24
|
-
"rules": {
|
|
25
|
-
}
|
|
26
|
-
}
|
package/.vscode/settings.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"editor.codeActionsOnSave": {
|
|
3
|
-
"source.fixAll.eslint": "explicit"
|
|
4
|
-
},
|
|
5
|
-
"eslint.validate": [
|
|
6
|
-
"javascript",
|
|
7
|
-
"javascriptreact",
|
|
8
|
-
"typescript",
|
|
9
|
-
"typescriptreact"
|
|
10
|
-
],
|
|
11
|
-
"eslint.workingDirectories": [
|
|
12
|
-
"./src"
|
|
13
|
-
],
|
|
14
|
-
"search.exclude": {
|
|
15
|
-
"**/node_modules/": true,
|
|
16
|
-
"**/dist/": true,
|
|
17
|
-
"**/package-lock.json": true,
|
|
18
|
-
"**/.git/": true,
|
|
19
|
-
},
|
|
20
|
-
"cSpell.words": [
|
|
21
|
-
"lambder",
|
|
22
|
-
"LMBDRAUTHID",
|
|
23
|
-
"LMBDRTOKEN"
|
|
24
|
-
],
|
|
25
|
-
}
|
|
26
|
-
|
package/deploy
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
# Exit immediately if a command exits with a non-zero status.
|
|
4
|
-
set -e
|
|
5
|
-
|
|
6
|
-
echo "Running tests..."
|
|
7
|
-
npm run test
|
|
8
|
-
|
|
9
|
-
echo "Tests passed! Proceeding with deployment..."
|
|
10
|
-
|
|
11
|
-
# Update the version, build the project, and publish
|
|
12
|
-
rm -rf ./dist/*
|
|
13
|
-
npm version patch --no-git-tag-version
|
|
14
|
-
npm run build
|
|
15
|
-
npm publish
|
|
16
|
-
|
|
17
|
-
echo "All operations completed successfully"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# git add .
|
|
21
|
-
# git commit -m ""
|
|
22
|
-
# git push -u origin main
|
package/dist/LambderUtils.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export default class LambderUtils {
|
|
2
|
-
private ejsPath?;
|
|
3
|
-
constructor({ ejsPath }?: {
|
|
4
|
-
ejsPath?: string;
|
|
5
|
-
});
|
|
6
|
-
private readEjsFileSync;
|
|
7
|
-
private checkEjsFileExist;
|
|
8
|
-
renderEjs(template: string, pageData: Record<string, any>): Promise<string>;
|
|
9
|
-
renderEjsFile(filePath: string, pageData: Record<string, any>): Promise<string>;
|
|
10
|
-
}
|
package/dist/LambderUtils.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import ejs from "ejs";
|
|
2
|
-
import { getFS, getPath } from "./node-polyfills.js";
|
|
3
|
-
export default class LambderUtils {
|
|
4
|
-
ejsPath;
|
|
5
|
-
constructor({ ejsPath } = {}) {
|
|
6
|
-
this.ejsPath = ejsPath;
|
|
7
|
-
}
|
|
8
|
-
;
|
|
9
|
-
async readEjsFileSync(filePath) {
|
|
10
|
-
const fs = await getFS();
|
|
11
|
-
const path = await getPath();
|
|
12
|
-
if (!fs || !path) {
|
|
13
|
-
throw new Error("File system operations are not available in browser environment");
|
|
14
|
-
}
|
|
15
|
-
if (!this.ejsPath) {
|
|
16
|
-
return "EJS PATH NOT SET!";
|
|
17
|
-
}
|
|
18
|
-
const ejsPath = path.resolve(this.ejsPath);
|
|
19
|
-
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
20
|
-
const absolutePath = path.resolve(ejsPath, normalizedFilePath);
|
|
21
|
-
if (!absolutePath.startsWith(ejsPath)) {
|
|
22
|
-
return "forbidden-ejs-path";
|
|
23
|
-
}
|
|
24
|
-
return await fs.promises.readFile(absolutePath, 'utf-8');
|
|
25
|
-
}
|
|
26
|
-
;
|
|
27
|
-
async checkEjsFileExist(filePath) {
|
|
28
|
-
const fs = await getFS();
|
|
29
|
-
const path = await getPath();
|
|
30
|
-
if (!fs || !path) {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
if (!this.ejsPath) {
|
|
34
|
-
return "EJS PATH NOT SET!";
|
|
35
|
-
}
|
|
36
|
-
const ejsPath = path.resolve(this.ejsPath);
|
|
37
|
-
const normalizedFilePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
38
|
-
const absolutePath = path.resolve(ejsPath, normalizedFilePath);
|
|
39
|
-
if (!absolutePath.startsWith(ejsPath)) {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
const stat = await fs.promises.stat(absolutePath);
|
|
44
|
-
return stat.isFile();
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
;
|
|
51
|
-
async renderEjs(template, pageData) {
|
|
52
|
-
const includeRenderedFile = async (filePath, partialData) => {
|
|
53
|
-
const template = await this.readEjsFileSync(filePath);
|
|
54
|
-
return await ejs.render(template, { page: pageData, partial: partialData, include: includeRenderedFile }, { async: true });
|
|
55
|
-
};
|
|
56
|
-
const renderedResult = await ejs.render(template, { page: pageData, include: includeRenderedFile }, { async: true });
|
|
57
|
-
return renderedResult;
|
|
58
|
-
}
|
|
59
|
-
;
|
|
60
|
-
async renderEjsFile(filePath, pageData) {
|
|
61
|
-
const doesFileExist = await this.checkEjsFileExist(filePath);
|
|
62
|
-
if (!doesFileExist) {
|
|
63
|
-
return "File not found: " + filePath;
|
|
64
|
-
}
|
|
65
|
-
const template = await this.readEjsFileSync(filePath);
|
|
66
|
-
return this.renderEjs(template, pageData);
|
|
67
|
-
}
|
|
68
|
-
;
|
|
69
|
-
}
|
|
70
|
-
;
|
package/docs/DYNAMODB_SETUP.md
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
# DynamoDB Session Table Setup Guide
|
|
2
|
-
|
|
3
|
-
This guide helps you set up a DynamoDB table for Lambder session management with all security features enabled.
|
|
4
|
-
|
|
5
|
-
## Table Creation
|
|
6
|
-
|
|
7
|
-
### Using Terraform
|
|
8
|
-
|
|
9
|
-
```hcl
|
|
10
|
-
resource "aws_dynamodb_table" "lambder_sessions" {
|
|
11
|
-
name = "lambder-sessions"
|
|
12
|
-
billing_mode = "PAY_PER_REQUEST"
|
|
13
|
-
hash_key = "pk"
|
|
14
|
-
range_key = "sk"
|
|
15
|
-
|
|
16
|
-
attribute {
|
|
17
|
-
name = "pk"
|
|
18
|
-
type = "S"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
attribute {
|
|
22
|
-
name = "sk"
|
|
23
|
-
type = "S"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
ttl {
|
|
27
|
-
enabled = true
|
|
28
|
-
attribute_name = "expiresAt"
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
tags = {
|
|
32
|
-
Purpose = "Session Management"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Enable Time to Live (TTL)
|
|
38
|
-
|
|
39
|
-
TTL automatically removes expired sessions from DynamoDB, saving storage costs.
|
|
40
|
-
|
|
41
|
-
### Using AWS Console
|
|
42
|
-
|
|
43
|
-
1. Go to DynamoDB Console
|
|
44
|
-
2. Select your table (`lambder-sessions`)
|
|
45
|
-
3. Navigate to **Additional settings** tab
|
|
46
|
-
4. Click **Edit** under **Time to Live (TTL)**
|
|
47
|
-
5. Enable TTL
|
|
48
|
-
6. Set **TTL attribute** to: `expiresAt`
|
|
49
|
-
7. Save changes
|
|
50
|
-
|
|
51
|
-
## IAM Permissions
|
|
52
|
-
|
|
53
|
-
Your Lambda function needs these permissions:
|
|
54
|
-
|
|
55
|
-
```json
|
|
56
|
-
{
|
|
57
|
-
"Version": "2012-10-17",
|
|
58
|
-
"Statement": [
|
|
59
|
-
{
|
|
60
|
-
"Effect": "Allow",
|
|
61
|
-
"Action": [
|
|
62
|
-
"dynamodb:GetItem",
|
|
63
|
-
"dynamodb:PutItem",
|
|
64
|
-
"dynamodb:DeleteItem",
|
|
65
|
-
"dynamodb:Query"
|
|
66
|
-
],
|
|
67
|
-
"Resource": [
|
|
68
|
-
"arn:aws:dynamodb:us-east-1:123456789012:table/lambder-sessions"
|
|
69
|
-
]
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Session Data Structure
|
|
76
|
-
|
|
77
|
-
Each session is stored as:
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"pk": "hash_of_user_id",
|
|
82
|
-
"sk": "random_64_char_hex",
|
|
83
|
-
"sessionToken": "hash_of_user_id:random_64_char_hex",
|
|
84
|
-
"csrfToken": "random_64_char_hex",
|
|
85
|
-
"sessionKey": "user_123",
|
|
86
|
-
"data": {
|
|
87
|
-
"userId": "user_123",
|
|
88
|
-
"username": "john_doe",
|
|
89
|
-
"role": "admin"
|
|
90
|
-
},
|
|
91
|
-
"createdAt": 1697712000,
|
|
92
|
-
"lastAccessedAt": 1697712300,
|
|
93
|
-
"expiresAt": 1700304000,
|
|
94
|
-
"ttlInSeconds": 2592000
|
|
95
|
-
}
|
|
96
|
-
```
|