lambder 3.2.6 → 3.3.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/dist/Lambder.d.ts
CHANGED
|
@@ -27,8 +27,11 @@ export type LambderIndexHtmlOptions = {
|
|
|
27
27
|
/** Methods that reach the index handler. Default: ["GET", "HEAD"]. */
|
|
28
28
|
methods?: string[];
|
|
29
29
|
/**
|
|
30
|
-
* Skip paths whose last segment
|
|
31
|
-
*
|
|
30
|
+
* Skip paths whose last segment contains a dot, treating them as missing
|
|
31
|
+
* assets rather than app routes. Default: false — real files have already
|
|
32
|
+
* been served by servePublicFiles at this point, and plenty of app routes
|
|
33
|
+
* carry dots (JWTs, coordinates, domain names, version numbers). Turn it
|
|
34
|
+
* on to get 404s instead of a 200 shell for missing-asset requests.
|
|
32
35
|
*/
|
|
33
36
|
skipFilePaths?: boolean;
|
|
34
37
|
/** 301-redirect trailing-slash paths to the canonical no-slash URL. Default: false. */
|
|
@@ -127,7 +130,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
127
130
|
enableSlidingExpiration?: boolean;
|
|
128
131
|
/** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
|
|
129
132
|
slidingWriteIntervalSeconds?: number;
|
|
130
|
-
/** Session cookie attributes, e.g. { domain: ".example.com" } for cross-subdomain sessions. */
|
|
133
|
+
/** Session cookie attributes, e.g. { domain: ".example.com" } for cross-subdomain sessions. `domain` may be a (hostname) => string function for multi-domain deployments. */
|
|
131
134
|
cookie?: LambderSessionCookieOptions;
|
|
132
135
|
partitionKey?: string;
|
|
133
136
|
sortKey?: string;
|
|
@@ -150,11 +153,12 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
150
153
|
servePublicFiles(options?: LambderPublicFilesOptions): this;
|
|
151
154
|
/**
|
|
152
155
|
* Serve the app shell for page requests that nothing else handled. Runs
|
|
153
|
-
* after servePublicFiles in the fallback chain,
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
156
|
+
* after servePublicFiles in the fallback chain, so real files are already
|
|
157
|
+
* gone; everything left is an app route (option `skipFilePaths` opts back
|
|
158
|
+
* into 404ing dotted paths). Only configured methods reach it, default
|
|
159
|
+
* GET/HEAD. Gated-out requests fall through to setRouteFallbackHandler.
|
|
160
|
+
* Without a handler, publicPath/index.html is served via res.templateFile
|
|
161
|
+
* (markers optional) with no-cache.
|
|
158
162
|
*/
|
|
159
163
|
serveIndexHtml(handler?: FallbackHandlerFunction, options?: LambderIndexHtmlOptions): this;
|
|
160
164
|
/** Apply the serveIndexHtml gates; null means fall through. */
|
package/dist/Lambder.js
CHANGED
|
@@ -121,11 +121,12 @@ export default class Lambder {
|
|
|
121
121
|
}
|
|
122
122
|
/**
|
|
123
123
|
* Serve the app shell for page requests that nothing else handled. Runs
|
|
124
|
-
* after servePublicFiles in the fallback chain,
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
124
|
+
* after servePublicFiles in the fallback chain, so real files are already
|
|
125
|
+
* gone; everything left is an app route (option `skipFilePaths` opts back
|
|
126
|
+
* into 404ing dotted paths). Only configured methods reach it, default
|
|
127
|
+
* GET/HEAD. Gated-out requests fall through to setRouteFallbackHandler.
|
|
128
|
+
* Without a handler, publicPath/index.html is served via res.templateFile
|
|
129
|
+
* (markers optional) with no-cache.
|
|
129
130
|
*/
|
|
130
131
|
serveIndexHtml(handler, options = {}) {
|
|
131
132
|
this.indexHtmlConfig = { handler: handler ?? null, options };
|
|
@@ -139,7 +140,7 @@ export default class Lambder {
|
|
|
139
140
|
const methods = (options.methods ?? ["GET", "HEAD"]).map((m) => m.toUpperCase());
|
|
140
141
|
if (!methods.includes(ctx.method.toUpperCase()))
|
|
141
142
|
return null;
|
|
142
|
-
if ((options.skipFilePaths ??
|
|
143
|
+
if ((options.skipFilePaths ?? false) && (ctx.path.split("/").pop() ?? "").includes("."))
|
|
143
144
|
return null;
|
|
144
145
|
if (options.redirectTrailingSlash && ctx.path.length > 1 && ctx.path.endsWith("/")) {
|
|
145
146
|
const target = ctx.path.replace(/\/+$/, "") || "/";
|
package/dist/LambderCaller.d.ts
CHANGED
|
@@ -41,7 +41,8 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
|
41
41
|
private fetchEndedHandler?;
|
|
42
42
|
private sessionTokenCookieKey;
|
|
43
43
|
private sessionCsrfCookieKey;
|
|
44
|
-
|
|
44
|
+
private sessionCookieDomain?;
|
|
45
|
+
constructor({ apiPath, apiVersion, isCorsEnabled, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, sessionCookieDomain, }: {
|
|
45
46
|
apiPath: string;
|
|
46
47
|
apiVersion?: string;
|
|
47
48
|
isCorsEnabled: boolean;
|
|
@@ -54,8 +55,11 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
|
54
55
|
fetchStartedHandler?: FetchStartEventHandler;
|
|
55
56
|
fetchEndedHandler?: FetchEndEventHandler;
|
|
56
57
|
apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
58
|
+
/** Must mirror the server's session cookie Domain, otherwise expired cookies cannot be cleared. */
|
|
59
|
+
sessionCookieDomain?: string | ((hostname: string) => string | undefined | null);
|
|
57
60
|
});
|
|
58
61
|
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
|
|
62
|
+
private clearSessionCookies;
|
|
59
63
|
apiRaw<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, options?: {
|
|
60
64
|
headers?: Record<string, any>;
|
|
61
65
|
versionExpiredHandler?: VoidFunction;
|
package/dist/LambderCaller.js
CHANGED
|
@@ -16,10 +16,12 @@ export default class LambderCaller {
|
|
|
16
16
|
fetchEndedHandler;
|
|
17
17
|
sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
18
18
|
sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
19
|
-
|
|
19
|
+
sessionCookieDomain;
|
|
20
|
+
constructor({ apiPath, apiVersion, isCorsEnabled = false, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, sessionCookieDomain, }) {
|
|
20
21
|
this.apiPath = apiPath ?? "/api";
|
|
21
22
|
this.apiVersion = apiVersion;
|
|
22
23
|
this.isCorsEnabled = isCorsEnabled;
|
|
24
|
+
this.sessionCookieDomain = sessionCookieDomain;
|
|
23
25
|
this.versionExpiredHandler = versionExpiredHandler;
|
|
24
26
|
this.sessionExpiredHandler = sessionExpiredHandler;
|
|
25
27
|
this.messageHandler = messageHandler;
|
|
@@ -35,6 +37,17 @@ export default class LambderCaller {
|
|
|
35
37
|
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
36
38
|
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
37
39
|
}
|
|
40
|
+
clearSessionCookies() {
|
|
41
|
+
const domainOption = this.sessionCookieDomain;
|
|
42
|
+
const hostname = typeof window !== "undefined" ? window.location.hostname : "";
|
|
43
|
+
const resolvedDomain = typeof domainOption === "function" ? domainOption(hostname) : domainOption;
|
|
44
|
+
for (const key of [this.sessionTokenCookieKey, this.sessionCsrfCookieKey]) {
|
|
45
|
+
// Host-only and domain-scoped cookies are distinct entries; clear both.
|
|
46
|
+
Cookies.remove(key);
|
|
47
|
+
if (resolvedDomain)
|
|
48
|
+
Cookies.remove(key, { domain: resolvedDomain, path: "/" });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
38
51
|
async apiRaw(apiName, payload, options) {
|
|
39
52
|
const headers = options?.headers;
|
|
40
53
|
const fetchTracker = { apiName, done: false, fetchEndCalled: false };
|
|
@@ -102,8 +115,7 @@ export default class LambderCaller {
|
|
|
102
115
|
return null;
|
|
103
116
|
}
|
|
104
117
|
if (data && data.sessionExpired) {
|
|
105
|
-
|
|
106
|
-
Cookies.set(this.sessionCsrfCookieKey, '', { expires: -1 });
|
|
118
|
+
this.clearSessionCookies();
|
|
107
119
|
if (this.sessionExpiredHandler) {
|
|
108
120
|
await this.sessionExpiredHandler();
|
|
109
121
|
}
|
|
@@ -2,8 +2,12 @@ import { LambderRenderContext, LambderSessionRenderContext } from "./LambderCont
|
|
|
2
2
|
import type LambderSessionManager from "./LambderSessionManager.js";
|
|
3
3
|
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
4
|
export type LambderSessionCookieOptions = {
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* e.g. ".example.com" to share sessions across subdomains. Pass a function to
|
|
7
|
+
* derive it from the request hostname when one deployment serves several
|
|
8
|
+
* apex domains; return undefined for a host-only cookie.
|
|
9
|
+
*/
|
|
10
|
+
domain?: string | ((hostname: string) => string | undefined | null);
|
|
7
11
|
path?: string;
|
|
8
12
|
sameSite?: "Strict" | "Lax" | "None";
|
|
9
13
|
secure?: boolean;
|
|
@@ -14,11 +14,14 @@ export default class LambderSessionController {
|
|
|
14
14
|
;
|
|
15
15
|
buildCookie(key, value, expiresAtMs, httpOnly) {
|
|
16
16
|
const { domain, path = "/", sameSite = "Lax", secure = true } = this.cookieOptions;
|
|
17
|
+
// Host header can carry a port; browsers match the Domain attribute on hostname only.
|
|
18
|
+
const hostname = (this.ctx.host || "").split(":")[0];
|
|
19
|
+
const resolvedDomain = typeof domain === "function" ? domain(hostname) : domain;
|
|
17
20
|
const parts = [
|
|
18
21
|
`${key}=${value}`,
|
|
19
22
|
`Expires=${new Date(expiresAtMs).toUTCString()}`,
|
|
20
23
|
`Path=${path}`,
|
|
21
|
-
...(
|
|
24
|
+
...(resolvedDomain ? [`Domain=${resolvedDomain}`] : []),
|
|
22
25
|
...(httpOnly ? ["HttpOnly"] : []),
|
|
23
26
|
`SameSite=${sameSite}`,
|
|
24
27
|
...(secure ? ["Secure"] : []),
|