@webtypen/webframez-core 0.3.75 → 0.3.77
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 +13 -0
- package/auth-client.js +19 -6
- package/dist/Auth/SessionAuth.d.ts +2 -0
- package/dist/Auth/SessionAuth.js +10 -1
- package/dist/Auth/WebAuth.js +1 -1
- package/dist/Hooks/WebframezHooks.d.ts +1 -0
- package/dist/Hooks/WebframezHooks.js +8 -4
- package/dist/Router/Router.d.ts +4 -0
- package/dist/Router/Router.js +174 -124
- package/docs/authentication.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -925,3 +925,16 @@ the text representation; their separate `icon` property controls navigation icon
|
|
|
925
925
|
## DataBuilder model forms
|
|
926
926
|
|
|
927
927
|
DataBuilder supports `@Field()`, parameterized `@Validates()`, server-side `ModelForm` classes and `Route.databuilder()` while retaining the v1 request protocol and legacy controller registrations. See [DataBuilder documentation](docs/databuilder.md).
|
|
928
|
+
|
|
929
|
+
## Routing and lifecycle performance
|
|
930
|
+
|
|
931
|
+
Route and domain patterns are compiled at registration and reused while matching.
|
|
932
|
+
`Router.init()` resets their caches; runtime registration and legacy direct route
|
|
933
|
+
stores still work. Exact-route precedence, domain ordering, parameters and wildcards
|
|
934
|
+
retain their existing behavior. Public regex builders still produce fresh regexes.
|
|
935
|
+
|
|
936
|
+
Lifecycle hooks skip event normalization when no listeners are registered. Use
|
|
937
|
+
`WebframezHooks.hasListeners(eventName)` before constructing an expensive payload.
|
|
938
|
+
Registered handlers retain their ordered, awaited execution and snapshot behavior.
|
|
939
|
+
The router applies this check before building telemetry payloads. Middleware keeps
|
|
940
|
+
its existing `next`/`reject` callback contract.
|
package/auth-client.js
CHANGED
|
@@ -28,17 +28,30 @@ exports.createAuthFetch = function createAuthFetch(options = {}, originalFetch =
|
|
|
28
28
|
|| ![401, 403].includes(response.status)) return response;
|
|
29
29
|
const failure = await response.clone().json().catch(() => null);
|
|
30
30
|
if (!["unauthorized", "invalid_csrf"].includes(failure?.code)) return response;
|
|
31
|
-
|
|
31
|
+
const performRefresh = () => {
|
|
32
32
|
const refreshHeaders = new Headers();
|
|
33
33
|
const csrf = csrfCookie(options.cookieName);
|
|
34
34
|
if (csrf) refreshHeaders.set("X-CSRF-Token", csrf);
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
return originalFetch(`${base}/refresh`, { method: "POST", credentials: "same-origin", headers: refreshHeaders });
|
|
36
|
+
};
|
|
37
|
+
const retry = () => {
|
|
38
|
+
const nextCsrf = csrfCookie(options.cookieName);
|
|
39
|
+
if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
|
|
40
|
+
return originalFetch(retryInput, { ...init, headers });
|
|
41
|
+
};
|
|
42
|
+
if (typeof navigator !== "undefined" && navigator.locks) {
|
|
43
|
+
// Hold the shared tab lock through the retry so another refresh cannot invalidate it.
|
|
44
|
+
return navigator.locks.request(`webframez-auth:${base}`, async () => {
|
|
45
|
+
const renewed = await performRefresh().then(result => result.ok, () => false);
|
|
46
|
+
return renewed ? retry() : response;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (!refreshing) {
|
|
50
|
+
refreshing = performRefresh().then(result => result.ok, () => false)
|
|
51
|
+
.finally(() => { refreshing = undefined; });
|
|
37
52
|
}
|
|
38
53
|
if (!await refreshing) return response;
|
|
39
|
-
|
|
40
|
-
if (nextCsrf) headers.set("X-CSRF-Token", nextCsrf);
|
|
41
|
-
return originalFetch(retryInput, { ...init, headers });
|
|
54
|
+
return retry();
|
|
42
55
|
};
|
|
43
56
|
};
|
|
44
57
|
|
|
@@ -65,6 +65,8 @@ export declare class SessionAuth {
|
|
|
65
65
|
verifyCsrf(token: string, csrf: string, kind?: "access" | "refresh"): Promise<boolean>;
|
|
66
66
|
/** One winner under concurrency. A previously consumed refresh token revokes its device session. */
|
|
67
67
|
refresh(token: string): Promise<SessionTokenPair | null>;
|
|
68
|
+
/** Trusted server refresh-secret lookup; never expose as a public endpoint. */
|
|
69
|
+
inspectRefresh(token: string): Promise<AuthSession | null>;
|
|
68
70
|
/** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
|
|
69
71
|
renewAccess(token: string): Promise<SessionTokenPair | null>;
|
|
70
72
|
/** Browser login metadata; arbitrary request data and token hashes are never exposed. */
|
package/dist/Auth/SessionAuth.js
CHANGED
|
@@ -144,6 +144,15 @@ class SessionAuth {
|
|
|
144
144
|
return this.pair(next, access, refresh);
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
+
/** Trusted server refresh-secret lookup; never expose as a public endpoint. */
|
|
148
|
+
inspectRefresh(token) {
|
|
149
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
150
|
+
const id = tokenSessionId(token);
|
|
151
|
+
if (!id)
|
|
152
|
+
return null;
|
|
153
|
+
return this.allowed(yield (yield this.rows()).findOne(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter())));
|
|
154
|
+
});
|
|
155
|
+
}
|
|
147
156
|
/** Trusted browser GET resumption: renew access without consuming or replaying refresh rotation. */
|
|
148
157
|
renewAccess(token) {
|
|
149
158
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -151,7 +160,7 @@ class SessionAuth {
|
|
|
151
160
|
if (!id)
|
|
152
161
|
return null;
|
|
153
162
|
const rows = yield this.rows();
|
|
154
|
-
const filter = Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter());
|
|
163
|
+
const filter = Object.assign(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }), this.liveFilter()), { accessExpiresAt: { $lte: Date.now() } });
|
|
155
164
|
const row = yield rows.findOne(filter);
|
|
156
165
|
if (!row || !(yield this.allowed(row)))
|
|
157
166
|
return null;
|
package/dist/Auth/WebAuth.js
CHANGED
|
@@ -96,7 +96,7 @@ class WebAuth {
|
|
|
96
96
|
return null;
|
|
97
97
|
const pair = yield this.sessions.renewAccess(token);
|
|
98
98
|
if (!pair)
|
|
99
|
-
return
|
|
99
|
+
return this.sessions.inspectRefresh(token);
|
|
100
100
|
this.writePair(res, pair);
|
|
101
101
|
const name = this.cookies.prefix + "access";
|
|
102
102
|
const cookies = (0, AuthSecurity_1.authHeader)(req, "cookie").split(";").map(value => value.trim()).filter(value => value && !value.startsWith(`${name}=`));
|
|
@@ -32,6 +32,7 @@ declare class WebframezHooksFacade {
|
|
|
32
32
|
on<TEventName extends WebframezHookEventName>(eventName: TEventName, handler: WebframezHookHandler<TEventName>): () => this;
|
|
33
33
|
off<TEventName extends WebframezHookEventName>(eventName: TEventName, handler: WebframezHookHandler<TEventName>): this;
|
|
34
34
|
clear(eventName?: WebframezHookEventName): this;
|
|
35
|
+
hasListeners(eventName: WebframezHookEventName): boolean;
|
|
35
36
|
emit<TEventName extends WebframezHookEventName>(eventName: TEventName, context: Omit<Partial<WebframezOperationContext>, "operation" | "phase"> & {
|
|
36
37
|
operationId: string;
|
|
37
38
|
}): Promise<void>;
|
|
@@ -56,8 +56,16 @@ class WebframezHooksFacade {
|
|
|
56
56
|
this.handlers = {};
|
|
57
57
|
return this;
|
|
58
58
|
}
|
|
59
|
+
hasListeners(eventName) {
|
|
60
|
+
var _a;
|
|
61
|
+
return (((_a = this.handlers[eventName]) === null || _a === void 0 ? void 0 : _a.size) || 0) > 0;
|
|
62
|
+
}
|
|
59
63
|
emit(eventName, context) {
|
|
60
64
|
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
const handlers = this.handlers[eventName];
|
|
66
|
+
if (!handlers || handlers.size < 1) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
61
69
|
const parsed = parseEventName(eventName);
|
|
62
70
|
const attributes = Object.assign({}, (context.attributes || {}));
|
|
63
71
|
if (context.error && attributes["error.type"] === undefined) {
|
|
@@ -67,10 +75,6 @@ class WebframezHooksFacade {
|
|
|
67
75
|
type: eventName,
|
|
68
76
|
context: Object.assign(Object.assign({}, context), { operation: parsed.operation, phase: parsed.phase, timestamp: context.timestamp || new Date(), attributes }),
|
|
69
77
|
};
|
|
70
|
-
const handlers = this.handlers[eventName];
|
|
71
|
-
if (!handlers || handlers.size < 1) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
78
|
for (const handler of Array.from(handlers)) {
|
|
75
79
|
try {
|
|
76
80
|
yield handler(event);
|
package/dist/Router/Router.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ declare class RouterFacade {
|
|
|
65
65
|
routesDELETE: {
|
|
66
66
|
[key: string]: RouteObject[];
|
|
67
67
|
};
|
|
68
|
+
private routeRegexCache;
|
|
69
|
+
private domainRegexCache;
|
|
68
70
|
/**
|
|
69
71
|
* Load the application-routes
|
|
70
72
|
*/
|
|
@@ -103,6 +105,8 @@ declare class RouterFacade {
|
|
|
103
105
|
extractParams(path: string, match: any): any;
|
|
104
106
|
escapeRegexPart(value: string): string;
|
|
105
107
|
buildRouteRegex(path: string): RegExp;
|
|
108
|
+
private getRouteRegex;
|
|
109
|
+
private getDomainRegex;
|
|
106
110
|
normalizeDomainString(value: string): string | null;
|
|
107
111
|
getRequestHostCandidates(request?: Request): string[];
|
|
108
112
|
getRouteDomainFilters(routeObj: RouteObject): string[];
|
package/dist/Router/Router.js
CHANGED
|
@@ -69,6 +69,8 @@ class RouterFacade {
|
|
|
69
69
|
* DELETE Route Store
|
|
70
70
|
*/
|
|
71
71
|
this.routesDELETE = {};
|
|
72
|
+
this.routeRegexCache = new Map();
|
|
73
|
+
this.domainRegexCache = new Map();
|
|
72
74
|
}
|
|
73
75
|
/**
|
|
74
76
|
* Load the application-routes
|
|
@@ -88,6 +90,8 @@ class RouterFacade {
|
|
|
88
90
|
this.routesPUT = {};
|
|
89
91
|
this.routesPATCH = {};
|
|
90
92
|
this.routesDELETE = {};
|
|
93
|
+
this.routeRegexCache.clear();
|
|
94
|
+
this.domainRegexCache.clear();
|
|
91
95
|
// Load routes
|
|
92
96
|
if (options.routesFunction) {
|
|
93
97
|
options.routesFunction();
|
|
@@ -139,6 +143,10 @@ class RouterFacade {
|
|
|
139
143
|
else {
|
|
140
144
|
routeConf.component = component;
|
|
141
145
|
}
|
|
146
|
+
this.getRouteRegex(path);
|
|
147
|
+
for (const domain of this.getRouteDomainFilters(routeConf)) {
|
|
148
|
+
this.getDomainRegex(domain);
|
|
149
|
+
}
|
|
142
150
|
if (type === "GET") {
|
|
143
151
|
if (!this.routesGET[path]) {
|
|
144
152
|
this.routesGET[path] = [];
|
|
@@ -273,6 +281,22 @@ class RouterFacade {
|
|
|
273
281
|
const regexString = "^" + escaped.replace(/__WFZ_TOKEN_(\d+)__/g, (all, tokenIndex) => placeholders[tokenIndex]) + "$";
|
|
274
282
|
return new RegExp(regexString);
|
|
275
283
|
}
|
|
284
|
+
getRouteRegex(path) {
|
|
285
|
+
let regex = this.routeRegexCache.get(path);
|
|
286
|
+
if (!regex) {
|
|
287
|
+
regex = this.buildRouteRegex(path);
|
|
288
|
+
this.routeRegexCache.set(path, regex);
|
|
289
|
+
}
|
|
290
|
+
return regex;
|
|
291
|
+
}
|
|
292
|
+
getDomainRegex(domain) {
|
|
293
|
+
let regex = this.domainRegexCache.get(domain);
|
|
294
|
+
if (!regex) {
|
|
295
|
+
regex = this.buildDomainRegex(domain);
|
|
296
|
+
this.domainRegexCache.set(domain, regex);
|
|
297
|
+
}
|
|
298
|
+
return regex;
|
|
299
|
+
}
|
|
276
300
|
normalizeDomainString(value) {
|
|
277
301
|
if (!value || value.trim() === "") {
|
|
278
302
|
return null;
|
|
@@ -342,7 +366,7 @@ class RouterFacade {
|
|
|
342
366
|
return new RegExp("^" + regexBody + "$", "i");
|
|
343
367
|
}
|
|
344
368
|
matchDomainFilter(hostname, domainFilter) {
|
|
345
|
-
const regex = this.
|
|
369
|
+
const regex = this.getDomainRegex(domainFilter);
|
|
346
370
|
const match = hostname.match(regex);
|
|
347
371
|
if (!match) {
|
|
348
372
|
return { matches: false, matchedDomain: null, wildcard: null };
|
|
@@ -411,7 +435,7 @@ class RouterFacade {
|
|
|
411
435
|
continue;
|
|
412
436
|
}
|
|
413
437
|
for (const routeObj of routeCandidates) {
|
|
414
|
-
const match = url.match(this.
|
|
438
|
+
const match = url.match(this.getRouteRegex(routeObj.path));
|
|
415
439
|
if (!match) {
|
|
416
440
|
continue;
|
|
417
441
|
}
|
|
@@ -442,14 +466,16 @@ class RouterFacade {
|
|
|
442
466
|
: this.mode === "aws-lambda" && options && options.event && options.event.requestContext && options.event.requestContext.http
|
|
443
467
|
? options.event.requestContext.http.method
|
|
444
468
|
: null;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
469
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.start")) {
|
|
470
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.start", {
|
|
471
|
+
operationId: httpOperationId,
|
|
472
|
+
name: initialMethod || (this.mode === "aws-lambda" ? "lambda" : "request"),
|
|
473
|
+
attributes: {
|
|
474
|
+
"http.request.method": initialMethod,
|
|
475
|
+
"webframez.mode": this.mode,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
}
|
|
453
479
|
let request;
|
|
454
480
|
let routeOperationId = null;
|
|
455
481
|
try {
|
|
@@ -478,15 +504,17 @@ class RouterFacade {
|
|
|
478
504
|
},
|
|
479
505
|
});
|
|
480
506
|
const errorResult = this.handleError(fallbackRequest, response, statusCode, e);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
507
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.error")) {
|
|
508
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
509
|
+
operationId: httpOperationId,
|
|
510
|
+
name: `${fallbackRequest.method || "HTTP"} unmatched`,
|
|
511
|
+
status: "error",
|
|
512
|
+
error: e,
|
|
513
|
+
request: fallbackRequest,
|
|
514
|
+
response,
|
|
515
|
+
attributes: Object.assign(Object.assign({ "http.request.method": fallbackRequest.method || null, "http.response.status_code": statusCode }, requestTelemetryAttributes(fallbackRequest)), { "webframez.mode": this.mode }),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
490
518
|
return errorResult;
|
|
491
519
|
}
|
|
492
520
|
const route = this.dissolve(request);
|
|
@@ -496,83 +524,95 @@ class RouterFacade {
|
|
|
496
524
|
}
|
|
497
525
|
if (!route) {
|
|
498
526
|
const notFoundResult = this.handleError(request, response, 404, "Not found '" + request.url + "' ...");
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
527
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
528
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
529
|
+
operationId: httpOperationId,
|
|
530
|
+
name: `${request.method} unmatched`,
|
|
531
|
+
status: "ok",
|
|
532
|
+
request,
|
|
533
|
+
response,
|
|
534
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404 }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
507
537
|
return notFoundResult;
|
|
508
538
|
}
|
|
509
539
|
if (!(route.component && typeof route.component === "function") && !(route.controller && route.method_name)) {
|
|
510
540
|
const missingRouteResult = this.handleError(request, response, 404, "Missing route function ...");
|
|
511
|
-
|
|
512
|
-
operationId: httpOperationId,
|
|
513
|
-
name: `${request.method} ${route.path}`,
|
|
514
|
-
status: "ok",
|
|
515
|
-
request,
|
|
516
|
-
response,
|
|
517
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
518
|
-
});
|
|
519
|
-
return missingRouteResult;
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
yield this.handleMiddleware(route, request, response);
|
|
523
|
-
if (request.method === "OPTIONS" && !request.skipOptionsForward) {
|
|
524
|
-
const optionsResult = this.handleReturn(request, response, "");
|
|
541
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
525
542
|
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
526
543
|
operationId: httpOperationId,
|
|
527
544
|
name: `${request.method} ${route.path}`,
|
|
528
545
|
status: "ok",
|
|
529
546
|
request,
|
|
530
547
|
response,
|
|
531
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code":
|
|
548
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
532
549
|
});
|
|
533
|
-
return optionsResult;
|
|
534
550
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"http.request.method": request.method,
|
|
543
|
-
"url.template": route.path,
|
|
544
|
-
"webframez.route": route.path,
|
|
545
|
-
"webframez.route.handler": route.controller && route.method_name
|
|
546
|
-
? `${route.controller.name || "Controller"}@${route.method_name}`
|
|
547
|
-
: "component",
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
if (route.controller && route.method_name) {
|
|
551
|
-
const controllerInstance = new route.controller();
|
|
552
|
-
if (!controllerInstance[route.method_name]) {
|
|
553
|
-
const unknownMethodResult = this.handleError(request, response, 404, "Unknown method '" + route.method_name + "'.");
|
|
554
|
-
yield WebframezHooks_1.WebframezHooks.emit("route.handler.error", {
|
|
555
|
-
operationId: routeOperationId,
|
|
556
|
-
parentOperationId: httpOperationId,
|
|
557
|
-
name: `${request.method} ${route.path}`,
|
|
558
|
-
status: "error",
|
|
559
|
-
error: new Error("Unknown route method"),
|
|
560
|
-
attributes: {
|
|
561
|
-
"http.request.method": request.method,
|
|
562
|
-
"http.response.status_code": 404,
|
|
563
|
-
"url.template": route.path,
|
|
564
|
-
"webframez.route": route.path,
|
|
565
|
-
"webframez.route.handler": `${route.controller.name || "Controller"}@${route.method_name}`,
|
|
566
|
-
},
|
|
567
|
-
});
|
|
551
|
+
return missingRouteResult;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
yield this.handleMiddleware(route, request, response);
|
|
555
|
+
if (request.method === "OPTIONS" && !request.skipOptionsForward) {
|
|
556
|
+
const optionsResult = this.handleReturn(request, response, "");
|
|
557
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
568
558
|
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
569
559
|
operationId: httpOperationId,
|
|
570
560
|
name: `${request.method} ${route.path}`,
|
|
571
561
|
status: "ok",
|
|
572
562
|
request,
|
|
573
563
|
response,
|
|
574
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code":
|
|
564
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": response.statusCode || 200, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
575
565
|
});
|
|
566
|
+
}
|
|
567
|
+
return optionsResult;
|
|
568
|
+
}
|
|
569
|
+
let result = null;
|
|
570
|
+
routeOperationId = WebframezHooks_1.WebframezHooks.createOperationId("route");
|
|
571
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.start")) {
|
|
572
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.start", {
|
|
573
|
+
operationId: routeOperationId,
|
|
574
|
+
parentOperationId: httpOperationId,
|
|
575
|
+
name: `${request.method} ${route.path}`,
|
|
576
|
+
attributes: {
|
|
577
|
+
"http.request.method": request.method,
|
|
578
|
+
"url.template": route.path,
|
|
579
|
+
"webframez.route": route.path,
|
|
580
|
+
"webframez.route.handler": route.controller && route.method_name
|
|
581
|
+
? `${route.controller.name || "Controller"}@${route.method_name}`
|
|
582
|
+
: "component",
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (route.controller && route.method_name) {
|
|
587
|
+
const controllerInstance = new route.controller();
|
|
588
|
+
if (!controllerInstance[route.method_name]) {
|
|
589
|
+
const unknownMethodResult = this.handleError(request, response, 404, "Unknown method '" + route.method_name + "'.");
|
|
590
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.error")) {
|
|
591
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.error", {
|
|
592
|
+
operationId: routeOperationId,
|
|
593
|
+
parentOperationId: httpOperationId,
|
|
594
|
+
name: `${request.method} ${route.path}`,
|
|
595
|
+
status: "error",
|
|
596
|
+
error: new Error("Unknown route method"),
|
|
597
|
+
attributes: {
|
|
598
|
+
"http.request.method": request.method,
|
|
599
|
+
"http.response.status_code": 404,
|
|
600
|
+
"url.template": route.path,
|
|
601
|
+
"webframez.route": route.path,
|
|
602
|
+
"webframez.route.handler": `${route.controller.name || "Controller"}@${route.method_name}`,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
607
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
608
|
+
operationId: httpOperationId,
|
|
609
|
+
name: `${request.method} ${route.path}`,
|
|
610
|
+
status: "ok",
|
|
611
|
+
request,
|
|
612
|
+
response,
|
|
613
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
576
616
|
return unknownMethodResult;
|
|
577
617
|
}
|
|
578
618
|
result = yield controllerInstance[route.method_name].bind(controllerInstance)(request, response);
|
|
@@ -581,40 +621,46 @@ class RouterFacade {
|
|
|
581
621
|
result = yield route.component(request, response);
|
|
582
622
|
}
|
|
583
623
|
const returnResult = this.handleReturn(request, response, result);
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
request,
|
|
601
|
-
response,
|
|
602
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": response.statusCode || 200, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
603
|
-
});
|
|
604
|
-
return returnResult;
|
|
605
|
-
}
|
|
606
|
-
catch (e) {
|
|
607
|
-
if (this.isMiddlewareRejectSignal(e)) {
|
|
608
|
-
const middlewareErrorResult = this.handleError(request, response, 500, e.reason);
|
|
609
|
-
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
624
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.end")) {
|
|
625
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.end", {
|
|
626
|
+
operationId: routeOperationId,
|
|
627
|
+
parentOperationId: httpOperationId,
|
|
628
|
+
name: `${request.method} ${route.path}`,
|
|
629
|
+
status: "ok",
|
|
630
|
+
attributes: {
|
|
631
|
+
"http.request.method": request.method,
|
|
632
|
+
"http.response.status_code": response.statusCode || 200,
|
|
633
|
+
"url.template": route.path,
|
|
634
|
+
"webframez.route": route.path,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
639
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
610
640
|
operationId: httpOperationId,
|
|
611
641
|
name: `${request.method} ${route.path}`,
|
|
612
|
-
status: "
|
|
613
|
-
error: e.reason || e,
|
|
642
|
+
status: "ok",
|
|
614
643
|
request,
|
|
615
644
|
response,
|
|
616
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code":
|
|
645
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": response.statusCode || 200, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
617
646
|
});
|
|
647
|
+
}
|
|
648
|
+
return returnResult;
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
if (this.isMiddlewareRejectSignal(e)) {
|
|
652
|
+
const middlewareErrorResult = this.handleError(request, response, 500, e.reason);
|
|
653
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.error")) {
|
|
654
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
655
|
+
operationId: httpOperationId,
|
|
656
|
+
name: `${request.method} ${route.path}`,
|
|
657
|
+
status: "error",
|
|
658
|
+
error: e.reason || e,
|
|
659
|
+
request,
|
|
660
|
+
response,
|
|
661
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 500, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
618
664
|
return middlewareErrorResult;
|
|
619
665
|
}
|
|
620
666
|
yield ErrorHandler_1.ErrorHandler.report(e, {
|
|
@@ -632,29 +678,33 @@ class RouterFacade {
|
|
|
632
678
|
});
|
|
633
679
|
const errorResult = this.handleError(request, response, 500, e);
|
|
634
680
|
if (routeOperationId) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
681
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.error")) {
|
|
682
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.error", {
|
|
683
|
+
operationId: routeOperationId,
|
|
684
|
+
parentOperationId: httpOperationId,
|
|
685
|
+
name: `${request.method} ${route.path}`,
|
|
686
|
+
status: "error",
|
|
687
|
+
error: e,
|
|
688
|
+
attributes: {
|
|
689
|
+
"http.request.method": request.method,
|
|
690
|
+
"http.response.status_code": 500,
|
|
691
|
+
"url.template": route.path,
|
|
692
|
+
"webframez.route": route.path,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.error")) {
|
|
698
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
699
|
+
operationId: httpOperationId,
|
|
638
700
|
name: `${request.method} ${route.path}`,
|
|
639
701
|
status: "error",
|
|
640
702
|
error: e,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
"url.template": route.path,
|
|
645
|
-
"webframez.route": route.path,
|
|
646
|
-
},
|
|
703
|
+
request,
|
|
704
|
+
response,
|
|
705
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 500, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
647
706
|
});
|
|
648
707
|
}
|
|
649
|
-
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
650
|
-
operationId: httpOperationId,
|
|
651
|
-
name: `${request.method} ${route.path}`,
|
|
652
|
-
status: "error",
|
|
653
|
-
error: e,
|
|
654
|
-
request,
|
|
655
|
-
response,
|
|
656
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 500, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
657
|
-
});
|
|
658
708
|
return errorResult;
|
|
659
709
|
}
|
|
660
710
|
});
|
package/docs/authentication.md
CHANGED
|
@@ -475,7 +475,9 @@ unsafe requests, revive expired sessions, or extend absolute expiry. Explicit
|
|
|
475
475
|
POST refresh rotates both secrets and retains replay protection. Middleware uses
|
|
476
476
|
this resumption automatically when a response is available. Browser `createAuthFetch`
|
|
477
477
|
refreshes rejected authentication/CSRF requests once, then repeats the rejected
|
|
478
|
-
request once; login failures and permission errors are never retried.
|
|
478
|
+
request once; login failures and permission errors are never retried. Web Locks
|
|
479
|
+
serialize refresh calls across tabs in browsers supporting that API; a per-window
|
|
480
|
+
single-flight promise remains the fallback.
|
|
479
481
|
|
|
480
482
|
Reset tokens have 256-bit random secrets; only a SHA-256 digest bound to the scope
|
|
481
483
|
and current password is stored. Per-account request cooldown is an atomic conditional
|
package/package.json
CHANGED